下面圍繞下面這張圖,談談如何構建一個基本的react-spa應用框架。 css
使用SPA必然要說到按需加載,目前最簡潔優雅的方案是使用webpack3 + react-router4 + react-loadable, 原理就是 webpack 的 Dynamic Imports。html
通俗的講,dynamic import,就是把JS代碼分紅N個頁面份數的文件,不在用戶剛進來就所有引入,而是等用戶跳轉路由的時候,再加載對應的JS文件。這樣作的好處就是加速首屏顯示速度,同時也減小了資源的浪費。react
webpack 的 Dynamic Imports 實現主要是利用 ECMAScript的 import() 動態加載特性,用於完成動態加載即運行時加載,而 import() 目前只是一個草案,若是須要用此方法,須要引入對應的轉換器,如 babel-plugin-syntax-dynamic-import。webpack
react-loadable是一個高階組件,參照官方文檔和 Code Splitting,單頁面的按需加載方案變得很是簡潔:git
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等配置項讓按需加載的過程更加友好。github
上文demo代碼中說到的魔術註釋值得說一下,這個是在webpack3新加上的。Webpack 2+ 開始,引入了Code Splitting-Async的新方法import(),用於動態引入ES Module。webpack將傳入import方法的模塊打包到一個單獨的代碼塊(chunk),可是卻不能像require.ensure同樣,爲生成的chunk指定chunkName,所以在webpack3中提出了Magic Comment用於解決該問題。web
異步加載chunck文件須要利用publicPath來補全生產模式的cdn資源地址。參考城危同窗在這篇文章中的觀點segmentfault
實踐下來關於JSONP地址:"本地開發、平常開發、預發、線上」等環節有一個共同的特色,不管環境 怎麼改變,chunk文件與主文件的相對路徑是不會改變的,那獲取runtime的JS地址便可肯定JSONP地 址,脫離環境、version和項目倉庫名。瀏覽器
經過在頁面入口文件中增長以下代碼,能夠兼容開發環境和生產環境對chunk文件的引用sass
/**
* 設置 __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 3.0實際上是在看了SEECONF上它山前輩的分享而被種草的。咱們但願使用Antd 3.0的視覺風格,讓後臺總體看起來更加明亮,所以,將組件庫升級爲Antd 3.0,同時使用react v16。這裏須要注意的是,由於antd2.x 默認是用12px, 而3.0 使用的是14px,若是升級的話,對2.x系列業務組件尺寸挑戰,對於舊組件可能會有一些兼容成本,好比須要組件內部對默認字體作一下設定。
CSS Evolution: From CSS, SASS, BEM, CSS Modules to Styled Components 比較全面的介紹了css技術的進化過程。
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 標籤在主應用程序中導入公共的樣式文件。
咱們須要兩個對應關係,菜單和路由的關係以及路由和組件的關係,即經過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對應的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的匹配。
結合前文的設計,咱們但願可以設計一個可複用 Layout 組件。
React-document-title提供了一種聲明式的方法來設置單頁應用的的文檔標題
antd的Layout提供了基本的佈局能力。仿照pro,咱們選擇"側邊兩列式佈局。頁面橫向空間有限時,側邊導航可收起"的形式,同時自定義收起觸發器。
const layout = (
<Layout>
<Sider></Sider>
<Layout>
<Header></Header>
<Content></Content>
</Layout>
</Layout>
)
複製代碼
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內展現路由組件的內容,咱們使用<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後臺基本框架的設計過程,省略了不少設計細節,也不涉及狀態管理方面的框架選型,只是對本身思考過程的一個回顧,但願對感興趣的同窗有幫助。