【第1303期】webpack4初探

前端早讀課2018-06-14 11:29:03

前言

webpack4發佈快半年了,不知道切換了嗎?今日早讀文章由騰訊@自然醒投稿分享。

@自然醒,前端新人,工作剛滿一年,目前就職於騰訊,鵝漫U品團隊。熱愛前端技術,希望在前端的路上越走越遠。

一、前言

2018/2/25,webpack4正式發佈,距離現在已經過去三個多月了,也逐漸趨於穩定,而且現在的最新版本都到了4.12.0(版本迭代快得真是讓人害怕)。

最新版本

很多人都說webpack複雜,難以理解,很大一部分原因是webpack是基於配置的,可配置項很多,並且每個參數傳入的形式多種多樣(可以是字符串、數組、對象、函數。。。),文檔介紹也比較模糊,這麼多的配置項各種排列組合,想想都複雜。而gulp基於流的方式來處理文件,無論從理解上,還是功能上都很容易上手。

//gulp
gulp
.src('./src/js/**/*.js')
.pipe('babel')
.pipe('uglifyjs')
.dest('./dist/js')
//webpack
module
.exports = {
 entry
: './src/main.js',
 output
: __dirname + '/dist/app.js',
 module
: {
   rules
: [{
     test
: /\.js$/,
     loader
: 'babel-loader'
   
}]
 
},
 plugins
: [
   
new require('uglifyjs-webpack-plugin')()
 
]
}

上面簡單對比了webpack與gulp配置的區別,當然這樣比較是有問題的,gulp並不能進行模塊化的處理。這裡主要是想告訴大家使用gulp的時候,我們能明確的知道js文件是先進行babel轉譯,然後進行壓縮混淆,最後輸出文件。而webpack對我們來說完全是個黑盒,完全不知道plugins的執行順序。正是因為這些原因,我們常常在使用webpack時有一些不安,不知道這個配置到底有沒有生效,我要按某種方式打包到底該如何配置?

為了解決上面的問題,webpack4引入了零配置的概念(Parcel ???),實際體驗下來還是要寫不少配置。
但是這不是重點,重點是官方宣傳webpack4能夠提升構建速度60%-98%,真的讓人心動。

二、到底怎麼升級

0、初始化配置

首先安裝最新版的webpack和webpack-dev-server,然後再安裝webpack-cli。webpack4將命令行相關的操作抽離到了webpack-cli中,所以,要使用webpack4,必須安裝webpack-cli。當然,如果你不想使用webpack-cli,社區也有替代方案webpack-command,雖然它與webpack-cli區別不大,但是還是建議使用官方推薦的webpack-cli。

npm i [email protected] webpack-dev-[email protected] --save-dev
npm i webpack
-cli --save-dev

webpack-cli除了能在命令行接受參數運行webpack外,還具備migrateinit功能。

migrate用來升級webpack配置,能將webpack1的api升級到webpack2,現在用處不大。

