里程碑2 基于Webpack5完成工程化建设

42 阅读10分钟

Webpack5工程化设计

前端工程化设计涉及将业务文件通过解析引擎转换为浏览器可识别的产物文件,并优化为模块分包、压缩等。核心在于通过解析引擎将业务文件解析成目标文件,并输出到公共目录,实现页面渲染。过程中需考虑不同文件类型、依赖关系、模块拆分及环境分流等。掌握工程化思想,而非具体工具配置,对后续项目选型及优化至关重要。

前端工程化的基本模型

1.前端工程化的基本模型包括业务文件、解析引擎和产物文件。 

2.业务文件包括vue、js、css、less、sass、png等不同类型的文件,存放在不同的目录中。

3.解析引擎负责将业务文件解析成浏览器可识别的js、html和css文件。 

4.产物文件是解析引擎生成的最终文件,存放在public目录下,可供koa等服务器渲染页面时使用。

解析引擎的工作流程

1.解析引擎的工作流程包括解析业务文件、分析依赖关系、模块打包和压缩优化。

 2.解析引擎会遍历业务文件,分析其依赖关系,并使用不同的解析器处理不同类型的文件。 

3.模块打包阶段将解析后的代码拆分成多个模块,根据路由依赖关系进行分包。 

4.压缩优化阶段对生成的代码进行压缩,减少文件体积,提高加载速度。

编译和依赖分析

1.编译阶段将源代码解析成浏览器可识别的代码。 

2.依赖分析阶段分析代码的依赖关系,确定模块间的依赖链。 

3.解析引擎根据依赖关系遍历业务文件,并使用不同的loader处理不同类型的文件。

模块打包和分包

1.模块打包阶段将解析后的代码拆分成多个模块,根据路由依赖关系进行分包。 

2.分包策略可以根据模块大小、依赖关系等因素进行配置。 

3.打包后的模块生成对应的js和css文件,存放在dist目录下。

环境分流和压缩优化

1.环境分流阶段根据不同的环境(生产环境和开发环境)进行不同的处理。 

2.生产环境需要进行代码压缩、js和css的压缩、图片压缩等优化措施。 

3.开发环境则保留源代码,便于调试和热更新。

前端工程化搭建

分包配置

optimization.splitChunks里添加配置,把 js 文件打包成3种类型

  • vender:第三方lib库,基本不会带动,除非依赖版本升级

  • common:公共代码,多个页面会引用的代码,改动较少

  • entry.{page}: 页面 entry 里的业务组件的代码,会经常改动

在项目里是多入口(每个 entry.*.js 一个页面),这会导致不同页面通常会共享大量依赖:

  • 第三方库(vue、axios、lodash…)通常都在多页面里用到

  • 一些公共工具函数/公共组件也可能在多个页面里用到

splitChunks 的 vendor/common 就是把这种“重复出现的代码”抽出来变成共享块,让每个页面的 entry bundle 变小,加载过的文件后续可以命中缓存

vendor:把第三方库统一抽到一起

vendor: {
  test: /[\\/]node_modules[\\/]/,
  name: "vendor",
  priority: 20,
  enforce: true,
  reuseExistingChunk: true,
},

作用:

  • 只要代码来自 node_modules,就尽量放进名为 vendor 的 chunk

  • enforce: true:更“强制”,减少第三方库散落到各个页面的 entry bundle

  • reuseExistingChunk: true:如果已经有相同的 vendor chunk,就复用它

结果:多页面共享同一份第三方库文件,用户切换页面时通常能命中缓存,不用重复下载 Vue、axios、lodash 等。

common:把业务公共代码抽出来(被多个入口复用的那部分)

common: {
  name: "common",
  minChunks: 2,
  minSize: 1,
  priority: 10,
  reuseExistingChunk: true,
},
  • minChunks: 2:至少被两个入口(两个页面)引用的模块,才有机会被抽成 commo

结果:页面之间如果都用到同一段“自己写的通用工具/公共组件”,它会从每个 entry 里被抽走,集中放到 common,减少重复。

splitChunks: {  chunks: "all",  cacheGroups: { vendor: ..., common: ... }}

这表示:vendor/common 的抽取规则,要同时考虑:

  1. 初始加载 chunk 里的重复模块(同步依赖也抽)

  2. 异步加载 chunk 里的重复模块(异步依赖也抽)

这样做的好处是:无论模块是首屏就用,还是后面按需才用,只要在多个地方出现,Webpack 都会尽量抽到公共的 vendor/common/runtime chunk,减少重复代码

a) chunks: "async"(只处理异步)

  • 首屏 initial chunk 里的重复模块:不抽公共 chunk(相当于抽取只发生在 async 那些 chunk 上)

  • 结果:可能会出现“首屏每个页面自己的业务 bundle 里带着一份重复代码”,公共复用没那么好。

b) chunks: "initial"(只处理同步/初始)

  • 异步 chunk 里的重复模块:不抽公共 chunk

  • 结果:用户点开某个懒加载模块时,可能每个场景仍然各自带重复的依赖包,按需加载时也不会最省。

c) chunks: "all"(你现在的)

  • initial + async 都抽:复用机会最大,chunk 数量/请求数要配合 maxInitialRequests/maxAsyncRequests 控制,否则会拆太碎。

生产环境和开发环境,有什么不一样的特殊处理?

开发环境(app/webpack/config/webpack.dev.js)
~热更新 HMR:把 webpack-hot-middleware/client... 注入到每个页面入口(除了 vendor),并启用 HotModuleReplacementPlugin,实现改代码不刷新整页。
~添加sourceMap,方便定位调试代码。
~资源从内存提供 + 跨域访问:配合 app/webpack/dev.js 的 webpack-dev-middleware,大多数产物在内存里(js/css等资源);并加了 Access-Control-Allow-* 头,方便页面/接口联调。
~只把 .tpl 写到磁盘:writeToDisk 只落地 .tpl,因为 Koa/Nunjucks 需要从磁盘读模板;JS/CSS 走内存更快。

