隨着互聯網的發展,一個網頁須要承載的功能愈來愈多。 對於採用單頁應用做爲前端架構的網站來講,會面臨着一個網頁須要加載的代碼量很大的問題,由於許多功能都集中的作到了一個 HTML 裏。 這會致使網頁加載緩慢、交互卡頓,用戶體驗將很是糟糕。html
致使這個問題的根本緣由在於一次性的加載全部功能對應的代碼,但其實用戶每一階段只可能使用其中一部分功能。 因此解決以上問題的方法就是用戶當前須要用什麼功能就只加載這個功能對應的代碼,也就是所謂的按需加載。前端
在給單頁應用作按需加載優化時,通常採用如下原則:react
被分割出去的代碼的加載須要必定的時機去觸發,也就是當用戶操做到了或者即將操做到對應的功能時再去加載對應的代碼。 被分割出去的代碼的加載時機須要開發者本身去根據網頁的需求去衡量和肯定。webpack
因爲被分割出去進行按需加載的代碼在加載的過程當中也須要耗時,你能夠預言用戶接下來可能會進行的操做,並提早加載好對應的代碼,從而讓用戶感知不到網絡加載時間。web
Webpack 內置了強大的分割代碼的功能去實現按需加載,實現起來很是簡單。瀏覽器
舉個例子,如今須要作這樣一個進行了按需加載優化的網頁:babel
其中 main.js 文件內容以下:網絡
window.document.getElementById('btn').addEventListener('click', function () { // 當按鈕被點擊後纔去加載 show.js 文件,文件加載成功後執行文件導出的函數 import(/* webpackChunkName: "show" */ './show').then((show) => { show('Webpack'); }) }); show.js 文件內容以下: module.exports = function (content) { window.alert('Hello ' + content); };
代碼中最關鍵的一句是 import(/* webpackChunkName: "show" */ './show') ,Webpack 內置了對 import(*) 語句的支持,當 Webpack 遇到了相似的語句時會這樣處理:react-router
import
返回一個 Promise,當文件加載成功時能夠在 Promise 的 then 方法中獲取到 show.js 導出的內容。import()
分割代碼後,你的瀏覽器而且要支持 Promise API 才能讓代碼正常運行, 由於 import()
返回一個 Promise,它依賴 Promise。對於不原生支持 Promise 的瀏覽器,你能夠注入 Promise polyfill。/* webpackChunkName: "show" */ 的含義是爲動態生成的 Chunk 賦予一個名稱,以方便咱們追蹤和調試代碼。 若是不指定動態生成的 Chunk 的名稱,默認名稱將會是 [id].js。 /* webpackChunkName: "show" */ 是在 Webpack3 中引入的新特性,在 Webpack3 以前是沒法爲動態生成的 Chunk 賦予名稱的。架構
爲了正確的輸出在 /* webpackChunkName: "show" */ 中配置的 ChunkName,還須要配置下 Webpack,配置以下:
module.exports = { // JS 執行入口文件 entry: { main: './main.js', }, output: { // 爲從 entry 中配置生成的 Chunk 配置輸出文件的名稱 filename: '[name].js', // 爲動態加載的 Chunk 配置輸出文件的名稱 chunkFilename: '[name].js', } };
其中最關鍵的一行是 chunkFilename: '[name].js ',
,它專門指定動態生成的 Chunk 在輸出時的文件名稱。 若是沒有這行,分割出的代碼的文件名稱將會是 [id].js 。 chunkFilename 具體含義見2-2 配置-Output。本實例提供項目完整代碼
在實戰中,不可能會有上面那麼簡單的場景,接下來舉一個實戰中的例子:對採用了 ReactRouter 的應用進行按需加載優化。 這個例子由一個單頁應用構成,這個單頁應用由兩個子頁面構成,經過 ReactRouter 在兩個子頁面之間切換和管理路由。
這個單頁應用的入口文件 main.js 以下:
import React, {PureComponent, createElement} from 'react'; import {render} from 'react-dom'; import {HashRouter, Route, Link} from 'react-router-dom'; import PageHome from './pages/home'; /** * 異步加載組件 * @param load 組件加載函數,load 函數會返回一個 Promise,在文件加載完成時 resolve * @returns {AsyncComponent} 返回一個高階組件用於封裝須要異步加載的組件 */ function getAsyncComponent(load) { return class AsyncComponent extends PureComponent { componentDidMount() { // 在高階組件 DidMount 時纔去執行網絡加載步驟 load().then(({default: component}) => { // 代碼加載成功,獲取到了代碼導出的值,調用 setState 通知高階組件從新渲染子組件 this.setState({ component, }) }); } render() { const {component} = this.state || {}; // component 是 React.Component 類型,須要經過 React.createElement 生產一個組件實例 return component ? createElement(component) : null; } } } // 根組件 function App() { return ( <HashRouter> <div> <nav> <Link to='/'>Home</Link> | <Link to='/about'>About</Link> | <Link to='/login'>Login</Link> </nav> <hr/> <Route exact path='/' component={PageHome}/> <Route path='/about' component={getAsyncComponent( // 異步加載函數,異步地加載 PageAbout 組件 () => import(/* webpackChunkName: 'page-about' */'./pages/about') )} /> <Route path='/login' component={getAsyncComponent( // 異步加載函數,異步地加載 PageAbout 組件 () => import(/* webpackChunkName: 'page-login' */'./pages/login') )} /> </div> </HashRouter> ) } // 渲染根組件 render(<App/>, window.document.getElementById('app'));
以上代碼中最關鍵的部分是 getAsyncComponent 函數,它的做用是配合 ReactRouter 去按需加載組件,具體含義請看代碼中的註釋。
因爲以上源碼須要經過 Babel 去轉換後才能在瀏覽器中正常運行,須要在 Webpack 中配置好對應的 babel-loader,源碼先交給 babel-loader 處理後再交給 Webpack 去處理其中的 import(*)
語句。 但這樣作後你很快會發現一個問題:Babel 報出錯誤說不認識 import(*)
語法。 致使這個問題的緣由是 import(*)
語法尚未被加入到在 3-1使用ES6語言中提到的 ECMAScript 標準中去, 爲此咱們須要安裝一個 Babel 插件 babel-plugin-syntax-dynamic-import ,而且將其加入到 . babelrc
中去:
{ "presets": [ "env", "react" ], "plugins": [ "syntax-dynamic-import" ] }
執行 Webpack 構建後,你會發現輸出了三個文件:
main.js:執行入口所在的代碼塊,同時還包括 PageHome 所需的代碼,由於用戶首次打開網頁時就須要看到 PageHome 的內容,因此不對其進行按需加載,以下降用戶能感知到的加載時間;
page-about.js:當用戶訪問 /about 時纔會加載的代碼塊;
page-login.js:當用戶訪問 /login 時纔會加載的代碼塊。
同時你還會發現 page-about.js 和 page-login.js 這兩個文件在首頁是不會加載的,而是會當你切換到了對應的子頁面後文件纔會開始加載。本實例提供項目完整代碼