$ webpack-cli migrate ./webpack.config.js
Reading webpack config
Migrating config from v1 to v2
-    loaders: [
+      rules: [
-        loader: 'babel',
-          query: {
+            use: [{
+              loader: 'babel-loader'
+            }],
+            options: {
-              loader: ExtractTextPlugin.extract('style', 'css!sass')
+              use: ExtractTextPlugin.extract({
+                fallback: 'style',
+                use: 'css!sass'
+              })
? Are you sure these changes are fine? Yes
✔︎ New webpack v2 config file is at /home/webpack-cli/build/webpack.config.js

init可以快速生成一個webpack配置文件的模版,不過用處也不大,畢竟現在的腳手架都集成了webpack的配置。

webpack-cli init
1. Will your application have multiple bundles? No // 如果是多入口應用,可以傳入一個object
2. Which module will be the first to enter the application? [example: './src/index'] ./src/index // 程序入口
3. What is the location of "app"? [example: "./src/app"] './src/app'
4. Which folder will your generated bundles be in? [default: dist]
5. Are you going to use this in production? No
6. Will you be using ES2015? Yes //是否使用ES6語法,自動添加babel-loader
7. Will you use one of the below CSS solutions? SASS // 根據選擇的樣式類型,自動生成 loader 配置
8. If you want to bundle your CSS files, what will you name the bundle? (press enter to skip)
9. Name your 'webpack.[name].js?' [default: 'config']: // webpack.config.js
Congratulations! Your new webpack configuration file has been created!

更詳細介紹請查看webpack-cli的文檔

1、零配置

零配置就意味著webpack4具有默認配置,webpack運行時,會根據mode的值採取不同的默認配置。如果你沒有給webpack傳入mode,會拋出錯誤,並提示我們如果要使用webpack就需要設置一個mode。

沒有使用mode

The ‘mode’ option has not been set, webpack will fallback to ‘production’ for this value. Set ‘mode’ option to ‘development’ or ‘production’ to enable defaults for each environment.
You can also set it to ‘none’ to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/

mode一共有如下三種配置:

  • none, 這個配置的意思就是不使用任何默認配置

  • development,開發環境下的默認配置


module.exports = {
 
//開發環境下默認啟用cache,在內存中對已經構建的部分進行緩存
 
//避免其他模塊修改,但是該模塊未修改時候,重新構建,能夠更快的進行增量構建
 
//屬於空間換時間的做法
 cache
: true,
 output
: {
   pathinfo
: true //輸入代碼添加額外的路徑註釋,提高代碼可讀性
 
},
 devtools
: "eval", //sourceMap為eval類型
 plugins
: [
   
//默認添加NODE_ENV為development
   
new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") }),
 
],
 optimization
: {
   namedModules
: true, //取代插件中的 new webpack.NamedModulesPlugin()
   namedChunks
: true
 
}
}
  • production,生產環境下的默認配置


module.exports = {
 performance
: {
   hints
: 'warning',
   maxAssetSize
: 250000, //單文件超過250k,命令行告警
   maxEntrypointSize
: 250000, //首次加載文件總和超過250k,命令行告警
 
}
 plugins
: [
   
//默認添加NODE_ENV為production
   
new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") })
 
],
 optimization
: {
   minimize
: true, //取代 new UglifyJsPlugin(/* ... */)
   providedExports
: true,
   usedExports
: true,
   
//識別package.json中的sideEffects以剔除無用的模塊,用來做tree-shake
   
//依賴於optimization.providedExports和optimization.usedExports
   sideEffects
: true,
   
//取代 new webpack.optimize.ModuleConcatenationPlugin()
   concatenateModules
: true,
   
//取代 new webpack.NoEmitOnErrorsPlugin(),編譯錯誤時不打印輸出資源。
   noEmitOnErrors
: true
 
}
}

其他的一些默認值:

module.exports = {
 context
: process.cwd()
 entry
: './src',
 output
: {
   path
: 'dist',
   filename
: '[name].js'
 
},
 rules
: [
   
{
     type
: "javascript/auto",
     resolve
: {}
   
},
   
{
     test
: /\.mjs$/i,
     type
: "javascript/esm",
     resolve
: {
       mainFields
:
       options
.target === "web" ||
       options
.target === "webworker" ||
       options
.target === "electron-renderer"
         
? ["browser", "main"]
         
: ["main"]
     
}
   
},
   
{
     test
: /\.json$/i,
     type
: "json"
   
},
   
{
     test
: /\.wasm$/i,
     type
: "webassembly/experimental"
   
}
 
]
}

如果想查看更多webpack4相關的默認配置,到這裡來。可以看到webpack4把很多插件相關的配置都遷移到了optimization中,但是我們看看官方文檔對optimization的介紹簡直寥寥無幾,而在默認配置的代碼中,webpack對optimization的配置有十幾項,反正我是怕了。

文檔對optimization的介紹

雖然api發生了一些變化,好的一面就是有了這些默認值,我們想通過webpack構建一個項目比以前要簡單很多,如果你只是想簡單的進行打包,在package.json中添加如下兩個script,包你滿意。

{
 
"scripts": {
   
"dev": "webpack-dev-server --mode development",
   
"build": "webpack --mode production"
 
},
}

開發環境使用webpack-dev-server,邊預覽邊打包再也不用f5,簡直爽歪歪;生產環境直接生成打包後的文件到dist目錄

2、loader與plugin的升級

loader的升級就是一次大換血,之前適配webpack3的loader都需要升級才能適配webpack4。如果你使用了不兼容的loader,webpack會告訴你:

DeprecationWarning: Tapable.apply is deprecated. Call apply on the plugin directly instead

DeprecationWarning: Tapable.plugin is deprecated. Use new API on .hooks instead

如果在運行過程中遇到這兩個警告,就表示你有loader或者plugin沒有升級。造成這兩個錯誤的原因是,webpack4使用的新的插件系統,並且破壞性的對api進行了更新,不過好在這只是警告,不會導致程序退出,不過建議最好是進行升級。對於loader最好全部進行一次升級,反正也不虧,百利而無一害。

關於plugin,有兩個坑,一個是extract-text-webpack-plugin,還一個是html-webpack-plugin

先說說extract-text-webpack-plugin,這個插件主要用於將多個css合併成一個css,減少http請求,命名時支持contenthash(根據文本內容生成hash)。但是webpack4使用有些問題,所以官方推薦使用mini-css-extract-plugin

⚠️ Since webpack v4 the extract-text-webpack-plugin should not be used for css. Use mini-css-extract-plugin instead.

這裡改動比較小,只要替換下插件,然後改動下css相關的loader就行了:

-const ExtractTextPlugin = require('extract-text-webpack-plugin')
+const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
 
module: {
   rules
: [
     
{
       test
: /\.css$/,
-       use: ExtractTextPlugin.extract({
-         use: [{
-           loader: 'css-loader',
-           options: {
-             minimize: process.env.NODE_ENV === 'production'
-           }
-         }],
-         fallback: 'vue-style-loader'
-       })
+       use: [
+         MiniCssExtractPlugin.loader,
+         {
+           loader: 'css-loader',
+           options: {
+           minimize: process.env.NODE_ENV === 'production'
+         }
+       ],
     
}
   
]
 
},
 plugins
:[
-   new ExtractTextPlugin({
+   new MiniCssExtractPlugin({
     filename
: 'css/[name].css',
   
}),
 
...
 
]
}

然後看看html-webpack-plugin,將這個插件升級到最新版本,一般情況沒啥問題,但是有個坑,最好是把chunksSortMode這個選項設置為none。

const HtmlWebpackPlugin = require('html-webpack-plugin')
module
.exports = {
 plugins
:[
   
new HtmlWebpackPlugin({
     filename
: 'index.html',
     template
: 'index.html',
     inject
: true,
     hash
: true,
     chunksSortMode
: 'none' //如果使用webpack4將該配置項設置為'none'
   
})
 
]
}

官方有個issues討論了這個問題,感興趣可以去看看。目前作者還在尋找解決方案中。

html-webpack-plugin issues

另外,webpack-dev-server也有個升級版本,叫做webpack-serve,功能比webpack-dev-server強大,支持HTTP2、使用WebSockets做熱更新,暫時還在觀望中,後續採坑。

3、webpack4的模塊拆分

webpack3中,我們經常使用CommonsChunkPlugin進行模塊的拆分,將代碼中的公共部分,以及變動較少的框架或者庫提取到一個單獨的文件中,比如我們引入的框架代碼(vue、react)。只要頁面加載過一次之後,抽離出來的代碼就可以放入緩存中,而不是每次加載頁面都重新加載全部資源。

CommonsChunkPlugin的常規用法如下:

module.exports = {
 plugins
: [
   
new webpack.optimize.CommonsChunkPlugin({ //將node_modules中的代碼放入vendor.js中
     name
: "vendor",
     minChunks
: function(module){
       
return module.context && module.context.includes("node_modules");
     
}
   
}),
   
new webpack.optimize.CommonsChunkPlugin({ //將webpack中runtime相關的代碼放入manifest.js中
     name
: "manifest",
     minChunks
: Infinity
   
}),
 
]
}

之前CommonsChunkPlugin雖然能用,但是配置不夠靈活,難以理解,minChunks有時候為數字,有時候為函數,並且如果同步模塊與異步模塊都引入了相同的module並不能將公共部分提取出來,最後打包生成的js還是存在相同的module。

現在webpack4使用optimization.splitChunks來進行代碼的拆分,使用optimization.runtimeChunk來提取webpack的runtime代碼,引入了新的cacheGroups概念。並且webpack4中optimization提供如下默認值,官方稱這種默認配置是保持web性能的最佳實踐,不要手賤去修改,就算你要改也要多測試(官方就是這麼自信)。

module.exports = {
 optimization
: {
   minimize
: env === 'production' ? true : false, //是否進行代碼壓縮
   splitChunks
: {
     chunks
: "async",
     minSize
: 30000, //模塊大於30k會被抽離到公共模塊
     minChunks
: 1, //模塊出現1次就會被抽離到公共模塊
     maxAsyncRequests
: 5, //異步模塊,一次最多隻能被加載5個
     maxInitialRequests
: 3, //入口模塊最多隻能加載3個
     name
: true,
     cacheGroups
: {
       
default: {
         minChunks
: 2,
         priority
: -20
         reuseExistingChunk
: true,
       
},
       vendors
: {
         test
: /[\\/]node_modules[\\/]/,
         priority
: -10
       
}
     
}
   
},
   runtimeChunk
{
     name
: "runtime"
   
}
 
}
}

有了這些默認配置,我們幾乎不需要任何成功就能刪除之前CommonChunkPlugin的代碼,好神奇。

什麼模塊會進行提取?

通過判斷splitChunks.chunks的值來確定哪些模塊會提取公共模塊,該配置一共有三個選項,initialasync all
默認為async,表示只會提取異步加載模塊的公共代碼,initial表示只會提取初始入口模塊的公共代碼,all表示同時提取前兩者的代碼。

這裡有個概念需要明確,webpack中什麼是初始入口模塊,什麼是異步加載模塊。e.g.

//webpack.config.js
module
.exports = {
 entry
: {
   main
: 'src/index.js'
 
}
}
//index.js
import Vue from 'vue'
import(/* webpackChunkName: "asyncModule" */'./a.js')
 
.then(mod => {
   console
.log('loaded module a', mod)
 
})
console
.log('initial module')
new Vue({})
//a.js
import _ from 'lodash'
const obj = { name: 'module a' }
export default _.clone(obj)

上面的代碼中,index.js在webpack的entry配置中,這是打包的入口,所以這個模塊是初始入口模塊。再看看index.js中使用了動態import語法,對a.js(該異步模塊被命名為asyncModule)進行異步加載,則a.js就是一個異步加載模塊。再看看index.jsa.js都有來自node_modules的模塊,按照之前的規則,splitChunks.chunks默認為async,所以會被提取到vendors中的只有webpackChunkName中的模塊。

chunks為async

如果我們把splitChunks.chunks改成all,main中來自node_modules的模塊也會被進行提取了。

module.exports = {
 optimization
: {
   splitChunks
: {
     chunks
: "all"
   
}
 
}
}
chunks為all

現在我們在index.js中也引入lodash,看看入口模塊和異步模塊的公共模塊還會不會像CommonsChunkPlugin一樣被重複打包。

//index.js
import Vue from 'vue'
import _ from 'lodash'
import(/* webpackChunkName: "asyncModule" */'./a.js')
 
.then(mod => {
   console
.log('loaded module a', mod)
 
})
console
.log('initial module')
console
.log(_.map([1,2,3], a => {
   
return a * 10
}))
new Vue({})
//a.js
import _ from 'lodash'
const obj = { name: 'module a' }
export default _.clone(obj)
解決了CommonsChunkPlugin的問題

可以看到之前CommonsChunkPlugin的問題已經被解決了,main模塊與asyncModule模塊共同的lodash都被打包進了vendors~main.js中。

提取的規則是什麼?

splitChunks.cacheGroups配置項就是用來表示,會提取到公共模塊的一個集合,也就是一個提取規則。像前面的vendor,就是webpack4默認提供的一個cacheGroup,表示來自node_modules的模塊為一個集合。

除了cacheGroups配置項外,可以看下其他的幾個默認規則。

  1. 被提取的模塊必須大於30kb;

  2. 模塊被引入的次數必須大於1次;

  3. 對於異步模塊,生成的公共模塊文件不能超出5個;

  4. 對於入口模塊,抽離出的公共模塊文件不能超出3個。

對應到代碼中就是這四個配置:

{
   minSize
: 30000,
   minChunks
: 1,
   maxAsyncRequests
: 5,
   maxInitialRequests
: 3,
}

三、贈送webpack常見優化方式

1、一個人不行,大家一起上

webpack是一個基於node的前端打包工具,但是node基於v8運行時只能是單線程,但是node中能夠fork子進程。所以我們可以使用多進程的方式運行loader,和壓縮js,社區有兩個插件就是專門幹這兩個事的:HappyPack、ParallelUglifyPlugin。

使用HappyPack

const path = require('path')
module
.exports = {
 module
: {
   rules
: [
     
{
       test
: /\.js$/,
       
// loader: 'babel-loader'
       loader
: 'happypack/loader?id=babel'
     
}
   
]
 
},
 plugins
: [
   
new require('happypack')({
     id
: 'babel',
     loaders
: ['babel-loader']
   
}),
 
],
};

使用ParallelUglifyPlugin

module.exports = {
 optimization
: {
   minimizer
: [
     
new require('webpack-parallel-uglify-plugin')({
       
// 配置項
     
}),
   
]
 
}
}
2、打包再打包

使windows的時候,我們經常會看到一些.dll文件,dll文件被稱為動態鏈接庫,裡面包含了程序運行時的一些動態函數庫,多個程序可以共用一個dll文件,可以減少程序運行時的物理內存。

webpack中我們也可以引入dll的概念,使用DllPlugin插件,將不經常變化的框架代碼打包到一個js中,比如叫做dll.js。在打包的過程中,如果檢測到某個塊已經在dll.js中就不會再打包。之前DllPlugin與CommonsChunkPlugin並能相互兼容,本是同根生相煎何太急。但是升級到webpack4之後,問題就迎刃而解了。

使用DllPlugin的時候,要先寫另外一個webpack配置文件,用來生成dll文件。

//webpack.vue.dll.js
const path = require('path')
module
.exports = {
 entry
: {
   
// 把 vue 相關模塊的放到一個單獨的動態鏈接庫
   vue
: ['vue', 'vue-router', 'vuex', 'element-ui']
 
},
 output
: {
   filename
: '[name].dll.js', //生成vue.dll.js
   path
: path.resolve(__dirname, 'dist'),
   library
: '_dll_[name]'
 
},
 plugins
: [
   
new require('webpack/lib/DllPlugin')({
     name
: '_dll_[name]',
     
// manifest.json 描述動態鏈接庫包含了哪些內容
     path
: path.join(__dirname, 'dist', '[name].manifest.json')
   
}),
 
],
};

然後在之前的webpack配置中,引入dll。

const path = require('path')
module
.exports = {
 plugins
: [
   
// 只要引入manifest.json就能知道哪些模塊再dll文件中,在打包過程會忽略這些模塊
   
new require('webpack/lib/DllReferencePlugin')({
     manifest
: require('./dist/vue.manifest.json'),
   
})
 
],
 devtool
: 'source-map'
};

最後生成html文件的時候,一定要先引入dll文件。

<html>
   
<head>
       
<meta charset="UTF-8">
   
</head>
   
<body>
       
<div id="app"></div>
       
<script src="./dist/vue.dll.js"></script>
       
<script src="./dist/main.js"></script>
   
</body>
</html>
3、你胖你先跑,部分代碼預先運行

前面的優化都是優化打包速度,或者減少重複模塊的。這裡有一種優化方式,能夠減少代碼量,並且減少客戶端的運行時間。

使用Prepack,這是facebook開源的一款工具,能夠運行你的代碼中部分能夠提前運行的代碼,減少在線上真實運行的代碼。

官方的demo如下:

//input
(function () {
 
function hello() { return 'hello'; }
 
function world() { return 'world'; }
 global
.s = hello() + ' ' + world();
})();
//output
s
= "hello world";

想在webpack中接入也比較簡單,社區以及有了對應的插件prepack-webpack-plugin,目前正式環境運用較少,還有些坑,可以繼續觀望。

module.exports = {
 plugins
: [
   
new require('prepack-webpack-plugin')()
 
]
};

這裡簡單羅列了一些webpack的優化策略,但是有些優化策略還是還是要酌情考慮。比如多進程跑loader,如果你項目比較小,開了之後可能變慢了,因為本來打包時間就比較短,用來fork子進程的時間,說不定都已經跑完了。記住過早的優化就是萬惡之源

四、總結

webpack4帶了很多新的特性,也大大加快的打包時間,並且減少了打包後的文件體積。期待webpack5的更多新特性,比如,以html或css為文件入口(鄙人認為html才是前端模塊化的真正入口,瀏覽器的入口就是html,瀏覽器在真正的親爹,不和爹親和誰親),默認開啟多進程打包,加入文件的長期緩存,更多的拓展零配置。

同時也要感謝前端社區其它的優秀的打包工具,感謝rollup,感謝parcel。

五、參考

  • webpack 為什麼這麼難用?

  • Webpack 4進階

  • RIP CommonsChunkPlugin

  • webpack 4: mode and optimization

  • webpack 4 不完全遷移指北


關於本文
作者:@自然醒
原文:
https://blog.shenfq.com/2018/06/09/webpack4初探/

最後,為你推薦


【第1241期】webpack4升級完全指南


【第1203期】webpack 4 發佈了!

閱讀原文

TAGS:使用模塊配置打包