從0到1設計一個react-spa後臺應用

下面圍繞下面這張圖,談談如何構建一個基本的react-spa應用框架。 css

image.png

按需加載

webpack3 + react-router4 + react-loadable

使用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

  1. 安裝 babel-plugin-syntax-dynamic-import,爲babel配置"syntax-dynamic-import"插件;
  2. 使用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等配置項讓按需加載的過程更加友好。github

Magic Comment

上文demo代碼中說到的魔術註釋值得說一下,這個是在webpack3新加上的。Webpack 2+ 開始,引入了Code Splitting-Async的新方法import(),用於動態引入ES Module。webpack將傳入import方法的模塊打包到一個單獨的代碼塊(chunk),可是卻不能像require.ensure同樣,爲生成的chunk指定chunkName,所以在webpack3中提出了Magic Comment用於解決該問題。web

publicPath

異步加載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和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技術的進化過程。

image.png
咱們須要尋求一個搭配當前的技術選型(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 標籤在主應用程序中導入公共的樣式文件。

image.png

覆蓋組件樣式

  • 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,咱們選擇"側邊兩列式佈局。頁面橫向空間有限時,側邊導航可收起"的形式,同時自定義收起觸發器。

undefined

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後臺基本框架的設計過程,省略了不少設計細節,也不涉及狀態管理方面的框架選型,只是對本身思考過程的一個回顧,但願對感興趣的同窗有幫助。

相關文章
相關標籤/搜索