对于开发者的不友好
我们要如何开发一个 webpack 的插件?
官方文档里确实写了一些关于如何开发插件的指南。但这份指南也只有 60 分刚及格的水平,它确实向你介绍了 webpack 插件的基础范例、基本概念以及一些 API,但当你读完这份简短的文档后想自己真的去开发一个插件时,你会发现文档里讲的东西真的远远不够。
我们不妨来看看现在 webpack 生态里那些成熟的插件是怎么写的,以 html-webpack-plugin 为例,这是一个广泛用于生成 html 文件的插件。在它的源码里你会发现,它引用了五个 webpack 内部自带的插件(源码在这里):
代码语言:javascript复制var NodeTemplatePlugin = require('webpack/lib/node/NodeTemplatePlugin');
var NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin');
var LoaderTargetPlugin = require('webpack/lib/LoaderTargetPlugin');
var LibraryTemplatePlugin = require('webpack/lib/LibraryTemplatePlugin');
var SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
嗯哼?这五个插件都是用来干什么的?
官方文档上对内置的插件一个字都没有提及,是的,甚至连 Plugins 这里都没有。官方的 wiki 上倒是写了,但真的真的太简略了,而且看起来很久没更新了。
再看另外一个同样常用的 uglifyjs-webpack-plugin,它倒是没依赖 webpack 的内置插件,不过也引用了 webpack 内部的两个文件:
代码语言:javascript复制import RequestShortener from 'webpack/lib/RequestShortener';
import ModuleFilenameHelpers from 'webpack/lib/ModuleFilenameHelpers';
文档里同样没有对这两个文件做任何介绍。令人欣慰(?)的是,这两个文件从文件名上看,起码是方法库(实际上也确实是),使用起来不会太复杂。
换句话说,如果你想给 webpack 写一个广为人知的插件,你就必须深入了解 webpack 的全部,这一点我不反对,毕竟 webpack 开发者和 webpack 使用者在能力的要求上有高低之分。但即使是有经验的开发者,遇到一个文档如此不完善的开源项目,也是很吃力的。很多能帮助开发者的东西本应该正大光明地写在文档和指南里,而不是隐藏在源代码里。
二、过重的插件体系
插件体系是 webpack 的核心,事实上,webpack 的大部分功能都是通过内部插件或者第三方插件来完成的。可以说,webpack 的生态就是建立在众多插件之上的。
但插件体系也同样有很多问题。
插件数量问题
先问一个问题,一个通过 webpack 构建的项目需要多少插件?
还是以一个标准的 vue-cli 生成的脚手架项目为例,一共有 7 个第三方插件:
代码语言:javascript复制"copy-webpack-plugin": "^4.0.1",
"extract-text-webpack-plugin": "^3.0.0",
"friendly-errors-webpack-plugin": "^1.6.1",
"html-webpack-plugin": "^2.30.1",
"webpack-bundle-analyzer": "^2.9.0",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"uglifyjs-webpack-plugin": "^1.1.1",
以及 7 个 webpack 内置插件:
HashedModuleIdsPlugin
ModuleConcatenationPlugin
CommonsChunkPlugin
DefinePlugin
HotModuleReplacementPlugin
NamedModulesPlugin
NoEmitOnErrorsPlugin
总共 14 个插件,我们按照平均一个插件含有 2-3 个配置项(这已经是往低了算了)来计算,14 个插件就有 30 多项配置,这已经是一个现代 webpack 开发、构建使用的很基础的配置了,真实的项目只会比这个更多。
要注意到,30 多个配置项带来的复杂程度是远胜于 30 行代码的。 因为配置项已经具有了比较高的抽象性,一项配置包含的副作用是要远大于一行代码的。比如下面是常常用于提取公共模块的 CommonsChunkPlugin
的配置:
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
async: 'vendor-async',
children: true,
minChunks: 3
})
如果你不是一个 webpack 老手的话,看到这 4 项配置肯定是一脸懵逼的:
name
该填什么?随便命个名就好吗?async
是什么?异步模块?那为什么是个字符串?children
是个啥?为什么不是Array
而是个boolean
?minChunks
这个数字是什么?chunk
又是什么?
然后你就去看了 CommonsChunkPlugin 的文档,十五分钟艰难的阅读之后,你会发现这四项配置都不简单,每一项的更改会给构建带来很大的影响。
然而坏消息是,像这样的配置在项目里整整有 30 多处!
所以我每次改一个项目的构建时,基本都是这样的:
面向配置的插件
在讨论这个话题之前,先回答两个问题:
- webpack 的插件先后顺序会影响构建结果吗?
- 如果插件顺序不同,会影响哪些东西?
实际上,这两个问题我找遍了官方文档,也没有提到插件的顺序会影响哪些东西,stackoverflow 上倒是找到了一个问题:Webpack: Does the order of plugins matter?
所以回答就是:插件的顺序有影响,但作用不明。
其实问题不止在插件的顺序先后上,就连一个插件到底对构建产生了哪些影响,我们也很难得知,除非你极其熟悉这个插件或者就是这个插件的作者。为什么会这样?根本原因就是,webpack 的插件是面向配置的,而不是面向过程的
什么叫面向过程?如果你知道或者使用过 gulp 这个自动化工具的话,应该会记得 gulp 管道的概念,即从源头那里得到源数据(js/css/html 源码、图片、字体等等),然后数据通过一个又一个组合起来的管道,最后输出成为构建的结果。写成伪代码的话,大概是这样:
代码语言:javascript复制gulp.src('某些源文件')
.pipe(处理一)
.pipe(处理二)
.pipe(处理三)
.dest('构建结果')
这种管道化,或者说面向过程的构建,非常容易 debug 或者修改,因为它构建的每一步过程,都整齐的按照顺序展示给你看了。想要修改其中任何一步的心智负担是很低的,因为它的处理机制非常纯函数。
然而如果是 webpack 的话,就类似这样:
代码语言:javascript复制{
plugins: [
插件一,
插件二,
插件三
]
}
这里,插件一二三是完全面向配置的,没有告诉你任何执行顺序,它们可能会在 webpack 构建的每个时间点触发,你只能从它们的功能上大致猜出它是在哪个时间点工作的。这就是为什么修改一些 webpack 的配置,就像要解开一条放在包里很久的耳机线一样,麻烦又闹心。
当然,这种配置化的插件也是有好处的,配置化代表了高集成度,当你只有 1-3 个插件时,维护这些配置的心智负担是可以接受的,并且比维护面向过程的配置更加方便。但当插件数量超过这个值的时候,构建的复杂程度就会呈指数式上升,我们之前就已经提到了,一个现代的 webpack 项目起码会有 14 个以上的插件以及至少 30 多项配置,这种情况下,面向过程就会好于面向配置,这就是为什么我一直觉得 gulp webpack 才是正确解决方案的原因。
当然还是要说一句,gulp 和 webpack 并不能直接比较,前者是一个 task runner,而后者是一个 module bundler,它们两者之间都有一些相互不可替代的功能。