写点什么

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

  • 2020-03-23
  • 本文字数:6647 字

    阅读完需:约 22 分钟

前端技术: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 去解决跨域问题,但其本质其实是在网页中嵌入一段,自然也就不能写入 Header 信息,这个和我们的初衷并不相符,无法满足诉求。所以对于这样的跨域问题,我们通过几个简单的参数配置,在请求发起和请求返回的两端,分别做了代理配置,从而“欺骗”了“源 Origin”,得以解决本地开发的跨域问题:</p><pre><code>devServer:&nbsp;{<br/>&nbsp;&nbsp;&nbsp;//&nbsp;...<br/>&nbsp;&nbsp;&nbsp;headers:&nbsp;{<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'Access-Control-Allow-Origin':&nbsp;'*',&nbsp;//&nbsp;CORS<br/>&nbsp;&nbsp;&nbsp;},<br/>&nbsp;&nbsp;&nbsp;proxy:&nbsp;{&nbsp;//&nbsp;for&nbsp;ajax&nbsp;cors<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'/h5/ajaxObj':&nbsp;{<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;target:&nbsp;'http://xxx.xxx.xxx.com',<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;onProxyReq:&nbsp;(proxyReq)&nbsp;=&gt;&nbsp;{<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;proxyReq.setHeader('Origin',&nbsp;'http://xxx.xxx.com');<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;},<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;onProxyRes:&nbsp;(proxyRes)&nbsp;=&gt;&nbsp;{&nbsp;//&nbsp;…},<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;},<br/>&nbsp;&nbsp;&nbsp;},<br/>&nbsp;},<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>()&nbsp;=&gt;&nbsp;import(/*&nbsp;webpackChunkName:&nbsp;&quot;chunkNameDisplay&quot;<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>


2020-03-23 10:002058

评论

发布
暂无评论
发现更多内容

史上最全的Java并发系列之Java多线程

自然

多线程 并发 8月月更

构建在Findora上的Forlend,具备隐私特性的借贷协议

BlockChain先知

【LeetCode】分割字符串的最大得分Java题解

Albert

LeetCode 8月月更

计算后缀表达式-算法与数据结构-栈的运用-C++语言实现

清风莫追

算法 数据结构, 8月月更

RT-Thread记录(七、IPC机制之邮箱、消息队列)

矜辰所致

ipc RT-Thread 8月月更

网络编程(三)数据链路相关知识

Albert Edison

Linux 网络编程 计算机网络 8月月更 数据链路

《Effective Java》第54条:返回零长度的数组或者集合,而不是null

okokabcd

Java

史上最全的Java并发系列之Java多线程(二)

自然

多线程 并发 8月月更

每日一R「06」内存管理

Samson

8月月更 ​Rust

开源教育论坛| ChinaOSC

CCF开源发展委员会

投研报告 -野心勃勃的meme项目 Lovely Inu($ lovely)

鳄鱼视界

手把手带你实战 AGP 7.x ASM 字节码插桩

如浴春风

android asm Gradle 签约计划第三季

苏彤,你的 Python Flask 编写生成二维码接口写完了

梦想橡皮擦

Python 爬虫 8月月更

一文带你打通Node流的"任督二脉"

战场小包

前端 Node 签约计划第三季

CCF开源发展委员会执委增选

CCF开源发展委员会

构建在Findora上的Forlend,具备隐私特性的借贷协议

小哈区块

史上最全的Java并发系列之Java内存模型

自然

多线程 并发 8月月更

开源云原生与行业应用 | ChinaOSC

CCF开源发展委员会

Kubernetes Docker Compose 迁移

CTO技术共享

开源 签约计划第三季 8月月更

IPv6报文头深度解析

穿过生命散发芬芳

ipv6 8月月更

构建在Findora上的Forlend,具备隐私特性的借贷协议

西柚子

开源雨林企业开源治理与贡献论坛| ChinaOSC

CCF开源发展委员会

Kubernetes LIST请求服务调优

CTO技术共享

开源 签约计划第三季 8月月更

【云原生】Docker入门 -- 阿里云服务器环境下安装Docker

Bug终结者

Docker 阿里云 云原生 服务器 8月月更

C++继承的基本语法与三种继承方式

CtrlX

c c++ 面向对象 继承 8月月更

如何应对核心员工提离职?

石云升

员工离职 职场经验 8月月更

Redis 多机

武师叔

8月月更

“红山开源”创新论坛 | ChinaOSC

CCF开源发展委员会

Kubernetes分布式持续交付Zadig

CTO技术共享

开源 签约计划第三季 8月月更

KubeSphere 新版本3.3.0解读

CTO技术共享

开源 签约计划第三季 8月月更

急如闪电快如风,彩虹女神跃长空,Go语言高性能Web框架Iris项目实战-初始化项目ep00

刘悦的技术博客

Go golang 框架 go语言 Go 语言

前端技术:Webpack 工程化最佳实践_文化 & 方法_阿里巴巴文娱技术_InfoQ精选文章