利用 React/Redux/React-Router 4/webpack 開發大型 web 項目時如何按需加載

  1. 如何設計一個大型 web 項目?
  2. React + webpack 如何按需加載?
  3. React + React-Router 4 + webpack 如何按需加載?
  4. React + Redux + React-Router 4 + webpack 如何按需加載?

實錄提要:javascript

  • bundle-loader 和 Webpack 內置的 import() 有什麼區別?
  • 按需加載可否支持經過請求後臺數據,動態配置頁面的的應用場景?
  • 參與過幾個 React 項目,被依賴包搞的暈暈的,不知道該怎麼選擇?
  • 什麼包應該放到 devDependencies 裏面?什麼包放到 depedencies 裏面?
  • 爲何是 react-router-redux 而不是傳統的 react-redux,其優點是什麼?
  • 按需加載時,每一個單獨的 bundle 都挺大的,爲何?
  • ECMAScript 每一年出一個版本,對應的 babel 也有一大堆,應該如何選擇?
  • 單頁項目過大,怎麼拆分不一樣模塊頁面到不一樣 js 來動態加載?

題外話

經驗尚淺,尚不足以教導,若理解有誤,望能指導三分,語言如有偏激,請理解我年輕氣盛。html

之因此寫這篇文章,是由於我最近一陣子經歷了一個部門的技術選型->項目實施這些技術->二次技術選型->技術版本升級的一個過程。開發業務應用爲主的咱們,不多有時間去研究某項技術的源碼,不加班趕項目進度就已經很慶幸了,大部分時間都花在瞭如何靈活使用市面上的一些技術體系。在這篇文章中,不涉及源碼範圍,我也沒去研究過源碼。寫這篇文章的初衷是分享個人想法和代碼示例,同時也但願看這篇文章的你可以給予寶貴的意見,讓我得以進步。前端

web應用讓人驚歎是從Gmail開始的,流暢的桌面版體驗吸引了不少人,今後web項目開始蓬勃發展。隨後,web應用也愈來愈複雜,爲了能讓web應用如同桌面版應用同樣流暢,出現了SPA。這就是今天我想說的,react/redux等等一系列的產品的出現都是爲了實現體驗度更佳的SPA。vue

兩年前,我開發web項目,都只是用javaweb,使用模板引擎,後端渲染出頁面。對於訪問量不是很大、單個頁面複雜度不是很高、項目的迭代週期不頻繁、二次開發的次數不多的系統,這種模式無疑很適用、性能也沒什麼大的影響,一個java程序員就能夠作到全棧。而事實上,我手頭的web項目並不是這麼簡單,隨着週期的迭代,項目愈來愈臃腫,後端代碼和前端代碼摻雜在一塊兒混亂不堪,當時我自認爲本身技術不錯,代碼寫的本身都認識,然而我離職以後發現一個不少人都知道的道理,一個技術真正好的程序員,寫出來的代碼是要可以讓他人讀的懂,最起碼要讓接替你繼續這個項目的人讀得懂,技術不行那是例外,當時我沒有作到。然後,進入新的公司,讓我可以有機會去對部門進行技術選型,主要是前端部分。我果斷選擇了先後端分離模式,我不想之前很差的地方繼續發生在之後。人員安排最好是這樣,專職的人作專職的事,全棧人員要可以補位。設計一個優質的web項目,最重要的是人,而不是技術!java

設計web最好是前端設計成SPA、後端設計成微服務,有不少企業使用react、vue、angular這些,結果是多頁應用,增長複雜度,下降頁面切換的流暢度。我真不知道他們是怎麼想的,首先多頁應用是不可取的,若是他們是開發webapp,封裝成apk,那就更不可取了!web項目設計成SPA,不少人會想,隨着代碼量的增大,首次加載的文件就會增大,沒錯,這時就須要用到code splitting。這也就是我今天要講的實際項目中如何進行按需加載。我見過有些開發人員將一個js文件拆分紅多個js文件,而每一個頁面都加載這些js文件,這顯然是不可取的,這樣子的不須要拆分。隨着如今網速的提高,首次加載文件稍微大點都是能夠接受的。首次加載後緩存在瀏覽器處,下次加載的時候會更快。引入第三方UI組件,基礎組件要單一,不能夠引入antd了再去引用bootstrap,還有用了react這些就不要在項目中出現jQuery,要純粹!python

廢話就講到這裏,下面介紹我將一個項目重構三次的過程,這三個過程裏有三種不一樣的按需加載方式。示例是我將實際項目刪減事後可運行的例子。code splitting和react/react-router是沒有直接關係的。react

1、react(v.0.14.8) / react-router(v.1.0.3) / webpack(v.1.13.3)

