本文所用示例的倉庫地址: gayhubjavascript
上一節咱們解決了工程的開發調試問題,項目的生產和開發環境也已配置完成,還約定了 Webpack 配置文件規範。但它還很粗糙,這一節咱們就來一塊兒打磨這套配置。css
在以前的配置中咱們使用使用 MiniCssExtractPlugin.loader
來代替 style-loader
,由於咱們須要把 CSS 從 JS 中分離出來。但 MiniCssExtractPlugin 目前還存在一個隱患,那就是它可能會影響到 hmr (熱模塊替換)功能,在它對 hmr 的支持前,咱們只能在生產環境中使用它。html
webpack.base.conf.js
vue
module.exports = {
module: {
rules: [
{
test: /\.styl(us)?$/,
use: [
process.env.NODE_ENV !== 'production' ?
'vue-style-loader' : {
loader: resolve('node_modules/mini-css-extract-plugin/dist/loader.js'),
options: {
publicPath: '../'
}
},
{
loader: 'css-loader',
options: {
importLoaders: 2 // 在 css-loader 前執行的 loader 數量
}
},
'postcss-loader',
{
loader: 'stylus-loader',
options: {
preferPathResolver: 'webpack' // 優先使用 webpack 用於路徑解析,找不到再使用 stylus-loader 的路徑解析
}
}
]
}
]
}
}
複製代碼
實際上我上次使用它時看見有 hmr 配置項,就覺得已經支持了,具體支持與否請看 MiniCssExtractPlugin Docsjava
當項目達到必定體量,打包速度、熱加載性能優化的需求就會被提出來,畢竟誰也不肯意修改後花上十幾秒甚至幾分鐘等待修改視圖更新。接下里我會介紹一些通用的優化策略,但須要注意的是,項目自己不能去踩一些沒法優化的坑,已知兩坑:超多頁( html-webpack-plugin 熱更新時更新全部頁面)和動態加載未指明明確路徑(打包目錄下全部頁面)。node
DllPlugin 和 DllReferencePlugin 絕對是優化打包速度的最佳利器,它能夠把部分公共依賴提早打包好,在以後的打包中就再也不打包這些依賴而是直接取用已經打包好的代碼,一般狀況能下降 20% ~ 40% 打包時間,固然它也有缺點:webpack
.html
文件中引入,濫用會致使首屏加載變慢但總歸來講是利大於弊。git
新增 webpack.dll.conf.js
程序員
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const { resolve } = require('./utils')
const libs = {
_frame: ['vue', 'vue-router', 'vuex'],
_utils: ['lodash']
}
module.exports = {
mode: 'production',
entry: { ...libs },
performance: false,
output: {
path: resolve('dll'),
filename: '[name].dll.js',
library: '[name]' // 與 DllPlugin.name 保持一致
},
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: []
}),
new webpack.DllPlugin({
name: '[name]',
path: resolve('dll', '[name].manifest.json'),
context: resolve('')
})
]
}
複製代碼
在 webpack.common.conf.js
使用 DllReferencePlugines6
webpack.common.conf.js
const { generateDllReferences, generateAddAssests } = require('./utils')
module.exports = {
plugins: [
...generateAddAssests(),
...generateDllReferences()
]
}
複製代碼
# add-asset-html-webpack-plugin 用於把 dll 添加到 `index.html` 的 script 標籤中
# glob 支持正則匹配文件
yarn add add-asset-html-webpack-plugin glob -D
複製代碼
utils.js
const webpack = require('webpack')
const glob = require('glob')
const AddAssestHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
const generateDllReferences = function() {
const manifests = glob.sync(`${resolve('dll')}/*.json`)
return manifests.map(file => {
return new webpack.DllReferencePlugin({
// context: resolve(''),
manifest: file
})
})
}
const generateAddAssests = function() {
const dlls = glob.sync(`${resolve('dll')}/*.js`)
return dlls.map(file => {
return new AddAssestHtmlWebpackPlugin({
filepath: file,
outputPath: '/dll',
publicPath: '/dll'
})
})
}
複製代碼
添加 npm scripts
package.json
"scripts": {
"dll": "webpack --config build/webpack.dll.conf.js"
},
複製代碼
而後就能夠用 yarn dll
打包配置好的全局公共依賴了,打包後會在 src/dll
目錄生成 *dll.js
和 *dll.json
,前者是依賴經壓縮合並後的文件( mode: production
),後者是 *dll.js
文件和原始依賴的映射文件,用於被 DllReferencePlugin 解析創建引用和 *dll.js
之間的映射關係。
構建中最耗時的兩步是 babel 和壓縮, babel 通常會配置忽略 node_modules
因此 DllPlugin 節約的是部分公共依賴的壓縮時間,因此你若是不想用 DllPlugin 也能夠在 externals
中將他們配置爲外部依賴,用其餘方式去壓縮並引入他們
在 webpack 4 中,是否生成 Source Map 以及生成怎樣的 Source Map 是由 devtool
配置控制的,選擇合理的 Source Map 能夠有效的縮短打包時間。在選擇前咱們仍是應該明白,不設置 Source Map 時打包是最快的,之因此須要 Source Map ,是由於打包後的代碼結構、文件名和打包前徹底不一致,當存在報錯時咱們只能直接定位到打包後的某個文件,沒法定位到源文件,極大程度增長了調試難度。而 Source Map 就是爲了加強打包後代碼的可調試性而存在的,因此咱們在開發環境老是須要它,在生產環境則有更多選擇。
devtool
可選配置有 none
、 eval
、 cheap-eval-source-map
等 13 種,各自功能和性能比較在 文檔 中有詳細介紹。
配置項由一個或多個單詞和連字符組成,每一個單詞都有其含義和性能損耗,每一個配置項最終意義就由這些單詞決定:
none
不生成 Source Map ,性能 +++eavl
每一個模塊由 eval
執行,不能正確顯示行數,不能用生產模式,性能 +++module
報錯顯示原始代碼,性能 -source
報錯顯示行列信息,顯示 babel 轉譯後代碼,性能 --cheap
低開銷模式,不映射列,性能 +inline
不生成單獨的 Source Map 文件,性能 o因爲開發模式建議顯示報錯源碼和行信息,因此 module
和 source
都是須要的,爲了性能咱們又須要 eval
和 cheap
,因此參照配置項能找到最適合開發環境的配置是 devtool: cheap-module-eval-source-map
。
生產環境因爲幾乎不存在調試需求( JS 相關調試),因此建議你們設置 devtool: none
,在須要調試的時候再更改設置爲 devtool: cheap-module-source-map
。
本小節中提到的優化其實幾乎都是咱們以前配置中的某一個默認配置
以前有提到過,壓縮是構建中耗時佔比較大的一環,咱們能夠啓用 terser-webpack-plugin 的多線程壓縮,減小壓縮時間。
module.exports = {
optimization: {
minimizer: [
new TerserJSPlugin({
parallel: true // 開啓多線程壓縮
})
]
}
}
複製代碼
module.exports = {
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
// 不轉譯 node_modules
// exclude: [resolve('node_modules')]
// 轉譯 src 目錄下的文件
include: [
resolve('src')
],
options: {
cacheDirectory: true // 默認目錄 node_modules/.cache/babel-loader
// cacheDirectory: resolve('/.cache/babel-loader')
}
}
]
}
}
複製代碼
vue-loader 的 cacheDirectory
配置項依賴 cache-loader
yarn add cache-loader -D
複製代碼
module.exports = {
module: {
rules: [
{
test: /\.vue$/,
use: {
loader: 'vue-loader',
options: {
prettify: false,
cacheDirectory: resolve('node_modules/.cache/vue-loader'),
cacheIdentifier: 'vue'
}
}
}
]
}
}
複製代碼
resolve.modules
告知 Webpack 解析模塊時應該搜索的目錄,能夠設置相對路徑和絕對路徑。設置相對路徑時,好比 resolve.modules: [node_modules]
,在解析依賴時會從當前目錄向上查找,直到找到 node_modules
目錄。設置絕對路徑時能減小了這個遍歷過程,直接定位目錄。
module.exports = {
resolve: {
modules: [resolve('node_modules')]
}
}
複製代碼
我以前也說了,這個優化項聊勝於無。
ES6 不只在原有對象上添加了一些經常使用方法,還新增了一些新的詞法和語法給開發者帶來了極大便利,它天然是不能缺席本項目的。但不一樣瀏覽器、不一樣版本對 ES6 的支持不一致,致使使用 ES6 是還存在些許阻礙,咱們須要用 babel-loader 把 ES6 的詞法和語法轉換爲 ES5。
前段時間剛介紹過 babel-loader / babel 7 ,這裏就再也不重複介紹,詳情見 babel-loader 使用指南
多人合做項目約定編碼規範是很是重要的,由於它能有效提升協做效率並抑制程序員怒氣值增加。固然我認爲我的項目也是須要的,由於 6 個月前的代碼和別人的代碼同樣。
EditorConfig 是一個跨編輯器的代碼規範解決方案,得到了衆多編輯器的支持(編輯器或插件實現支持),這意味着不一樣編輯器能夠格式化出一樣風格的代碼,好比 vscode 和 sublime 。配置方式是在項目根目錄增長一個 .editorconfig
文件,部分編輯器能夠經過命令一鍵生成,一般其配置以下:
.editorconfig
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false
複製代碼
root
是否爲頂級配置文件,一般設置爲 true
表示搜索到該配置文件後再也不繼續向上查找配置indent_style
Tab 縮進方式indent_size
縮進大小,上面示例就表示:縮進表現爲兩個空格charset
編碼方式trim_trailing_whitespace
是否刪除行尾空格insert_final_newline
文件是否以空行結束Javascript 是一門動態語言(弱類型語言),靈活但易錯,因此在協做開發中須要制定一些規則保證各個成員輸出風格一致的代碼。 eslint 正是用於應對這個問題的開源工具,你能夠設定規則,它則基於規則檢查 Javascript 是否合法,不合法則返回錯誤或警告。
安裝
yarn add eslint -D
複製代碼
初始化配置文件
npx eslint --init
複製代碼
根據提示選擇須要的項並安裝對應的 plugin 和 config ,我這裏還須要安裝 eslint-config-standard
和 eslint-plugin-vue
yarn add eslint-config-standard eslint-plugin-vue -D
複製代碼
.eslintrc.js
module.exports = {
"env": {
"browser": true,
"es6": true
},
"extends": [
"plugin:vue/essential",
"standard"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"vue"
],
"rules": {
}
}
複製代碼
在 rules 配置相應規則
見中文文檔
有時候咱們會發現有些生成物的大小不對勁,但在控制檯又很難看出來緣由,這個時候就須要模塊分析工具的幫助。這裏我推薦使用 webpack-bundle-analyzer ,它會啓動一個服務,在瀏覽器中很清楚地展示生成物和源文件的映射關係和層級,如圖所示(圖來源於 Github ):
yarn add webpack-bundle-analyzer -D
複製代碼
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
複製代碼
在構建層面優化用戶體驗在於如下幾個方面: 縮小生成代碼體積、合理的加載方式、合理減小 HTTP 請求數,本小節主要講前二者。
減小 HTTP 的優化,咱們在使用 url-loader 處理圖片時就說起到了
Webpack 4 在 production
模式下是會默認壓縮 JS 代碼的,使用 TerserWebpackPlugin,但 CSS 不會( Webpack 5 會做爲內置功能 ),因此咱們須要 OptimizeCSSAssetsPlugin 的幫助。
yarn add optimize-css-assets-webpack-plugin -D
複製代碼
Webpack 插件使用大多大同小異,但在 Webpack 4 中使用這個插件須要特別注意,使用它時會重寫 optimization.minimizer
選項,而壓縮 JS 的插件 TerserWebpackPlugin 剛好就在這個選項的默認值中,重寫會致使默認值失效,因此你還須要顯式地聲明 TerserWebpackPlugin 實例。
webpack.prod.conf.js
const TerserJSPlugin = require("terser-webpack-plugin")
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin")
module.exports = {
optimization: {
minimizer: [
new TerserJSPlugin({
parallel: true // 開啓多線程壓縮
}),
new OptimizeCSSAssetsPlugin({})
]
}
}
複製代碼
壓縮是生產環境下的優化,開發環境去設置它反而會影響到熱加載性能、拔苗助長
代碼分離有兩個優勢:
剝離公共代碼和依賴,避免重複打包
避免單個文件體積過大
總加載體積一致,瀏覽器加載多個文件一般快於單個文件
咱們先在項目中加上會被 home 和 page-a 公共引用的資源: src/utils/index.js
& src/styles/main.styl
,而後再在兩個頁面分別引用他們,以 page-a.vue
舉例。
爲了方便檢查生成代碼,咱們設置 mode: development
以得到未被壓縮的代碼
page-a.vue
<template>
<div class="page-a">
<h1>
This is page-a
</h1>
</div>
</template>
<script>
import { counter } from '@/utils'
export default {
name: 'page-a',
created() {
counter()
console.log('page-a:', counter.count)
}
}
</script>
<style lang="stylus" scoped>
@import '~@/styles/main.styl';
.page-a {
background: blue;
}
</style>
複製代碼
執行 yarn build
打包項目,而後咱們就會在 home.vue
對應的生成物( dist/css/views/home.[contentHash].css
和 dist/views/home.[contentHash].js.
)中看到,他們包含了 src/styles/main.styl
和 src/utils/index.js
文件中的所需內容。然而,咱們再去檢查 page-a.vue
對應的生成物,發現他們一樣包含了這些內容,因此一份源碼被打包到了兩個頁面對應的生成物中。
被重複打包是由於這兩個頁面同時引用了他們,當引用次數是 3 次、 10 次或者更多,這些公共資源(包括公共依賴)甚至能夠佔到生成物體積的 95% 以上,這顯然是不可接受的。
爲了解決公共資源被重複打包問題,咱們就須要 SplitChunksPlugin 的幫助,它能夠把代碼分離成不一樣的 bundle ,在頁面須要時被加載。另外 SplitChunksPlugin 是 webpack 4 的內置插件,因此咱們不須要去獨立安裝它。
webpack.prod.conf.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
minSize: 1, // 正常設置 20000+ 即 20k+ ,但這裏咱們的公共文件只有幾行代碼,因此設置爲 1
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '/',
name(mod, chunks) {
return ${chunks[0].name}
},
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
}
複製代碼
執行 yarn build
打包項目,咱們能夠看到只有 dist/views/home.[contentHash].js.
(或者一個單獨的 JS 中) 纔會包含 utils.js
內容,而 dist/views/page-a.[contentHash].js.
中只有引用: _utils__WEBPACK_IMPORTED_MODULE_0__["counter"]
。
咱們可使用 VSCode 來調試打包配置代碼( nodejs ),獲得 name
函數中的 mod / chunks
的對象結構,根據信息返回咱們須要的文件名。
固然你也能夠用 -inspect
來調試代碼。
// 命名和代碼分離息息相關,這裏僅爲使用示例,具體命名請根據項目狀況更改
name(mod, chunks) {
if (chunks[0].name === 'app') return 'app.vendor'
if (/src/.test(mod.request)) {
let requestName = mod.request.replace(/.*\\src\\/, '').replace(/"/g, '')
if (requestName) return requestName
} else if (/node_modules/.test(mod.request)) {
return 'dependencies/' + mod.request.match(/node_modules.[\w-]+/)[0].replace(/node_modules./, '')
}
return null
}
複製代碼
更多的狀況是設置魔法註釋來規定文件名,而不是經過 name 函數設置,由於後者每每會將一些不應分離的代碼分離
上面咱們分離代碼,解決了項目中部分代碼被重複打包到多個生成物中的問題,有效地縮小了生成物體積,但其實咱們還能夠在此基礎上進一步縮小體積,這就涉及本小節的概念 tree shaking 。
tree shaking 是一個術語,一般用於描述移除 JavaScript 上下文中的未引用代碼(dead-code)。它依賴於 ES2015 模塊語法的 靜態結構 特性,例如 import 和 export。
你能夠將應用程序想象成一棵樹。綠色表示實際用到的 source code(源碼) 和 library(庫),是樹上活的樹葉。灰色表示未引用代碼,是秋天樹上枯萎的樹葉。爲了除去死去的樹葉,你必須搖動這棵樹,使它們落下。
咱們回頭看在使用 SplitChunksPlugin 時生成的文件,能夠發現 say
函數沒有使用可是卻被打包進來了,它其實是無用的代碼,也就是文檔中說的 dead-code 。要刪除這些代碼,只須要把 mode
修改成 production
(讓 tree shaking 生效),再次打包~
不過須要注意的是, tree shaking 能移除無用代碼的同時,也有必定的反作用(錯誤識別無用代碼)。好比你可能會遇到 UI 組件庫沒有樣式的問題,這個問題緣由在於 tree shaking 不只對 JS 生效,也對 CSS 生效 。咱們一般在導入 CSS 時使用 import 'xxx.min.css'
, ES6 的靜態導入 + 生產環境知足了 tree shaking 的生效條件,而且 Webpack 沒法判斷 CSS 有效,因此它被當作了 dead-code 而後被刪除。爲了解決這個問題,你能夠在 package.json
中添加一個 sideEffects
選項,告知 Webpack 那些文件是能夠直接引入而不用 tree shaking 檢查的,使用以下:
package.json
{
"sideEffects": [
"*.css",
"*.styl(us)?"
]
}
複製代碼
合理的資源加載方式有時比縮小代碼體積更重要
按需加載又名懶加載,是指當須要依賴的頁面被打開採起加載這個依賴,這樣就減小了主頁的負擔,提高首屏渲染速度。而要作到按需加載,你只需在導入依賴的時候用 import()
或 require.ensure
這兩種動態加載方式。咱們添加 lodash
依賴來作測試: yarn add lodash
page-a.vue
// 靜態加載
import _ from 'lodash'
// 懶加載
// import(/* webpackChunkName: "dependencies/lodash" */ 'lodash')
export default {
name: 'page-a',
created() {
console.log(_.now())
}
}
複製代碼
此時啓動一下開發服務,咱們能夠看到,雖然這裏用了靜態加載,但其實 lodash 依賴仍是在點擊進入了 page-a 纔會被加載(懶加載)。由於咱們在使用設置路由的時候,就已經使用過了 import()
動態加載(這一點我忘記了),因此 page-a 頁面的靜態資源也一塊兒變做了懶加載。
咱們再看一下以前的動態加載語句 import(/* webpackChunkName: "views/home" */ '@/views/home/main.vue')
,這其中有一個值得注意的知識點 /* webpackChunkName: "views/home" */
,它是 Webpack 的魔法註釋,這裏是經過魔法註釋指定生成 chunk 的文件名,因此該 src/views/home/main.vue
文件打包後的 JS 就在 dist/views/home.[contentHash].js
。
上面講到使用魔法註釋爲生成物命名,其實預加載 preload 和預取 prefetch 也是經過魔法註釋來設置的。這裏是官方文檔上有他們的異同介紹:
- preload chunk 會在父 chunk 加載時,以並行方式開始加載。prefetch chunk 會在父 chunk 加載結束後開始加載。
- preload chunk 具備中等優先級,並當即下載。prefetch chunk 在瀏覽器閒置時下載。
但在個人測試中,不管是 preload 仍是 prefetch 都是並行加載的,但他們優先級會比當前頁面所需依賴更低,不會影響到頁面加載。你能夠在 main.js
中添加如下代碼進行測試:
src/main.js
// 對比測試
// import 'lodash'
// 預加載
// import(/* webpackPreload: true, webpackChunkName: "dependencies/lodash" */ 'lodash')
// 預取
import(/* webpackPrefetch: true, webpackChunkName: "dependencies/lodash" */ 'lodash')
複製代碼
本小節的測試結果因爲和文檔不符,但願你們自行驗證,不可偏信
用預加載和預取處理體積較大的依賴效果尤其明顯,好比圖表、富文本編輯器
有時咱們會在項目中直接引入一些體積不小 JS 庫(本文以 lodash 舉例), Webpack 會去解析並壓縮它們。但仔細想一想,lodash 自己就存在已經壓縮好的版本 lodash/lodash.min.js
,再加上其體積也不小不必再與其餘 JS 合併( 未壓縮 700k ,壓縮後 70k ),咱們去解析和壓縮它的意義並不大。因此咱們能夠把它放到 static
文件夾(或 CDN),並在 index.html
中用 script
標籤引入,若是項目中有引入 lodash ( import 'lodash'
)則能夠配置 externals
在打包時忽略它,沒有則不用。
若是這裏想用 link[ref=prefetch/preload]
進行預加載,那麼必定不要忘了在合適的地方再用 script
標籤引入,預加載只是爲了緩存
新增 /static
文件夾,加入 lodash.min.js
代碼改動
yarn add copy-webpack-plugin -D
複製代碼
/build/webpack.prod.conf.js
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
externals: {
lodash: {
commonjs: 'lodash',
umd: 'lodash',
root: '_' // 默認執行環境已經存在全局變量: _ ,瀏覽器中就是 window._
}
},
plugins: [
new CopyWebpackPlugin([
{
from: 'static/',
to: 'static/'
}
])
]
}
複製代碼
/src/main.js
import _ from 'lodash'
console.log(_.now())
複製代碼
/index.html
<body>
<script type="text/javascript" src="static/lodash.min.js"></script>
</body>
複製代碼
有些朋友可能認爲解析 lodash 可讓 Webpack 知道哪些函數是沒用到的,而後 tree shaking 掉它們,但其實 lodash 並非 ES6 模塊語法的靜態導出,因此 tree shaking 不會生效。若是項目並非重度依賴 lodash ,只是使用了其中幾個函數,建議導入單個函數,以下:
import now from 'lodash/now'
console.log(now())
複製代碼
module.noParse
在文檔中被介紹也能夠忽略打包某些模塊,但遺憾的是當前它還無甚用處。由於要讓它生效你就不能在代碼中去引用相關依賴,實際上沒有引用就不會記錄到依賴圖中,天然就不會被打包(因此這個配置項什麼都沒作)。