生产环境(app/webpack/config/webpack.prod.js)
~产物落盘 + 固定公开路径:输出到 /app/public/dist/prod,publicPath: "/dist/prod",用于线上静态资源加载。
~CSS 抽离:用 MiniCssExtractPlugin 把 CSS 从 JS 里抽出来,便于缓存与并行加载。
~清理旧产物:CleanWebpackPlugin build 前清空目录,避免脏文件影响发布。
~压缩优化:CssMinimizerPlugin 压缩 CSS;TerserPlugin 压缩 JS 并 drop_console: true 去掉 console.log。
~构建加速(多线程):用 HappyPack 并行跑 babel-loader 和 css-loader。
跨域资源策略:crossOriginLoading: "anonymous" + HtmlWebpackInjectAttributesPlugin({ crossorigin: "anonymous" }),让浏览器请求静态资源时不带凭证,适配一些错误上报/跨域加载场景。

为什么生产要把 CSS 抽离成单独文件?

~开发不抽离(用 style-loader 打进 JS):
CSS 通过 JS 动态插到页面,改 CSS 时可以直接走 HMR,和改 JS 一样快,不用为单独处理 CSS 做一套逻辑。
~产物主要在内存里,不关心文件数量、体积,构建简单、热更简单即可
生产要抽离(如 MiniCssExtractPlugin):
~缓存:JS 经常因业务迭代变,CSS 相对稳定。抽成独立 .css 后,CSS 和 JS 可以各自带 hash,改 JS 时 CSS 文件名不变,浏览器能继续用旧缓存,减少重复下载。
~并行:浏览器可以同时请求 JS 和 CSS,而不是等一个大的 JS(里含 CSS)再解析,首屏更容易更快。
~体积与解析:大段 CSS 放在 JS 里要等 JS 执行完再插样式;独立 CSS 可尽早被下载和解析,不阻塞 JS 执行。
~语义:生产环境希望「样式是样式、脚本是脚本」,便于 CDN、缓存策略、监控按资源类型区分。
一句话: 生产要缓存友好、加载和解析更高效,所以把 CSS 抽成单独文件;开发优先简单 + HMR,用 style-loader 打进 JS 更合适

加速打包的工具

1、用 Webpack 5 自带的持久化缓存
module.exports = {
cache: {
type: 'filesystem', // 把缓存写到磁盘,下次构建复用
},
// ...
};

2、thread-loader(多线程,替代 HappyPack)
和 HappyPack 思路类似,但官方维护、兼容 Webpack 5,把耗时的 loader 放到 worker 里跑
// 放在耗时的 loader 前面,如 babel-loader
{
test: /.js$/,
use: [
'thread-loader',
'babel-loader',
],
}
文件内容
→ 先进入 thread-loader
→ thread-loader 把(内容 + 后面的 babel-loader)一起发到 worker
→ worker 里执行 babel-loader,得到转译结果
→ 结果从 worker 传回主进程
→ 主进程拿到结果,继续后面的 loader 或输出
注意:thread 有通信开销,只对耗时大的 loader(如 babel)才有收益
~主进程和 worker 是 不同线程,数据要 序列化(复制) 过去、结果再 复制 回来,有固定成本
~如果 loader 本身很快(例如只改几行字符串),那 传数据的时间可能比真正计算还长,多线程反而更慢
~babel 转译 是 CPU 密集、单文件耗时相对长,所以「在 worker 里算」的收益大于「传数据的开销」,适合用 thread-loader;

3、esbuild-loader 用 esbuild(Go)做 JS/TS 转译,比 Babel 快一个数量级,配置简单
swc-loader 用 SWC(Rust)替代 Babel,兼容 Babel 生态,速度接近 esbuild

手写热更新模块替代web-dev-server

因为需要做服务端渲染,打包生成的tpl文件不能存储在内存中,而是需要放到磁盘中供koa解析,所以需要自己搭建一套热更新服务,基于erxpress和webpack的两个中间件

使用webpack-dev-middleware监听业务文件的变化,业务文件发生变化,就会触发解析引擎
首先是解析编译,因为浏览器只能识别js、css还有静态资源文件,像我们项目中的vue文件就需要vue-loader解析为浏览器需要的文件,js文件中有es6的语法也需要babel解析为es5的语法,css文件需要css-loader等等,不同的文件类型使用不同的loader进行解析

接下来是模块的分包,因为多页面会被打包为多个入口js,里面会引用很多第三方包和自定义的方法。如果a方法被page1和page2同时使用,那么访问page1 a方法的代码被下载一次,访问page2 a方法的代码还会被下载一次,所以对a方法单独分包,体现在页面的bundle文件里就会是一个单独的script标签(包含名称a和hash值),这样在访问page1再访问page2时,a方法就会命中缓存,不会再被下载

接下来是压缩优化,根据环境分流,生产环境压缩js和css文件,把css抽离为单独文件,不混合在js里。输出产物到磁盘里面。开发环境讲js、css等资源存入内存,将tpl文件存入磁盘供koa读取

通过webpack-hot-middleware与客户端建立通道(socket或sse)

浏览器和它通信的流程是:

  1. 浏览器加载页面 JS(里面包含 HMR client)

  2. HMR client 会去请求 HMR_PATH(例如 /__webpack_hmr)

  3. hotMiddleware 在服务端监听构建结果,有更新时把更新“推送/通知”给浏览器

  4. 浏览器再让 Webpack runtime 热替换对应模块(不刷新整页)