本文是学习青训营课程《构建 Webpack 知识体系》与《Vite 知识体系》的学习笔记。虽然目前看不太明白但是还是学习一把用于自我复盘:
Webpack
重要概念
Module是离散功能块,相比于完整程序提供了更小的接触面。精心编写的模块提供了可靠的抽象和封装界限,使得应用程序中每个模块都具有条理清楚的设计和明确的目的。Chunk用于管理捆绑过程。输出束(bundle)由chunk组成,其中有几种类型(例如 entry 和 child )。通常,chunk 直接与 bundle 相对应,但是有些配置不会产生一对一的关系。chunk(代码块)是一些模块 (module) 的封装单元。于 webpack 运行时的 seal 封包阶段生成,且直到资源构建阶段都会持续发生变化的代码块,在此期间插件通过各种钩子事件侵入各个编译阶段对 chunk 进行优化处理。Bundle由许多不同的模块生成,包含已经经过加载和编译过程的源文件的最终版本。bundle(包) 是 webpack 进程执行完毕后输出的最终结果,是对 chunk 进行编译压缩打包等处理后的产出。通常与构建完成的 chunk 为一对一的关系。Entry Point入口起点,告诉 webpack 从哪里开始,并遵循着依赖图知道要打包哪些文件。
流程
- 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler
- 编译:从 Entry 出发,针对每个 Modlue 串行调用对应的Loader去翻译文件的内容,再找到该 Module 的依赖,递归编译
- 输出:将编译后的 Module 组合成为 Chunk,将 Chunk 转换成文件,输出到文件系统中
01. Webpack - Loader
带有副作用的内容转译器。链式调用:pitch -> 解析资源 -> 执行
大多数Loader做的事情就是将source转译为另一种形式的output
css-loader将 css 转换为__WEBPACK_DEFAULT_EXPORT__ = ".a{ xxx }"格式html-loader将 html 转换为__WEBPACK_DEFAULT_EXPORT__ = "<!DOCTYPE xxx"格式vue-loader更复杂一些,会将.vue文件转化为多个 JavaScript 函数,分别对应 template、js、css、custom block
module.exports = function(source, sourceMap?, data?) {
// source为loader的输入,可能是文件内容,也可能是上一个loader处理结果
return source
}
// 上例通过 return 语句返回处理结果,
// 除此之外 Loader 还可以以 callback 方式返回更多信息,
// 供下游 Loader 或者 Webpack 本身使用
export default function loader(content, map) {
// ...
linter.printOutput(linter.lint(content))
this.callback(null, content, map)
}
this.callback(
// 异常信息,loader正常运行时传递null值即可
err: Error | null,
// 转译结果
content: string | Buffer,
// 源码的 sourceMap 信息
sourceMap?: SourceMap,
// 任意需要在Loader间传递的值
// 经常用来传递 ast 对象,避免重复解析
data?: any
)
Loader 可能导致的缓存问题
背景:在Loader中执行的各种资源内容转译操作通常都是CPU密集型——> 这放在单线程的 Node 场景下可能导致性能问题;又或者异步 Loader 会挂起后续的加载器队列直到异步Loader触发回调,或可能导致整个加载器链条的执行时间过长
解决方案:默认情况下 Webpack 会缓存Loader的执行结果直到资源或资源依赖发生变化,可以通过 this.cachable显示声明不作缓存
Loader 链式调用的优缺点
链式调用的优点
- 保持单个 Loader 的单一职责,一定程度上降低代码的复杂度;
- 细粒度的功能能够被组装成复杂而灵活的处理链条,提升单个 Loader 的可复用性。
链式调用的缺点
-
Loader 链条一旦启动之后,需要所有 Loader 都执行完毕才会结束,没有中断的机会 —— 除非显式抛出异常
-
某些场景下并不需要关心资源的具体内容,但 Loader 需要在 source 内容被读取出来之后才会执行。
——> 解决方案:
pitch
Loader + Jest 单元测试
在 Loader 中编写单元测试收益非常高。
- 创建在 Webpack 实例,并运行 Loader
- 获取 Loader 执行结果,比对、分析判断是否符合预期
- 判断执行过程中是否出错
Loader 调试技巧
开发 Loader 的过程中,有一些小技巧能够提升调试效率,包括:
- 使用 ndb 工具实现断点调试
- 使用
npm link将 Loader 模块链接到测试项目 - 使用
resolveLoader配置项将 Loader 所在的目录加入到测试项目中
// webpack.config.js
module.exports = {
resolveLoader:{
modules: ['node_modules','./loaders/'],
}
}
02. Webpack - Plugin
Plugin 是 Webpack 另一套扩展机制,功能更强,能够在各个对象的钩子中插入特化处理逻辑,它可以覆盖 Webpack 全生命流程,能力、灵活性、复杂度都会比 Loader 强很多
html-webpack-plugin:简化 HTML 文件创建 (依赖于 html-loader)serviceworker-webpack-plugin:为网页应用增加离线缓存功能webpack-bundle-analyzer: 可视化 Webpack 输出文件的体积 (业务组件、依赖第三方模块)webpack-parallel-uglify-plugin: 多进程执行代码压缩,提升构建速度
Loader和Plugin的区别
Loader 本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。 翻译官,对其他类型的资源进行转译的预处理工作。在module.rules 中配置
Plugin 插件,可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入。
03. Webpack 相关问题
- Babel 具体有什么功能?
Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
babel的主要工作流程分为三个阶段,
解析(parse),转换(transform),生成(generate)解析(parse): 通过@babel/parser把源代码字符串转成抽象语法树(AST),在解析过程中主要是两个阶段:词法分析和语法分析转换(transform): 通过@babel/traverse遍历抽象语法树(AST),并调用Babel配置文件中的插件,对抽象语法树(AST)进行增删改生成(generate): 通过@babel/generator把转换后的抽象语法书(AST)生成目标代码
- Babel/core 是干嘛的?
@babel/core是 babel的核心,主要作用就是根据我们的配置文件转换代码,配置文件一般是.babelrc(静态文件)或babel.config.js(可编程),主要作用如下:- 加载和处理配置(config)
- 加载插件
- 调用 Parser 进行语法解析,生成 AST
- 调用 Traverser 遍历AST,并使用访问者模式应用'插件'对 AST 进行转换
- 生成代码,包括SourceMap转换和源代码生成
- Tree-Shaking 是什么?
树摇,用于删除 Dead Code ---> 开启 tree-shaking:对于工具类库如
Lodash有奇效- 代码没有被用到,unreachable
- 执行结果不会被用到
- 代码只读不写
- 说一下热更新
HMRWebpack的热更新可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。 HMR的核心就是客户端从服务端拉取更新后的文件 chunk diff当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起
Ajax请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起jsonp请求获取该chunk的增量更新。
umi框架
开发框架 常用框架,支持目录路由,和Antd一起用,常用来写管理系统。
umi webpack // 查看当前的 webpack 配置
小缺点:
- 内置支持 less,不支持 sass 和 stylus,需要通过 chainWebpack 配置或者 umi 插件支持
- 无法直接修改 webpack 配置,需要通过 chainWebpack 配置。
- webpack 默认使用版本是 4, 最新的版本 5 需要手动开启
Vite
是 React的好朋友,开发过程中它利用浏览器native ES Module特性导入组织代码,生产中利用rollup作为打包工具。使用的是ES Module,所以代码中不可以使用CommonJs;
- 光速启动
- 热模块替换
- 按需编译
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
项目根目录创建vite.config.js,可以对vite项目进行深度配置。比webpack的配置文件少很多内容,对于小白如我极为友好。
补充小知识:
require.js属于遵循AMD规范,AMD推崇依赖前置Sea.js遵循CMD规范,CMD推崇依赖就近- js 中现在比较成熟的有四种模块加载方案。
- 第一种是
CommonJS方案- 第二种是
AMD方案- 第三种是
CMD方案- 第四种方案是
ES6提出的方案,使用 import 和 export 的形式来导入导出模块
Gulp
Gulp基于流,主要用于构建任务和处理任务流,可以执行一系列的任务来处理文件,如编译Sass、压缩图片、合并文件等。易于维护。
Webpack模块化按需加载。主要用于模块打包和构建应用程序,可以将前端项目中的各种资源文件打包成静态资源。它提供了丰富的功能,如代码分割、模块热替换、动态导入等,可以帮助开发者构建复杂的前端应用。
在实际开发中当中会将两种混合使用。
Webpack与Vite
平时开发接触的比较多的是Webpack,用于开发小程序。PC端管理系统则使用基于Webpack的umi。自己练习小DEMO则一般选择vite,因为启动快,方便自己平时练习。
Vite开发环境启动更快的原因
Vite是基于esbuild预构建依赖。而esbuild是采用go语言编写,因为go语言的操作是纳秒级别,而js是以毫秒计数,所以vite比用js编写的打包器快10-100倍。webpack先打包,再启动开发服务器;vite直接启动开发服务器【因为现代浏览器本身就支持ES Modules】,请求哪个模块再对哪个模块进行实时编译;- 启动的时候不需要打包,也就无需分析模块依赖、编译,所以启动速度非常快。
- 热更新方面:
Vite当某个模块内容改变时,会让浏览器去重新请求该模块,而不webpack则需要重新将该模块的所有依赖重新编译。 vite使用传统的rollup进行打包
package.json
- 下载的包信息(地址,版本号等),不包含依赖包信息
- 执行
npm install时,node会先从package.json读取dependencies信息,再与 node_modules中的模块对比,下载没有的,已有的则检查更新. dependencies生产环境使用的依赖。默认情况下直接安装到dependencies中devDependencies: 开发环境的依赖包:eg. webpack, vite, babel,bundleDependencies: 打包依赖。它的值是一个数组,在发布包时,bundleDependencies里面的依赖都会被一起打包。peerDependencies: 同伴依赖.不会被自动安装,通常用于表示与另一个包的依赖与兼容性关系optionalDependencies: 可选依赖,它不会阻塞主功能的使用,安装或者引入失败也无妨。