最近新起了一個多頁項目,以前都未使用 webpack4 ,因而準備上手實踐一下。這篇文章主要就是一些配置介紹,對於正準備使用 webpack4 的同窗,能夠作一些參考。javascript
webpack4 相比以前的 2 與 3,改變很大。最主要的一點是不少配置已經內置,使得 webpack 能「開箱即用」。固然這個開箱即用不可能知足全部狀況,可是不少以往的配置,其實能夠不用了。好比在以前,壓縮混淆代碼,須要增長uglify
插件,做用域提高(scope hosting)須要增長ModuleConcatenationPlugin
。而在 webpack4 中,只須要設置 mode
爲 production
便可。固然,若是再強行增長這些插件也不會報錯。css
因此我建議,若是你們想遷移到 webpack4,仍是從 0 開始作加法,參考歷史,從新作一個配置。而不是從歷史的配置裏刪刪減減,再升級爲 webpack4。這樣 webpack4 的配置會顯得更精簡。html
打包優化主要就是多頁應用構建時,對全部頁面加載的依賴進行合理打包。這個目前業界都已經有了不少實踐,包括 webpack4,也有不少文章介紹。我再補充幾個不容易注意的小細節。有些點我不詳細介紹,不熟悉 webpack 配置的同窗可能會不明白,能夠搜索對應關鍵詞,網上確定有很是詳細的文章介紹。前端
首先,構建多頁應用,每每會抽離以下幾個 chunk 包:vue
common
:將被多個頁面同時引用的依賴包打到一個 common chunk 中。網上大部分教程是被引入兩次即打入 common。我建議能夠根據本身頁面數量來調整,在個人工程中,我設置引入次數超過頁面數量的 1/3 時,纔會打入 common 包。dll
: 將每一個頁面都會引用的且基本不會改變的依賴包,如 react/react-dom 等再抽離出來,不讓其餘模塊的變化污染 dll 庫的 hash 緩存。manifest
: webpack 運行時(runtime)代碼。每當依賴包變化,webpack 的運行時代碼也會發生變化,如若不將這部分抽離開來,增長了 common 包 hash 值變化的可能性。page.js
而後咱們會給打出的 chunk 包名,注入 contentHash,以實現最大緩存效果。在咱們分 chunk 的過程當中,最關鍵的一個思想就是,每次迭代發佈,儘可能減小 chunk hash 值的改變。這個在業界也有不少很是多的實踐,好比這篇文章:https://github.com/pigcan/blo...java
不過在 webpack4 中,咱們不用再增長這麼多插件啦,一個 optimization 配置徹底就能搞定。node
我先貼上個人 webpack 的 optimization 配置,而後我再對其作一些介紹,加深你們印象react
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 } } } }
在 webpack4 以前,抽離 manifest,須要使用 CommonsChunkPlugin,配置一個指定 name 屬性爲'manifest'的 chunk。在 webpack4 中,無需手動引入插件,配置 runtimeChunk 便可。webpack
這個配置能讓咱們以必定規則抽離想要的包,咱們可能會抽好幾個包,如 verdor + common,因此 splitChunks 中提供 cacheGroups 字段,cacheGroups 每增長一個 key,就至關於多一個抽包規則。git
在網上不少教程中,dll 每每是專門再加一個 webpack 配置,使用 DllPlugin 來構建 dll 庫,再在本身項目工程的 webpack 中利用 DllReferencePlugin 來映射 dll 庫。雖然這樣構建速度會快很多,可是,哎,是真 TM 煩.....
我是一個很怕煩的人,我情願在 webpack4 中利用 splitChunks,配好規則,再抽離對應的 dll 包。固然這個你們能夠本身根據實際狀況選擇方案。
除了 dll 與 common 兩個 chunk,我還加了一個 polyfill。這是由於咱們用的某些新的庫或者使用某些 ES6+語法(如 async/await)須要 runtime 墊片。好比我工程中使用了 react16,須要增長Map
/Set
/requestAnimationFrame
(https://reactjs.org/docs/java...。那我必須在 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 個,所以須要從新配置值。
稍微瞭解過 webpack 運行機制的同窗會知道,項目工程中加載的 module,webpack 會爲其分配一個 moduleId,映射對應的模塊。這樣產生的問題是一旦工程中模塊有增刪或者順序變化,moduleId 就會發生變化,進而可能影響全部 chunk 的 content hash 值。只是由於 moduleId 變化就致使緩存失效,這確定不是咱們想要的結果。
在 webpack4 之前,經過 HashedModuleIdsPlugin
插件,咱們能夠將模塊的路徑映射成 hash 值,來替代 moduleId,由於模塊路徑是基本不變的,故而 hash 值也基本不變。
但在 webpack4 中,只須要optimization
的配置項中設置 moduleIds
爲 hashed
便可。
除了 moduleId,咱們知道分離出的 chunk 也有其 chunkId。一樣的,chunkId 也有因其 chunkId 發生變化而致使緩存失效的問題。因爲manifest
與打出的 chunk 包中有chunkId
相關數據,因此一旦如「增刪頁面」這樣的操做致使 chunkId 發生變化,可能會影響不少的 chunk 緩存失效。
在 webpack4 之前,經過增長NamedChunksPlugin
,使用 chunkName 來替換 chunkId,實現固化 chunkId,保持緩存的能力。在 webpack4 中,只需在optimization
的配置項中設置 namedChunks
爲 true
便可。
在 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
出來已經挺久了,文章寫的有點兒滯後了,因此不少我以爲應該你們都明白的地方就沒詳細寫了。若是還有什麼疑問的話,歡迎評論~~
原文地址:https://segmentfault.com/a/1190000016685119