webpack
什么是webpack?
用于构建 JavaScript 应用程序的静态模块打包器,将各类型资源进行打包
webpack 基本概念
- entry:入口
- output: 出口
- module: 模块,Webpack 会从配置的 entry 开始递归找出所有依赖的模块。
- chunk: 代码块,打包过程中的中间产物,用于代码合并和分割
- bundle: 输出文件,根据模块之间的依赖关系生成的最终输出文件
- loader:模块转换器,用于把模块原内容按照需求转换成新内容。
- plugin:扩展插件,在构建过程中,通过不同时机暴露出来的事件,处理不同的事情,解决loader无法实现的内容
原理
1.运行流程:
- 初始化参数:从配置文件和Shell语句中读取与合并参数,得出最终的参数。执行配置插件实例化
new Plugin()
- 开始编译:通过初始化参数初始化
compiler
对象,加载所有配置的插件plugin
,执行对象的run
方法,开始执行编译 - 确定入口:根据配置的
entry
找出所有的入口文件 - 编译模块:从入口文件出发,调用所有配置的
loader
对模块进行转换,再找出所有依赖模块,通过递归编译入口所有依赖文件 - 完成编译模块:得到编译后的内容和依赖关系
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的
Chunk
,再把每个Chunk
转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会 - 输出完成:根据
output
出口文件,把文件写入到文件系统
总结:初始化所有参数,执行插件实例化,初始化compiler
对象,加载插件,执行run
方法开始编译。基于入口entry
文件递归遍历依赖文件,通过loader
编译所有文件。处理模块合并、分割成Chunk
。基于出口文件output
输出
2.热更新HMR
作用:代码修改,浏览器页面跟着修改
原理:
- 使用
webpack-dev-server
(后面简称 WDS)托管静态资源,同时以 Runtime 方式注入 HMR 客户端代码 - 浏览器加载页面后,与 WDS 建立 WebSocket 连接
- Webpack 监听到文件变化后,增量构建发生变更的模块,并通过 WebSocket 发送
hash
事件 - 浏览器接收到
hash
事件后,请求manifest
资源文件,确认增量变更范围 - 浏览器加载发生变更的增量模块
- Webpack 运行时触发变更模块的
module.hot.accept
回调,执行代码变更逻辑
总结:基于webpack-dev-server
托管静态资源,当webpack
发现文件变化后,通过websocket
发送hash
事件,通知浏览器执行变更
开启热更新:
// webpack.config.js
module.exports = {
// ...
devServer: {
// 必须设置 devServer.hot = true,启动 HMR 功能
hot: true
}
};
3.Tree shaking
作用:移除未使用的代码
原理:
- make阶段:收集模块导出变量并记录到模块依赖关系图中
- seal阶段:遍历模块依赖关系图并标记那些导出变量有没有被使用
- 构建阶段:利用Terser将没有被用到的导出语句删除
总结:通过遍历依赖关系图,标记未使用的模块,在构建时删除
Loader
Loader 的职责
以处理 SCSS 文件为例:
- SCSS 源代码会先交给 sass-loader 把 SCSS 转换成 CSS;
- 把 sass-loader 输出的 CSS 交给 css-loader 处理,找出 CSS 中依赖的资源、压缩 CSS 等;
- 把 css-loader 输出的 CSS 交给 style-loader 处理,转换成通过脚本加载的 JavaScript 代码;
module.exports = {
module: {
rules: [
{
// 增加对 SCSS 文件的支持
test: /.scss$/,
// SCSS 文件的处理顺序为先 sass-loader 再 css-loader 再 style-loader
use: [
'style-loader',
{
loader:'css-loader',
// 给 css-loader 传入配置项
options:{
minimize:true,
}
},
'sass-loader'],
},
]
},
};
通过loader
转换,将无法直接运用的代码,转化成可以运行的代码。保持每个loader
职责单一
Loader的Demo
处理source
源文件内容,并通过return
输出一个编译后的文件
module.exports = function(source) {
// source 为 compiler 传递给 Loader 的一个文件的原内容
// 该函数需要返回处理后的内容,这里简单起见,直接把原内容返回了,相当于该 Loader 没有做任何转换
return source;
};
Plugin
事件Compiler 和 Compilation
定义:
- Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;
- Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。
区别: Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。
Plugin的Demo
读取输出资源、代码块、模块及其依赖,并且可以修改输出资源的内容。代码如下:
class Plugin {
apply(compiler) {
compiler.plugin('emit', function (compilation, callback) {
// compilation.chunks 存放所有代码块,是一个数组
compilation.chunks.forEach(function (chunk) {
// chunk 代表一个代码块
// 代码块由多个模块组成,通过 chunk.forEachModule 能读取组成代码块的每个模块
chunk.forEachModule(function (module) {
// module 代表一个模块
// module.fileDependencies 存放当前模块的所有依赖的文件路径,是一个数组
module.fileDependencies.forEach(function (filepath) {
});
});
// Webpack 会根据 Chunk 去生成输出的文件资源,每个 Chunk 都对应一个及其以上的输出文件
// 例如在 Chunk 中包含了 CSS 模块并且使用了 ExtractTextPlugin 时,
// 该 Chunk 就会生成 .js 和 .css 两个文件
chunk.files.forEach(function (filename) {
// compilation.assets 存放当前所有即将输出的资源
// 调用一个输出资源的 source() 方法能获取到输出资源的内容
let source = compilation.assets[filename].source();
});
});
// 这是一个异步事件,要记得调用 callback 通知 Webpack 本次事件监听处理结束。
// 如果忘记了调用 callback,Webpack 将一直卡在这里而不会往后执行。
callback();
})
}
}
实践优化
1.缩小文件搜索范围
优化loader配置
- test 正确书写正则匹配
- cacheDirectory 开启缓存
- include 匹配需要编译的文件
- exclude 过滤编译的文件
注:不同loader开启缓存配置不同
module.exports = {
module: {
rules: [
{
// 如果项目源码中只有 js 文件就不要写成 /.jsx?$/,提升正则表达式性能
test: /.js$/,
// babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启
use: ['babel-loader?cacheDirectory'],
// 只对项目根目录下的 src 目录中的文件采用 babel-loader
include: path.resolve(__dirname, 'src'),
},
]
},
};
优化 resolve.alias 配置
配置项通过别名来把原导入路径映射成一个新的导入路径
module.exports = {
resolve: {
// 减少耗时的递归解析操作
alias: {
'@': path.resolve(__dirname, './src'),
}
},
};
2.压缩代码
js
UglifyJsPlugin
:通过封装 UglifyJS 实现压缩。ParallelUglifyPlugin
:多进程并行处理压缩
css
- cssnano css-loader配置
minimize
3.代码分割
SplitChunksPlugin
: 公共模块抽取
4.CDN加速
将静态文件上传到服务器上配置图片路径,通过配置publicPath
设置前缀路径
5.Tree Shaking
保留ESModule
,修改 .babelrc
文件为如下:
{
"presets": [
[
"env",
{
"modules": false
}
]
]
}
6.按需加载
分割代码的功能去实现按需加载
/* webpackChunkName: "名称" */
举例:
- 网页首次加载时只加载
main.js
文件,网页会展示一个按钮,main.js
文件中只包含监听按钮事件和加载按需加载的代码。 - 当按钮被点击时才去加载被分割出去的
show.js
文件,加载成功后再执行show.js
里的函数。
window.document.getElementById('btn').addEventListener('click', function () {
// 当按钮被点击后才去加载 show.js 文件,文件加载成功后执行文件导出的函数
import(/* webpackChunkName: "show" */ './show').then((show) => {
show('Webpack');
})
});
module.exports = function (content) {
window.alert('Hello ' + content);
};
Webpack,配置如下:
module.exports = {
// JS 执行入口文件
entry: {
main: './main.js',
},
output: {
// 为从 entry 中配置生成的 Chunk 配置输出文件的名称
filename: '[name].js',
// 为动态加载的 Chunk 配置输出文件的名称
chunkFilename: '[name].js',
}
};
常用loader
1.加载文件
- raw-loader:把文本文件的内容加载到代码中去,在 3-20加载SVG 中有介绍。
- file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件,在 3-19加载图片、3-20加载 SVG、4-9 CDN 加速 中有介绍。
- url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去,在 3-19加载图片、3-20加载 SVG 中有介绍。
- source-map-loader:加载额外的 Source Map 文件,以方便断点调试,在 3-21加载 Source Map 中有介绍。
- svg-inline-loader:把压缩后的 SVG 内容注入到代码中,在 3-20加载 SVG 中有介绍。
- node-loader:加载 Node.js 原生模块
.node
文件。 - image-loader:加载并且压缩图片文件。
- json-loader:加载 JSON 文件。
- yaml-loader:加载 YAML 文件。
2.编译模版
- pug-loader:把 Pug 模版转换成 JavaScript 函数返回。
- handlebars-loader:把 Handlebars 模版编译成函数返回。
- ejs-loader:把 EJS 模版编译成函数返回。
- haml-loader:把 HAML 代码转换成 HTML。
- markdown-loader:把 Markdown 文件转换成 HTML。
3.转换脚本语言
- babel-loader:把 ES6 转换成 ES5,在3-1使用 ES6 语言中有介绍。
- ts-loader:把 TypeScript 转换成 JavaScript,在3-2使用 TypeScript 语言中有遇到。
- awesome-typescript-loader:把 TypeScript 转换成 JavaScript,性能要比 ts-loader 好。
- coffee-loader:把 CoffeeScript 转换成 JavaScript。
4.转换样式文件
- css-loader:加载 CSS,支持模块化、压缩、文件导入等特性。
- style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。
- sass-loader:把 SCSS/SASS 代码转换成 CSS,在3-4使用 SCSS 语言中有介绍。
- postcss-loader:扩展 CSS 语法,使用下一代 CSS,在3-5使用 PostCSS中有介绍。
- less-loader:把 Less 代码转换成 CSS 代码。
- stylus-loader:把 Stylus 代码转换成 CSS 代码。
5.检查代码
- eslint-loader:通过 ESLint 检查 JavaScript 代码,在 3-16检查代码中有介绍。
- tslint-loader:通过 TSLint 检查 TypeScript 代码。
- mocha-loader:加载 Mocha 测试用例代码。
- coverjs-loader:计算测试覆盖率。
6.其它
- vue-loader:加载 Vue.js 单文件组件,在3-7使用 Vue 框架中有介绍。
- i18n-loader:加载多语言版本,支持国际化。
- ignore-loader:忽略掉部分文件,在3-11构建同构应用中有介绍。
- ui-component-loader:按需加载 UI 组件库,例如在使用 antd UI 组件库时,不会因为只用到了 Button 组件而打包进所有的组件。
常用plugin
1.用于修改行为
- define-plugin:定义环境变量,在4-7区分环境中有介绍。
- context-replacement-plugin:修改
require
语句在寻找文件时的默认行为。 - ignore-plugin:用于忽略部分文件。
2.用于优化
- commons-chunk-plugin:提取公共代码,在4-11提取公共代码中有介绍。
- extract-text-webpack-plugin:提取 JavaScript 中的 CSS 代码到单独的文件中,在1-5使用 Plugin 中有介绍。
- prepack-webpack-plugin:通过 Facebook 的 Prepack 优化输出的 JavaScript 代码性能,在 4-13使用 Prepack 中有介绍。
- uglifyjs-webpack-plugin:通过 UglifyES 压缩 ES6 代码,在 4-8压缩代码中有介绍。
- webpack-parallel-uglify-plugin:多进程执行 UglifyJS 代码压缩,提升构建速度。
- imagemin-webpack-plugin:压缩图片文件。
- webpack-spritesmith:用插件制作雪碧图。
- ModuleConcatenationPlugin:开启 Webpack Scope Hoisting 功能,在4-14开启 ScopeHoisting中有介绍。
- dll-plugin:借鉴 DDL 的思想大幅度提升构建速度,在4-2使用 DllPlugin中有介绍。
- hot-module-replacement-plugin:开启模块热替换功能。
3.其它
- serviceworker-webpack-plugin:给网页应用增加离线缓存功能,在3-14 构建离线应用中有介绍。
- stylelint-webpack-plugin:集成 stylelint 到项目中,在3-16检查代码中有介绍。
- i18n-webpack-plugin:给你的网页支持国际化。
- provide-plugin:从环境中提供的全局变量中加载模块,而不用导入对应的文件。
- web-webpack-plugin:方便的为单页应用输出 HTML,比 html-webpack-plugin 好用。
Vite
介绍
一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:
特点:
- 快速的冷启动:
esbuild
预构建 - 即时的模块热更新:基于
ESM
的HMR
,同时利用浏览器缓存策略提升速度 - 按需加载:利用浏览器
ESM
支持,实现按需加载
依赖解析和预构建
依赖解析
- 预构建 它们可以提高页面加载速度,并将 CommonJS / UMD 转换为 ESM 格式。预构建这一步由 esbuild 执行,这使得 Vite 的冷启动时间比任何基于 JavaScript 的打包器都要快得多。
- 重写导入为合法的 URL,例如
/node_modules/.vite/my-dep.js?v=f3sf2ebd
以便浏览器能够正确导入它们。 - 解析后的依赖请求会以 HTTP 头
max-age=31536000,immutable
强缓存,以提高在开发时的页面重载性能。
预构建目的
- CommonJS 和 UMD 兼容性: 开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因此,Vite 必须先将作为 CommonJS 或 UMD 发布的依赖项转换为 ESM。
- 性能: Vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能
热更新
一套原生 ESM 的 HMR API。 具有 HMR 功能的框架可以利用该 API 提供即时、准确的更新,而无需重新加载页面或清除应用程序状态。