webpack 为什么这么难用?

如今对于每一个前端工程师来说,webpack 已经成为了一项基础技能,它基本上包办了本地开发、编译压缩、性能优化的所有工作,从这个角度上来说,webpack 确实是伟大的,它的诞生意味着一整套工程化体系开始普及,并且慢慢统一了前端自动构建的让前端开发彻底告别了之前的刀耕火种时代。现在 webpack 之于前端开发,正如同 gcc/g++ 之于 C/C++,是一个你无论如何都绕不开的工具。

但是,即使它如此伟大,也有一个巨大的问题,那就是 webpack 实在是太难用了!!!

我从多年前的 webpack 1.0 时代就一直在用它,现在也不能说完全掌握了它,很多时候真的让我产生了怀疑,究竟是因为我的能力不足,还是因为 webpack 自身的设计就太难用?随着我接触到越来越多的前端项目,听到越来越多的吐槽,我也越发地相信,是 webpack 自身的问题,导致它变得如此复杂又难用。

举个简单的例子,一个 vue-cli 生成的最简单的脚手架项目,开发、构建相关的文件就有 14 个之多,代码超过 800 行,而真实的项目只会比这个更多:

所以,既然这篇文章的标题是《webpack 为什么这么难用?》,那我们就好好在这里分析一下,webpack 难用的根本原因。


一、文档极其不完善

是的,这就是第一位的原因。

我作为参加过 webpack 中文文档翻译的人,真的想说 webpack 即使经过了这么多年的不断迭代,如今的文档依然还是是一坨那啥。作为一个开源项目,设计好不好、易用性怎么样、扩展性怎么样这些问题都是仁者见仁智者见智的,但文档写得很烂这一点上,真的没有任何可以开脱的理由。

对于使用者的不友好

比如,webpack 的插件体系可以说是 webpack 最核心的一部分功能了,基本上一个项目的构建中,大部分任务都是由各种插件完成的。然而,官方文档上对于插件的介绍只有寥寥几句话:webpack · Plugins,甚至推荐你直接去看 webpack 的源码:

更糟的是,现有的文档里(包括 webpack 一些插件的文档也是),大部分内容都是在告诉你 “你这样做就可以了”,而没有解释 “你为什么需要这么做” 以及 “你这么做了会有哪些后果”。

比如,在 target 配置上,官方文档里列举了你可以构建到哪些 target,如 nodenode-webkitelectron-main,但都只是简单的一句话带过:

想知道 target 为 electron-main 时,和浏览器环境的打包有什么不同?对不起,官方文档不想告诉你,看源码或者去 stackoverflow 上搜吧。

官方文档语焉不详的直接后果就是,当你遇到了任何问题,你都没办法在文档里得到直接的回答,而是需要看无数的代码、github issue、stackoverflow、博客文章,然后在自己的项目里反反复复试了好多次,才能大致解决问题。而这种所谓的“解决问题”,一般都是个人经验性的,意味着其它任何一个人想要解决这个问题,都要重复一遍这个流程,时间成本大量上升。

这就是为什么使用 webpack 的时候,经常会出现下面的哲学三问:

  • 这是 webpack 的问题吗?
  • 我要怎么解决这个问题?
  • 咦我是怎么解决的?

对于开发者的不友好

我们要如何开发一个 webpack 的插件?

官方文档里确实写了一些关于如何开发插件的指南。但这份指南也只有 60 分刚及格的水平,它确实向你介绍了 webpack 插件的基础范例、基本概念以及一些 API,但当你读完这份简短的文档后想自己真的去开发一个插件时,你会发现文档里讲的东西真的远远不够。

我们不妨来看看现在 webpack 生态里那些成熟的插件是怎么写的,以 html-webpack-plugin 为例,这是一个广泛用于生成 html 文件的插件。在它的源码里你会发现,它引用了五个 webpack 内部自带的插件源码在这里):

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 内部的两个文件:

