Webpack
概述
前端项目由什么构成?
前端项目是由各种资源构成的
这些资源可以依赖手工,但
- 依赖手工,比如有50个JS文件...操作,过程繁琐
- 当代码文件之间有依赖的时候,就得严格按依赖顺序书写
- 开发与生产环境一致,难以接入TS或JS新特性
- 比较难接入Less、Sass 等工具
- JS、图片、CSS资源管理模型不一致
发展历程
这些都是旧时代非常突出的问题,对开发效率影响非常大,直到...
出现了很多工程化工具,某种程度上正是这些工具的出现,才有了”前端工程”这一概念
什么是Webpack
Webpack本质上是一种前端资源编译、打包工具
- 多份资源文件打包成一个 Bundle
- 支持Babel、Eslint、TS、CoffeScript、Less、 Sass
- 支持模块化处理css、图片等资源文件支持HMR+开发服务器
- 支持持续监听、持续构建支持代码分离
- 支持Tree-shaking
- 支持SourceMap
核心流程
示例
-
安装:
npm i -D webpack webpack-cli
-
编辑配置文件
- 执行编译命令:
npx webpack
会把项目里管理的JS文件打包成一个文件,并且处理好互相引入的过程。
核心流程
- 入口处理:从
entry
文件开始,启动编译流程 - 依赖解析:从
entry
文件开始,根据require
orimport
等语句找到依赖资源。 - 资源解析:根据
module
配置,调用资源转移器,将png、css等非标准JS资源转义为JS内容。 - 合并打包:将转译后的资源内容合并打包为可直接在浏览器运行的JS文件
注:递归调用2、3,直到所有资源处理完毕
小结
Webpack本质上做的就是模块化+一致性的事情
- 多个文件资源合并成一个,减少 http 请求数
- 支持模块化开发
- 支持高级JS特性
- 支持 Typescript、CoffeeScript 方言
- 统一图片、CSS、字体等其它资源的处理模型
- Etc...
如何使用
配置项
关于Webpack的使用方法,基本都围绕“配置”展开,而这些配置大致可划分为两类:
- 流程类:作用于流程中某个or若干个环节,直接影响打包效果的配置项
- 入口处理:
entry
、context
- 依赖解析:
resolve
、externals
- 资源解析:
module
- 合并打包:
optimization
、mode
、target
- 入口处理:
- 工具类:主流程之外,提供更多工程化能力的配置项
最常用的一些配置项:
entry/output
module/plugins
mode
watch/devServer/devtool
使用流程-起步
- 声明入口
entry
- 声明出口
output
- 运行
npx webpack
使用流程-处理CSS
- 安装Loader:
npm add -D css-loader style-loader
- 添加
module
处理css文件
思考题:
-
Loader 有什么作用?为什么这里需要用到css-loader, style-loader?
用来在Webpack里将不同类型的文件(例如CSS、图片、字体等)转化为可以被浏览器理解的JavaScript模块。
由于Webpack本身只理解JavaScript文件,loader的作用是将其他文件类型处理后嵌入到JavaScript中,以便Webpack可以管理和打包它们。
-
与旧时代——在HTML文件中维护css相比,这种方式会有什么优劣处?
- 优点:易于维护、性能优化、模块化、作用域控制(样式隔离)
- 缺点:配置复杂、学习成本高
-
有没有接触过Less、Sass、Stylus 这一类CSS 预编译框架?如何在 Webpack接入这些工具?
-
CSS预编译框架提供了变量、嵌套规则、混入、继承等功能,可以极大地提升CSS的组织性和功能性。
-
使用方式(和处理CSS相似)
- 安装Loader:
npm install sass sass-loader --save-dev
- 添加
module
处理sass文件等。。。
- 安装Loader:
-
使用流程-接入Babel
babel是一种转义JS代码的工具,可以解决js代码对低版本兼容性的问题
- 安装依赖:
npm i -D @babel/core @babel/preset-env babel-loader
- 声明产物出口
output
- 执行
npx webpack
思考题:
- Babel具体有什么功能? - 语法转换、插件拓展、转义JSX、类型注释
- Babel与Webpack分别解决了什么问题?为何两者能协作到一起了?
- Babel:将现代 JavaScript 转换为向后兼容的版本,使代码可以在旧的环境中运行。
- Webpack:将项目的多个模块和资源打包成少量的 bundle 文件,优化性能和加载速度。
- 协作:通过
babel-loader
将 Babel 的编译能力集成到 Webpack 的打包流程中, 从而实现既兼容旧环境,又优化资源加载的构建方案。
参考资料:
@babel/preset-typescript · Babel
使用流程-生成HTML
- 安装依赖:
npm i -D html-webpack-plugin
- 声明产物出口
output
- 执行
npx webpack
思考题:
- 相比于手工维护HTML内容,这种自动生成的方式有什么优缺点?
- 优点:自动更新资源、避免重复维护的错误、统一管理模板和内容、可集成更多自动化流程
- 缺点:学习配置成本高、生成页面的灵活性受限、调试和排错过程复杂、增加代码的构建时间
参考资料:
使用工具-HMR
Hot Module Replacement - 模块热替换(页面实时刷新)
-
开启HMR
{ devServer: { hot: true } }
-
启动Webpack:
npx webpack serve
使用工具-Tree-Shaking
Tree-Shaking-树摇,用于删除 Dead Code(没有用到的代码)
Dead Code:
- 代码没有被用到,不可到达
- 代码的执行结果不会被用到
- 代码只读不写
开启tree-shaking:
{
mode: "production"
optimization: {
usedExports: true
}
}
PS:对工具类库如 Lodash 有奇效
其他工具
Webpack还有缓存、Sourcemap、性能监控、日志、代码压缩、分包等工具
Loader
引入
在默认情况下,Webpack只能处理js的资源。为了处理非标准JS资源,就设计出了资源翻译模块——Loader,用来将非JS资源翻译为标准JS(包括CSS也会被转换成JS)。
使用
-
安装依赖
-
配置module配置
module: { rules: [ { test: 正则匹配,如/\.less$/i, use: [loader列表] } ] }
链式调用
- less-loader:实现 less => css 的转换
- css-loader:将CSS 包装成类似module.exports = "${css}"的内容,包装后的内容符合 JavaScript 语法
- style-loader:将 css 模块包进 require 语句,并在运行时调用 injectStyle 等 函数将内容注入到页面的 style 标签
其它特性
- 链式执行
- 支持异步执行
- 分normal、pitch两种模式
如何编写
常见Loader
站在使用角度,建议掌握这些常见Loader的功能、配置方法
思考题
-
Loader 输入是什么?要求的输出是什么?
- 输入:
Loader
的输入通常是一个模块的源代码,可能是文本、样式、图片或其他静态文件。 - 输出:
Loader
的输出是 JavaScript 模块代码,通常以字符串形式返回,并会被 Webpack 打包成最终输出的 JavaScript 文件。这可以包括经过编译、转换或其他形式处理后的代码。
- 输入:
-
Loader 的链式调用是什么意思?如何串联多个 Loader?
- 链式调用的概念:Webpack 中允许多个
Loader
串联工作,称为“链式调用”。每个Loader
会依次处理输入文件的数据,并将处理结果传递给下一个Loader
。 - 串联方式:Webpack 会从==最后一个==
Loader
开始调用链式中的各个Loader
。比如use: ['style-loader', 'css-loader', 'sass-loader']
的配置中,sass-loader
会首先将 Sass 文件转换为 CSS,然后css-loader
处理 CSS 文件的模块化,最后style-loader
将 CSS 注入到 JavaScript 中。
- 链式调用的概念:Webpack 中允许多个
-
Loader 中如何处理异步场景?
在异步场景中,可以使用
this.async()
方法来处理异步逻辑。 调用this.async()
后,它会返回一个callback
函数,Loader
可以在处理完异步操作后调用该callback
,并将处理结果作为参数传递。
Plugin
概念
很多知名工具,如:
- VS Code、Web Storm、Chrome、Firefox
- Babel、Webpack、Rollup、Eslint
- Vue、Redux、Quill、Axios
等等,都设计了所谓“插件”架构,为什么? - 提升工具的拓展性
如图,是webpack的编译流程,这是一个特别复杂的过程,那么:
- 新人需要了解整个流程细节,上手成本高
- 功能迭代成本高,牵一发动全身
- 功能僵化,作为开源项目而言缺乏成长性(更少的人会愿意参与)
也就是说 心智成本高 => 可维护性低 => 生命力弱
插件架构的精髓:对扩展开放,对修改封闭
甚至,Webpack 本身的很多功能也是基于插件实现的
使用插件
- 下载依赖(一般加-D)
- 导入插件并加入配置中
Dashboard可以帮我们美化编译结果
编写插件
插件围绕钩子
展开
钩子的核心信息:
- 时机:编译过程的特定节点,Webpack会以钩子形式通知插件此刻正在发生什么事情;
- 上下文:通过tapable 提供的回调机制,以参数方式传递上下文信息;
- 交互:在上下文参数对象中附带了很多存在side effect 的交互接口,插件可以通过这些接口改变
如图,
- 时机:
compier.hooks.compilation
- 参数:
compilation
等 - 交互:
dependencyFactories.set
思考题
-
Loader 与插件有什么区同点?
- Loader
- 作用:专门用于转换某一类型的模块内容,例如将 TypeScript 文件编译成 JavaScript,将 Sass 编译为 CSS。
- 工作方式:文件级别的转换,Loader 将文件内容作为输入,对其进行处理后返回 JavaScript 模块输出。
- 用法:通常在
module.rules
中配置,用于特定文件类型的转换(如 JS、CSS 等)。
- 插件(Plugin):
- 作用:增强 Webpack 的构建能力,进行更广泛的构建过程控制,如打包优化、代码注入、文件生成等。
- 工作方式:直接作用于 Webpack 的生命周期,不局限于单个文件,而是可以访问和改变整个构建流程。
- 用法:在
plugins
数组中配置,通过 Webpack 提供的 API 对打包过程中的生命周期事件进行操作。
- 相似点:
- 两者都可以改变构建输出、提高项目构建的灵活性,且可以在一定程度上配合使用。
- 都可自定义,提供灵活扩展方式,用户可以编写自定义的
Loader
或Plugin
满足特定需求。
- Loader
-
“钩子”有什么作用?如何监听钩子函数?
-
作用:钩子(Hook)是 Webpack 生命周期中的事件。插件使用这些钩子在构建的不同阶段执行特定的代码,从而控制和自定义 Webpack 的行为。例如,钩子可以用于监听构建开始、完成、模块处理等不同阶段,适时执行自定义逻辑。
-
如何监听钩子:插件可以通过
compiler.hooks.<hookName>.tap
方法监听钩子,并在钩子触发时执行函数。Webpack 提供了多个生命周期钩子,以下是示例:class MyPlugin { apply(compiler) { // 在编译开始时触发 compiler.hooks.compile.tap('MyPlugin', (params) => { console.log('构建开始...'); }); // 在编译结束时触发 compiler.hooks.done.tap('MyPlugin', (stats) => { console.log('构建完成'); }); } }
-
常用的钩子类型:
- 同步钩子(Sync Hook):按顺序执行所有注册的回调,构建流程会等待钩子执行完成。
- 异步钩子(Async Hook):支持异步操作,比如 AsyncSeriesHook、AsyncParallelHook 等, 可以通过 tapAsync 或 tapPromise 进行异步回调。
-
意义:钩子为插件提供了扩展能力,让开发者可以根据项目需求灵活地加入自定义逻辑,改变和控制构建过程。
-
参考资料
如何学习
- 入门应用
- 理解打包流程
- 熟练掌握常用配置项、Loader、插件的使用方法,能够灵活搭建集成Vue、React、Babel、Eslint、Less、 Sass、图片处理等工具的Webpack 环境
- 掌握常见脚手架工具的用法,例如:Vue—cli、create—react-app. @angular/cli
- 进阶
- 理解Loader、Plugin 机制,能够自行开发 Webpack 组件
- 理解常见性能优化手段,并能用于解决实际问题理解前端工程化概念与生态现状
- 大师级
- 阅读源码,理解Webpack 编译、打包原理,甚至能够参与共建
参考资料:
Vite
构建工具
前端工程的痛点
前端构建工具的意义
概要介绍
Vite 概览
业界案例
当下问题
两大行业趋势
- 全球浏览器对原生ESM的普遍支持(目前占比92%以上)
- 基于原生语言(Go、Rust)编写前端编译工具链,如 Go 语言编写的Esbuild、Rust 编写的 SWC
浏览器原生ESM支持
基于原生ESM的开发服务优势
- 无需打包项目源代码
- 天然的按需加载
- 可以利用文件级的浏览器缓存
基于 Esbuild 的编译性能优化
Esbuild——基于 Golang 开发的前端工具,具备如下能力:
- 打包器:Bundler(对标Webpack)
- 编译器:Transformer(对标Babel)
- 压缩器:Minifier(对标JS压缩工具)
其性能极高,在Vite中被深度使用
内置的 Web 构建能力
Vite 开箱即用的功能等价于
webpack、webpack-dev-server、css-loader、style-loader、less-loader、sass-loader、postcss-loader、file-loader
MiniCssExtractPlugin、HTMLWebpackPlugin
因为Vite对常见的Web开发的需求进行了封装,并且内置了一些默认的最佳实践。
举例:webpack.config.ts
等价于 vite.config.ts
上手实战
vite的特点:响应迅速、开箱即用
项目初始化
在创建时选择需要的初始化参数即可初始化成功,注意安装依赖和启动项目需要进入目录后操作。
这里如果初始化时选择了ts,我们会看到build
命令是tsc && vite build
,是因为vite底层使用的vite build
没有使用ts
的类型支持,所以需要借助ts官方工具进行ts的类型检查。
使用Sass/Scss & CSS Modules
运行pnpm i sass -D
安装sass
在页面中导入sass文件,并在Header组件中应用这个样式。
其中.module是用上了css module,模块化的CSS代码更不容易出现类名的相互影响。
使用静态资源
这里以svg图片为例,页面上会渲染出src相对于项目根路径的一个url,vite作为dev server会把图片内容返回浏览器。
除了常见的图片格式,Vite也内置了对于JSON、Worker、WASM资源的加载支持。参考文档
使用HMR(默认开启)
HMR(Hot Module Replacement,热模块替换)是一种在开发时快速更新页面的技术,不需要刷新整个页面,只更新发生变化的模块。
值得一提的是,vite的HMR支持了对状态的保存,也就是说热模块替换后,组件里的值不会被重置。
生产环境 Tree Shaking(默认开启)
概念:Tree Shaking 是一种用于移除 JavaScript 项目中未使用代码的优化技术,主要用于打包过程中,通过检测代码的依赖关系,将没有用到的代码“摇掉”(删除),从而减小最终输出文件的大小,提升加载性能。
核心机制:Tree Shaking 的核心机制基于 ES6 模块系统的静态结构(import 和 export),它可以在打包时分析模块依赖关系,确定哪些代码是未引用的,从而安全地移除这些代码。
使用场景:主要适用于模块化的前端项目,尤其是那些使用第三方库的项目,可以自动去除没有用到的库函数或模块,减小打包后的体积。例如,如果从 lodash 中引入了一个 map 函数,Tree Shaking 会剔除 lodash 中其他未引用的函数。
优化原理:
- 基于 ESM 的 import/export 语句依赖关系,与运行时状态无关
- 在构建阶段将未使用到的代码进行删除
说明:因为默认也开启了代码压缩,如果要更直观的看到压缩效果,请配置build: {minify: false}
以关闭代码压缩。
注意:CommonJS
格式由于require
的部分可能依赖运行时计算的结果,所以不能做到Tree Shaking
整体架构
关键技术:依赖预打包
- 为什么要进行预打包?
- 避免 node_modules 过多的文件请求
- 将CommonJS 格式转换为ESM格式
- 实现原理:
- 服务启动前扫描代码中用到的依赖
- 用Esbuild对依赖代码进行预打包
- 改写import语句,指定依赖为预构建产物路径
关键技术:单文件编译
- 实现:用 Esbuild 编译 TS/JSX
- 优势:编译速度提升10-100×
- 局限性:
- 不支持类型检查
- 不支持语法降级到ES5(最低ES6)
关键技术:代码压缩
Esbuild 作为默认压缩工具,替换传统的Terser、Uglify.js 等压缩工具
关键技术:插件机制
- 开发阶段—>模拟Rollup插件机制
- 生产环境—>直接使用Rollup
- 插件兼容性具体可查阅:Vite Rollup Plugins
进阶路线
深入双引擎
Vite 插件开发
为什么需要插件机制?
- 抽离核心逻辑
- 易于拓展
通过使用不同的Hook,我们可以在不同的构建阶段插入自定义的逻辑
开发Vite插件→配置文件引入
参考资料:
复杂度较低的插件:JSON插件
复杂度中等的插件:esbuild插件
复杂度较高的插件:react插件
学习方法:
- 先看文档,过一遍插件钩子的功能
- 然后多学习其它插件的实现,掌握套路
代码分割(拆包)
拆包前:打成一个产物文件,无法进行并发请求、缓存复用率低
拆包后:
参考资料:
Configuration Options | Rollup
JS编译工具(Babel)
出现的原因:
- JavaScript 语法标准繁,浏览器支持程度不一
- 开发者需要用到高级语法
参考资料:
语法安全降级
问题:以Promise语法为例,IE11没有支持,要进行语法降级
参考文档:
服务端渲染(SSR)
是一种常见的渲染模式,用于提升首屏性能和SEO优化。
构建阶段:
代码执行阶段:
参考文档:
深入了解底层标准
重点特性:CJS规范、ESM规范、HTTP2.0特性
参考资料:
ES modules: A cartoon deep-dive
Vite 社区生态
Github 40k+ star(可参考 webpack 61.3 K, rollup 21.8 k),并且目前还在持续维护
官方提供插件:
- @vitejs/plugin-vue,提供Vue 3支持
- @vitejs/plugin-vue-jsx,提供 Vue 3 JSX 支持
- @vitejs/plugin-react,提供 React 支持
- @vitejs/plugin-legacy,提供低版本浏览器降级支持
海量社区插件:awesome-vite: ⚡️ A curated list of awesome things related to Vite.js
众多框架内置:Nuxt、SvelteKit、Astro、Vitepress