前端技术:Webpack 工程化最佳实践

一、引言

1. 前端构建工具的演变

回想在 2015-2016 年的时候,开发者们开始渐渐把视线从大量使用 Task Runner 的 Grunt 工具,转移到 Gulp 这种 Pipeline 形式的工具。 Gulp 还可以配合上众多个性化插件(如 gulp-streamify),从而使得整个前端的准备工作链路,变得清晰易控,如刷新页面、代码的编译和压缩等等。自动化“流水线”工具取代了很多繁杂的手动工作,可以说,是具有跨时代意义的。之于 Webpack 而言,其本质是是基于“模块化”思想的一个“JS 预编译”解决方案,诞生初期,和其相似的方案还有 Browserify,和 Webpack 属于同门不同派别的还有 sea.js 或 require.js,这二者需“在线依赖”解释器编译。

时至今日,多数日常工作接触的项目,已经可以完全的舍弃 Gulp 了。但工作中有时还会接触一些老项目,其中 Gulp 的使用和维护屡见不鲜。2019 年初之时,通过一个老项目(gulp 3.x + webpack 3.x)的技术升级,借机了解了 gulp 4.x 的动态,又不禁让人回想起 gulp-browserify,和 gulp-webpack(五年前发布,目前改名为 webpack-stream)。所以,Webpack 做为某一个垂直方向的解决方案,当然可以 manaually built-in Gulp 中。在拿 Webpack“方案”和 Gulp 类“工具”去做正面比较的时候,需要明晰两者解决问题的范围和思路。如今再次回顾历史,对技术的发展演变顺序,能有一个基本客观的概念。

在 2017 年的时候,Gulp 和 Webpack 在用户的使用率和“将继续使用”的意向上,还不分伯仲。但从《State of Javascript 2019》中可以看到,Webpack 已经完全碾压了其它工具和类库,成为了首屈一指被大家广泛使用、讨论的 Build Tool。2018 年 2 月 25 日 Webpack 发布了 4.0.0 正式版本;对不少项目进行了 Webpack 4.10.2 版本的升级后,又将部分项目升级到了 4.29.0 最新版本。这一系列的“跟进式升级”中,一方面是在不断融入 Webpack 对于模块构建的新思路和理念,为了能够更好的适应其未来的变化,另一方面是在一个好的方案中不断尝试,结合项目的基础设施优化,从而提高效能,保障产品稳定。

2. 本次回顾

Webpack 工具虽说只是前端项目 CI 流程的一个小部分(构建 build),就它自身而言,所涉及到的 Node 知识和包依赖管理经验,是一整块技能。细节来看,里面涉及了 Webpack 自己的包和第三方 plugin 生态,还要配合恰当的 babel、typescript、flow.js、eslint 配置等多个生态,去处理 Javascript 语言本身的编译 / 转译。以及,正确管理本地静态资源文件和远端 CDN 资源文件路径(打包配置决定打包结果),涉及到了跨域知识和 Node 层服务配置、模板配置知识。更进一步还有,NPM 众多包的版本管理等让人头疼的问题。其中琐碎细节数不胜数,当所有第三方工具正确使用的前提下,也许还有些 plugin 小工具,需要开发者去自研发。知识谱系之大,可见一斑。

本文不描述 Webpack Docs 使用指南,也不描述第三方插件的使用“指北”。更多的是结合过往项目经验,记录实践得出的使用技巧,也记录一些走过的弯路所带来的问题,希望对其它众多的前端技术人能够起到一点借鉴作用。(Package Checking List:React: 16.3.2,Babel: 7.0.0, Webpack: 4.29.0,Node: 11.8.0)

二、文件结构

在 4.x 版本中的早期,CLI 工具集里的命令是 Webpack 主包自带的,但在 Webpack 4.x 后期的版本,将 webpack-cli 作为独立包剔除出去,需要手动单独安装才可以执行 tnpm run start 这样的脚本命令。其次,对于开发 / 日常环境(dev)和预发 / 生产环境(prod)来说,打包的策略是截然不同的:

1. 对于 dev 日常环境:

1)方便的 debug 和 troubleshooting,有比较强的 source mapping;

2)希望能够得到颗粒度较小、且有根据变动代码针对性的的加载(live reloading/hot module replacement);

3)希望可以做一些代理 Proxy 相关的调试;

4)可以方便的根据开发者的情况,对本地的 dev-server 进行配置等。

2. 对于 Prod 生产环境:

1)通过压缩 Javscript/CSS 代码,获取更小的文件加载体积;

2)通过包的拆解来得到更优的加载策略,从而降低 load time;

3)比较轻量的 source mapping(当然,当你需要一些 trace 信息做日志和报警的时候是另外一番情景);

4)线上的产品的一些个性诉求(比如,对同一份 Javascript 代码也许要匹配不同的样式文件)等。

3. 通常评估效率维度主要有以下几个,文中提到的数据来源主要属于前三个:

  • 本地开发 compile(w/ DLL or NO DLL);
  • 本地开发 re-compile(w/ DLL or NO DLL);
  • 本地测试 build(webpack analyse 分析的重点部分);
  • 云构建时长 (NO DLL or 配置化 OSS 支撑 DLL)。

