前端的打包工具從之前的browserify、grunt、gulp到現如今的rollup、webpack,湧現出了很多優秀的打包工具,而目前最火的無疑是webpack,無論是當前熱門的框架還是工具庫很多都選擇了它作為打包工具,因此在開發中webpack作為打包工具是一個很好的選擇。在最近的專案開發中我也用到了webpack,其中也碰到了不少優化方面的問題,這裡總結一下webpack打包優化的一些細節和方法。
首先,這次專案用到的是vue的全家桶,在webpack的配置方面直接用的是vue-cli
生成的預設配置,專案打包完成後發現生成的vendor.js
檔案體積特別大,其次打包過程相當緩慢,因此想嘗試各種方式對其進行優化。
定位體積大的模組
要想對打包體積進行優化,首先得找到體積大的模組,在這裡我們可以使用webpack外掛webpack-bundle-analyzer
來檢視整個專案的體積結構對比,它是以treemap的形式展現出來,很形象直觀,還有一些具體的互動形式。既可以檢視你專案中用到的所有依賴,也可以直觀看到各個模組體積在整個專案中的佔比。
該外掛的使用方法可以直接通過npm install webpack-bundle-analyzer --save-dev
安裝,並在webpack的配置資訊中的plugins: [new BundleAnalyzerPlugin()]
中新增即可。對於vue-cli
中的配置方式,預設是安裝了該外掛,但是沒有啟用,找到config/index.js
檔案在build
下面會有bundleAnalyzerReport
的配置,預設是process.env.npm_config_report
,這裡建議在package.json
的scripts
中新增一行"analyz": "npm_config_report=true npm run build"
,這樣每次想啟用該外掛時只需要npm run analyze
即可。
提取公共模組
對於webpack,它在模組化打包上有兩點是其核心功能,一是它支援大量的模組型別,無論是TypeScript
、CoffeeScript
還是sass
、stylus
等語言它都支援,二是它可以通過配置來控制打包檔案的粒度,這個下面會講到。
在開發中我們往往會將所有的依賴庫單獨提取出來,而不與我們的專案程式碼混在一起,這時我們會用到一個webpack自帶的外掛CommonsChunkPlugin
,從名字上就可以看出它是一個提取公共模組的外掛。從它的文件中可以看出可以傳入一個物件最為引數,在使用中常用的三個引數分別為:
- name(names)
- minChunks
- chunks
name
好理解,指的就是最後打包檔案的名字,而如果使用的是names
的話,傳入的必須是一個字串陣列。minChunks
如果傳入的是一個數字的話,指的是如果該模組被其他模組的引用次數達到了這個數值的話該模組就會被打包。如果傳入的是一個函式的話,其返回值必須是布林型別來指明這個模組是否應該被打包進公共模組。而chunks
則會指定一個字串陣列,如果設定了該引數,則打包的時候只會從其中指定的模組中提取公共子模組。
下面通過幾個例項來說明這個外掛是如何工作的。
假設有兩個模組chunk1.js
和chunk2.js
以及兩個專案檔案a.js
和b.js
,結構大致如下:
// a.js
require('./chunk1');
require('./chunk2');
require('jquery');
// b.js
require('./chunk1');
require('./chunk2');
require('vue');
// webpack配置如下
module.exports = {
entry: {
main: './main.js',
main1: './main1.js',
jquery:["jquery"],
vue:["vue"]
},
output: {
path: __dirname + '/dist',
filename: '[name].js'
},
plugins: [
new CommonsChunkPlugin({
name: ["common","jquery","vue","load"],
minChunks:2
})
] };
複製程式碼
最終的打包結果是:jquery
被打包到jquery.js
,vue
被打包到vue.js
,common.js
打包的是公共模組(chunk1和chunk2)。使用該外掛打包時,會將滿足minChunks
的模組打包到name
陣列的第一個塊裡,然後陣列後面的依次打包,首先從entry
中找,如果沒有則產生一個空塊。name
陣列中最後一個塊打包的是webpack的runtime程式碼,在使用的時候必須先載入該塊。
現在看一看vue-cli
對於該外掛的配置檔案:
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module, count) {
// 將node_modules中的依賴模組全部提取到vendor檔案中
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
// webpack在使用CommonsChunkPlugin時會生成一段runtime程式碼,並且打包進vendor中。
// 這樣即使不改變vendor程式碼,每次打包時runtime會變化導致vendor的hash變化,這裡
// 把獨立的runtime程式碼抽離出來來解決這個問題
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
}),
複製程式碼
移除不必要的檔案
在專案中我通過方法一定位到幾處體積佔用較大的庫,其中一個便是moment.js
這個日期處理庫。對於一個日期處理的功能,為何這個庫會佔用如此大的體積,仔細檢視發現當引用這個庫的時候,所有的locale
檔案都被引入,而這些檔案甚至在整個庫的體積中佔了大部分,因此當webpack打包時移除這部分內容會讓打包檔案的體積有所減小。
webpack自帶的兩個庫可以實現這個功能:
- IgnorePlugin
- ContextReplacementPlugin
IgnorePlugin
的使用方法如下:
// 外掛配置
plugins: [
// 忽略moment.js中所有的locale檔案
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
],
// 使用方式
const moment = require('moment');
// 引入zh-cn locale檔案
require('moment/locale/zh-cn');
moment.locale('zh-cn');
複製程式碼
ContextReplacementPlugin
的使用方法如下:
// 外掛配置
plugins: [
// 只載入locale zh-cn檔案
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /zh-cn/),
],
// 使用方式
const moment = require('moment');
moment.locale('zh-cn');
複製程式碼
通過以上兩種方式,moment.js
的體積大致能縮減為原來的四分之一。
模組化引入
在專案中我使用了lodash
這個很常用的工具庫,然而在程式碼定位的時候發現這個庫也佔了不少的體積。仔細想想,我們在使用這類工具庫的時候往往只使用到了其中的很少的一部分功能,但卻把整個庫都引入了。因此這裡也可以進一步優化,只引用需要的部分。
import {chain, cloneDeep} from 'lodash';
// 可以改寫為
import chain from 'lodash/chain';
import cloneDeep from 'lodash/cloneDeep';
複製程式碼
這樣就可以只打包我們需要的部分功能。
通過CDN引用
對於一些必要的庫,但又無法對該庫進行更好的體積優化的話,可以嘗試通過外部引入的方式來減小打包檔案的體積。採用該方法只需要在cdn站點找到需要引用的庫的外部連結,以及對webpack進行簡單配置即可。
// 在html中新增script引用
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
複製程式碼
// 這裡externals的key指的是使用時需要require的包名,value指的是該庫通過script引入後在全域性註冊的變數名
externals: {
jquery: 'jQuery'
}
// 使用方法
require('jquery')
複製程式碼
通過DLLPlugin
和 DLLReferencePlugin
拆分檔案
如果專案過大,打包的時間會相當的長,如果頻繁更新上線則會不斷對程式碼進行編譯打包,浪費很多時間。這時我們便可以將那些不常更新的框架和庫(如vue.js等)進行單獨的編譯打包,這樣每次開發上線就只需要對我們的開發檔案進行編譯打包,這樣可以極大地省去不必要的打包時間。而這種方法需要DLLPlugin
和DLLReferencePlugin
兩個外掛的配合來完成。
DLLPlugin
在使用這個外掛時,我們需要單獨建立一個配置檔案,這裡命名為webpack.dll.config.js
,配置如下:
module.exports = {
entry: {
lib: ['vue', 'vuex', 'vue-resource', 'vue-router']
},
output: {
path: path.resolve(__dirname, '../dist', 'dll'),
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath,
library: '[name]_library'
},
plugins: [
new webpack.DefinePlugin({
'process.env': '"production"'
}),
/**
* path: manifest.json輸出檔案路徑
* name: dll物件名,跟output.library保持一致
*/
new webpack.DllPlugin({
context: __dirname,
path: path.resolve(__dirname, '../dist/dll', 'lib.manifest.json'),
name: '[name]_library'
})
]
}
複製程式碼
這裡要注意幾點:
entry
中寫明所有要單獨打包的模組output
的library
屬性可以將dll包暴露出來DLLPlugin
的配置中,path
指明manifest.json
檔案的生成路徑,name
暴露出dll的函式名
執行該配置檔案便可生成打包檔案和manifest.json
檔案。
DLLReferencePlugin
對於該外掛的配置,不需要像上面一樣單獨寫配置檔案,只需要在生產配置檔案中新增如下程式碼:
new webpack.DllReferencePlugin({
context: __dirname, // 同dll配置的路徑保持一致
manifest: require('../dist/dll/lib.manifest.json') // manifest的位置
}),
複製程式碼
然後執行webpack,發現打包的速度得到了極大地提升,也不用每次更新程式碼的時候重複編譯打包這些依賴庫了。
其他
對於webpack的打包優化我大致就總結了上面的一些方法,而為了讓頁面更快的載入,有更好的使用者體驗,我們並不只是從打包上優化,也可以有其他方面的優化,這裡我也簡單提一下我使用過的方法。
開啟Gzip壓縮
開啟gzip壓縮可以減少HTTP傳輸的資料量和時間,從而減少客戶端請求的響應時間,由於降低了請求時間,頁面的載入速度也會得到提升,會有更快的渲染速度,極大地改善了使用者體驗。由於現在基本上所有的主流瀏覽器都支援Gzip的壓縮方式,只需要對伺服器進行相關設定即可,這裡就不具體講如何配置伺服器。
壓縮混淆程式碼
我們平常也會對程式碼進行壓縮混淆,可以通過UglifyJS
等工具來對js程式碼進行壓縮,同時可以去掉不必要的空格、註釋、console資訊等,也可以有效的減小程式碼體積。
總結
本文到這裡就結束了,主要是對webpack的打包優化部分做了些講解,當然能力和時間有限,只研究了部分方法,可能會有其他更多的優化方法,無論是從編譯打包的體積還是速度上都能有更好的優化。接觸了一段時間的webpack發現作為一個打包工具實在是過於複雜,無論從開始的官方文件還是到新的高階特性,都很難去完全掌握,還得需要自己不斷去實踐去深入研究才行。