webpack、vite
type
status
date
slug
summary
tags
category
icon
password
Blocking
Blocked by
top
URL
Sub-item
Parent item
构建流程
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
初始化参数
:读取webpack
的配置参数。
开始编译
:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
确定入口
:根据配置中的 entry 找出所有的入口文件
解析模块
:从入口文件(entry
)开始解析,并且找到其导入的依赖模块,递归遍历分析,形成依赖关系树;
编译模块
:对不同文件类型的依赖模块文件使用对应的Loader
进行编译,最终转为Javascript
文件;
输出资源
:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
输出完成
:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
在以上过程中,
Webpack
会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。简单说
- 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler
- 编译:从 Entry 出发,针对每个 Module 串行调用对应的 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行编译处理
- 输出:将编译后的 Module 组合成 Chunk,将 Chunk 转换成文件,输出到文件系统中
Loader
编写思路
Loader 支持链式调用,所以开发上需要严格遵循“单一职责”,每个 Loader 只负责自己需要负责的事情。
- Loader 运行在 Node.js 中,我们可以调用任意 Node.js 自带的 API 或者安装第三方模块进行调用
- Webpack 传给 Loader 的原内容都是 UTF-8 格式编码的字符串,当某些场景下 Loader 处理二进制文件时,需要通过 exports.raw = true 告诉 Webpack 该 Loader 是否需要二进制数据
- 尽可能的异步化 Loader,如果计算量很小,同步也可以
- Loader 是无状态的,我们不应该在 Loader 中保留状态
- 使用 loader-utils 和 schema-utils 为我们提供的实用工具
- 加载本地 Loader 方法
- Npm link
- ResolveLoader
常用的Loader
source-map-loader
:加载额外的 Source Map 文件,以方便断点调试
babel-loader
:把 ES6 转换成 ES5
ts-loader
: 将 TypeScript 转换成 JavaScript
awesome-typescript-loader
:将 TypeScript 转换成 JavaScript,性能优于 ts-loader
sass-loader
:将SCSS/SASS代码转换成CSS
css-loader
:加载 CSS,支持模块化、压缩、文件导入等特性
style-loader
:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS
postcss-loader
:扩展 CSS 语法,使用下一代 CSS,可以配合 autoprefixer 插件自动补齐 CSS3 前缀
eslint-loader
:通过 ESLint 检查 JavaScript 代码
tslint-loader
:通过 TSLint检查 TypeScript 代码
mocha-loader
:加载 Mocha 测试用例的代码
vue-loader
:加载 Vue.js 单文件组件
cache-loader
: 可以在一些性能开销较大的 Loader 之前添加,目的是将结果缓存到磁盘里
Plugin
原理
webpack
基于发布订阅模式,在运行的生命周期中会广播出许多事件,插件通过监听这些事件,就可以在特定的阶段执行自己的插件任务,从而实现自己想要的功能。Webpack 的 Tapable 事件流机制保证了插件的有序性,使得整个系统扩展性良好。常用的Plugin
otModuleReplacementPlugin
:模块热替换
define-plugin
:定义环境变量 (Webpack4 之后指定 mode 会自动配置)
ignore-plugin
:忽略部分文件
html-webpack-plugin
:简化 HTML 文件创建 (依赖于 html-loader)
web-webpack-plugin
:可方便地为单页应用输出 HTML,比 html-webpack-plugin 好用
terser-webpack-plugin
: 支持压缩 ES6 (Webpack4)
webpack-parallel-uglify-plugin
: 多进程执行代码压缩,提升构建速度
mini-css-extract-plugin
: 分离样式文件,CSS 提取为独立文件,支持按需加载 (替代extract-text-webpack-plugin)
serviceworker-webpack-plugin
:为网页应用增加离线缓存功能
clean-webpack-plugin
: 目录清理
ModuleConcatenationPlugin
: 开启 Scope Hoisting
speed-measure-webpack-plugin
: 可以看到每个 Loader 和 Plugin 执行耗时 (整个打包耗时、每个 Plugin 和 Loader 耗时)
webpack-bundle-analyzer
: 可视化 Webpack 输出文件的体积 (业务组件、依赖第三方模块)
Loader和Plugin的区别
Loader
本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。
因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作。Plugin
就是插件,基于事件流框架 Tapable
,插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。Loader
在 module.rules 中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object,内部包含了 test(类型文件)、loader、options (参数)等属性。Plugin
在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入。source map是什么?生产环境怎么用?
source map
是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre map。map文件只要不打开开发者工具,浏览器是不会加载的。
线上环境一般有三种处理方案:
hidden-source-map
:借助第三方错误监控平台 Sentry 使用
nosources-source-map
:只会显示具体行数以及查看源代码的错误栈。安全性比 sourcemap 高
sourcemap
:通过 nginx 设置将 .map 文件只对白名单开放(公司内网)
webpack 性能优化
- 升级 webpack 版本,3升4,实测是提升了几十秒的打包速度
多进程/多实例构建
:thread-loader
压缩代码
:通过对应的plugin来压缩css、js代码。
图片压缩
:通过配置 image-webpack-loader。
缩小打包作用域
:- exclude/include (确定 loader 规则范围)
- resolve.modules 指明第三方模块的绝对路径 (减少不必要的查找)
- resolve.mainFields 只采用 main 字段作为入口文件描述字段 (减少搜索步骤,需要考虑到所有运行时依赖的第三方模块的入口文件描述字段)
- resolve.extensions 尽可能减少后缀尝试的可能性
- noParse 对完全不需要解析的库进行忽略 (不去解析但仍会打包到 bundle 中,注意被忽略掉的文件里不应该包含 import、require、define 等模块化语句)
- IgnorePlugin (完全排除模块)
充分利用缓存提升二次构建速度
:- babel-loader 开启缓存
- terser-webpack-plugin 开启缓存
- 使用 cache-loader 或者 hard-source-webpack-plugin
Tree shaking
:ESM项目开启 useExport标记,使用 tree-shaking。
- gzip压缩:打包过程可以进行gzip压缩,优化静态资源文件的体积。
代码分割
代码分割的本质其实就是在
源代码直接上线
和打包成唯一脚本main.bundle.js
这两种极端方案之间的一种更适合实际场景的中间状态。「用可接受的服务器性能压力增加来换取更好的用户体验。」
源代码直接上线:虽然过程可控,但是http请求多,性能开销大。
打包成唯一脚本:一把梭完自己爽,服务器压力小,但是页面空白期长,用户体验不好。
tree-sharking
Tree shaking(摇树)是一种用于优化 JavaScript代码的技术,旨在通过静态分析的方式去除未使用的代码(dead code)从而减小最终的代码包大小。
Tree shaking的实现原理如下:
- 静态分析:Tree shaking利用es6的静态分析技术,分析模块之间的依赖关系,从入口模块开始递归地遍历整个模块依赖图。
- 标记依赖:在遍历过程中,对于每个模块,通过识别模块中的导入(import)和导出(export)语句,标记出模块与模块之间的依赖关系。
- 标记使用:在遍历过程中,对于每个模块,通过识别模块中的变量引用和函数调用,标记出实际被使用的代码。
- 剔除未使用的代码:根据标记的依赖和使用信息,从每个模块中剔除未被使用的代码。这样,最终的打包结果就不会包含未使用的代码,从而减小了代码的体积。
Tree shaking的关键在于静态分析和标记依赖/使用的过程。通过这些分析和标记的信息,工具可以确定哪些代码是被使用的,哪些代码是未使用的,然后进行相应的优化。
需要注意的是,Tree shaking只能消除那些静态可分析的未使用代码。对于动态导入(dynamic import)或通过字符串拼接等方式引入的模块,无法在构建时进行静态分析,因此无法进行有效的优化。在这种情况下,需要使用代码分割等技术来实现动态加载和按需加载的效果。
Babel
大多数JavaScript Parser遵循
estree
规范,Babel 最初基于 acorn
项目(轻量级现代 JavaScript 解析器)
Babel大概分为三大部分:- 解析:将代码转换成 AST
- 词法分析:将代码(字符串)分割为token流,即语法单元成的数组
- 语法分析:分析token流(上面生成的数组)并生成 AST
- 转换:访问 AST 的节点进行变换操作生产新的 AST
- Taro就是利用 babel 完成的小程序语法转换
- 生成:以新的 AST 为基础生成代码
vite 为什么快
- 按需动态编译。
初始化时
- webpack启动时,需要打包,先把所有文件build一遍,从入口开始遍历所有依赖文件,然后编译成打包后的多个js文件,最后打包到bundler里面。
- vite启动时不需要打包,当浏览器需要哪个文件时,再对模块内容进行编译。利用现代浏览器本身支持esm,会自动的向依赖的module发送请求。
热更新
- webpack每次都需要把所有模块重新编译一遍,再把改动的模块发送给浏览器。
- vite当改动某个模块时,仅需要让浏览器重新请求模块即可。
- esbuild构建
esbuild使用go语言编写,编译速度比用js编写的webpack 快10-100倍。
webpack与vite区别
构建速度
Webpack: Webpack的构建速度相对较慢,尤其在大型项目中,因为它需要分析整个依赖图,进行多次文件扫描和转译。
Vite: Vite以开发模式下的极速构建著称。它利用ES模块的特性,只构建正在编辑的文件,而不是整个项目。这使得它在开发环境下几乎是即时的。
开发模式
Webpack: Webpack通常使用热模块替换(HMR)来实现快速开发模式,但配置相对复杂。
Vite: Vite的开发模式非常轻量且快速,支持HMR,但无需额外配置,因为它默认支持。
配置复杂度
Webpack: Webpack的配置相对复杂,特别是在处理不同类型的资源和加载器时。
Vite: Vite鼓励零配置,使得项目起步非常简单,但同时也支持自定义配置,使其适用于复杂项目。
插件生态
Webpack: Webpack拥有庞大的插件生态系统,适用于各种不同的需求。
Vite: Vite也有相当数量的插件,但相对较小,因为它的开发模式和构建方式减少了对一些传统插件的需求。
编译方式
Webpack: Webpack使用了多种加载器和插件来处理不同类型的资源,如JavaScript、CSS、图片等。
Vite: Vite利用ES模块原生支持,使用原生浏览器导入来处理模块,不需要大规模的编译和打包。
应用场景
Webpack: 适用于复杂的大型项目,特别是需要大量自定义配置和复杂构建管道的项目。
Vite: 更适用于小到中型项目,或者需要快速开发原型和小型应用的场景。
打包原理
Webpack: Webpack的打包原理是将所有资源打包成一个或多个bundle文件,通常是一个JavaScript文件。
Vite: Vite的打包原理是保持开发时的模块化结构,使用浏览器原生的导入机制,在生产环境中进行代码分割和优化