webpack多入口应用增量构建如何最高提速97%
结论先行
选取了「增量编译」
方案优化后当时构建最快的一次,跟全量构建最差的数据对比,相当于为构建提速
(125 - 2.81) / 125 = 97.75%
ps:这张图就是因为构建的时间达到了自己维护项目以来最短时间才截下来的。当时是修改了一个 html 上的文案,一个 js 上的部分代码,同时也确实这个入口页面比较简单
背景
之前维护的一个项目,基于 express + webpack + vue 的多页面 node 相关的前端项目 服务端渲染部分,使用 node 去请求接口,并使用 ejs 模板前端渲染部分,使用 vue 在前端请求接口然后渲染
当时技术部使用的发布工具为 walle(相关的文档地址可点击)
为了满足业务:1. 需要一套自动化的上线流程;2. 需要保证线上发布和回滚的时间
当时自己设计了如下的一套方案:
设计两个 git 仓库的原因
当线上出现问题之后,希望的是能在 1 分钟内回滚代码,如果只有一个源码仓库,构建的过程所需要耗时在线上有问题的情况下个人是无法接受的。
前端构建所做的事情
- 执行 git pull
- webpack 生产环境构建
- 上传 js,css,img 到 cdn
- 复制 node 相关代码
- 提交「线上代码仓库」commit
Tips:在「线上代码仓库」之前包含的只有 node 相关 js 文件和页面 ejs 文件,开发这是不需要关心这个仓库,只有在 walle 发布平台去发布线上才会接触到,在之前公司中,操作上线的是 QA 人员
存在的问题
在整个方案中,「前端构建」阶段,是瓶颈所在,具体的问题如下:
- webpack 多页面应用,当时已有入口有 154 个,而且入口会随着时间越增越多,构建也会越来越慢
- 一个细小的改动,如果一段文案,一部分样式,一张图片的链接,但却需要构建整个项目
- 项目是 node 和前端代码在一起的,部分更新是只需要修改 node 端的代码,理论是不需要前端构建的
- 多页面入口新增之后,公用的 mainfest以及一些代码没有改动的入口生成的 js 文件名会改变
- 在入口如此多的情况下,无论如何优化,也只会从“很慢”优化成“慢”
Tips:
入口的定义:规定的文件夹层级下,同时包含 html 文件和 js 文件的即为 webpack 入口
入口多:入口除了业务中需要的页面,还包含之前的历史活动页面,虽然后期优化使用了单个页面然后通过id关联不同配置的方式实现活动,但历史的活动不太想动代码
构建时间:通过将图片压缩功能剔除,dll剔除公用库打包,启用多进程方式(使用thread-loader启用2个进程,4核阿里云服务器,启动进程过多会阻塞正常别的测试服务),构建时间一直在100-125s之间,有时候遇到服务器资源不够会更慢
解决方案
思考了良久,「增量编译」是一个很好的解决方案,即只会编译变化部分的内容,具体的方案如下:
如何达到「增量编译」
我们的项目都是在git仓库中的,每个线上的版本都会有一个唯一的 commit 的 hash 值。通过两个commit 的 hash值可以获取到,相对于线上的版本,新版中修改了什么文件。使用的是开源库「nodegit-kit」
如何获取文件依赖
项目中文件的类型:html、img、scss、js、ts、vue
难点
- 文件依赖之间的复杂度
- 代码的引入使用了 webpack 的 alias,需要能还原回本身的地址
- js 的引入混合使用了 esm 和 require
- vue 文件包含了 3 部分,依赖的确定更为复杂
-
- template 部分会有图片依赖
- js 部分会有图片、js、scss、ts、vue的依赖
- scss 部分会有 图片、scss的依赖
解决方案
在了解 webpack 的依赖获取方式原理过程中,发现了「dependency-tree」这个包,最终选择了依赖于「dependency-tree」封装的「madge」来获取(因为不仅能获取依赖,还能生成文件依赖图)。
但是 直接使用madge还是存在部分的问题:
- 无法获取 scss 对 img 的依赖
- 无法获取 vue 文件的依赖
- 无法获取 html 文件的依赖
madge 获取 js 的依赖获取是最有效的,这点跟 webpack 很是相似,而 webpack 要解决非js文件的方案就是是用 loader
。loader 就是用于将非 js 的文件转化为 js 文件。所以针对遇到同样的问题,我们可以先利用 loader 将非 js 的文件转化为 js 文件,然后再使用 madge 去获取转化为js的文件依赖,也就是相当于这个文件的依赖了。
如何使用loader
我们都知道,loader 其实就是一个函数,只是在 loader-runner 调用执行的过程中将 this 指向生成的loaderContext 对象,因为我们不需要真实去执行 loader,只需要获取返回的内容,所以可以在 loader 函数然后在执行的时候,将 loader 的 this 指向一个虚假的 loaderContext 对象,这个对象上会包含 loader 执行需要的参数。为了能够保证获取文件的依赖的准确性,调用 loader 解析生成的内容会保留在跟原本文件同一个地方。
html-loader和css-loader的使用
对于 html、scss 文件会直接调用 html-loader 和 css-loader 来转化为 js 文件然后获取这个 js 文件的依赖即可。
处理 html 文件中 html 源文件和处理后的文件 demo:github.com/lzkui2013/m…
处理 scss 文件中 scss 源文件和处理后的文件 demo:github.com/lzkui2013/m…
核心的实现代码:
// html-loader 处理 html 文件
const loaderDealData = HtmlLoader.bind(fakeWebpack)(sourceFile);
// ...
fs.outputFileSync(`${dir}/${fileName}.js`, outCon);
// ... 更多代码省略,更多查看下面「处理 html 文件方法」github 地址
// css-loader 处理 scss 文件
cssLoader.bind(fakeWebpack)(sourceFile);
// ...
fs.outputFileSync(`${dir}/${fileName}.js`,
content.replace(/new URL/g, 'require'));
// ... 更多代码省略,更多查看下面「处理 scss 文件方法」github 地址
处理 html 文件方法:github.com/lzkui2013/m…
处理 scss 文件方法:github.com/lzkui2013/m…
vue-loader的使用
解决完 html 和 scss 文件获取依赖的问题,我们再看 vue 文件。要想获取 vue 的依赖,第一步就需要了解 vue-loader 如何处理 vue 文件,从代码中看,大体做了以下几步:
1.将 vue 文件转化为js,以项目中一个 vue 文件为示例,vue-loader 处理完后顶部会有如下依赖:
import { render, staticRenderFns }
from "./global-dialog.vue?vue&type=template&id=1adb22c4&"
import script from "./global-dialog.vue?vue&type=script&lang=js&"
export * from "./global-dialog.vue?vue&type=script&lang=js&"
import style0 from "./global-dialog.vue?vue&type=style&index=0&lang=scss&"
// ...省略,包含一些执行代码,热更新代码,没有 import 依赖相关
// 可以自己查看 vue 项目在 chrome 中的 webpack 文件夹下内容,会更加清晰
vue-loader 实现这部分功能代码地址:github.com/vuejs/vue-l…
2.将以上代码返回 webpack 之后,根据以上的 import 会继续调用 vue-loader
但因为这次每个调用 vue 的地址上都带有一个 type 属性,所以会调用 selectBlock 方法
if (incomingQuery.type) { return selectBlock(/*省略,可自行查看代码*/)}
vue-loader 代码地址:github.com/vuejs/vue-l…
3.调用 selectBlock 之后,在 selectBlock 方法中会将每个部分的内容赋值一个文件后缀,然后在调用 loaderContext.callback,相当于调用别的 loader 去处理,如以下就是 vue-loader 对 template 部分代码的处理:
// template
if (query.type === `template`) {
// if we are receiving a query with type it can only come from a *.vue file
// that contains that block, so the block is guaranteed to exist.
const template = descriptor.template!
if (appendExtension) {
loaderContext.resourcePath += '.' + (template.lang || 'html')
}
loaderContext.callback(null, template.content, template.map)
return
}
// ...以下省略
vue-loader 代码地址:github.com/vuejs/vue-l…
vue-loader 的流程我们基本已经理解,那么利用 vue-loader 来实现我们需要的依赖的获取就变得很清晰了
1. 将 vue 文件拆分为三部分;2. 将template部分转为js;3. 再将 scss 部分再转为js
实现上,可以直接将路径上的参数都加上,然后加上 vue 文件的内容,直接调用 selectBlock 方法,再将返回的内容保存下来即可。
对于 template 部分的代码,vue-loader 会直接将其按照 .html 的方式处理,不符合我们的预期,所以我们直接使用 vue-loader/lib/loaders/templateLoader 方法转化为 js。拆分后 scss 部分的内容处理,直接调用上面处理 scss 的方法即可,最终的结果会生成 4 个文件获取依赖整合以后就是这个 vue 文件的所有依赖了。
处理 vue 文件中 vue 源文件和处理后的文件 demo:github.com/lzkui2013/m…
处理 vue 文件方法:github.com/lzkui2013/m…
我们每个拆分出来的文件,命名都是有一定的规则,当我们使用 madge 获取完相应的依赖之后,需要按照命名的规则,将所有的依赖整合并还原为原本文件的依赖,然后再清空所有生成的新文件
最终整体流程的实现流程如下
可以查看 demo 中通过以上方案获取的整个项目所有文件依赖关系的图(这也是选择 madge 的原因,可以生成依赖图):raw.githubusercontent.com/lzkui2013/m…
缓存设计
文件依赖当获取完一次之后,如果文件在 git 仓库中没有变动,那么依赖也是不会变化的,也不需要每次都去重新获取依赖,每次 git 比对文件变化之后,只需要更新内容有变化的文件的依赖关系即可。
所以当第一次构建之后本地会有一个依赖树json缓存文件,文件中包含的内容:
- 上一次构建的 git commit hash
- 代码中每个文件依赖的文件列表
本地缓存 json 文件 demo:github.com/lzkui2013/m…
总结
- 本案例是基于 vue 的多页面应用,但其实可以适用于所有的多页面,别的技术栈处理起来理论会更加简单
- 对于单页面应用的构建是更加的复杂,当前「增量编译」的优化方案有一定的参考作用,具体还需要更深入了解单页面构建中 webpack 如何进行模块拆分、如何合并输出文件的规则等
希望这次文章让大家能有所收获。
本文中「增量构建」实现方案代码 GitHub地址:github.com/lzkui2013/m…
转载自:https://juejin.cn/post/7053059974850674695