一、webpack
(一) 打包流程
- 初始化:Webpack 从配置文件(webpack.config.js 或命令行参数)中读取配置信息,包括入口起点(entry)、输出配置(output)、加载器(loader)、插件(plugin)等。
- 启动编译过程:当接收到构建指令后,Webpack 启动一个新的编译周期。在此阶段,它会创建一个 Compiler 对象,该对象负责整个项目的构建流程。
- 读取并解析入口: Compiler 对象根据配置的入口起点开始读取和解析模块。首先处理 JavaScript 文件,并递归地找出所有依赖模块。
- 构建依赖图: Webpack 通过静态分析生成一个详细的模块依赖关系图,其中包含每个模块及其对应的导入和导出信息。
- 预处理与转换模块(loader): 在遍历依赖图的过程中,Webpack 使用配置好的加载器对不同类型的模块进行预处理和转换,例如将 SASS/SCSS 转换为 CSS,将 TypeScript 转换为 JavaScript 等。
- 执行插件钩子(plugin) : 插件在整个构建过程中扮演着关键角色。它们可以在不同的生命周期钩子函数中执行自定义操作,如资源优化、代码分割、环境变量注入等。
- 代码生成与优化: 根据模块间的依赖关系,Webpack 将各个模块的内容合并成最终的 bundle 文件,并在生产模式下执行 Tree Shaking、Scope Hoisting、UglifyJS 压缩等优化操作。
- 生成输出文件: Webpack 根据配置中的 output 参数生成打包后的文件,包括 JS bundle、CSS bundle、图片和其他资源,并将它们放置到指定的目录结构中。
- 完成编译 : 所有资源打包完成后,Webpack 结束当前编译周期,并输出结果。如果配置了监听模式(watch mode),则会持续监听文件变化并重新触发构建流程。
简化版
- Webpack CLI 启动打包流程;
- 载入 Webpack 核心模块,创建 Compiler 对象;
- 使用 Compiler 对象开始编译整个项目;
- 从入口文件开始,解析模块依赖,形成依赖关系树;
- 递归依赖树,将每个模块交给对应的 Loader 处理;
- 合并 Loader 处理完的结果,将打包结果输出到 dist 目录。
通俗版
Webpack 启动后,会根据我们的配置,找到项目配置的入口起点(entry),然后顺着入口文件中的代码,根据代码中出现的 import(ES Modules)或者是 require(CommonJS)之类的语句,解析推断出来这个文件所依赖的资源模块,然后再分别去解析每个资源模块的依赖,周而复始,最后形成整个项目中所有用到的文件之间的依赖关系树
有了这个依赖关系树过后, Webpack 会遍历(递归)这个依赖树,找到每个节点对应的资源文件,然后根据配置选项中的 Loader 配置,交给对应的 Loader 去加载这个模块,最后将加载的结果放入 bundle.js(打包结果)中,从而实现整个项目的打包
(二) 核心概念
- 入口(Entry) :入口是 Webpack 构建流程的起点。在配置文件中,开发者指定一个或多个入口模块, webpack从入口开始递归地找出所有依赖关系,并构建一个完整的模块依赖图。
- 输出(Output) :输出配置告诉 Webpack 打包后的资源应该放在哪里以及如何命名。你可以设置输出目录、公共路径、打包后文件的名称等信息。Webpack 会根据模块依赖关系图将所有经过处理的模块合并成一个或多个 bundle 。
- 模块(Module) : 任何类型的文件都可以视为模块。它可以是 JavaScript 文件、CSS 样式表、图片、字体等。Webpack 使用各种加载器(Loader)来解析和转换不同类型的模块。
- 加载器(Loader): 加载器用于预处理文件,它们负责将非 JavaScript 模块转换为有效的模块,或者对现有模块进行编译、压缩等操作。
- 插件(Plugin): 插件提供了 Webpack 更加灵活且强大的功能扩展。它们在 Webpack 构建生命周期的不同阶段执行任务,如代码优化、资源管理、环境变量注入、资产管理和性能分析等。
- 模式(Mode): 提供了两种模式:development(开发模式)和 production(生产模式)。不同的模式下,Webpack 会有不同的默认优化策略和行为。
- Tree Shaking: 通过静态分析移除未使用的模块或函数的技术,旨在减少最终生成的 bundle 大小,提升代码利用率。
(三) Loader
- 单一原则: 每个 Loader 只做一件事;
- 链式调用: Webpack 会按顺序链式调用每个 Loader;
- 统一原则: 遵循 Webpack制定的设计规则和结构,输入与输出均为字符串,各个 Loader 完全独立,即插即用;
常用Loader:
- babel-loader:用于将 ES6 及以上版本的 JavaScript 代码转译成向后兼容的 ES5 代码,同时也支持 JSX(React)和其他 Babel 转换插件。
- css-loader:处理 CSS 文件,允许你像导入 JavaScript 模块一样 import 或 require() CSS 文件,并且可以解析 CSS 中的 @import 和 url() 引用。
- style-loader:将 CSS 作为内联样式直接注入到 HTML 文档中,或者创建 标签插入到 DOM 中。通常与 css-loader 结合使用以实现样式表的加载和应用。
- postcss-loader:在 CSS 后处理阶段添加对自动前缀、变量替换、CSS Modules 等特性的支持,配合不同的 PostCSS 插件使用。
- less-loader / sass-loader:分别用于编译 Less 和 Sass/SCSS 文件为 CSS。
- file-loader / url-loader:对于图片或其他资源文件,file-loader 会复制它们到输出目录并返回一个指向新路径的 URL,而 url-loader 允许设置一个大小限制,当文件大小小于该限制时,会将其转为 Data URI 方式内联到 JavaScript 或 CSS 中。
- ts-loader / awesome-typescript-loader:这两个 Loader 都用于将 TypeScript 文件转换为 JavaScript。
- eslint-loader:在编译期间执行 ESLint 检查,确保代码符合特定的编码规范。
(四) plugin
Webpack 插件是基于事件驱动机制和 Tapable 系统,核心原理是通过监听和响应编译过程中的事件钩子,在恰当的时间点介入并控制构建流程,从而扩展和定制 Webpack 的功能
Webpack 事件流编程范式的核心是基础类 Tapable,是一种 观察者模式 的实现事件的订阅与广播
- Webpack 插件是一个具有 apply 方法的 JavaScript 类或函数对象。当用户在 Webpack 配置文件中引入并实例化一个插件时(例如 new MyWebpackPlugin(options)),这个实例会被添加到配置的 plugins 数组中。在 Webpack 启动编译过程时,它会遍历这个数组,并对每个插件调用其 apply 方法,传入一个 compiler 对象。
- 插件通过调用 compiler.hooks 或 compilation.hooks 上的 .tap、.tapAsync 或 .tapPromise 方法来监听特定的事件(即钩子)。当这些事件在编译过程中触发时,相应的插件回调函数就会被执行。
- 当对应的编译阶段触发钩子时,Webapck 会按照注册顺序依次调用已挂载到该钩子上的所有插件方法。这些方法可以在适当的时机修改输出资源、处理额外任务、优化构建结果等
- 所有插件完成自己的工作后,Webpack 继续进行后续的编译和打包流程,直到最终生成目标文件。
javascript
class MyWebpackPlugin{
// 注册插件时,会调用 apply 方法
// apply 方法接收 compiler 对象
// 通过 compiler 上提供的 Api,可以对事件进行监听,执行相应的操作
apply(compiler){
// compilation 是监听每次编译循环
// 每次文件变化,都会生成新的 compilation 对象并触发该事件
compiler.plugin('compilation',function(compilation) {})
}
}
// webpack.config.js 注册插件
module.export = {
plugins:[
new MyWebpackPlugin(options),
]
}
Compiler 与 Compilation 对象
- compiler: compiler 对象代表整个 Webpack 环境,可以简单的理解为 Webpack 实例,它包含了当前 Webpack 中的所有配置信息,全局唯一,只在启动时完成初始化创建,随着生命周期逐一传递。
- Compilation: compilation 对象则是在每次构建过程中生成的一个新实例,包含了当前模块依赖图、编译生成的资源等信息,并且提供了更细粒度的构建步骤相关的钩子函数,同时通过它提供的 api,可以监听每次编译过程中触发的事件钩子,Compilation对应每次编译,每轮编译循环均会重新创建。
常用插件
- UglifyjsWebpackPlugin : 压缩、混淆代码;
- CommonsChunkPlugin: 代码分割;
- ProvidePlugin: 自动加载模块;
- html-webpack-plugin: 生成 HTML 文件,通常用来注入编译后的 JavaScript 和 CSS 文件链接。它可以根据模板文件(如 index.html)自动生成带有所有 bundle 资源引用的 HTML 页面;
- extract-text-webpack-plugin / mini-css-extract-plugin: 抽离样式,生成 css 文件; DefinePlugin: 定义全局变量;
- optimize-css-assets-webpack-plugin: 进一步优化 CSS 资源,去除重复样式或进行压缩;
- webpack-bundle-analyzer: 代码分析;
- compression-webpack-plugin: 对输出的资源文件进行 Gzip 压缩,从而减小传输体积;
- happypack: 使用多进程,加速代码构建;
- EnvironmentPlugin: 定义环境变量;
(五) webpack 热更新实现原理
HMR(Hot Module Replacement)
- 当修改了一个或多个文件;
- 文件系统接收更改并通知 webpack;
- webpack 重新编译构建一个或多个模块,并通知 HMR 服务器进行更新;
- HMR Server 使用 webSocket 通知 HMR runtime 需要更新,HMR 运行时通过 HTTP 请求更新 jsonp
- HMR 运行时替换更新中的模块,如果确定这些模块无法更新,则触发整个页面刷新
(六) webpack 层面如何做性能优化
- 缩小编译范围
- 针对特定文件类型使用loader,避免全局匹配。
- include/exclude: 指定搜索范围/排除不必要的搜索范围
- modules: 指定模块路径,减少递归搜索
- alias:缓存目录,避免重复寻址
- 缓存利用
- 使用cache-loader等缓存机制,避免重复编译未改变的模块
- DLLPlugin 和 DLLReferencePlugin 也可以提前进行打包并缓存,避免每次都重新编译
- 忽略node_moudles
- babel-loader,忽略node_moudles,避免编译第三方库中已经被编译过的代码。babel也可以缓存编译
- 多进程并发
- 使用thread-loader或者HappyPack(现已不再维护,推荐使用thread-loader)来并发处理loader任务
- 使用webpack-parallel-uglify-plugin或terser-webpack-plugin的并行压缩选项
- source-map
- 开发使用cheap-module-eval-source-map 生产使用hidden-source-map
- Tree Shaking
- 启用mode: 'production'以激活webpack的最小化模式和tree shaking特性。
- 使用ES6模块导入导出,确保静态分析能够剔除未使用的代码。
- Scope Hoisting
- 开启后,体积更小,创建函数作用域更小,代码可读性更好
- 资源压缩和优化
- 对JS、CSS资源启用压缩,如使用TerserWebpackPlugin压缩JavaScript,MiniCssExtractPlugin配合CSS压缩工具压缩CSS。
- 压缩图片和其他资源,如使用image-webpack-loader或file-loader配合compression-webpack-plugin压缩图片和gzip压缩输出文件
(七) vite为什么比webpack快
- Vite 利用了现代浏览器对原生 ES 模块(ESM)的支持,在开发环境下,它通过 HTTP 服务直接提供源代码 ,在启动项目时,Vite 只需加载并转换入口模块以及所需的直接依赖,而不是一次性编译整个项目,从而实现了快速的启动速度和热更新。
- Vite 采取的是即时编译策略,只有当浏览器请求某个模块时,Vite 才会去编译那个模块。
- Vite 使用 esbuild 进行依赖预构建,Webpack 中使用的 Babel,esbuild 在解析和转换代码的速度上有显著优势
- Vite 的开发模式下尽量避免不必要的优化,例如在开发阶段暂不进行 Tree Shaking 和 Scope Hoisting,只关注快速的开发反馈循环,这也加快了开发构建的速度