显微镜下的webpack4的新特性:mode详解

5,255 阅读7分钟

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 混淆压缩