兼容IE8+及現代瀏覽器,示例代碼地址->https://github.com/love-fay/fay-webpack-redux-code-splitting/tree/master/reactwebpack

先貼下依賴:git

依賴

由於業務的需求,須要兼容到IE8,不得不被動地選擇低版本庫。處於對react的首次使用,並無加入redux相關技術。程序員

在react-router 1.x版本中Route組件上擁有getComponent、onEnter參數(4.x以後被移除),getComponent是異步的,因此咱們能夠在這個參數裏進行按需加載,getComponent這個函數有兩個參數nextState、callback,根據nextState.pathname能夠獲取到路由地址,而後再利用webpack的require.ensure異步加載所屬組件的js文件,最後經過callback將該組件返回。示例代碼以下:

<Route path="app" getComponent={this.getUumsComponent} onEnter={this.requireAuth}/>
getUumsComponent = (nextState, callback) ={    let pathname = nextState.pathname; switch (pathname) { case 'app': require.ensure([], (require) ={ callback(null, require('../app/components/App')); }, 'App'); break; default : historyConfig.pushState({nextPathname: pathname}, '/404'); }};

打包事後主要文件的對比:

code splitting

code splitting

not code splitting

not code splitting

這種方式的按需加載就這些,很簡單~

2、react(v.15.6.1) / react-router(v.4.2.2)b / webpack(v.3.5.6)

兼容IE9+及現代瀏覽器,示例代碼地址->https://github.com/love-fay/fay-webpack-redux-code-splitting/tree/master/react-rr4

先貼下依賴:

依賴

此次決定拋棄IE8,甚至都不想兼容IE。不少人口口聲聲說用戶體驗、用戶需求,卻一味地去支持IE8,甚至還有支持IE6的!其實用戶體驗和需求不是用戶單方面的要求,還有就是開發方須要去改變用戶習慣、引導用戶對將來的需求,在這基礎上不斷地提升用戶體驗。(你不告知你的用戶有個瀏覽器叫作谷歌瀏覽器,他這輩子就會以爲IE就是瀏覽器,瀏覽器就是IE。)

此次主要是將react/react-router/webpack進行了升級,並升級到最新(當時的最新)。

按需加載其實跟react-router沒多大關係,只不過須要藉助它更好的完成按需加載這項任務。react-router升級到4後,便沒有了getComponent這個參數,因此咱們得換種方式,react-router4官方示例也提供了code splitting的方法,利用webpack結合bundle-loader,它是在require.ensure基礎上封裝的,更友好的實現異步加載過程。

bundle-loader能夠在webpack文件中進行配置,這裏我就不介紹了,webpack官方文檔都有寫。我這裏是寫在代碼裏的。我簡單說下,基本跟react-router4官方文檔說的差很少。

首先先寫一個bundle.js這個組件,代碼以下:

import React, { Component } from 'react';import PropTypes from 'prop-types';class Bundle extends Component { static propTypes = { load: PropTypes.any, children: PropTypes.any, }; state = { mod: null, }; componentWillMount () { this.load(this.props); } componentWillReceiveProps (nextProps) { if (nextProps.load !== this.props.load) { this.load(nextProps); } } load (props) { this.setState({ mod: null, }); props.load((mod) ={ this.setState({ mod: mod['default'] ? mod['default'] : mod, }); }); } render () { return this.state.mod ? this.props.children(this.state.mod) : <div></div>; }}export default Bundle;

而後在用到須要按需加載的組件的組件中,引入的時候,在文件路徑前面使用bundle-loader?lazy&name=[App]!,以下:

import loadApp from 'bundle-loader?lazy&name=[App]!../../app/components/App';

而後好比我這裏使用 <Route path="/app" component={App}/> 加載這個App組件,咱們須要用到剛纔本身寫的bundle組件:

import Bundle from '../bundle/components/Bundle';const App = (props) =( <Bundle load={loadApp}> {(App) ={ return <App {...props}/>; }} </Bundle>);

打包事後主要文件的對比:

code splitting

code splitting

not code splitting

not code splitting

到這裏,這第二種方式介紹完了,很簡單~

2、react(v.16.1.1) / redux(v.3.7.2) / react-router(v.4.2.2) / webpack(v.3.8.1)

兼容IE9+及現代瀏覽器,示例代碼地址->https://github.com/love-fay/fay-webpack-redux-code-splitting/tree/master/react-rr4-redux

先貼下依賴:

enter image description here