import RequestShortener from 'webpack/lib/RequestShortener';
import ModuleFilenameHelpers from 'webpack/lib/ModuleFilenameHelpers';

文档里同样没有对这两个文件做任何介绍。令人欣慰(?)的是,这两个文件从文件名上看,起码是方法库(实际上也确实是),使用起来不会太复杂。

换句话说,如果你想给 webpack 写一个广为人知的插件,你就必须深入了解 webpack 的全部,这一点我不反对,毕竟 webpack 开发者webpack 使用者在能力的要求上有高低之分。但即使是有经验的开发者,遇到一个文档如此不完善的开源项目,也是很吃力的。很多能帮助开发者的东西本应该正大光明地写在文档和指南里,而不是隐藏在源代码里。


二、过重的插件体系

插件体系是 webpack 的核心,事实上,webpack 的大部分功能都是通过内部插件或者第三方插件来完成的。可以说,webpack 的生态就是建立在众多插件之上的。

但插件体系也同样有很多问题。

插件数量问题

先问一个问题,一个通过 webpack 构建的项目需要多少插件?

还是以一个标准的 vue-cli 生成的脚手架项目为例,一共有 7 个第三方插件:

"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 源码、图片、字体等等),然后数据通过一个又一个组合起来的管道,最后输出成为构建的结果。写成伪代码的话,大概是这样:

gulp.src('某些源文件')
    .pipe(处理一)
    .pipe(处理二)
    .pipe(处理三)
    .dest('构建结果')

这种管道化,或者说面向过程的构建,非常容易 debug 或者修改,因为它构建的每一步过程,都整齐的按照顺序展示给你看了。想要修改其中任何一步的心智负担是很低的,因为它的处理机制非常纯函数。

然而如果是 webpack 的话,就类似这样:

{
    plugins: [
        插件一,
        插件二,
        插件三
    ]
}

这里,插件一二三是完全面向配置的,没有告诉你任何执行顺序,它们可能会在 webpack 构建的每个时间点触发,你只能从它们的功能上大致猜出它是在哪个时间点工作的。这就是为什么修改一些 webpack 的配置,就像要解开一条放在包里很久的耳机线一样,麻烦又闹心。

当然,这种配置化的插件也是有好处的,配置化代表了高集成度,当你只有 1-3 个插件时,维护这些配置的心智负担是可以接受的,并且比维护面向过程的配置更加方便。但当插件数量超过这个值的时候,构建的复杂程度就会呈指数式上升,我们之前就已经提到了,一个现代的 webpack 项目起码会有 14 个以上的插件以及至少 30 多项配置,这种情况下,面向过程就会好于面向配置,这就是为什么我一直觉得 gulp + webpack 才是正确解决方案的原因。

当然还是要说一句,gulp 和 webpack 并不能直接比较,前者是一个 task runner,而后者是一个 module bundler,它们两者之间都有一些相互不可替代的功能。


三、配置化是银弹吗?

在日常业务中,特别是大公司的一些运营性质的业务里,我们常常会看到 “某某业务已经实现完全配置化” 这样的字眼,在这个语境里,配置化代表了低维护成本、高灵活性、高封装性。

在技术的世界里,配置化也同样是个好东西,很多工具都会宣称自己是完全配置化的,只要你的项目里加入一个配置文件,那么这个工具就可以帮你做很多很多的事情,babeleslintstylelint,还有本文讨论的 webpack 都是如此。

所以配置化是不是就是所有工具进化的终点了呢?它是不是能解决所有的问题呢?

软件工程上有一句耳熟能详的话:“没有银弹”,指的是复杂的软件工程问题无法靠简单的答案来解决。在前端工程构建这个问题上,也同样不例外。

如何解决前端工程的构建?webpack 给出的答案是:通过 webpack + loader + plugin,让一切资源构建可配置。 这在它诞生的那个时代看来,是非常厉害的,一份简单的配置文件就帮你搞定了所有资源构建的问题。

