在react-router4中進行代碼拆分(基於webpack)
本文主要參考簡書上的文章:在react-router4中進行代碼拆分(基於webpack),做了一些細節的補充,方便大家理解。
前言
隨著前端項目的不斷擴大,一個原本簡單的網頁應用所引用的js文件可能變得越來越龐大。尤其在近期流行的單頁面應用中,越來越依賴一些打包工具(例如webpack),通過這些打包工具將需要處理、相互依賴的模塊直接打包成一個單獨的bundle文件,在頁面第一次載入時,就會將所有的js全部載入。但是,往往有許多的場景,我們並不需要在一次性將單頁應用的全部依賴都載下來。例如:我們現在有一個帶有許可權的"訂單後台管理"單頁應用,普通管理員只能進入"訂單管理"部分,而超級用戶則可以進行"系統管理";或者,我們有一個龐大的單頁應用,用戶在第一次打開頁面時,需要等待較長時間載入無關資源。這些時候,我們就可以考慮進行一定的代碼拆分(code splitting)。
實現方式
簡單的按需載入
代碼拆分的核心目的,就是實現資源的按需載入。考慮這麼一個場景,在我們的網站中,右下角有一個類似聊天框的組件,當我們點擊圓形按鈕時,頁面展示聊天組件。
btn.addEventListener(click, function(e) { // 在這裡載入chat組件相關資源 chat.js});
從這個例子中我們可以看出,通過將載入chat.js的操作綁定在btn點擊事件上,可以實現點擊聊天按鈕後聊天組件的按需載入。而要動態載入js資源的方式也非常簡單(方式類似熟悉的jsonp)。通過動態在頁面中添加<script>標籤,並將src屬性指向該資源即可。
btn.addEventListener(click, function(e) { // 在這裡載入chat組件相關資源 chat.js var ele = document.createElement(script); ele.setAttribute(src,/static/chat.js); document.getElementsByTagName(head)[0].appendChild(ele);});
代碼拆分就是為了要實現按需載入所做的工作。想像一下,我們使用打包工具,將所有的js全部打包到了bundle.js這個文件,這種情況下是沒有辦法做到上面所述的按需載入的,因此,我們需要講按需載入的代碼在打包的過程中拆分出來,這就是代碼拆分。那麼,對於這些資源,我們需要手動拆分么?當然不是,還是要藉助打包工具。下面就來介紹webpack中的代碼拆分。
代碼拆分
這裡回到應用場景,介紹如何在webpack中進行代碼拆分。在webpack有多種方式來實現構建是的代碼拆分。
import()
這裡的import不同於模塊引入時的import,可以理解為一個動態載入的模塊的函數(function-like),傳入其中的參數就是相應的模塊。例如對於原有的模塊引入import react from react可以寫為import(react)。但是需要注意的是,import()會返回一個Promise對象(一定記得,後面的講解和這裡息息相關)。因此,可以通過如下方式使用:
btn.addEventListener(click, e => { // 在這裡載入chat組件相關資源 chat.js import(/components/chart).then(mod => { someOperate(mod); //mod是module的簡寫,表示載入成功後的非同步組件 });});
可以看到,使用方式非常簡單,和平時我們使用的Promise並沒有區別。當然,也可以再加入一些異常處理:
btn.addEventListener(click, e => { import(/components/chart).then(mod => { someOperate(mod); }).catch(err => { console.log(failed); });});
當然,由於import()會返回一個Promise對象,因此要注意一些兼容性問題。解決這個問題也不困難,可以使用一些Promise的polyfill來實現兼容(babel-polyfill)。可以看到,動態import()的方式不論在語意上還是語法使用上都是比較清晰簡潔的。
require.ensure()
在webpack 2的官網上寫了這麼一句話:
require.ensure() is specific to webpack and superseded by import().
所以,在webpack 2裡面應該是不建議使用require.ensure()這個方法的。但是目前該方法仍然有效,所以可以簡單介紹一下。包括在webpack 1中也是可以使用。下面是require.ensure()的語法:
require.ensure(dependencies: String[], callback: function(require), errorCallback: function(error), chunkName: String)
require.ensure()接受三個參數:
第一個參數dependencies是一個數組,代表了當前require進來的模塊的一些依賴;
第二個參數callback就是一個回調函數。其中需要注意的是,這個回調函數有一個參數require,通過這個require就可以在回調函數內動態引入其他模塊。值得注意的是,雖然這個require是回調函數的參數,理論上可以換其他名稱,但是實際上是不能換的,否則webpack就無法靜態分析的時候處理它;
第三個參數errorCallback比較好理解,就是處理error的回調;
第四個參數chunkName則是指定打包的chunk名稱。
因此,require.ensure()具體的用法如下:
btn.addEventListener(click, e => { require.ensure([], require => { let chat = require(/components/chart); someOperate(chat); }, error => { console.log(failed); }, mychat);});
對比上面的import,這裡的區別就是一個用回調,一個用promise而已。
Bundle Loader
除了使用上述兩種方法,還可以使用webpack的一些組件。例如使用Bundle Loader:
npm i --save bundle-loader
使用require("bundle-loader!./file.js")來進行相應chunk的載入。該方法會返回一個function,這個function接受一個回調函數作為參數。
let chatChunk = require("bundle-loader?lazy!./components/chat");chatChunk(function(file) { someOperate(file);});
和其他loader類似,Bundle Loader也需要在webpack的配置文件中進行相應配置。Bundle-Loader的代碼也很簡短,如果閱讀一下可以發現,其實際上也是使用require.ensure()來實現的,通過給Bundle-Loader返回的函數中傳入相應的模塊處理回調函數即可在require.ensure()的中處理,代碼最後也列出了相應的輸出格式:
/*Output format: var cbs = [], data; module.exports = function(cb) { if(cbs) cbs.push(cb); else cb(data); } require.ensure([], function(require) { data = require("xxx"); var callbacks = cbs; cbs = null; for(var i = 0, l = callbacks.length; i < l; i++) { callbacks[i](data); } });*/
react-router v4 中的代碼拆分
最後,回到實際的工作中,基於webpack,在react-router4中實現代碼拆分。react-router 4相較於react-router 3有了較大的變動。其中,在代碼拆分方面,react-router 4的使用方式也與react-router 3有了較大的差別。
在react-router 3中,可以使用Route組件中getComponent這個API來進行代碼拆分。getComponent是非同步的,只有在路由匹配時才會調用。但是,在react-router 4中並沒有找到這個API,那麼如何來進行代碼拆分呢?
這裡就可以使用之前說到的兩種方式來處理:import()或require.ensure()。
我們首先需要一個非同步載入的包裝組件(高階組件)Bundle。Bundle的主要功能就是接收一個組件非同步載入的方法,並返回相應的react組件:
如果使用require.ensure的語法來做這個高階組件,代碼是這樣的:
export default class Bundle extends Component { constructor(props) { super(props); this.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) => { // require.ensure語法使用的是回調 this.setState({ mod: mod.default ? mod.default : mod }); }); } render() { return this.state.mod ? this.props.children(this.state.mod) : null; }}
如果使用import語法,代碼是這樣的:
export default class Bundle extends Component { constructor(props) { super(props); this.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 }); //注意這裡,使用Promise對象; mod.default導出默認 props.load().then((mod) => { this.setState({ mod: mod.default ? mod.default : mod }); }); } render() { return this.state.mod ? this.props.children(this.state.mod) : null; }}
仔細讀一下這個高階組件,你會發現它的作用很簡單,拿到非同步載入的請求,得到組件後,將組件動態的傳給被包裝的子組件。
我們接著來做這個被包裝子組件的編碼:
require.ensure語法中,這個組件這麼寫:
const Chat = (props) => ( <Bundle load={(cb) => { require.ensure([], require => { cb(require(./component/chat)); }); }}> {(Chat) => <Chat {...props}/>} </Bundle>);
import語法中,這個組件這麼寫:
const Chat = (props) => ( <Bundle load={() => import(./component/chat)}> {(Chat) => <Chat {...props}/>} </Bundle>);
這樣,Chat就是一個標準的非同步組件,然後我們就可以在路由中直接這麼寫了:
<BrowserRouter> <div> <Route path=/ component={Chat}/> </div></BrowserRouter>
最近,webpack推薦了react-loadable這個非同步組件載入模塊,關於這個模塊的使用,我在下一篇文章中整理一下。
推薦閱讀:
TAG:reactrouter | Webpack | React |