多頁應用 Webpack4 配置優化與踩坑記錄
最近新起了一個多頁專案,之前都未使用 webpack4,於是準備上手實踐一下。這篇文章主要就是一些配置介紹,對於正準備使用 webpack4 的同學,可以做一些參考。
webpack4 相比之前的 2 與 3,改變很大。最主要的一點是很多配置已經內建,使得 webpack 能“開箱即用”。當然這個開箱即用不可能滿足所有情況,但是很多以往的配置,其實可以不用了。比如在之前,壓縮混淆程式碼,需要增加uglify
外掛,作用域提升(scope hosting)需要增加ModuleConcatenationPlugin
。而在 webpack4 中,只需要設定mode
為production
即可。當然,如果再強行增加這些外掛也不會報錯。
所以我建議,如果大家想遷移到 webpack4,還是從 0 開始做加法,參考歷史,重新做一個配置。而不是從歷史的配置裡刪刪減減,再升級為 webpack4。這樣 webpack4 的配置會顯得更精簡。
打包優化
打包優化主要就是多頁應用構建時,對所有頁面載入的依賴進行合理打包。這個目前業界都已經有了很多實踐,包括 webpack4,也有很多文章介紹。我再補充幾個不容易注意的小細節。有些點我不詳細介紹,不熟悉 webpack 配置的同學可能會不明白,可以搜尋對應關鍵詞,網上肯定有非常詳細的文章介紹。
首先,構建多頁應用,往往會抽離如下幾個 chunk 包:
common dll manifest page.js
然後我們會給打出的 chunk 包名,注入 contentHash,以實現最大快取效果。在我們分 chunk 的過程中,最關鍵的一個思想就是,每次迭代釋出,儘量減少 chunk hash 值的改變。這個在業界也有很多非常多的實踐,比如這篇文章:ofollow,noindex">github.com/pigcan/blog…
不過在 webpack4 中,我們不用再增加這麼多外掛啦,一個 optimization 配置完全就能搞定。
我先貼上我的 webpack 的 optimization 配置,然後我再對其做一些介紹,加深大家印象
const commonOptions = { chunks: 'all', reuseExistingChunk: true } export default { namedChunks: true, moduleIds: 'hashed', runtimeChunk: { name: 'manifest' }, splitChunks: { maxInitialRequests: 5, cacheGroups: { polyfill: { test: /[\\/]node_modules[\\/](core-js|raf|@babel|babel)[\\/]/, name: 'polyfill', priority: 2, ...commonOptions }, dll: { test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/, name: 'dll', priority: 1, ...commonOptions }, commons: { name: 'commons', minChunks: Math.ceil(pages.length / 3), // 至少被1/3頁面的引入才打入common包 ...commonOptions } } } } 複製程式碼
runtimeChunk
在 webpack4 之前,抽離 manifest,需要使用 CommonsChunkPlugin,配置一個指定 name 屬性為'manifest'的 chunk。在 webpack4 中,無需手動引入外掛,配置 runtimeChunk 即可。
splitChunks
這個配置能讓我們以一定規則抽離想要的包,我們可能會抽好幾個包,如 verdor + common,所以 splitChunks 中提供 cacheGroups 欄位,cacheGroups 每增加一個 key,就相當於多一個抽包規則。
在網上很多教程中,dll 往往是專門再加一個 webpack 配置,使用 DllPlugin 來構建 dll 庫,再在自己專案工程的 webpack 中利用 DllReferencePlugin 來對映 dll 庫。雖然這樣構建速度會快不少,但是,哎,是真 TM 煩.....
我是一個很怕煩的人,我情願在 webpack4 中利用 splitChunks,配好規則,再抽離對應的 dll 包。當然這個大家可以自己根據實際情況選擇方案。
除了 dll 與 common 兩個 chunk,我還加了一個 polyfill。這是因為我們用的某些新的庫或者使用某些 ES6+語法(如 async/await)需要 runtime 墊片。比如我工程中使用了 react16,需要增加Map
/Set
/requestAnimationFrame
(reactjs.org/docs/javasc…
)。那我必須在 dll 庫載入之前增加 polyfill,因此我將所有 core-js 與 babel 引入的包專門打進 polyfill,保證後續載入的 chunk 能執行。priority
欄位用來配置 chunk 的引入優先順序,一般的專案應該都是 polyfill > dll > common > page。
splitChunks 中配置項maxInitialRequests
表示在一個入口(entry)中,最大初始請求 chunk 數(不包含按需載入的,即 dom 中 script 引入的 chunk),預設值是 3。我現在 cacheGroups 中已經有三個,又因為配置了 runtimeChunk,會打出 manifest,故而總共有 4 個 chunk 包,超出了預設 3 個,因此需要重新配置值。
moduleIds
稍微瞭解過 webpack 執行機制的同學會知道,專案工程中載入的 module,webpack 會為其分配一個 moduleId,對映對應的模組。這樣產生的問題是一旦工程中模組有增刪或者順序變化,moduleId 就會發生變化,進而可能影響所有 chunk 的 content hash 值。只是因為 moduleId 變化就導致快取失效,這肯定不是我們想要的結果。
在 webpack4 以前,通過HashedModuleIdsPlugin
外掛,我們可以將模組的路徑對映成 hash 值,來替代 moduleId,因為模組路徑是基本不變的,故而 hash 值也基本不變。
但在 webpack4 中,只需要optimization
的配置項中設定moduleIds
為hashed
即可。
namedChunks
除了 moduleId,我們知道分離出的 chunk 也有其 chunkId。同樣的,chunkId 也有因其 chunkId 發生變化而導致快取失效的問題。由於manifest
與打出的 chunk 包中有chunkId
相關資料,所以一旦如“增刪頁面”這樣的操作導致 chunkId 發生變化,可能會影響很多的 chunk 快取失效。
在 webpack4 以前,通過增加NamedChunksPlugin
,使用 chunkName 來替換 chunkId,實現固化 chunkId,保持快取的能力。在 webpack4 中,只需在optimization
的配置項中設定namedChunks
為true
即可。
css 相關
在 webpack4 以前,使用extract-text-webpack-plugin
外掛將 css 從 js 包中分離出來單獨打包。在 webpack 中則需要換成MiniCssExtractPlugin
。並且在生產環境或者需要 HMR(模組熱替換)時,要用MiniCssExtractPlugin.loader
替換style-loader
。
注意,這裡有個坑。由於開發環境我們會配置熱更新,css 的熱更新目前MiniCssExtractPlugin.loader
自身還待支援,故而還需要增加css-hot-loader
。
切記,css-hot-loader
一定不能在生產環境下使用。否則每次構建過程所有 js chunk 包的 contentHash 值都會不一致,進而導致所有 js 快取失效。
因為生產環境增加這個配置不會有任何報錯,頁面也能正常構建,故而容易忽視。
簡化多頁應用的入口檔案
使用react
/vue
等框架的同學知道,我們一般需要一個入口index.js
,如這樣:
import React from 'react' import ReactDOM from 'react-dom' import App from './app' ReactDOM.render(<App />, document.getElementById('root')) 複製程式碼
如果你還需要使用dva
,或者給所有 react 頁面增加一個 layout 功能的話,可能就會變成這樣:
import React from 'react' import dva from 'dva' import Model from './model' import Layout from '~@/layout' import App from './app' const app = dva() app.router(() => ( <Layout> <App /> </Layout> )) app.model(Model) app.start(document.getElementById('root')) 複製程式碼
如果每個頁面都這樣,略略有點兒難受,因為程式員最怕寫重複的東西了。但是它又必須要有,沒辦法抽離成一個單獨檔案。因為這個是入口檔案,而多頁工程,每個頁面必須要有自己的入口檔案,即使他們長得一模一樣。於是,我們的資源目錄就會是這樣:
- src - layout.js - pages - pageA - index.js - app.js - model.js - pageB - index.js - app.js - model.js 複製程式碼
因為所有的 index 都一樣,我理想中的頁面的入口檔案僅僅需要app.js
就好,像這樣:
- src - layout.js - pages - pageA - app.js - model.js - pageB - app.js - model.js 複製程式碼
作為一名前端開發工程師,Node
對於我們來說,應該是熟練運用的工具,而不是僅僅拿別人已經封裝好的各類工具。
在這個問題中,我們大可以在 webpack 構建前,通過Node
的檔案系統(File System
),對應我們的每個頁面,通過同一個入口檔案模板,建立一些臨時入口檔案:
- src - .entires - pageA.js - pageB.js - layout.js - pages 複製程式碼
然後將這些臨時檔案,作為 webpack 的 entry 配置。程式碼如下:
const path = require('path') const fs = require('fs') const glob = require('glob') const rimraf = require('rimraf') const entriesDir = path.resolve(process.cwd(), './src/.entries') const srcDir = path.resolve(process.cwd(), './src') // 返回webpack entry配置 module.exports = function() { if (fs.existsSync(entriesDir)) { rimraf.sync(entriesDir) } fs.mkdirSync(entriesDir) return buildEntries(srcDir) } function buildEntries(srcDir) { return getPages(srcDir).reduce((acc, current) => { acc[current.pageName] = buildEntry(current) return acc }, {}) } // 獲取頁面資料,只考慮一級目錄 function getPages(srcDir) { const pagesDir = `${srcDir}/pages` const pages = glob.sync(`${pagesDir}/**/app.js`) return pages.map(pagePath => { return { pageName: path.relative(pagesDir, p).replace('/app.js', ''), // 取出page資料夾名 pagePath: pagePath } }) } // 構建臨時入口檔案 function buildEntry({ pageName, pagePath }) { const fileContent = buildFileContent(pagePath) const entryPath = `${entriesDir}/${pageName}.js` fs.writeFileSync(entryPath, fileContent) return entryPath } // 替換模板中的 App 模組地址,返回臨時入口檔案內容 function buildFileContent(pagePath) { return ` import React from 'react' import dva from 'dva' import Model from './model' import Layout from '~@/layout' import App from 'PAGE_APP_PATH' const app = dva() app.router(() => ( <Layout> <App /> </Layout> )) app.model(Model) app.start(document.getElementById('root')) `.replace(PAGE_APP_PATH, pagePath) } 複製程式碼
這樣一來,我們就簡單的去掉了重複的入口檔案,還增加了一個 layout 的功能。這只是簡單的程式碼,實際專案可能還有多級目錄,多個 model 等等,需要自己再定製啦。
webpack4
出來已經挺久了,文章寫的有點兒滯後了,所以很多我覺得應該大家都明白的地方就沒詳細寫了。如果還有什麼疑問的話,歡迎評論~~