顯微鏡下的webpack4的新特性:mode詳解

小美娜娜發表於2018-10-18

webpack4支援的一個新特性就是zero配置,不需要config,也可以打包,這對於懶癌患者很有誘惑力,但是這也意味著我們不清楚零配置發生了寫什麼,也不知道打包出來的檔案是否符合我們的心意,全部都是佛系打包。不過作為專案的親爹親媽,還是要對自己的孩子負責,每個打包過程都是要可控的。本文就是詳解不同mode下,webpack打包都發生了些什麼事。

我們來看一下MODE這個引數,他有三個引數productiondevelopmentnone,前兩個是有預設的外掛,而最後一個則是什麼都沒有,也就是說設定為none的話,webpack就是最初的樣子,無任何預設,需要從無到有開始配置。

在webpack的配置中,其他配置都可以沒有!但是mode是必備的,如果不加mode,官方雖然會打包,但同時也會給你一個警告:

WARNING in configuration 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: webpack.js.org/concepts/mo…

意思很簡單,就是mode沒有被設定的情況下,系統就會給你一個預設的production模式。

mode配置很簡單,就只有3個值,任君挑選。none這個引數,相信大家都能理解,那麼我們就研究下其他兩個productiondevelopment,這為什麼要有這兩個狀態,以及兩者在webpack打包中都幹了些啥事。

如何在打包中區分productiondevelopment的狀態

在mode為productiondevelopment的狀態下,為了兼顧兩個狀態下的程式執行,webpack建立了一個全域性變數process.env.NODE_ENV,等同於在外掛plugins中加入了new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development|production") }),用來區分不同的狀態,同時可以在程式中區分程式狀態。

那麼我們該如何在coding的時候進行區分呢?因為process.env.NODE_ENV是全域性變臉給,所以可以這樣來引用值,假設mode:production

if ("development" === process.env.NODE_ENV){
    ....
}else{
    ....
}
複製程式碼

編譯之後:

if ("development" === "production"){
    ....
}else{
    ....
}
複製程式碼

也就是最後process.env.NODE_ENV會被替換為一個常量。這個小功能可以幫助我們在寫業務JS的時候,區分線上版本與開發版本。

none模式下的模組打包

在沒有任何優化處理的情況下,按照webpack預設的情況下打包出來的模組是怎麼樣的呢?下方是一個簡易的例子,我們可以看出,他將模組打包至陣列之中,呼叫模組的時候,就是直接呼叫模組在此陣列中的一個序號。然後沒有壓縮混淆之類的優化,連註釋都幫我們標的好好的,比如匯入 /* harmony import / ,/ harmony default export */。

[
    /* 0 */
    (function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    __webpack_require__.r(__webpack_exports__);
    /* harmony import */ var _page2_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
    console.log(_page2_js__WEBPACK_IMPORTED_MODULE_0__["default"])
    }),
    /* 1 */
    (function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    __webpack_require__.r(__webpack_exports__);
    let str="page1"
    /* harmony default export */ __webpack_exports__["default"] = (str);
    })
]
複製程式碼

但是無論是在開發環境development下,還是在正式壞境production下,這個程式碼都是不過關的,對於開發環境,此程式碼可讀性太差,對於正式環境,此程式碼不夠簡潔,因此,為了減少一些重複操作,webpack4提供的development|production可以很大程度上幫我們做掉一大部分事,我們要做的就是在這些事的基礎上加功能。

development模式下,webpack做了那些打包工作

development是告訴程式,我現在是開發狀態,也就是打包出來的內容要對開發友好。在此mode下,就做了以下外掛的事,其他都沒做,所以這些外掛可以省略。

// webpack.development.config.js
module.exports = {
+ mode: 'development'
- devtool: 'eval',
- plugins: [
-   new webpack.NamedModulesPlugin(),
-   new webpack.NamedChunksPlugin(),
-   new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") }),
- ]
}
複製程式碼

我們看看NamedModulesPluginNamedChunksPlugin這兩個外掛都做了啥,原本我們的webpack並不會給打包的模組加上姓名,一般都是按照序號來,從0開始,然後載入第幾個模組。這個對機器來說無所謂,查詢載入很快,但是對於人腦來說就是災難了,所以這個時候給各個模組加上姓名,便於開發的時候查詢。

沒有NamedModulesPlugin,模組就是一個陣列,引用也是按照在陣列中的順序引用,新增減模組都會導致序號的變化,就是webpack預設打包下的情況,參考上一節。

有了NamedModulesPlugin,模組都擁有了姓名,而且都是獨一無二的key,不管新增減多少模組,模組的key都是固定的。

{

"./src/index.js":   (function(module, __webpack_exports__, __webpack_require__) {
                        "use strict";
                        __webpack_require__.r(__webpack_exports__);
                        var _page2_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/page2.js");
                        console.log(_page2_js__WEBPACK_IMPORTED_MODULE_0__["default"])
                    }),
"./src/page2.js":    (function(module, __webpack_exports__, __webpack_require__) {
                        "use strict";
                        __webpack_require__.r(__webpack_exports__);
                        let str="page1"
                         __webpack_exports__["default"] = (str);
                    })
}
複製程式碼

除了NamedModulesPlugin,還有一個NamedChunksPlugin,這個是給配置的每個chunks命名,原本的chunks也是陣列,沒有姓名。

             Asset      Size  Chunks             Chunk Names
          index.js  4.04 KiB       0  [emitted]  index
          page2.js  3.75 KiB       1  [emitted]  page2
複製程式碼
             Asset      Size           Chunks             Chunk Names
          index.js   4.1 KiB            index  [emitted]  index
          page1.js  4.15 KiB            page1  [emitted]  page1