此次又一次對引用的技術進行了更新,同時加入了redux,項目複雜度的提升,組件之間的交流變得複雜,此時就須要用到redux。有些開發人員會以爲好煩,不斷地升級,不斷地改造,很費時費力,何時才能穩定,其實否則,項目的穩定不表明技術的不變,穩定是相對的。若是想要一勞永逸的話,就不要讓公司給你漲工資了,公司也想一勞永逸~之後人工智能一旦鋪開到企業級開發中,將會致使大量在安逸中度過的程序員失業!學習是無止境的,學習也是人一生免費的技能,曾經後端Java一家獨大的時候,spring3穩定的時候,不少後端程序員就開始陷入了一勞永逸的幻覺當中,致使他們中的不少人一度抱怨前端是在瞎折騰~這就比如有自行車爲何要造汽車的理論是同樣的~我是以Java程序員入行的,很清楚Java寫後端的時候,輪子不少,不少程序員就是使用CV大法,甚至不少項目經理啊什麼的就說程序員是搬運工,代碼不就是增刪改查麼~

使用了redux後,全局只有一個Store,而這個Store在頁面打開的時候就已經聲明瞭,因而讓我很糾結如何按需加載。後來我瞭解到redux這個東西的存在,內部運用了react中的context,同時這個context算是隱藏着的祕密。利用它我能夠改變全局的Store。我這裏使用了react-redux,在頂級組件處加入。

import {Provider} from 'react-redux';<Provider store={store}> ......</Provider>

而後在須要引入store信息的子組件處利用它提供的connect方法將store派發下去,這裏派發是根據上下文context。項目中少不了用到路由,這時候,我使用了react-router-redux(必定要5.x版本npm i react-router-redux@next),在總的reducer中加入routerReducer,而後在寫路由組件的部分的頂級處使用。

import createBrowserHistory from 'history/createBrowserHistory';import { ConnectedRouter} from 'react-router-redux';const history = createBrowserHistory();<ConnectedRouter history={history}> ......</ConnectedRouter>

讓咱們再回到上一個代碼片,其中的store來源以下:

import configureStore from '../Store';let store = configureStore();

Store.js

import {createStore, applyMiddleware, compose} from 'redux';import { createLogger } from 'redux-logger';const logger = createLogger();import { routerMiddleware } from 'react-router-redux';import createHistory from 'history/createBrowserHistory';import createSagaMiddleware from 'redux-saga';const history = createHistory();const rMiddleware = routerMiddleware(history);const win = window;export const sagaMiddleware = createSagaMiddleware();const middlewares = [rMiddleware, sagaMiddleware];if (process.env.NODE_ENV !== 'production') { middlewares.push(require('redux-immutable-state-invariant').default());}const storeEnhancers = compose( applyMiddleware(...middlewares, logger), (win && win.devToolsExtension) ? win.devToolsExtension() : (f) =f,);import createReducer from './reducers';export function injectAsyncStore(store, asyncReducers, sagas) { asyncReducers && injectAsyncReducers(store, asyncReducers); sagas && injectAsyncSagas(store, sagas);}function injectAsyncReducers(store, asyncReducers) { let flag = false; for (let key in asyncReducers) { if(Object.prototype.hasOwnProperty.call(asyncReducers, key)) { if (!store.asyncReducers[key]) { store.asyncReducers[key] = asyncReducers[key]; flag = true; } } } flag && store.replaceReducer(createReducer(store.asyncReducers));}function injectAsyncSagas(store, sagas) { for (let key in sagas) { if(Object.prototype.hasOwnProperty.call(sagas, key)) { if (!store.asyncSagas[key]) { store.asyncSagas[key] = sagas[key]; store.sagaMiddleware.run(sagas[key]); } } }}export default function configureStore() { let store = createStore(createReducer(), {}, storeEnhancers); store.asyncReducers = {}; store.asyncSagas = {}; store.sagaMiddleware = sagaMiddleware; return store;}

reducers.js

import { combineReducers } from 'redux';import { routerReducer } from 'react-router-redux';export default function createReducer(asyncReducers) { const reducers = { ...asyncReducers, router: routerReducer }; return combineReducers(reducers);}

我沒有進行刪減,主要是動態改變store中兩個東西,一個是reducer還有一個就是saga。異步請求這塊我用的是redux-saga,雖然官方文檔上露臉的是redux-thunk和redux-promise,可是後起之秀redux-saga作到低耦合,在項目中做爲獨立的一層出現,不與action creator和reducer耦合。還有就是它強大的異步流程控制。

再來看看路由部分是怎麼寫的:

<Provider store={store}> <ConnectedRouter history={history}> <Switch> <Route path='/app' component={App}/> </Switch> </ConnectedRouter></Provider>

這裏的App組件即是咱們要按需加載的組件。我是按照模塊來組織個人代碼的,先來看下App模塊的代碼排版:

app