但是当时间的推移,一个前端项目的构建变得越来越复杂,webpack 的配置也越来越多,维护起来越来越难,这个时候,也就慢慢诞生了诸如 create-react-appvue-cli 这样的脚手架工具,在 webpack 的基础上进一步封装,来帮你自动生成 webpack 的配置。这个时候,webpack 更多地变成了一个“底层”工具,而这些脚手架才是你实际上的“构建工具”,或者说,这些脚手架提供的配置,才是你真正的构建配置

为什么会这样?

这个问题的根源在于,webpack 现在提供的配置的封装性已经不够了,它面对一个如今复杂得多的大型前端工程,仅有的配置已经没办法像几年前那样为我们屏蔽掉大部分的构建细节了,所以在它的基础上诞生了如此多的脚手架工具帮我们进一步封装复杂性。

所以我们现在可以回答这一段的标题了:配置化是解决复杂度的银弹吗?当然不是,因为配置会随着复杂度的提升,而也逐渐变得复杂,维护越来越难,直到超过某个临界值,就会需要在它的基础上进一步封装,产生新的配置化。


四、前端工程构建的未来

正如我在上一章所说的,随着复杂度的上升,需要不断地封装复杂性,以让维护配置的心智成本降到可以接受的程度。而在前端构建工具上,截止到趋势也正是如此:

  • 前端的远古时代我们不需要构建,因为这时的前端项目还很简单,原始的 html/js/css 就足以应付需求,手工处理这些资源方便又快捷。
  • 随着前端的复杂化,手工处理的效率越来越低,grunt、gulp 这样的自动化工具就诞生了,它们屏蔽掉了很多资源处理的细节问题,让资源的处理可以自动完成。
  • 随着构建流程越来越多、资源种类越来越多、ECMAScript 的语言特性愈加复杂、开始区分开发/测试/生产环境等等因素,gulpfile/grunt 这样的工具已经不能满足我们的需求,我们需要的是一整套完整的配置化的构建方案,而 webpack 就是这样一种方案。
  • 随着 webpack 配置越来越复杂,维护成本也越来越高,于是诞生了很多脚手架工具,帮你生成 webpack 的配置,封装起 webpack 的复杂性。

那么未来的下一代前端构建工具是怎样的呢?

现在广泛使用的这些脚手架工具,终究依赖的是 webpack,我们实际上需要的是集成度更高、封装性更高(甚至零配置)的构建工具。更详细地说,下一代前端构建工具,必然会有下面的某些特性:

  • 内置的功能更多,比如自带 babel、dev-server、HMR、sourceMap 等等功能;
  • 配置更少,甚至零配置;
  • 更低成本区分开发、测试、生产环境;
  • 性能更好,整合冗长的构建流程,支持多核 CPU 等;
  • 对于新型模块的支持:异步模块、WebAssembly 模块等。

事实上,这也就是部分 webpack 4.0 将会有的新特性,以及前段时间看到的 parcel 也具有其中的某些特点(虽然它现在看起来还很不成熟)。未来这样的构建工具只会越来越多。


总结

这篇文章很久之前就在构思了,只是近期在工作上集中遇到了很多 webpack 的坑,让我彻底有动力来吐槽一下它的种种不是。

webpack 为什么这么难用?本文给出的答案浓缩起来就是两点:

  • 文档不完善,导致使用者和开发者遇到问题都很难下手;
  • 项目需要使用的插件数量太多,且面向配置,导致维护成本指数级上升。

这些问题未来会有改善吗?当然。其实,这篇文章其实有标题党的嫌疑,更准确的标题应该是:

现在的 webpack 为什么这么难用?》

因为这篇文章里提到的问题,都会在 webpack 4.0 中得到改善。

额……至于它的文档嘛……算了不提了不提了 O__O "…

关注下面的标签,发现更多相似文章
评论
说说你的看法