複製程式碼

NamedChunksPlugin其實就提供了一個功能就是你可以自定義chunks的名字,假如我再不同的包中有相同chunk名,怎麼辦?這個時候就要在進行區分了,我麼可以用所有的依賴模組名加本上的模組名。因為Chunk.modules已經廢棄了,現在用其他的方法來代替chunk.mapModules,然後重新命名chunk的名字:

new webpack.NamedChunksPlugin((chunk) => {
    return chunk.mapModules(m => {
        return path.relative(m.context, m.request)
    }).join("_")
}),      
複製程式碼

看一眼做這一行程式碼的效果,我們可以看到Chunks這邊已經重新命名了,這樣可以很大程度上解決chunks重名的問題:

             Asset      Size             Chunks             Chunk Names
          index.js   4.1 KiB  index.js_page2.js  [emitted]  index
          page2.js  3.78 KiB           page2.js  [emitted]  page2
複製程式碼

總結:development也就給我們省略了命名的過程,其他的還是要自己加的。

production

在正式版本中,所省略的外掛們,如下所示,我們會一個個分析。

// webpack.production.config.js
module.exports = {
+  mode: 'production',
-  plugins: [
-    new UglifyJsPlugin(/* ... */),
-    new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }),
-    new webpack.optimize.ModuleConcatenationPlugin(),
-    new webpack.NoEmitOnErrorsPlugin()
-  ]
}
複製程式碼

UglifyJsPlugin

我們第一個需要處理的就要混淆&壓縮JS了吧,這個時候就要請出UglifyJs了,在webpack中他的名字是const UglifyJsPlugin = require('uglifyjs-webpack-plugin');,這樣就可以使用他了。

不過new UglifyJsPlugin(),這個外掛我們可以在optimize中配置,效果是一樣的,那麼我們是不是就不用再匯入一個新的外掛了,這樣反而會拖慢webpack的就打包速度。

optimization:{
    minimize: true,
},
複製程式碼

將外掛去除,混淆壓縮放入optimization,這樣webpack速度快的飛起了。只有第一次打包會慢,之後再打包就快了。

ModuleConcatenationPlugin

webpack.optimize.ModuleConcatenationPlugin()這個外掛的作用是什麼呢?官方文件上是這麼描述的:

記住,此外掛僅適用於由 webpack 直接處理的 ES6 模組。在使用轉譯器(transpiler)時,你需要禁用對模組的處理(例如 Babel 中的 modules 選項)。

NoEmitOnErrorsPlugin

最後一個外掛就是webpack.NoEmitOnErrorsPlugin(),這個就是用於防止程式報錯,就算有錯誤也給我繼續編譯,很暴力的做法呢。

others

還有一些預設的外掛配置,也就是可以不在plugins中引用的配置:

flagIncludedChunks

flagIncludedChunks這個配置的作用是,看結果:

未啟用

   Asset       Size  Chunks             Chunk Names
index.js   1.02 KiB       0  [emitted]  index
page1.js  970 bytes       1  [emitted]  page1
複製程式碼

啟用後,如果只有二個檔案似乎表現不明顯,於是我增加了三個檔案,page1呼叫page2,index呼叫page1,那麼一目瞭然,在這裡的chunks就是所有引用模組的id。

  Asset       Size   Chunks             Chunk Names
index.js   1.08 KiB  0, 1, 2  [emitted]  index
page1.js   1.01 KiB     1, 2  [emitted]  page1
page2.js  971 bytes        2  [emitted]  page2
複製程式碼

OccurrenceOrderPlugin

webpack.optimize.OccurrenceOrderPlugin這個外掛的作用是按照chunk引用次數來安排出現順序,因為這讓經常引用的模組和chunk擁有更小的id。將上面的例子加上這個配置執行下就是這樣的。

   Asset       Size   Chunks             Chunk Names
page2.js  969 bytes        0  [emitted]  page2
page1.js   1.01 KiB     1, 0  [emitted]  page1
index.js   1.08 KiB  2, 0, 1  [emitted]  index
複製程式碼

SideEffectsFlagPlugin

webpack.optimize.SideEffectsFlagPlugin()這個外掛如果需要生效的話,需要兩個條件,一個是匯入的模組已經標記了sideEffect,即package.json中的sideEffects這個屬性為false,第二個就是當前模組引用了次無副作用的模組,而且沒有使用。那麼在打包的時候,就不會將這個模組打包到檔案中。

總結

實際上production mode下,與官方文件相比,他的配置更加等同於如下配置:

module.exports = {
    mode:"none",
    optimization:{
        flagIncludedChunks:true,
        minimize: true,
    },
    plugins: [
        new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }),
        new webpack.optimize.ModuleConcatenationPlugin(),
        new webpack.NoEmitOnErrorsPlugin(),
        new webpack.optimize.SideEffectsFlagPlugin()
    ]
}
複製程式碼

production各外掛參考文件

name effect
FlagDependencyUsagePlugin 標記沒有用到的依賴,這個外掛無法通過webpack獲取,我只能通過強行匯入webpack/lib下的class檔案來匯入。
SideEffectsFlagPlugin 用於處理tree shaking的,tree shakingsideEffect這個外掛的作用就是,如果當前的模組沒有引用,而且package.json中的sideEffects為false,那麼打包的時候就可以將此包剔除。stackoverflow上有用的答案
FlagIncludedChunksPlugin 給當前chunk包含的chunkid加入chunk名之中
ModuleConcatenationPlugin 作用域提升
NoEmitOnErrorsPlugin 阻止任何報錯
OccurrenceOrderPlugin 按照呼叫次數來給chunks排序
UglifyJsPlugin 混淆壓縮

相關文章