這張圖中sagas.js是用來處理異步請求的,bundle.js和lazy.js以及公用的bundle.js是用來完成code splitting的。

lazy.js【須要懶加載的文件】

import appSagas from './sagas';import appReducer from './reducer';import view from './views/app';const reducer = { appReducer: appReducer};const sagas = { appSagas: appSagas};export {sagas, reducer, view};

bundle.js【code splitting】

import React from 'react';import Bundle from '../../bundle/views/bundle';import load from 'bundle-loader?lazy&name=[App]!./bundle';import {injectAsyncStore} from '../../Store';export default (props) ={ return ( <Bundle load={(store, cb) ={ load((target) ={ const {reducer, view, sagas} = target; injectAsyncStore(store, reducer, sagas); cb(view); }) }}> {(View) ={ return <View {...props}/> }} </Bundle> );};

公用的bundle.js【對其進行了改造,加入了store】

import React, { Component } from 'react';import PropTypes from 'prop-types';class Bundle extends Component { static propTypes = { load: PropTypes.any, children: PropTypes.any, }; static contextTypes = { store: PropTypes.object }; state = { mod: null, }; componentWillMount () { this._isMounted = true; this.load(this.props); } componentWillUnmount() { this._isMounted = false; } componentWillReceiveProps (nextProps) { if (nextProps.load !== this.props.load) { this.load(nextProps); } } load (props) { this.setState({ mod: null, }); props.load(this.context.store, (mod) ={ if (this._isMounted) { this.setState({ mod: mod['default'] ? mod['default'] : mod, }); } }); } render () { return this.state.mod ? this.props.children(this.state.mod) : <div>組件加載中...</div>; }}export default Bundle;

index.js【對外暴露的組件】

import view from './bundle';export {view};

code splitting就完成了~固然我又進行了更改,下文有講。

這裏要說下公用的bundle.js中的this.context.store,這裏必定要定義contextTypes,否則獲取不到this.context,固然官方沒有提供這個api,也不推薦使用,可是按需加載就得須要它,而且咱們要謹慎使用它便可,由於this.context一旦改變,它關聯的上下文就會從新render,因此加載某個頁面的時候,把它所要使用到的reducer和sagas也都關聯進去,這樣加載這個頁面其餘組件的時候就已經存在相關的reducer和sagas,不須要再改變上下文的store了。還有組件設計很重要,若是不合理會致使頁面不可控。lazy.js中的reducer和sagas是個對象,好比app這個組件中若是嵌套了其餘組件,而這些其餘組件中須要引入reducer和sagas,這時能夠將這些reducer和sagas結合到app模塊的lazy.js中。不加入也能夠,這時須要靈活運用shouldComponentUpdate這個生命週期來控制頁面。

從上面的代碼中,能夠發現每一個模塊的bundle.js中存在相似的代碼,這樣咱們能夠給其剝離出來,這不是必須的,由於剝離出來後,咱們須要約定好每一個模塊lazy.js中必須是export {sagas, reducer, view},固然也能夠約定其餘,一致就行。這樣代碼進過改造後,每一個模塊的bundle.js代碼就能夠分離到公共的bundle.js和模塊中的index.js中,代碼以下。

bundle.js中改動的代碼片:

load (props) {    this.setState({ mod: null, }); props.load((mod) ={ const {reducer, view, sagas} = mod; injectAsyncStore(this.context.store, reducer, sagas); if (this._isMounted) { this.setState({ mod: view['default'] ? view['default'] : view, }); } });}

index.js【以app模塊爲例】

import React from 'react';import Bundle from '../../bundle/views/bundle';import load from 'bundle-loader?lazy&name=[App]!./lazy';const view = (props) ={ return ( <Bundle load={load}> {(View) ={ return <View {...props}/> }} </Bundle> );};export {view};

打包事後主要文件的對比:

code splitting

code splitting

not code splitting

not code splitting

關於組件設計,使用reactjs的時候組件設計必定要足夠的扁平化,也就是平級,這樣不只提升了計算的效率,同時也會不多出現父組件中嵌套子組件,而父組件更新的時候,子組件也跟着更新,實際上子組件並不想更新。固然遇到逼不得已嵌套的狀況的時候,可使用shouldComponentUpdate這個組件存在時期的生命週期來控制子組件是否render。可喜的是react16版本中B組件不嵌套在A組件中,渲染後出如今A組件裏,也能夠掛載到任何一個組件裏,這就是portals

看到這裏,你會發現其實code splitting跟react和react-router沒多大關係,直接的聯繫是redux和webpack,因此這種方式同時也適用於其餘使用redux和webpack這種相似的技術體系。

react技術棧是目前前端最美的技術棧。

相關文章
相關標籤/搜索