在 Webpack 的新版本中, webpack-merge: 4.2.1 这个独立包的使用,开发者使用 webpack.common.js 文件对开发和生产环境中的公共部分进行配置,webpack.dev.js 针对开发环境,webpack.prod.js 针对生产环境。区分后,两种环境的配置差异,一目了然:

(图:webpack 配置文件结构)

关于 cz.config.js 和 flowGlobalVars.js 里面“话题点”颇多,不在此处重点描述。

如果需要 DLL 配置 (在后面的优化部分会重点讲),还需要单独加入一个 webpack.dll.js 打包的配置文件。当然,dll 其实也是一个普通的文件 Output,我们可以在 webpack.common.js 文件中 module.exports 时,写两个区分开。通过这种不是很常见的灵活写法(Exporting multiple configurations),可以更多的去理解文件的 I/O 和 module 模块的概念。

三、基础 / 自定义配置

1. CommonsChunkPlugin 被取代

被移入到了 webpack.optimization.splitChunks 中。有关拆包切分和颗粒度控制,这个其实从 Webpack 的层面已经为我们做了很多优化,自身也是有一套基础默认的优化策略的。类比来看, React 生态里面 diff 算法本身也是有策略机制的,更多的优化,使用者可以在这个对象里面加入回调方法,自己去细化控制。

这里需要特别注意的是 cacheGroups,当不明确哪些内容需要被 cache 时,或者是颗粒度不好把控时,这样的切分会给我们带来非常多的冗余文件。定义一个 vendors 对象,那么我们的 output 文件(不包含 chunksFiles)的每一个都会生成一个 cache 文件。加入 output 的有 app.bundle.js 和 polyfill.bundle.js,一旦加入这个 vendors 对象,打包的时候会额外的生成两份文件,分别是 vendors-app.js 和 vendors-polyfill.js。虽然不用担心这两个文件内容会重新打包代码进去,里面只是放一些 cache 索引,但这两个文件如果在不确定要用他们来做什么的时候,cacheGroups 的设置,需要重新认真去考虑。

2. OccurrenceOrderPlugin

本身不再是一个 webpack 类下面的构造器,而是被重新命名(之前的名称因为单词拼写错误了),然后放入到新的位置,调用起来需要重新去书写:new webpack.optimize.OccurrenceOrderPlugin()。

3. terser(默认的内置压缩工具包)

webpack.optimization.minimizer 的新版本中,default built-in 的工具已经由旧有的 uglifyJS 变成了 terserJS,旧的 uglify 已经被 depreacted 处理,相信不久之后的状态就会变成 legacy,新的 terser 更好的性能,对 ES6+ 的语法支持的更多,也同时兼容了 babel 7 的生态,同步其它第三方库代码压缩后的诉求。目前我在使用的是 terser-webpack-plugin,和普通的 terser 配置的参数上有一些差异,需要自己手动引入(官方文档推荐)。

4. module.rules.exclude[0]

module.rules.exclude[0] 的文件地址书写,要求更加严格( 4.11.0 以后的版本)。

以往我们在对 module.rules 做配置时,有些文件不希望被遍历到,那么我们通过 exclude 这个参数配置,将其跳过,有时候会使用’src/contianer/xx.jsx’这样的写法,如果是多个 path 索引,那就放到一个 Array 中就好。但这种写法,在新版本中是不被允许的,我们只能使用 path.resolve() 或 /regExp/ 的写法去声明文件路径地址。(Bonus Basic Tips,如何用正则书写并集和特定路径,如我希望 include 所有 src 加上一个指定的 npm 包 :/(src\/.*)|(node_modules\/.*@ali\/lark-components)/)

5. alias 和绝对路径

webpack 在打包的时候,通常需要对文件的路径去做查找、搜索,它需要明确知道文件的引用位置和引用关系,从而能够完整的知道整个映射 mapping 关系。减少这方面的开销,我们可以考虑去配置 alias,从而以绝对路径的写法代替大量相对路径写法。好处的话,一方面是帮助 webpack 更快的去定位文件位置,另一方面书写起来,也不再用被输入 ‘…/…/ ’ 还是 ‘…/…/…/ ’ 而困扰。

  • Webstorm 寻找绝对路径:在配置里面对 webpack 配置项加入 webpack 文件路径就好,Webstorm IDE 会自己找到对应的 alias 关系;
  • VSCode 寻找绝对路径:插件层面没有发现太好的办法,如果项目正在使用 typescript,可以在 tsconfig.json 里面配置相关的编译项,可以达到和上面 Webstorm 同样的效果。

6. 大图片上传 CDN

上传 CDN 后可以大幅减小包体积。另外,webpack 也不需要再去关注那些图片的文件索引路径了。项目稍微大一些,本地图片 5Mb ~ 10Mb 的情况非常普遍,亟待优化。

7. devServer Proxy 的代理能力

去调研这个能力,得益于一次请求层的改造。诉求是希望 Token 不再显示传递,而是通过塞到 Header 去实现。在本地开发的环境,我们通常使用 jsonp 去解决跨域问题,但其本质其实是在网页中嵌入一段