下面圍繞下面這張圖,談談如何構建一個基本的react-spa應用框架。
按需載入
webpack3 + react-router4 + react-loadable
使用SPA必然要說到按需載入,目前最簡潔優雅的方案是使用webpack3 + react-router4 + react-loadable, 原理就是 webpack 的 Dynamic Imports。
通俗的講,dynamic import,就是把JS程式碼分成N個頁面份數的檔案,不在使用者剛進來就全部引入,而是等使用者跳轉路由的時候,再載入對應的JS檔案。這樣做的好處就是加速首屏顯示速度,同時也減少了資源的浪費。
webpack 的 Dynamic Imports 實現主要是利用 ECMAScript的 import() 動態載入特性,用於完成動態載入即執行時載入,而 import() 目前只是一個草案,如果需要用此方法,需要引入對應的轉換器,如 babel-plugin-syntax-dynamic-import。
react-loadable是一個高階元件,參照官方文件和
Code Splitting,單頁面的按需載入方案變得非常簡潔:
- 安裝 babel-plugin-syntax-dynamic-import,為babel配置”syntax-dynamic-import”外掛;
- 使用react-loadable
import Loadable from `react-loadable`;
import LoadingIndicator from `components/LoadingIndicator`;
const DataSandBox = Loadable({
//自從webpack2.4開始,可以在動態匯入中使用魔術註釋來指定模組的chunk名字
loader: () => import(/* webpackChunkName: "chunckName" */`../routers/module/index`),
loading: LoadingIndicator
});
複製程式碼
LoadingIndicator是封裝好的一個在非同步載入階段的loading展示,除此之外,react-loadable還提供了delay和timeout等配置項讓按需載入的過程更加友好。
Magic Comment
上文demo程式碼中說到的魔術註釋值得說一下,這個是在webpack3新加上的。Webpack 2+ 開始,引入了Code Splitting-Async的新方法import(),用於動態引入ES Module。webpack將傳入import方法的模組打包到一個單獨的程式碼塊(chunk),但是卻不能像require.ensure一樣,為生成的chunk指定chunkName,因此在webpack3中提出了Magic Comment用於解決該問題。
publicPath
非同步載入chunck檔案需要利用publicPath來補全生產模式的cdn資源地址。參考城危同學在這篇文章中的觀點
實踐下來關於JSONP地址:”本地開發、日常開發、預發、線上”等環節有一個共同的特點,無論環境
怎麼改變,chunk檔案與主檔案的相對路徑是不會改變的,那獲取runtime的JS地址即可確定JSONP地
址,脫離環境、version和專案倉庫名。
通過在頁面入口檔案中增加如下程式碼,可以相容開發環境和生產環境對chunk檔案的引用
/**
* 設定 __webpack_public_path__, 相容日常、預發、線上環境
*/
const js = document.scripts;
const url = js[js.length - 1].src.split("?")[0];
const urlSplit = url.split("/");
urlSplit.pop();
urlSplit.pop();
__webpack_public_path__ = urlSplit.join("/") + "/";
複製程式碼
Antd和React的版本
Antd 3.0其實是在看了SEECONF上它山前輩的分享而被種草的。我們希望使用Antd 3.0的視覺風格,讓後臺整體看起來更加明亮,因此,將元件庫升級為Antd 3.0,同時使用react v16。這裡需要注意的是,因為antd2.x 預設是用12px, 而3.0 使用的是14px,如果升級的話,對2.x系列業務元件尺寸挑戰,對於舊元件可能會有一些相容成本,比如需要元件內部對預設字型做一下設定。
樣式方案
CSS modules
CSS Evolution: From CSS, SASS, BEM, CSS Modules to Styled Components 比較全面的介紹了css技術的進化過程。
我們需要尋求一個搭配當前的技術選型(React)的最優方案,解決兩個問題:避免樣式覆蓋和便於實現樣式的複用。
css不是程式語言,但如果說要給它加一個作用域的概念的話,那就是:只有全域性作用域。
無論分拆為多少個css檔案,無論用怎樣的方式引入,所有的樣式規則都位於同一作用域,只要選擇符近似,就有發生覆蓋的可能。
CSS Modules是一種技術流的組織css程式碼的策略,通過工具解決了BEM依靠開發人員選擇唯一class名的工作,無法改變css全域性作用域的本性,而是依靠動態生成class名這一手段(利用webpack的css-loader),來實現區域性作用域。顯然,這樣的class名就可以是唯一的,不管原本的css程式碼寫得有多隨便,都可以這樣轉換得到不衝突的css程式碼。
要使用CSS Modules,必須想辦法把變數風格的class名注入到html中,這時,虛擬DOM風格的React,搭配CSS Modules會很容易:有了CSS “本地作用域”,所有的 React 元件都可以在邏輯和呈現狀態上進行完全的隔離。
使用CSS Modules只需要在webpack中給css-loader加上如下兩個引數:
名稱 | 型別 | 預設值 | 描述 |
---|---|---|---|
modules | {Boolean} | false | 啟用/禁用 CSS modules |
localIdentName | {String} | [hash:base64] | 配置生成的識別符號(ident),推薦設定[local]___[hash:base64:5] |
js 檔案的改變就是在設定 className 時,用一個物件屬性取代了原來的字串。
import classNames from `classnames`;
import styles from `./dialog.css`;
export default class Dialog extends React.Component {
render() {
const cx = classNames({
[styles.confirm]: !this.state.disabled,
[styles.disabledConfirm]: this.state.disabled
});
return <div className={styles.root}>
<a className={cx}>Confirm</a>
...
</div>
}
}
複製程式碼
如何與全域性樣式共存
在實際工程中,需要諸如reset/normalize,Settings等一些通用的全域性設定。開啟css modules設定後,所有的樣式預設都是local模式,這時,可以使用:global 標籤在主應用程式中匯入公共的樣式檔案。
覆蓋元件樣式
- CSS Modules 不會覆蓋屬性選擇器,所以可以利用屬性選擇器來解決這個問題;
- 引入的 antd 元件類名沒有被 CSS Modules 轉化,所以被覆蓋的類名,如 .ant-select-selection 必須放到 :global 中,為了防止對其他同類元件造成影響,可以給元件新增 className,只對這類元件進行覆蓋,也可以利用外層類名實現這種限制。
路由與佈局
資料驅動的路由配置
我們需要兩個對應關係,選單和路由的關係以及路由和元件的關係,即通過url找到menu再載入元件這樣一個過程。
url到元件的轉換包括兩個入口,一個是通過menu點選,一個是通過Link跳轉。
Route可以幫我們解決url到component的轉換,即根據path來載入對應的component。那剩下的工作就是定義一個物件來儲存關係,並實現一個通過url找到對應選單項的方法。
參考Antd pro剛對內釋出時候的原始碼,可以設計一個公共的nav.js用來管理url、選單和路由(模組元件)三者的關係。結合前文提到的按需載入策略,基本結構如下:
import BasicLayout from "components/Layouts/BasicLayout.js";
// 按路由拆分程式碼
import Loadable from "react-loadable";
import LoadingIndicator from "components/LoadingIndicator/LoadingIndicator";
//概覽頁
const DashBoard = Loadable({
loader: () => import(/* webpackChunkName: "DashBoard" */"../routers/DashBoard/index"),
loading: LoadingIndicator
});
/*將需要的路由元件包裝成動態載入的形式,然後配置到navData資料結構裡面*/
......
const navData = [
{
component: BasicLayout,
layout: "BasicLayout",
name: "首頁", // for breadcrumb
path: "",
children: [
{
name: "概覽",
icon: "dashboard",
path: "dashboard",
component: DashBoard
},
{
name: "特徵管理",
icon: "bars",
path: "feature",
children: [
{
name: "明星人臉庫",
icon: "star",
path: "face",
component: StarFaceManage,
}
]
},
{
name: "資料沙盤",
icon: "play-circle",
path: "sandbox",
component: DataSandBox,
isLink:true
}
]
}
];
export function getNavData() {
return navData;
}
export { navData };
複製程式碼
name,icon是選單的展示屬性,path代表其對應的url片段,children提供選單無限向下擴充套件的能力,只有葉子節點才有對應的component。通過這種結構,可以遞迴地渲染出對應的選單結果。針對Link形式的跳轉,將isLink設定為true,不在選單的結構中顯示,但可以通過path讓route識別到。這樣,形成了一個通過資料驅動的路由配置。
url到選單的對映
url到選單的對映就是:不同的url對應的openKeys和selectedKeys屬性是啥。
下面是一個基本(只提供一種佈局)的主頁面的程式碼結構:
import BasicLayout from "components/Layouts/BasicLayout";
import { Router, Switch, Route } from "react-router-dom";
import { createBrowserHistory } from "history";
const history = createBrowserHistory();
/**
* 設定 __webpack_public_path__, 相容日常、預發、線上環境
*/
const js = document.scripts;
const url = js[js.length - 1].src.split("?")[0];
const urlSplit = url.split("/");
urlSplit.pop();
urlSplit.pop();
__webpack_public_path__ = urlSplit.join("/") + "/";
/**
* 基礎資訊配置 window.GV通過diamond配置
*/
const Globol_Values = window.GV || {};
//登陸使用者
const user = (Globol_Values.user && JSON.parse(Globol_Values.user)) || {};
const baseConfigs = {
//平臺logo
siteLogo: Globol_Values.siteLogo || "",
.......
};
const App = () => (
<Router history={history}>
<Switch>
<Route
path="/"
render={props =>
(
<BasicLayout {...props} currentUser={user} {...baseConfigs} />
)
}
/>
</Switch>
</Router>
);
ReactDOM.render(<App />, document.getElementById("app"));
複製程式碼
Router會建立一個history物件並用其保持追蹤當前location,在location有變化時對網頁進行重新渲染。通過渲染的元素會被傳入一些引數。分別是match物件,當前location物件以及history物件(由router建立)。locations 是一個含有描述URL不同部分屬性的物件,結構如下:
// 一個基本的location物件
{ pathname: `/`, search: ``, hash: ``, key: `abc123` state: {} }
複製程式碼
利用這個特性,BasicLayout在每次url變化時,可以接收父元件傳入的props中的location物件,並通過pathname屬性來進行menu的匹配。
基於 React Router 4 的可複用 Layout 元件
結合前文的設計,我們希望能夠設計一個可複用 Layout 元件。
動態標題設定
React-document-title提供了一種宣告式的方法來設定單頁應用的的文件標題
基本佈局
antd的Layout提供了基本的佈局能力。仿照pro,我們選擇”側邊兩列式佈局。頁面橫向空間有限時,側邊導航可收起”的形式,同時自定義收起觸發器。
const layout = (
<Layout>
<Sider></Sider>
<Layout>
<Header></Header>
<Content></Content>
</Layout>
</Layout>
)
複製程式碼
Sider
Sider是側邊欄,功能就是展示選單,同時可以根據橫向空間展開收起。自定義觸發器首先需要把trigger屬性設定為null。breakpoint這個屬性很有意思,是觸發響應式佈局的斷點,
//antd中對breakpoint 的規範定義 也是響應式柵格的邊界
{
xs: `480px`,
sm: `576px`,
md: `768px`,
lg: `992px`,
xl: `1200px`,
xxl: `1600px`,
}
複製程式碼
<Sider
trigger={null}
collapsible
collapsed={this.state.collapsed}
breakpoint="md"
onCollapse={this.onCollapse}
width={256}
className={styles.sider}
>
</Sider>
複製程式碼
breakpoint=”md”即body的寬度大於768時,sider就會收起。樣式上,sider的min-height需要設定為100vh,即預設高度佔滿整個瀏覽器的視窗。
參考pro的原始碼,我們可以得到啟發,sider可以通過breakpointer來動態的改變佈局,那麼根據antd的柵格規範,使用 react-container-query 動態給 layout 根據不同的寬度加 classname,那麼裡面包含的所有dom都可以根據這個來調整樣式。
import DocumentTitle from "react-document-title";
import { ContainerQuery } from "react-container-query";
//定義ContainerQuery的引數
const query = {
"screen-xs": {
maxWidth: 575
},
"screen-sm": {
minWidth: 576,
maxWidth: 767
},
"screen-md": {
minWidth: 768,
maxWidth: 991
},
"screen-lg": {
minWidth: 992,
maxWidth: 1199
},
"screen-xl": {
minWidth: 1200
}
};
複製程式碼
一個有動態標題和自適應能力的基本佈局結構
<DocumentTitle title={this.getPageTitle()}>
<ContainerQuery query={query}>
{params => <div className={classNames(params)}>{layout}</div>}
</ContainerQuery>
</DocumentTitle>
複製程式碼
Content
Content內展示路由元件的內容,我們使用<Switch>
元件來包裹一組<Route>
。<Switch>
會遍歷自身的子元素(即路由)並對第一個匹配當前路徑的元素進行渲染。將nav.js中定義的關係資料傳入,生成這組Route結構。
<Content style={{ margin: "24px 24px 0", height: "100%" }}>
<Switch>
{getRouteData("BasicLayout").map(item => (
<Route
exact={item.exact}
key={item.path}
path={item.path}
component={item.component}
/>
))}
<Route
path={"/forbidden/:routerName"}
component={ForbiddenPage}
/>
<Redirect exact from="/" to={defaultRoute} />
<Route component={PageNotFound} />
</Switch>
</Content>
複製程式碼
總結
本文總結了一個react-SPA後臺基本框架的設計過程,省略了很多設計細節,也不涉及狀態管理方面的框架選型,只是對自己思考過程的一個回顧,希望對感興趣的同學有幫助。