- 如何設(shè)計(jì)一個(gè)大型 web 項(xiàng)目?
- React + webpack 如何按需加載?
- React + React-Router 4 + webpack 如何按需加載?
- React + Redux + React-Router 4 + webpack 如何按需加載?
實(shí)錄提要:
- bundle-loader 和 Webpack 內(nèi)置的 import() 有什么區(qū)別?
- 按需加載能否支持通過(guò)請(qǐng)求后臺(tái)數(shù)據(jù),動(dòng)態(tài)配置頁(yè)面的的應(yīng)用場(chǎng)景?
- 參與過(guò)幾個(gè) React 項(xiàng)目,被依賴(lài)包搞的暈暈的,不知道該怎么選擇?
- 什么包應(yīng)該放到 devDependencies 里面?什么包放到 depedencies 里面?
- 為什么是 react-router-redux 而不是傳統(tǒng)的 react-redux,其優(yōu)勢(shì)是什么?
- 按需加載時(shí),每個(gè)單獨(dú)的 bundle 都挺大的,為什么?
- ECMAScript 每年出一個(gè)版本,對(duì)應(yīng)的 babel 也有一大堆,應(yīng)該如何選擇?
- 單頁(yè)項(xiàng)目過(guò)大,怎么拆分不同模塊頁(yè)面到不同 js 來(lái)動(dòng)態(tài)加載?
題外話
經(jīng)驗(yàn)尚淺,尚不足以教導(dǎo),若理解有誤,望能指導(dǎo)三分,語(yǔ)言若有偏激,請(qǐng)理解我年輕氣盛。
之所以寫(xiě)這篇文章,是因?yàn)槲易罱魂囎咏?jīng)歷了一個(gè)部門(mén)的技術(shù)選型->項(xiàng)目實(shí)施這些技術(shù)->二次技術(shù)選型->技術(shù)版本升級(jí)的一個(gè)過(guò)程。開(kāi)發(fā)業(yè)務(wù)應(yīng)用為主的我們,很少有時(shí)間去研究某項(xiàng)技術(shù)的源碼,不加班趕項(xiàng)目進(jìn)度就已經(jīng)很慶幸了,大部分時(shí)間都花在了如何靈活使用市面上的一些技術(shù)體系。在這篇文章中,不涉及源碼范圍,我也沒(méi)去研究過(guò)源碼。寫(xiě)這篇文章的初衷是分享我的想法和代碼示例,同時(shí)也希望看這篇文章的你能夠給予寶貴的意見(jiàn),讓我得以進(jìn)步。
web應(yīng)用讓人驚嘆是從Gmail開(kāi)始的,流暢的桌面版體驗(yàn)吸引了很多人,從此web項(xiàng)目開(kāi)始蓬勃發(fā)展。隨后,web應(yīng)用也越來(lái)越復(fù)雜,為了能讓web應(yīng)用如同桌面版應(yīng)用一樣流暢,出現(xiàn)了SPA。這就是今天我想說(shuō)的,react/redux等等一系列的產(chǎn)品的出現(xiàn)都是為了實(shí)現(xiàn)體驗(yàn)度更佳的SPA。
兩年前,我開(kāi)發(fā)web項(xiàng)目,都只是用javaweb,使用模板引擎,后端渲染出頁(yè)面。對(duì)于訪問(wèn)量不是很大、單個(gè)頁(yè)面復(fù)雜度不是很高、項(xiàng)目的迭代周期不頻繁、二次開(kāi)發(fā)的次數(shù)很少的系統(tǒng),這種模式無(wú)疑很適用、性能也沒(méi)什么大的影響,一個(gè)java程序員就可以做到全棧。而事實(shí)上,我手頭的web項(xiàng)目并非這么簡(jiǎn)單,隨著周期的迭代,項(xiàng)目越來(lái)越臃腫,后端代碼和前端代碼摻雜在一起混亂不堪,當(dāng)時(shí)我自認(rèn)為自己技術(shù)不錯(cuò),代碼寫(xiě)的自己都認(rèn)識(shí),然而我離職之后發(fā)現(xiàn)一個(gè)很多人都知道的道理,一個(gè)技術(shù)真正好的程序員,寫(xiě)出來(lái)的代碼是要能夠讓他人讀的懂,最起碼要讓接替你繼續(xù)這個(gè)項(xiàng)目的人讀得懂,技術(shù)不行那是例外,當(dāng)時(shí)我沒(méi)有做到。而后,進(jìn)入新的公司,讓我能夠有機(jī)會(huì)去對(duì)部門(mén)進(jìn)行技術(shù)選型,主要是前端部分。我果斷選擇了前后端分離模式,我不想以前不好的地方繼續(xù)發(fā)生在以后。人員安排最好是這樣,專(zhuān)職的人做專(zhuān)職的事,全棧人員要能夠補(bǔ)位。設(shè)計(jì)一個(gè)優(yōu)質(zhì)的web項(xiàng)目,最重要的是人,而不是技術(shù)!
設(shè)計(jì)web最好是前端設(shè)計(jì)成SPA、后端設(shè)計(jì)成微服務(wù),有很多企業(yè)使用react、vue、angular這些,結(jié)果是多頁(yè)應(yīng)用,增加復(fù)雜度,降低頁(yè)面切換的流暢度。我真不知道他們是怎么想的,首先多頁(yè)應(yīng)用是不可取的,如果他們是開(kāi)發(fā)webapp,封裝成apk,那就更不可取了!web項(xiàng)目設(shè)計(jì)成SPA,很多人會(huì)想,隨著代碼量的增大,首次加載的文件就會(huì)增大,沒(méi)錯(cuò),這時(shí)就需要用到code splitting。這也就是我今天要講的實(shí)際項(xiàng)目中如何進(jìn)行按需加載。我見(jiàn)過(guò)有些開(kāi)發(fā)人員將一個(gè)js文件拆分成多個(gè)js文件,而每個(gè)頁(yè)面都加載這些js文件,這顯然是不可取的,這樣子的不需要拆分。隨著現(xiàn)在網(wǎng)速的提升,首次加載文件稍微大點(diǎn)都是可以接受的。首次加載后緩存在瀏覽器處,下次加載的時(shí)候會(huì)更快。引入第三方UI組件,基礎(chǔ)組件要單一,不可以引入antd了再去引用bootstrap,還有用了react這些就不要在項(xiàng)目中出現(xiàn)jQuery,要純粹!
廢話就講到這里,下面介紹我將一個(gè)項(xiàng)目重構(gòu)三次的過(guò)程,這三個(gè)過(guò)程里有三種不同的按需加載方式。示例是我將實(shí)際項(xiàng)目刪減過(guò)后可運(yùn)行的例子。code splitting和react/react-router是沒(méi)有直接關(guān)系的。
一、react(v.0.14.8) / react-router(v.1.0.3) / webpack(v.1.13.3)
兼容IE8+及現(xiàn)代瀏覽器,示例代碼地址->https://github.com/love-fay/fay-webpack-redux-code-splitting/tree/master/react。
先貼下依賴(lài):
因?yàn)闃I(yè)務(wù)的需求,需要兼容到IE8,不得不被動(dòng)地選擇低版本庫(kù)。處于對(duì)react的首次使用,并沒(méi)有加入redux相關(guān)技術(shù)。
在react-router 1.x版本中Route組件上擁有g(shù)etComponent、onEnter參數(shù)(4.x之后被移除),getComponent是異步的,所以我們可以在這個(gè)參數(shù)里進(jìn)行按需加載,getComponent這個(gè)函數(shù)有兩個(gè)參數(shù)nextState、callback,根據(jù)nextState.pathname可以獲取到路由地址,然后再利用webpack的require.ensure異步加載所屬組件的js文件,最后通過(guò)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'); }};
打包過(guò)后主要文件的對(duì)比:
code splitting
not code splitting
這種方式的按需加載就這些,很簡(jiǎn)單~
二、react(v.15.6.1) / react-router(v.4.2.2)b / webpack(v.3.5.6)
兼容IE9+及現(xiàn)代瀏覽器,示例代碼地址->https://github.com/love-fay/fay-webpack-redux-code-splitting/tree/master/react-rr4。
先貼下依賴(lài):
這次決定拋棄IE8,甚至都不想兼容IE。很多人口口聲聲說(shuō)用戶體驗(yàn)、用戶需求,卻一味地去支持IE8,甚至還有支持IE6的!其實(shí)用戶體驗(yàn)和需求不是用戶單方面的要求,還有就是開(kāi)發(fā)方需要去改變用戶習(xí)慣、引導(dǎo)用戶對(duì)未來(lái)的需求,在這基礎(chǔ)上不斷地提高用戶體驗(yàn)。(你不告知你的用戶有個(gè)瀏覽器叫做谷歌瀏覽器,他這輩子就會(huì)覺(jué)得IE就是瀏覽器,瀏覽器就是IE。)
這次主要是將react/react-router/webpack進(jìn)行了升級(jí),并升級(jí)到最新(當(dāng)時(shí)的最新)。
按需加載其實(shí)跟react-router沒(méi)多大關(guān)系,只不過(guò)需要借助它更好的完成按需加載這項(xiàng)任務(wù)。react-router升級(jí)到4后,便沒(méi)有了getComponent這個(gè)參數(shù),所以我們得換種方式,react-router4官方示例也提供了code splitting的方法,利用webpack結(jié)合bundle-loader,它是在require.ensure基礎(chǔ)上封裝的,更友好的實(shí)現(xiàn)異步加載過(guò)程。
bundle-loader可以在webpack文件中進(jìn)行配置,這里我就不介紹了,webpack官方文檔都有寫(xiě)。我這里是寫(xiě)在代碼里的。我簡(jiǎn)單說(shuō)下,基本跟react-router4官方文檔說(shuō)的差不多。
首先先寫(xiě)一個(gè)bundle.js這個(gè)組件,代碼如下:
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;
然后在用到需要按需加載的組件的組件中,引入的時(shí)候,在文件路徑前面使用bundle-loader?lazy&name=[App]!,如下:
import loadApp from 'bundle-loader?lazy&name=[App]!../../app/components/App';
然后比如我這里使用 <Route path="/app" component={App}/>
加載這個(gè)App組件,我們需要用到剛才自己寫(xiě)的bundle組件:
import Bundle from '../bundle/components/Bundle';const App = (props) =( <Bundle load={loadApp}> {(App) ={ return <App {...props}/>; }} </Bundle>);
打包過(guò)后主要文件的對(duì)比:
code splitting
not code splitting
到這里,這第二種方式介紹完了,很簡(jiǎn)單~
二、react(v.16.1.1) / redux(v.3.7.2) / react-router(v.4.2.2) / webpack(v.3.8.1)
兼容IE9+及現(xiàn)代瀏覽器,示例代碼地址->https://github.com/love-fay/fay-webpack-redux-code-splitting/tree/master/react-rr4-redux。
先貼下依賴(lài):
這次又一次對(duì)引用的技術(shù)進(jìn)行了更新,同時(shí)加入了redux,項(xiàng)目復(fù)雜度的提高,組件之間的交流變得復(fù)雜,此時(shí)就需要用到redux。有些開(kāi)發(fā)人員會(huì)覺(jué)得好煩,不斷地升級(jí),不斷地改造,很費(fèi)時(shí)費(fèi)力,什么時(shí)候才能穩(wěn)定,其實(shí)不然,項(xiàng)目的穩(wěn)定不代表技術(shù)的不變,穩(wěn)定是相對(duì)的。如果想要一勞永逸的話,就不要讓公司給你漲工資了,公司也想一勞永逸~以后人工智能一旦鋪開(kāi)到企業(yè)級(jí)開(kāi)發(fā)中,將會(huì)導(dǎo)致大量在安逸中度過(guò)的程序員失業(yè)!學(xué)習(xí)是無(wú)止境的,學(xué)習(xí)也是人一輩子免費(fèi)的技能,曾經(jīng)后端Java一家獨(dú)大的時(shí)候,spring3穩(wěn)定的時(shí)候,很多后端程序員就開(kāi)始陷入了一勞永逸的幻覺(jué)當(dāng)中,導(dǎo)致他們中的很多人一度抱怨前端是在瞎折騰~這就好比有自行車(chē)為什么要造汽車(chē)的理論是一樣的~我是以Java程序員入行的,很清楚Java寫(xiě)后端的時(shí)候,輪子很多,很多程序員就是使用CV大法,甚至很多項(xiàng)目經(jīng)理啊什么的就說(shuō)程序員是搬運(yùn)工,代碼不就是增刪改查么~
使用了redux后,全局只有一個(gè)Store,而這個(gè)Store在頁(yè)面打開(kāi)的時(shí)候就已經(jīng)聲明了,于是讓我很糾結(jié)如何按需加載。后來(lái)我了解到redux這個(gè)東西的存在,內(nèi)部運(yùn)用了react中的context,同時(shí)這個(gè)context算是隱藏著的秘密。利用它我可以改變?nèi)值腟tore。我這里使用了react-redux,在頂級(jí)組件處加入。
import {Provider} from 'react-redux';<Provider store={store}> ......</Provider>
然后在需要引入store信息的子組件處利用它提供的connect方法將store派發(fā)下去,這里派發(fā)是根據(jù)上下文context。項(xiàng)目中少不了用到路由,這時(shí)候,我使用了react-router-redux(一定要5.x版本npm i react-router-redux@next
),在總的reducer中加入routerReducer,然后在寫(xiě)路由組件的部分的頂級(jí)處使用。
import createBrowserHistory from 'history/createBrowserHistory';import { ConnectedRouter} from 'react-router-redux';const history = createBrowserHistory();<ConnectedRouter history={history}> ......</ConnectedRouter>
讓我們?cè)倩氐缴弦粋€(gè)代碼片,其中的store來(lái)源如下:
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);}
我沒(méi)有進(jìn)行刪減,主要是動(dòng)態(tài)改變store中兩個(gè)東西,一個(gè)是reducer還有一個(gè)就是saga。異步請(qǐng)求這塊我用的是redux-saga,雖然官方文檔上露臉的是redux-thunk和redux-promise,但是后起之秀redux-saga做到低耦合,在項(xiàng)目中作為獨(dú)立的一層出現(xiàn),不與action creator和reducer耦合。還有就是它強(qiáng)大的異步流程控制。
再來(lái)看看路由部分是怎么寫(xiě)的:
<Provider store={store}> <ConnectedRouter history={history}> <Switch> <Route path='/app' component={App}/> </Switch> </ConnectedRouter></Provider>
這里的App組件便是我們要按需加載的組件。我是按照模塊來(lái)組織我的代碼的,先來(lái)看下App模塊的代碼排版:
這張圖中sagas.js是用來(lái)處理異步請(qǐng)求的,bundle.js和lazy.js以及公用的bundle.js是用來(lái)完成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【對(duì)其進(jìn)行了改造,加入了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【對(duì)外暴露的組件】
import view from './bundle';export {view};
code splitting就完成了~當(dāng)然我又進(jìn)行了更改,下文有講。
這里要說(shuō)下公用的bundle.js中的this.context.store,這里一定要定義contextTypes,不然獲取不到this.context,當(dāng)然官方?jīng)]有提供這個(gè)api,也不推薦使用,但是按需加載就得需要它,并且我們要謹(jǐn)慎使用它即可,因?yàn)閠his.context一旦改變,它關(guān)聯(lián)的上下文就會(huì)重新render,所以加載某個(gè)頁(yè)面的時(shí)候,把它所要使用到的reducer和sagas也都關(guān)聯(lián)進(jìn)去,這樣加載這個(gè)頁(yè)面其他組件的時(shí)候就已經(jīng)存在相關(guān)的reducer和sagas,不需要再改變上下文的store了。還有組件設(shè)計(jì)很重要,如果不合理會(huì)導(dǎo)致頁(yè)面不可控。lazy.js中的reducer和sagas是個(gè)對(duì)象,比如app這個(gè)組件中如果嵌套了其他組件,而這些其他組件中需要引入reducer和sagas,這時(shí)可以將這些reducer和sagas結(jié)合到app模塊的lazy.js中。不加入也可以,這時(shí)需要靈活運(yùn)用shouldComponentUpdate這個(gè)生命周期來(lái)控制頁(yè)面。
從上面的代碼中,可以發(fā)現(xiàn)每個(gè)模塊的bundle.js中存在類(lèi)似的代碼,這樣我們可以給其剝離出來(lái),這不是必須的,因?yàn)閯冸x出來(lái)后,我們需要約定好每個(gè)模塊lazy.js中必須是export {sagas, reducer, view},當(dāng)然也可以約定其他,一致就行。這樣代碼進(jìn)過(guò)改造后,每個(gè)模塊的bundle.js代碼就可以分離到公共的bundle.js和模塊中的index.js中,代碼如下。
bundle.js中改動(dòng)的代碼片:
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};
打包過(guò)后主要文件的對(duì)比:
code splitting
not code splitting
關(guān)于組件設(shè)計(jì),使用reactjs的時(shí)候組件設(shè)計(jì)一定要足夠的扁平化,也就是平級(jí),這樣不僅提高了計(jì)算的效率,同時(shí)也會(huì)很少出現(xiàn)父組件中嵌套子組件,而父組件更新的時(shí)候,子組件也跟著更新,實(shí)際上子組件并不想更新。當(dāng)然遇到逼不得已嵌套的情況的時(shí)候,可以使用shouldComponentUpdate這個(gè)組件存在時(shí)期的生命周期來(lái)控制子組件是否render。可喜的是react16版本中B組件不嵌套在A組件中,渲染后出現(xiàn)在A組件里,也可以掛載到任何一個(gè)組件里,這就是portals。
看到這里,你會(huì)發(fā)現(xiàn)其實(shí)code splitting跟react和react-router沒(méi)多大關(guān)系,直接的聯(lián)系是redux和webpack,所以這種方式同時(shí)也適用于其他使用redux和webpack這種類(lèi)似的技術(shù)體系。
react技術(shù)棧是目前前端最美的技術(shù)棧。