隨着互聯網的發展,一個網頁需要承載的功能越來越多。 對於采用單頁應用作為前端架構的網站來說,會面臨着一個網頁需要加載的代碼量很大的問題,因為許多功能都集中的做到了一個 HTML 里。 這會導致網頁加載緩慢、交互卡頓,用戶體驗將非常糟糕。
導致這個問題的根本原因在於一次性的加載所有功能對應的代碼,但其實用戶每一階段只可能使用其中一部分功能。 所以解決以上問題的方法就是用戶當前需要用什么功能就只加載這個功能對應的代碼,也就是所謂的按需加載。
如何使用按需加載
在給單頁應用做按需加載優化時,一般采用以下原則:
- 把整個網站划分成一個個小功能,再按照每個功能的相關程度把它們分成幾類。
- 把每一類合並為一個 Chunk,按需加載對應的 Chunk。
- 對於用戶首次打開你的網站時需要看到的畫面所對應的功能,不要對它們做按需加載,而是放到執行入口所在的 Chunk 中,以降低用戶能感知的網頁加載時間。
- 對於個別依賴大量代碼的功能點,例如依賴 Chart.js 去畫圖表、依賴 flv.js 去播放視頻的功能點,可再對其進行按需加載。
被分割出去的代碼的加載需要一定的時機去觸發,也就是當用戶操作到了或者即將操作到對應的功能時再去加載對應的代碼。 被分割出去的代碼的加載時機需要開發者自己去根據網頁的需求去衡量和確定。
由於被分割出去進行按需加載的代碼在加載的過程中也需要耗時,你可以預言用戶接下來可能會進行的操作,並提前加載好對應的代碼,從而讓用戶感知不到網絡加載時間。
用 Webpack 實現按需加載
Webpack 內置了強大的分割代碼的功能去實現按需加載,實現起來非常簡單。
舉個例子,現在需要做這樣一個進行了按需加載優化的網頁:
- 網頁首次加載時只加載 main.js 文件,網頁會展示一個按鈕, main.js 文件中只包含監聽按鈕事件和加載按需加載的代碼。
- 當按鈕被點擊時才去加載被分割出去的 show.js 文件,加載成功后再執行 show.js 里的函數。
其中 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 遇到了類似的語句時會這樣處理:
- 以 ./show.js 為入口新生成一個 Chunk;
- 當代碼執行到 import 所在語句時才會去加載由 Chunk 對應生成的文件。
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 的應用進行按需加載優化。 這個例子由一個單頁應用構成,這個單頁應用由兩個子頁面構成,通過 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 這兩個文件在首頁是不會加載的,而是會當你切換到了對應的子頁面后文件才會開始加載。本實例提供項目完整代碼