利用 React/Redux/React-Router 4/webpack 開發大型 web 專案時如何按需載入
- 如何設計一個大型 web 專案?
- React + webpack 如何按需載入?
- React + React-Router 4 + webpack 如何按需載入?
- React + Redux + React-Router 4 + webpack 如何按需載入?
實錄提要:
- bundle-loader 和 Webpack 內建的 import() 有什麼區別?
- 按需載入能否支援通過請求後臺資料,動態配置頁面的的應用場景?
- 參與過幾個 React 專案,被依賴包搞的暈暈的,不知道該怎麼選擇?
- 什麼包應該放到 devDependencies 裡面?什麼包放到 depedencies 裡面?
- 為什麼是 react-router-redux 而不是傳統的 react-redux,其優勢是什麼?
- 按需載入時,每個單獨的 bundle 都挺大的,為什麼?
- ECMAScript 每年出一個版本,對應的 babel 也有一大堆,應該如何選擇?
- 單頁專案過大,怎麼拆分不同模組頁面到不同 js 來動態載入?
題外話
經驗尚淺,尚不足以教導,若理解有誤,望能指導三分,語言若有偏激,請理解我年輕氣盛。
之所以寫這篇文章,是因為我最近一陣子經歷了一個部門的技術選型->專案實施這些技術->二次技術選型->技術版本升級的一個過程。開發業務應用為主的我們,很少有時間去研究某項技術的原始碼,不加班趕專案進度就已經很慶幸了,大部分時間都花在瞭如何靈活使用市面上的一些技術體系。在這篇文章中,不涉及原始碼範圍,我也沒去研究過原始碼。寫這篇文章的初衷是分享我的想法和程式碼示例,同時也希望看這篇文章的你能夠給予寶貴的意見,讓我得以進步。
web應用讓人驚歎是從Gmail開始的,流暢的桌面版體驗吸引了很多人,從此web專案開始蓬勃發展。隨後,web應用也越來越複雜,為了能讓web應用如同桌面版應用一樣流暢,出現了SPA。這就是今天我想說的,react/redux等等一系列的產品的出現都是為了實現體驗度更佳的SPA。
兩年前,我開發web專案,都只是用javaweb,使用模板引擎,後端渲染出頁面。對於訪問量不是很大、單個頁面複雜度不是很高、專案的迭代週期不頻繁、二次開發的次數很少的系統,這種模式無疑很適用、效能也沒什麼大的影響,一個java程式設計師就可以做到全棧。而事實上,我手頭的web專案並非這麼簡單,隨著週期的迭代,專案越來越臃腫,後端程式碼和前端程式碼摻雜在一起混亂不堪,當時我自認為自己技術不錯,程式碼寫的自己都認識,然而我離職之後發現一個很多人都知道的道理,一個技術真正好的程式設計師,寫出來的程式碼是要能夠讓他人讀的懂,最起碼要讓接替你繼續這個專案的人讀得懂,技術不行那是例外,當時我沒有做到。而後,進入新的公司,讓我能夠有機會去對部門進行技術選型,主要是前端部分。我果斷選擇了前後端分離模式,我不想以前不好的地方繼續發生在以後。人員安排最好是這樣,專職的人做專職的事,全棧人員要能夠補位。設計一個優質的web專案,最重要的是人,而不是技術!
設計web最好是前端設計成SPA、後端設計成微服務,有很多企業使用react、vue、angular這些,結果是多頁應用,增加複雜度,降低頁面切換的流暢度。我真不知道他們是怎麼想的,首先多頁應用是不可取的,如果他們是開發webapp,封裝成apk,那就更不可取了!web專案設計成SPA,很多人會想,隨著程式碼量的增大,首次載入的檔案就會增大,沒錯,這時就需要用到code splitting。這也就是我今天要講的實際專案中如何進行按需載入。我見過有些開發人員將一個js檔案拆分成多個js檔案,而每個頁面都載入這些js檔案,這顯然是不可取的,這樣子的不需要拆分。隨著現在網速的提升,首次載入檔案稍微大點都是可以接受的。首次載入後快取在瀏覽器處,下次載入的時候會更快。引入第三方UI元件,基礎元件要單一,不可以引入antd了再去引用bootstrap,還有用了react這些就不要在專案中出現jQuery,要純粹!
廢話就講到這裡,下面介紹我將一個專案重構三次的過程,這三個過程裡有三種不同的按需載入方式。示例是我將實際專案刪減過後可執行的例子。code splitting和react/react-router是沒有直接關係的。
一、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/react。
先貼下依賴:
因為業務的需求,需要相容到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
not code splitting
這種方式的按需載入就這些,很簡單~
二、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
not code splitting
到這裡,這第二種方式介紹完了,很簡單~
二、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。
先貼下依賴:
這次又一次對引用的技術進行了更新,同時加入了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模組的程式碼排版:
這張圖中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
not code splitting
關於元件設計,使用reactjs的時候元件設計一定要足夠的扁平化,也就是平級,這樣不僅提高了計算的效率,同時也會很少出現父元件中巢狀子元件,而父元件更新的時候,子元件也跟著更新,實際上子元件並不想更新。當然遇到逼不得已巢狀的情況的時候,可以使用shouldComponentUpdate這個元件存在時期的生命週期來控制子元件是否render。可喜的是react16版本中B元件不巢狀在A元件中,渲染後出現在A元件裡,也可以掛載到任何一個元件裡,這就是portals。
看到這裡,你會發現其實code splitting跟react和react-router沒多大關係,直接的聯絡是redux和webpack,所以這種方式同時也適用於其他使用redux和webpack這種類似的技術體系。
react技術棧是目前前端最美的技術棧。
本文首發於GitChat,未經授權不得轉載,轉載需與GitChat聯絡。
閱讀全文: http://gitbook.cn/gitchat/activity/5a0015e2d6f4ca0a7ab25690
一場場看太麻煩?成為 GitChat 會員,暢享 1000+ 場 Chat !點選檢視
相關文章
- webpack v3 結合 react-router v4 做 dynamic import — 按需載入(懶載入)WebReactImport
- React16+Redux+Router4+Koa+Webpack伺服器端渲染(按需載入,熱更新)ReactReduxWeb伺服器
- 「 react+react-router4+redux+webpack3+antd 」 專案實踐ReactReduxWeb
- Webpack按需載入秒開應用Web
- react-router4 實現按需載入React
- webpack4+react16+react-router-dom4從零配置到優化,實現路由按需載入(下)WebReact優化路由
- webpack4+react16+react-router-dom4從零配置到優化,實現路由按需載入(上)WebReact優化路由
- react暴露後,webpack4.19.1實現按需載入antdReactWeb
- [譯] 如何利用 Webpack4 提升你的 React.js 開發效率WebReactJS
- 腦闊疼的webpack按需載入Web
- 做一個react+redux+webpack專案後的感想ReactReduxWeb
- 使用webpack4 配置按需載入,減小lodash打包體積Web
- react-native大型專案開發實踐React
- 使用 TypeScript + React + Redux 進行專案開發(入門篇,附原始碼)TypeScriptReactRedux原始碼
- react+react-router+react-redux全家桶小專案開發過程分享ReactRedux
- 基於react16 webpack3 搭建前端spa基礎框架 react-router的4種非同步載入方式ReactWeb前端框架非同步
- Webpack 4 構建大型專案實踐 / 微前端Web前端
- Webpack 4 構建大型專案實踐 / 優化Web優化
- 如何在非 React 專案中使用 ReduxReactRedux
- Webpack的Code Splitting實現按需載入Web
- 基於 Webpack 4 和 React hooks 搭建專案WebReactHook
- 深入淺出的webpack4構建工具---webpack+vue+router 按需載入頁面(十五)WebVue
- VUE如何實現按需載入?Vue
- 基於webpack4搭建的react專案框架WebReact框架
- react-router4的按需載入實踐(基於create-react-app和Bundle元件)ReactAPP元件
- react-router4基於react-router-config的路由拆分與按需載入React路由
- web前端基於vue的大型專案分模組開發Web前端Vue
- 從零開始使用webpack 4, Babel 7建立一個React專案WebBabelReact
- 如何利用React.js開發出強大Web應用ReactJSWeb
- 如何優雅地在React專案中使用ReduxReactRedux
- webpack loader—自己寫一個按需載入外掛Web
- 在Webpack中使用Code Splitting實現按需載入Web
- webpack4下import()模組按需載入,打包按需切割模組,減少包體積,加快首頁請求速度WebImport
- Webpack 4.x搭建react開發環境WebReact開發環境
- React 入門-redux 和 react-reduxReactRedux
- vue按需載入Vue
- 如何在大型專案中使用Git子模組開發Git
- 使用bundle-loader非同步載入react-router非同步React