一、引言
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 去解决跨域问题,但其本质其实是在网页中嵌入一段,自然也就不能写入 Header 信息,这个和我们的初衷并不相符,无法满足诉求。所以对于这样的跨域问题,我们通过几个简单的参数配置,在请求发起和请求返回的两端,分别做了代理配置,从而“欺骗”了“源 Origin”,得以解决本地开发的跨域问题:</p><pre><code>devServer: {<br/> // ...<br/> headers: {<br/> 'Access-Control-Allow-Origin': '*', // CORS<br/> },<br/> proxy: { // for ajax cors<br/> '/h5/ajaxObj': {<br/> target: 'http://xxx.xxx.xxx.com',<br/> onProxyReq: (proxyReq) => {<br/> proxyReq.setHeader('Origin', 'http://xxx.xxx.com');<br/> },<br/> onProxyRes: (proxyRes) => { // …},<br/> },<br/> },<br/> },<br/></code></pre><h2>四、优化性能 by Node / Happypack</h2><p>基础配置和需要的自定义配置已经有了,整个项目的构建时间有可能还是非常不理想的,当前本文提及的测试项目,大概有 57s 的时间,还是有很多地方没有补足的,可优化的空间非常大。</p><p>第一步可以先关注下 Node 版本,经过测试,是对整体速度可以至少提升 30%的事情,尤其是在 Node V8 版本到 V10 的时候,以下是之前在另一个项目做技术改造时记录到的数据:</p><table><thead><tr><th>Node 版本</th><th>v 8.x</th><th>v 10.x</th></tr></thead><tbody><tr><td>compile</td><td>32s - 36s</td><td>26s</td></tr><tr><td>re</td><td>compile</td><td>8s - 9s</td></tr></tbody></table><p>但是这次,在把项目直接升级到了 v 11.x 后发现,有带 node-sass 的项目编译构建都崩溃了。才意识到,node-sass 的版本也需要相应的版本更新。也测试了 Babel v 6.x 到 v 7.x 版本的升级效果,本来以为 babel 的大版本升级会带来显著的编译速度提高,实际上却并不理想(基本可以忽略不计)。</p><p>打算开启多线程能力,去处理模块化打包里面那些本是单线程执行的 loaders 们的工作。 Happypack 的提升效率对整个项目的首次编译而言,效果是 20%左右,比较明显。加入 Happypack 能力的时候,有两点需要注意:</p><ul><li>其对 file-loader 和 url-loader 的支持不好,可以考虑不加,毕竟我们项目里面图片类(最好上传 CDN)的和非常规格式的文件只是小部分;</li><li>这次也尝试了把 ts-loader 加入到多线程中,但是也出现了不少编译问题。大概率怀疑是我个人的配置问题,但过程中去看 issues 见到了不少 ts-loader 和 ts 生态依赖兼容性的问题。目前这个项目.ts 只是少数文件,作为一种尝试,大部分文件还都是.jsx 和.js,所以针对 ts 也先不加入 Happypack 能力了。</li></ul><h2>五、优化性能 by DLL/ Optimization</h2><p>首先需要借助一些工具来进行分析,如:webpack-bundle-analyzer ,通过这个工具我们可以对整个构建(用于生产,Webpack Analyse 针对的 build 过程,不是 compile)过程和结果进行数据、图形上的分析,从而得知问题具体出现在了哪里。进而得知 DLL 所需拆分的内容是什么。以下内容是在第一次分析时得出的:</p><p><img src="https://static001.infoq.cn/resource/image/eb/cf/eb19ddbdb77740e3c333587c24c9bfcf.png" alt=""/></p><p>这个图片的 3532 modules 和 62 chunks 可以看到具体的模块以及 chunks 划分后的情况。更加直观的我们来看下面这张图,可以看到 Parsed 的尺寸,入口文件(7.09MB)和主 chunk(2.04MB,主要是一些首页就需要加载的 node_module)的大小都很夸张,并且 node_modules 里面的包基本上是一一打包、整整齐齐:</p><p><img src="https://static001.infoq.cn/resource/image/a8/9c/a8d7d85d04abf5c786fdbcec16bb279c.png" alt=""/></p><p>有了这些分析结果,对应解法的思路就很清晰了:首先要抽离常用的 node_modules(这是 DLL 的意义),然后要逐个分析,把不被经常用到的 node_module 们(仅被某些页面使用,不具有公共特点)也抽出去。</p><p>对于 React 项目中的 React, React-Dom, React-router, Redux 等,还要一些第三方比较大的库,比如 antv 或者 G2 相关的,也要进行 DLL 抽离了:</p><p><img src="https://static001.infoq.cn/resource/image/2c/c1/2c9b0471cfbfb2a01f656ac26e7142c1.png" alt=""/></p><center>(modules 数量由 3532 降低到 1500,编译时间缩短了三倍)</center><p>在做了上述 DLL 的抽离后其实效果已经很明显了,进一步的提升空间,可以对 optimization 进行了配置(用法详见官方文档):</p><p>1)terser;</p><p>2)chunksAll;</p><p>3)no mimimizer sourceMap。</p><h2>六、结尾</h2><p>本文大概主要介绍了一些工具衍变背景、基础的组织结构和自定义配置,以及如何通过分析工具去来做性能优化,其中很多小的细节没办法一一提到,比如我们看到加载的 chunk 都是 hash 值的时候,如何能够辨别是什么组件呢:解法是可以在路由处通过配置 moduleName 的方式去做:</p><pre><code>() => import(/* webpackChunkName: "chunkNameDisplay"<br/>*/'../containers/UserList/chunkNameDisplay')<br/></code></pre><p>诸如此类,实在繁多。随着 Webpack 5.x 版本的陆续发布和众多团队使用之后,也许很多东西又会有大的改变。并且各种框架的集成已经越来越丰富,更多的解放程序员在工程化维护上的双手,我们关注工程化的演进,看看 Webpack 生态会给我们带来什么样的惊喜。</p><p><strong>作者介绍</strong>:</p><p>阿里文娱前端开发专家 芃苏</p><p><strong>相关阅读</strong></p><p><a href="https://www.infoq.cn/article/Hvrk1b4eBT5ftGLPqm8Q">电影垂直行业的云智开放平台如何炼成?</a></p><p><a href="https://www.infoq.cn/article/1mdwJYDeUDFT92vKy1Jt">阿里工程师带你了解 B 端垂类营销中心如何设计?</a></p><p><a href="https://www.infoq.cn/article/3CZuzwZVoRpGCG60t4Xo">云智前端技术如何赋能场馆院线?</a></p><p><a href="https://www.infoq.cn/article/YRFUdHJ1iI2CSsE9iqZ4">60 秒售出 5 万张票!电影节抢票技术揭秘</a></p><p><a href="https://www.infoq.cn/article/qODRHtSEPI0Xe3iZ8u7T">电影行业提升 DCP 传输效率,还能这样做!</a></p><p><a href="https://www.infoq.cn/article/NEVU9NawMhVeeuET0vfu">超大型场馆的绘座选座解决方案</a></p><p><a href="https://www.infoq.cn/article/6he5gohhU05b4hWOfy2C">大型赛事稳定性保障:Dpath 为世界军人运动会护航</a></p><p><a href="https://www.infoq.cn/article/JhDDwGeDDUvQBlyNWidT">世界顶级赛事的票务支撑:百万座位与限时匹配</a></p>
评论