前言
前端开发时一般都会使用框架(React、Vue)、ES6模块化语法、Less或者Sass等 css预处现器等语法。这样的代码要想在浏览器运行,必须要编译成浏览器能够识别的 JS、Css等语法。而打包工具的作用就是压缩代码、兼容性处理、提升代码性能、代码编译等。
常见的打包工具包括Grunt、Gulp、Parcel、Webpack、Rollup、Vite等等,目前市面上最常用的是webpack。
第一章 Webpack的基本配置
1. 基本使用
Webpack 是一个静态资源打包工具。它会以一个或多个文件作为打包的入口,将我们整个项目所有文件编译组合成一个或多个文件输出。输出的文件就是编译好的文件bundle,可以在浏览器端运行。
Webpack本身的功能是有限的:
- 开发模式:只能编译JS中的ES Module语法
- 生产模式:不仅可以编译JS中的ES Module语法,还可以压缩JS代码
- 直接使用JS代码
index.html主文件
未打包的main.js文件
控制台报错
- 使用打包后的JS代码
第一步:初始化一个package.json文件npm init -y
第二步:下载webpack npm i webpack webpack-cli -D
第三步:打包指定目录文件npx webpack ./src/main.js --mode=development
development模式
production模式
控制台正常输出
2. 核心概念
- Entry:入口文件,webpack编译的起点,即从哪个文件开始打包
- output:输出,webpack打包完的文件从哪里输出。其中output.filename对应initial chunk文件名称,output.chunkFilename对应non-initial chunk文件名称
- Loader:加载器,webpack本身只能处理JS、json资源文件,其他资源需要借助loader处理
- Plugin:插件,扩展webpack功能
- mode:模式,分为development开发模式和production生产模式
3. 其他概念
- Compiler:编译管理器,webpack启动后会创建compiler对象,该对象一直存活到编译结束
- Compilation:单次编译过程的管理器,每次触发重新编译时,都会创建一个新的compilation对象
- Dependence:依赖对象,webpack基于该类型记录模块间依赖关系
- Module:webpack内部所有资源都会以“module”对象形式存在,所有关于资源的操作、转译、合并都是以 “module” 为基本单位进行的
- Chunk:编译完成准备输出时,webpack会将module按特定的规则组织成一个一个的chunk,这些chunk某种程度上跟最终输出一一对应
4. Webpack基本配置
在根目录下创建一个Webpack.config.js文件并完成基础配置
const path = require("path");
module.exports = {
// 入口,相对路径
entry: "./src/main.js",
// 输出,绝对路径
output: {
path: path.resolve(__dirname,"dist"), // 路径
filename: "main.js",// 文件名
},
// 加载器
module: {rules: []},
// 插件
plugins:[],
// 模式
mode: "development"
}
可以在output中添加clean配置,自动清除上一次的打包资源。
output: {
path: path.resolve(__dirname,"dist"), // 路径
filename: "main.js",// 文件名
clean: true, // 在生成文件之前清空output目录
},
5. create-react-app中webpack的默认配置
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization:{
splitChunks:{ // 拆分模块
chunks: 'all'
},
minimizer:[
new TerserPlugin({
terserOptions:{
compress:{
pure_funcs:['console.log'], // 移除console.log代码
}
}
})
]
},
mode: 'development', // 开发模式
entry: { // 多入口打包
case:{
import:'./src/case.tsx',
dependOn:'vendor' // 共享模块的chunk名称
},
list:{
import:'./src/list.tsx',
dependOn:'vendor'
}
},
devServer:{
hot:true // HMR
},
output: {
clean: true, // 自动清除上次打包内容
path: path.join(__dirname, 'dist'),
filename: '[name]-[contenthash].js',
},
resolve: {
extensions: ['.tsx', '.ts']
},
plugins:[
new HtmlWebpackPlugin({
title:'管理输出', // html文件标题
filename: 'index.html', // html文件名字
}),
],
module: {
rules: [
{
test: /\.(js|ts|jsx|tsx)$/, // 解析.tsx文件
include: path.appSrc, // 只包含app/src/文件夹下面的文件
use: [{
loader: "esbuild-loader",
options: {
loader: "tsx",
target: "es2015"
},
}]
},
{
test: /\.css$/,
use: ['style-loader',{
loader:'css-loader',
options:{
sourceMap:true
}
}]
},
{
test: /\.html$/,
use: 'html-loader'
}
],
},
};
第二章 资源文件的处理
1. 处理样式资源
1.1 style-loader
作用:把CSS插入到DOM中,推荐将style-loader与css-loader一起使用
// 下载
npm install --save-dev style-loader
// 使用加载器
module: {
rules: [
{
test: /\.css$/i, // 正则表达式匹配文件
use: ["style-loader", "css-loader"], // 从右到左依次使用loader处理
}
]
},
1.2 css-loader
css-loader会对@import和url()进行处理,就像js解析import/require()一样。
// 下载
npm install --save-dev css-loader
// 使用加载器
module: {
rules: [
{
test: /\.css$/i, // 正则表达式匹配文件
use: ["style-loader", "css-loader"], // 从右到左依次使用loader处理
}
]
},
1.3 less-loader
作用:将Less编译为CSS的loader
// 下载
npm install less less-loader --save-dev
// 使用加载器
module: {
rules: [
{
test: /\.less$/i,
use: ['style-loader','css-loader','less-loader'],
},
]
},
1.4 sass-loader
作用:加载Sass/SCSS文件并将他们编译为CSS。
// 下载
npm install sass-loader sass --save-dev
// 使用加载器
module: {
rules: [
{
test: /\.s[ac]ss$/i,
use: [
// 将JS字符串生成为style节点
'style-loader',
// 将CSS转化成CommonJS模块
'css-loader',
// 将Sass编译成CSS
'sass-loader',
],
},
]
},
1.5 stylus-loader
作用:将Stylus文件编译为CSS
// 下载
npm install stylus stylus-loader --save-dev
// 使用加载器
module: {
rules: [
{
test: /.styl$/,
loader: "stylus-loader",
},
]
},
2. 处理图片资源
2.1 简单配置
Webpack4处理图片资源时需要使用file-loader和url-loader两个加载器,而Webpack5已经内置了两个Loader,使用时不需要单独下载安装,只需要简单配置即可。
module: {
rules: [
{
test: /\.(png|jpe?g|gif|webp)$/i,
type: "asset",
}
]
},
图片作为背景图片,通过url引入
2.2 资源模块
资源模块(asset module)是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外loader。包括以下内容:
- raw-loader:将文件导入为字符串
- url-loader:将文件作为data URI内联到bundle中
- file-loader:将文件发送到输出目录
一般情况下webpack将按照默认条件,自动地在resource和inline之间进行选择:小于8kb的文件,将会视为inline模块类型,否则会被视为resource模块类型。可以通过在webpack配置的module rule层级中,设置Rule.parser.dataUrlCondition.maxSize选项来修改此条件。
- resource资源:直接发送到输出目录,其路径会被被注入到bundle中
- inline资源:文件会作为data URI注入到bundle中,格式为base64,可以减少网络请求
module: {
rules: [
{
test: /\.(png|jpe?g|gif|webp)$/i,
type: "asset",
parser: {
dataUrlCondition: {
maxSize: 100 * 1024 // 小于100kb转化为inline资源
}
}
}
]
},
🤔:url-loader和file-loader的区别是什么?
🙋:大致总结如下
首先概念不同:file-loader可以指定要复制和放置资源文件的位置,以及如何使用版本哈希命名以获得更好的缓存。url-loader允许你有条件地将文件转换为内联的base-64 URL (当文件小于给定的阈值),这会减少小文件的HTTP请求数。如果文件大于该阈值,会自动的交给file-loader处理。
处理图片资源方式不同:file-loader将文件上的import和require解析为url,并将该文件发射到输出目录中。url-loader可以识别图片的大小,并把图片转换成base64,从而减少代码的体积,如果图片超过设定的限制,就会继续用file-loader处理。
2.3 自定义输出文件
默认情况下,asset/resource模块以[hash][ext][query]文件名发送到输出目录中,可以通过在webpack配置中设置output.assetModuleFilename来修改此模板字符串。
特点:不能对asset/resource模块下的内容进行区分
output: {
// 所有输出文件的路径
path: path.resolve(__dirname,"dist"),
// 入口文件对应的输出文件名称
filename: "main.js",
// asset/resource模块的输出路径和名称配置
assetModuleFilename: 'images/[hash][ext][query]'
},
也可以通过generator.filename单独设置某个resource模块,输出到指定目录下。
{
test: /\.(png|jpe?g|gif|webp)$/i,
type: "asset",
parser: {
dataUrlCondition: {
maxSize: 100 * 1024 // 100kb
}
},
// 将图片资源输出到dist/static/images目录中
// 文件名为前8位hash值 + 文件扩展名 + 其他扩展字段
generator: {
filename: 'static/images/[hash:8][ext][query]'
}
}
3. 处理字体图标资源
字体图标资源也属于资源模块,但是不需要转化为base-64格式,所以需要使用Resource。
{
test: /\.(ttf|Woff2?)$/i,
type:"asset/resource",
generator: {
filename: 'static/media/[hash:8][ext][query]'
}
}
4. 处理其他资源
例如音频、视频等标资源也属于资源模块,同样也不需要转化为base-64格式,所以需要使用Resource。
{
test: /\.(map3|map4|avi)$/i,
type:"asset/resource",
generator: {
filename: 'static/media/[hash:8][ext][query]'
}
}
5. 处理JS资源
Webpack对JS的处理是有限的,只能编译JS中ES模块化语法,不能编译其他语法,导致JS不能在IE等浏览器中运行,所以需要做一些兼容性处理。
- Babel:JS兼容性处理
- Eslint:代码格式校验
需要先完成Eslint检测代码格式无误后,再由Babel做代码兼容性处理。
5.1 Eslint
作用:用来检测js和jsx语法的工具,可以扩展各种功能
- 配置文件
- .eslintrc.*:新建位于项目根目录的文件,可以是.js或者.json格式
- package.json中eslintConfig:直接在package文件中添加配置,Eslint会自动查找和读取对应配置规则
- 使用
// 下载
npm install eslint-webpack-plugin eslint --save-dev
// 添加配置文件.eslintrc.js
module.exports = {
// 继承eslint规则
extends:["eslint:recommended"],
env:{
node:true, // 启用node中的全局变量
browser:true, // 启用浏览器中的全局变量
},
parserOptions:{
ecmaVersion: 6,
sourceType: "module"
},
rules:{
"no-var": 2,
}
}
// 添加eslint忽略文件.eslintignore,忽略打包后的dist文件夹
dist
5.2 babel
作用:将ES6语法转换为向后兼容的JS代码,以便能够运行在当前和旧版本的浏览器中
- 配置文件
- babel.config.*:新建位于根目录的文件,格式为.js或者.json
- .babelrc.*:新建位于根目录的文件,格式为.js或者.json
- package.json的babel:直接在package文件中添加配置
- 使用
// 下载
npm install -D babel-loader @babel/core @babel/preset-env
// 使用加载器
{
test: /\.m?js$/,
exclude: /(node_modules)/, // 忽略node包
loader: 'babel-loader',
}
// 添加外部的babel.config.js文件,配置预设规则
module.exports = {
presets: ['@babel/preset-env'] // 智能预设
}
- 处理JS语法
@babel-loader
特点:只能转换基本的语法,如let、const、箭头函数等。对于一些高级语法如 Promise、async、数组新API等,不能进行转换
module.export = {
module: {
rules: [
{
test: /\.js$/, // js文件
exclude: /node_modules/, // 排除node包
loader: 'babel-loader',
options: {
preset: ['@babel/preset-env'] // 智能预设
}
}
]
}
}
@babel/polyfill
特点:解决所有js语法的兼容问题,不需要在webpack中进行配置,下载完依赖后,直接在对应的js文件中import引入即可。
import '@babel/polyfill'
const promise = new Promise(resolve => {
setTimeout(() => {
console.log('定时器执行完成')
}, 1000)
})
缺点:可以转换所有ES6语法的新特征,即使没有使用到,导致打包后文件体积过大。
core-js
特点:core-js是目前最常用的ES语法兼容问题的解决方案,按需加载,灵活使用,可以指定兼容的浏览器版本。
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
preset: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage', //按需加载
corejs: { version: 3 }, //指定 core-js 版本
targets: { //指定兼容性做到哪几个版本的浏览器
chrome: '60',
firefox: '60',
ie: '9',
safari: '10',
edge: '17'
}
}
]
]
}
}
]
以promise为例剖析core-js对于ES6高级语法的处理。
// 引入 core-js 中的 Promise 多态
import 'core-js/es/promise';
// 使用 Promise
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('异步操作完成');
}, 1000);
});
myPromise.then((message) => {
console.log(message); // 将在1秒后输出 "异步操作完成"
});
6. 处理html资源
作用:自动生成一个HTML5文件, 在body中使用script标签引入所有webpack生成的bundle
// 下载
npm install --save-dev html-webpack-plugin
// 使用插件
plugins:[
new ESLintPlugin({
context: path.resolve(__dirname,"src")
}),
new HtmlWebpackPlugin({
// 配置模版,生成的html文件中会自动保留模板格式
template: path.resolve(__dirname,"public/index.html")
})
],
第三章 搭建开发服务器
1. 自动化打包
作用:自动化编译代码,取消手动输入npx webpack指令操作
// 下载
npm install --save-dev webpack-dev-server
// 添加配置项
module.esports = {
// 开发服务器配置
devServer:{
host: "localhost", // 启动服务器域名
port: "3000", // 启动服务器端口号
open: true // 是否自动打开浏览器
},
}
// 使用
npx webpack server
浏览器自动弹出3000窗口
2. 生产模式
生产模式即开发完部署上线,生产模式需要对代码进行优化,让其运行性能更好。优化主要从两个角度出发:
- 优化代码运行性能
- 优化代码打包迪度
一般在项目中会拆分生产模式和开发模式的配置文件,并在package.json中通过不同的指令分别启动。
3. 生产模式优化配置
3.1 提取输出CSS文件
插件:MiniCssExtractPlugin
作用:将CSS提取到单独的文件中,为每个包含CSS的JS文件创建一个CSS文件,并且支持CSS和SourceMaps的按需加载。
- MiniCssExtractPlugin.loader:将css文件以link方式引入
- style-loader:将css样式放到style内联样式中
// 下载
npm install --save-dev mini-css-extract-plugin
// 使用 MiniCssExtractPlugin.loader代替style-loader,并在plugin中引入
module: {
rules: [
{
test: /\.css$/i, // 正则表达式匹配文件
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
]
},
plugins:[
new MiniCssExtractPlugin({
filename: "css/main.css"
})
],
MiniCssExtractPlugin-loader
style-loader
3.2 压缩CSS文件
生产模式默认开启了js和html压缩,针对css,需要使用插件对其进行压缩。
插件:ss-minimizer-webpack-plugin
// 下载
npm install css-minimizer-webpack-plugin --save-dev
// 使用
plugins:[
new CssMinimizerPlugin()
]
第四章 Webpack优化设置
- 优化构建速度
- 升级版本
- noParse忽略部分文件解析
- DllPlugin和DllReferencePlugin避免重复编译第三方库
- 开启多进程loader转换、压缩文件
- oneOf控制loader作用
- include/exclude排除打包文件
- 优化开发体验
- sourceMap映射
- HMR热模块替换
- 减少代码体积
- 区分环境
- 压缩资源,JS、CSS、图片
- Tree Shaking
- splitChunk分离代码
- gzip压缩
- cache缓存
- 运行性能
- 按需加载,动态引入
- preload/prefetch
- core-js
- PWA
1. SourceMap
SourceMap是一个源代码映射的系统,可以生成源代码与构建后代码一一映射的文件。SourceMap会生成一个xxx.map文件,里面包含源代码和构建后代码在每一行、每一列的映射关系,当构建后代码出错了,会通过xxx.map文件,从构建后代码出错位置找到映射后源代码出错位置,从而让浏览器提示源代码文件出错位置,帮助我们更快的找到错误根源。
-
开发模式:cheap-module-source-map
优点: 打包编译速度快,只包含行映射
缺点: 没有列映射
mode: "production",
devtool: "cheap-module-source-map"
-
生产模式:source-map
优点:既包含行映射,又包含列映射
缺点:打包编译速度慢
mode: "production",
devtool: "source-map"
2. 模块热替换
模块热替换(HMR)功能会在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面。主要是通过以下几种方式,来显著加快开发速度:
- 保留在完全重新加载页面期间丢失的应用程序状态
- 只更新变更内容,从而节省开发时间
- 在源代码中CSS/JS产生修改时,会立刻在浏览器中进行更新,相当于在浏览器devtools中直接更改样式
devServer:{
host: "localhost", // 启动服务器域名
port: "3000", // 启动服务器端口号
open: true, // 是否自动打开浏览器
hot: true, // 是否开启HMR热模块替换功能,Webpack5默认开启
},
HRM的原理实际上是 webpack-dev-server(WDS)和浏览器之间维护了一个websocket服务。当本地资源发生变化后,webpack会先将打包生成新的模块代码放入内存中,然后WDS向浏览器推送更新,并附带上构建时的hash,让客户端和上一次资源进行对比。客户端对比出差异后会向WDS发起Ajax请求获取到更改后的内容(文件列表、hash),通过这些信息再向WDS发起jsonp请求获取到最新的模块代码。
3. OneOf
作用:规定一个文件只能被一个loader处理,提升打包速度
module: {
rules: [{
oneOf: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
{
test: /\.less$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'],
},
]
}]
}
4. Include/Exclude
针对JS文件做处理,提升打包编译速度。
- include:包含,只处理xx文件
- exclude:排除,除了xxx文件以外其他文件都需要处理
{
test: /\.m?js$/,
exclude: /(node_modules)/, // 忽略node包
loader: 'babel-loader',
}
{
test: /\.m?js$/,
include: path.resolve(__dirname,"./src"),
loader: 'babel-loader',
}
new ESLintPlugin({
context: path.resolve(__dirname, "src"),
exclude: "node_modules", // exclude的默认值
}),
5. Cache
每次打包时js文件都要经过Eslint检查和Babel编译,速度比较慢。我们可以缓存之前的Eslint检查和Babel编译的结果,这样第二次打包时速度就会更快了。
// 缓存babel编译
{
test: /\.m?js$/,
include: path.resolve(__dirname,"./src"),
loader: 'babel-loader',
options:{
cacheDirectory: true, // 开启babel缓存
cacheCompression: false // 关闭缓存文件压缩
}
}
// 缓存eslint检查
plugins: [
new ESLintPlugin({
context: path.resolve(__dirname, "src"),
exclude: "node_modules", // exclude的默认值
cache: true, // 开启缓存
cacheLocation: path.resolve(__dirname, "./node_modules/.cache/eslintcache") // 缓存路径
}),
]
6. Thead多进程
当项目越来越庞大时,打包速度就会越来越慢,影响最严重的就是JS的打包速度。而对js文件处理主要就是eslint、babel、Terser三个工具,所以要想提升js文件的运行速度,可以开启多进程同时处理js文件,从而提升打包速度。
多进程打包指的是开启电脑的多个进程同时干一件事,速度更快。
⚠️:请在特别耗时的操作中使用,因为每个进程启动就有大约600ms左右开销。而启动进程的数量不得大于CPU的核数
1)获取CPU核数
const os = require("os");
const threads = os.cpus().length;
2)babel解析:开启多进程,设置进程数量
{
test: /\.m?js$/,
include: path.resolve(__dirname, "./src"),
use: [
{
loader: 'thread-loader', // 开启多进程
options: {
works: threads // 进程数量
}
},
{
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启babel缓存
cacheCompression: false // 关闭缓存文件压缩
}
},
],
}
3)eslint校验:开启多进程,设置进程数量
optimization: {
minimizer: [
new CssMinimizerPlugin(), // 压缩css
new TerserWebpackPlugin({ // 开启多进程、设置进程数量
parallel: threads
})
]
}
7. Tree Shaking
用于描述移除JavaScript中的没有使用的代码,前提是必须依赖ES Hodule模块化。
Webpack5目前已经内置了Tree Shaking,无需过多的配置。
Tree Shaking的工作原理如下:
-
ES6模块系统:Tree Shaking的基础是ES6模块系统,它具有静态特性,意味着模块的导入和导出关系在编译时就已经确定,不会受到程序运行时的影响
-
静态分析:在Webpack的构建过程中,会通过静态分析依赖图,从入口文件开始,逐级追踪每个模块的依赖关系,以及模块之间的导入和导出关系
-
标记未使用代码:在分析模块依赖时,Webpack会标记每个变量、函数、类和导入,以确定它们是否被实际使用。如果一个导入的模块只是被导入而没有被使用,或者某个模块的部分代码没有被使用,Webpack会将这些未使用的部分标记为
"unused" -
删除未使用代码:在代码标记为未使用后,Webpack会在最终的代码生成阶段,通过工具(如
UglifyJS等)删除这些未使用的代码,包括未使用的模块、函数、变量和导入等
8. Babel文件体积优化
Babel为编译的每个文件都插入了辅助代码,如公共方法的辅助代码_extend。但是有一些辅助代码会被重复添加到每一个需要它的文件中,造成文件体积过大。通过将捕助代码作为一个独立模块,从而避免重复引入问题。
@babel/plugin-transform-runtime:禁用了Babel自动对每个文件的 runtime注入,改为引入@babel/plugin-transform-runtin内的所有辅助代码。通过减少定义和重复引入,从而减少文件体积。
// 下载
npm i @babel/plugin-transform-runtime -D
// 使用
{
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启babel缓存
cacheCompression: false, // 关闭缓存文件压缩
plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积
}
}
9. 图片压缩
image-minimizer-webpack-plugin插件,可以对本地静态图片进行压缩处理,从而减少代码体积。
压缩图片的模式分为有损压缩和无损压缩两种:
- 无损压缩:imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo
- 有损压缩:imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo
// 下载
npm i image-minimizer-webpack-plugin imagemin -D
// 使用-无损压缩
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.imageminGenerate,
options: {
plugins: [
['gifsicle',{interlaced: true}],
['jpegtran',{progressive: true}],
['optipng',{optimizationLevel: 5}],
[ 'svgo', { plugins: [ 'preset-default', 'prefixIds', { name: 'sortAttrs', params: {xmlnsOrder: 'alphabetical'} } ]
}
]
]
}
}
})
10. Code Split
Code Split,通过将代码分割打包,从而可以按需加载,优化加载速度。
代码分调的作用:
- 分剩文件:将打包生成的文件进行分割,生成多个js文件
- 按需加载:需要哪个文件就加载哪个文件
1)多入口打包
entry:{
app: "./src/app.js",
main: "./src/main.js"
}
2)多入口提取公共模块
optimization: {
splitChunks: {
chunks: "all"
}
}
3)多入口按需加载
// import()动态加载语法,返回值为promise对象
import(./count.js).then((res)=>{
...
}).catch((err)=>{
...
})
4)模块命名
// 对某个引入模块命名
import(/* webpackChunkName:"math" */'./math.js')
// 打包输出文件名称使用
module.exorts = {
output:{
chunkFilename:"static/js/[name].js"
}
}
11. Preload/Prefetch
Webpack 4.6.0提供了预先拉取(prefetching)和预先加载(preloading)的功能,使用这些声明可以修改浏览器处理异步chunk的方式。
1)含义
- Preload:预先加载,浏览器会立即加载资源,异步chunk和父级chunk并行加载。父级chunk下载完成后页面就可以显示,同时不影响异步chunk的下载。
import(
`./utilities/divide`
/* webpackPreload: true */
/* webpackChunkName: "utilities" */
)
- Prefetch:预先拉取,浏览器会在空闲时间下载该模块,且下载是发生在父级chunk加载完成之后。
import(
`./utilities/divide`
/* webpackPrefetch: true */
/* webpackChunkName: "utilities" */
)
2)共同点
- 只加载资源,不会执行资源
- 可以缓存资源
3)区别
- Preload加载优先级高,Prefetch加载优先级低
- Preload只能加载当前页面需要使用的资源,Prefetch可以加载当前页面使用到的资源,也可以加载下一个页面使用到的资源
4)适用场景
- 当前页面优先级高的资源用Preload加载
- 下一个页面需要使用的资源用Prefetch加载
Webpack中使用这两个属性需要通过中间件,不能直接在入口文件index.js中使用
12. Core-js
core-js是用来做ES6以及以上API的polyfill的工具。polyfill翻译过来叫做垫片/补丁,就是用社区上提供的一段代码,让我们在不兼容某些新特性的浏览器上使用该新特性。
core-js一般会和babel一起使用,为ES6及以上语法生成对应的兼容性实现方案,并且会在dist文件夹下面生成一个新的打包文件。
// 下载
npm i core-js
// 配合babel.config.js使用
module.exports = {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: "usage", // 按需加载自动引入
coreJs: 3
}
]
]
}
13. PWA
浙进式网络应用程序(progressive web application PWA)是一种可以提供类似于native app(原生应用程序)体验的Web App的技术。其中最重要的是在离线(offline) 时,应用程序能够继续运行功能。
其内部是通过Service Workers技术实现的。
// 下载
npm i workbox-webpack-plugin --save-dev
// 添加配置
plugins: [
new WorkboxPlugin({
clientsClaim: true,
skipWaiting: true
})
]
// 使用-main.js
if("servicelorker" in navigator{
window.addEventListener("load", ()=>{
navigator.serviceWorker
.register("/service-worker.js")
.then((registration)=>{console.lor("sW registered: ",registration)})
.catch((registrationError)=>{console.log("sW registration failed: ", reristrationError)})
性能优化概述
-
source Map:开发或上线时代码报错能有更加准确的错误提示
-
HMR:开发时只重新编译打包更新变化了的代码,不变的代码使用缓存,从而使更新速度更快
-
oneOf:资源文件一旦被某个loader处理了,就不会继续遍历其他loader,打包速度更快
-
Include/Exclude:排除或只检测某些文件,处理的文件更少,速度更快
-
Cache:对eslint和babel处理的结果进行缓存,让第二次打包速度更快
-
Thead:多进程处理esint和babel任务,速度更快(需要注意的是,进程启动通信都有开销的,要在比较多代码处理时使用才有效)
-
Tree shaking:移除没有使用的多余代码,让代码体积更小
-
babel/plugin-transform-runtime:对babel进行处理,让辅助代码从中引入,而不是每个文件都生成辅助代码,从而体积更小
-
Image hininizer:对项目中图片进行压缩,体积更小,请求速度更快。(本地项目静态图片才需要进行压缩)
-
code Split:对代码进行分割成多个js文件,从而使单个文件体积更小,并行加载速度更快
-
import():动态导入语法,按需加载
-
Preload/Prefetch:对代码进行提前加载,提升用户体验
-
Network cache:对输出资源文件进行更好的命名,利于缓存处理,提升用户体验
-
core-js:对js进行兼容性处理,使代码能运行在低版本浏览器中
-
PWA:实现代码离线访问功能,提升用户体验
第五章 webpack核心模块
1. Loader
1.1 loader工作原理
- 作用
Loader是帮助webpack将不同类型的文件转换为webpack可识别的模块。
- 原理
loader在底层就是一个函数,当webpack解祈资源时,会调用相应的loader方法处理。loader()接收三个参数:
- content:文件内容
- map:与SourceMap相关数据
- meta:其他loader传递的数据
module.exports = function(content, map, meta){
... ....
return content;
}
1.2 分类
- 优先级分类
- pre:前置loader
- normal:普通loader
- inline:内联loader
- post:后置loader
- 不同优先级的执行顺序
- 不同优先级loader:pre > normal >inline > post
- 相同优先级loader:从右到左,从下到上
- 配置优先级
- 配置方式:在webpack.config.js文件中指定loader为pre、normal、post loader中一种,不添加任何指定时,默认为normal loader
- 内联方式:在每个import语句中显式指定loader为inline loader
// pre loader
{
enforce: "pre"
test: /\.js$/,
loader: "loader1"
},
// normal loader
{
test: /\.js$/,
loader: "loader2"
}
// post loader
{
enforce: "post"
test: /\.js$/,
loader: "loader3"
}
// inline loader
// 使用style-loader和css-loader处理styles.css文件
import Styles from 'style-loader!css-loader?modules!./styles.css";
// 前面添加一个“!”,跳过normal loader
import Styles from '!style-loader!css-loader?modules!./styles.css";
// 前面添加一个“-!”,跳过 pre、normal loader
import Styles from '-!style-loader!css-loader?modules!./styles.css";
// 前面添加一个“!!”,跳过pre、normal、postloader
import Styles from '!!style-loader!css-loader?modules!./styles.css";
- Loader分类
- 同步loader
// 只有一个loader时
module.exports = function(content, map, meta){
// 不需要向下传递参数和source-map
return content;
}
// 有多个loader连用时
module.exports = function(content, map,meta){
// err:代表是否有错误
// content:处理后的内容
// source-map:继续向下传递source-map
// meta:给下一个loader传递的参数
this.callback(null,content,map,meta);
}
- 异步loader
module.exports = function(content , map, meta){
// 获取异步的回调函数
const callback = this.async();
setTimeout(() => {
// 参数和同步回调函数一致
callback(null, content, map, meta);
}, 1000);
}
- Raw loader raw loader表示具有raw属性的loader,属性值为布尔值。可以是同步也可以是异步的loader,区别是其接收到的content是Buffer格式的流数据
function mayLoader(content){
// Buffer流,一般用于操作图片、图标等资源文件
return content;
}
mayLoader.raw = true;
module.exports = mayLoader;
- pitch loader
pitch loader表示具有pitch属性的loader,属性值为函数。可以是同步也可以是异步的loader,特点是执行顺序会早于其他loader。
如连用三个loader处理文件资源时,会从左到右依次执行每个loader的pitch方法,然后在从右到左依次执行每个loader方法。
正常的pitch函数无返回值,如果在执行过程中某个pitch有返回值,则会中断执行顺序,转而执行前一个pitch所在的loader方法。
module.exports = function(content){
console.log('normal loader');
return content;
}
module.exports.pitch = function(){
consel.log("pitch loader");
}
pitch无返回值时的执行顺序
pitch2有返回值时的执行顺序
1.3 Loader常用API
| 方法名 | 含义 | 使用 |
|---|---|---|
| this.async | 异步回调loader,返回this.callback | const callback = this.async() |
| this.callback | 同步或异步调用的、可以返回多个结果的函数 | this.callback(err,content,sourceMap?,meta?) |
| this.getOptions(schema) | 获取loader的options配置,schema为验证规则 | this.getOptions(schema) |
| this.emitFile | 生成一个文件 | thisemitFile(name,content,sourceMap) |
| this.utils.contextify | 返回一个相对路径 | this.utils.contextify(context,request) |
| this.utils.absolutify | 返回一个绝对路径 | this.utils.absolutify(context,request) |
1.4 自定义Loader
- 清除console.log代码的loader
module.exports = function (content){
return content.replace(/consolel.log\(.* );?/g,"");
}
- 低版本浏览器适配loader
const babel = require( @babel/core");
const schema = require("./schema.json");
module.exports = function (content) {
// 异步loader
const callback = this.async();
const options = this.getOptions(schema);
// 使用babe1对代码进行编译
babel.transform(content, options, function(err, result){
if (err){
callback(err);
}else{
callback(null, result.code);
}
});
}
2. Plugin
2.1 Plugin原理分析
- 作用
Plugin 可以扩展 webpack,加入一些自定义的构建行为,使 webpack 可以执行更广泛的任务,拥有更强的构建能力。
- 工作原理
webpack 在编译的过程中,会触发一系列 Tapable 钩子事件,插件的作用就是找到对应的钩子,往钩子中挂载任务。当 webpack 构建的时候,插件注册的事件就会随着钩子的触发而执行。
第一步:获取Webpack编译器中的数据;
第二步:在合适的时机,监听Webpack的事件,如compile、emit、done等;
第三步:响应Webpack事件,并对编译期间的资源进行处理,例如对JavaScript、CSS等进行压缩、合并、优化等;
第四步:处理完成后,向Webpack编译器返回相应的处理结果,以便Webpack最终生成相应的文件;
常用的插件工作原理:
html-webpack-plugin:通过在Compilation对象中的html-webpack-plugin-before-html-processing钩子函数中添加生成的html文件内容来实现自动输出html文件clean-webpack-plugin:在编译之前清除目标目录中的文件,就是通过在Compiler对象中监听webpack工作流程的钩子函数来实现的
- 工作流程
1️⃣:webpack 中的 plugin 是一个类(构造函数),通过在 plugins 配置中实例化进行调用
plugins:[new HTMLWebpackPlugin()],
2️⃣:在 plugin 的原型对象上指定了一个 apply 方法,入参是 compiler 对象
apply(compiler){}
3️⃣:指定一个事件钩子,并调用内部提供的API
4️⃣:完成内部操作后,调用 webpack 提供的 callback 方法
// 自定义plugin
class TestPlugin{
constructor(){}
// webpack将compiler对象传递给apply方法
apply(compiler){
// 挂载钩子
compiler.hooks.environmenttap("TestPlugin",()=>{})
compiler.hooks.emit.tap("TestPlugin",(compilation)=>{}
compiler.hooks.emit.tapAsync("Testplugin",(compilation, callback) => {
setTimeout(()=>{
// 调用webpack的回调函数
callback();
}, 2000);
})
}
}
module.exports = TestPlugin;
2.2 webpack钩子
钩子的本质就是事件,为了方便开发者直接介入和控制编译过程,webpack把编译过程中触发的各类关键事件封装成事件接口暴露了出来,这些接口被称为 hooks(钩子)。
Tapable为webpack提供了统一的插件接口(钩子)类型定义,它是webpack的核心功能库。webpack中目前有十种hooks:
Tapable提供了三个方法给插件,用于注入不同类型的自定义构建行为:
- top:即可以注册同步钩子和异步钩子
- topAsync:回调函数方式注册异步钩子
- tapPromise:Promise方式注册异步钩子
plugin通过Tapable注册事件。对于监听事件的触发,同步钩子通过call方法,异步钩子通过callAsync方法和promise方法。
2.3 编译管理器
plugin功能的实现主要依赖于compiler和complation对象,两者都继承自Tapable对象
- Compiler对象
compiler对象中保存着完整的Webpack环境配置,每次启动webpack构建时,都会创建一个独一无二的compiler,它有以下主要属性:
- compiler.options:访问本次启动webpack时所有的配置文件,包括但不限于 loaders、entry、output、plugin等等完整配置信息。
- compiler.inputFileSystem、compiler.outputFileSysten:进行文件操作,相当于Nodejs中fs
- compiler.hooks:注册Tapable的不同种类Hook,从而可以在compiler生命周期中植入不同的逻辑
- Compilation对象
compilation对象代表一次资源的构建,compilation实例能够访问所有的模块和它们的依赖。一个compilation对象会对构建依赖图中所有横块进行编译,在编译阶段横块会被加载(load)、封存(seal)、优化(optimize)、分块(chunk)、哈希(hash)和重新创建(restore)。
它有以下主要属性:
- compilation.modules:访问所有横块,打包的每一个文件都是一个横块
- compilation.chunks:chunk即是多个modules组成而来的一个代码块,入口文件引入的资源组成一个chunk,通过代码分割的模块又是另外的chunk
- compilation.assets:访问本次打包生成所有文件的结果
- compilation.hooks:注册tapable的不同种类Hook,用于在compilation编译模块阶段进行逻辑添加以及修改
compiler对象结构
compilation对象结构
2.4 自定义plugin
创建一个自定义的打包zip插件
const JsZip = require('jszip');
class ZipPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
compiler.hooks.emit.tapPromise('1', (compilation) => {
const assets = compilation.assets;
const zip = new JsZip();
for(let filename in assets) {
zip.file(filename, assets[filename].source())
}
// nodebuffer是node环境中的二进制形式;blob是浏览器环境
return zip.generateAsync({type: 'nodebuffer'}).then((content) =>{
assets[this.options.name] = {
source(){return content},
}
return new Promise((resolve, reject) => {
resolve(compilation);
})
})
})
}
}
module.exports = ZipPlugin;
webpack.config.js中使用插件:
module.exports = {
plugins: [
new ZipPlugin({
name: 'my.zip'
})
]
}
3. Loader和Plugin的区别
1)功能不同
- Loader本质是一个函数,它是一个转换器。webpack只能解析原生js文件,对于其他类型文件就需要用loader进行转换。
- Plugin是一个插件,用于增强webpack功能。webpack在运行的生命周期中会广播出许多事件,Plugin可以监听这些事件,在合适的时机通过webpack提供的API改变输出结果。
2)用法不同
- Loader的配置是在
module.rules中,类型为数组,每⼀项都是⼀个 Object,⾥⾯描述了对于什么类型的⽂件(test),使⽤什么加载(loader)和使⽤的参数(options) - Plugin的配置在
plugins中,类型为数组,每一项是一个Plugin的实例,参数都通过构造函数传入的。
第六章 Webpack工作原理
1. 核心功能
webpack最核心的功能:内容转化+资源合并
- 初始化
- 初始化参数:参数 = 配置文件、配置对象、Shell 参数 + 默认配置
- 创建编译器对象:通过参数创建Compiler对象
- 初始化编译环境:注入内置插件、注册模块工厂、初始化RuleSet集合、加载配置的插件
- 开始编译:执行compiler对象的run方法
- 确定入口:根据entry找出所有入口文件
- 转换入口:调用compilition.addEntry将入口文件转换为dependence对象
- 构建
- 编译模块(make):根据dependence对象创建module对象,调用loader将模块转译为标准JS内容,调用JS解释器将内容转换为AST对象,从中找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
- 完成模块编译:上一步递归处理所有能触达到的模块后,得到module 集合以及 module之间的依赖关系图
- 生成
- 输出资源(seal):根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个Chunk转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
- 写入文件系统(emitAssets):在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
2. 初始化阶段
- 整合参数:process.args + webpack.config.js
- 校验参数:validateSchema
- 合并参数:getNormalizedWebpackOptions + applyWebpackOptionsBaseDefaults
- 基于参数创建compiler对象:new Compiler
- 插入外部plugin插件:遍历plugins集合,执行插件的apply方法
- 加载内置plugin插件:new WebpackOptionsApply().process
3. 构建阶段
- 构建module子类:根据文件类型调用handleModuleCreate 构建 module 子类
- 转义module内容:调用runLoaders将各类资源转译为 JavaScript 文本
- 解析JS文本:调用 acorn 将 JS 文本解析为AST
- 遍历AST,处理依赖
module => AST => dependences => module
🤔:webpack与babel区别?
🙋:相同点:webpack构建阶段会读取源码,解析为AST集合,babel解析阶段会读取源码解析为AST集合。不同点:Webpack只遍历AST集合,babel会对AST做等价转换
🤔:webpack如何识别资源依赖?
🙋:遍历AST集合,通过识别require/import之类的导入语句,确定依赖关系
4. 生成阶段
- 构建chunkGroup对象
- 将module分配给chunk:遍历compilation.modules集合,将module按entry/动态引入的规则分配给不同的Chunk对象
- 记录assets输出规则:遍历module/chunk ,调用compilation.emitAssets方法将assets信息记录到 compilation.assets对象中
- 将assets写入文件系统
- 控制流回转到compiler对象
entry及entry触达到的模块,组合成一个chunk;
使用动态引入语句引入的模块,各自组合成一个chunk;
5. 资源形态流转
1)compiler.make
- entry文件以dependence对象形式加入compilation的依赖列表,dependence对象记录entry的类型、路径等信息
- 根据dependence调用对应的工厂函数创建module对象,之后读入 module 对应的文件内容,调用 loader-runner 对内容做转化,转化结果若有其它依赖则继续读入依赖资源,重复此过程直到所有依赖均被转化为module
2)compilation.seal
- 遍历module集合,根据entry配置及引入资源的方式,将module分配到不同的chunk
- 遍历 chunk 集合,调用compilation.emitAsset方法标记chunk的输出规则,即转化为assets集合
3)compiler.emitAssets:将assets写入文件系统
第七章 模块联邦
Webpack模块联邦(Webpack Module Federation)是 Webpack 5 中引入的一项新功能,它允许不同的 Webpack构建 之间共享代码并动态加载依赖项。具体来说,它允许将应用程序拆分成多个独立的 Webpack构建(或称为远程应用程序),这些构建可以在运行时共享代码和依赖项。
通过使用 Webpack模块联邦,不同的团队可以独立地构建和部署其应用程序的各个部分,而不必将所有代码都打包到一个大的 JavaScript 文件中。这可以提高应用程序的性能和可维护性,同时使得不同团队之间的合作更加容易。
模块 一
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
mode: "development",
devServer: {
port: 8081,
},
plugins: [
new ModuleFederationPlugin({
name: "microFrontEnd1",
filename: "remoteEntry.js",
exposes: {
"./MicroFrontEnd1Index": "./src/index",
},
}),
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
};
模块 二
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
mode: "development",
devServer: {
port: 8082,
},
plugins: [
new ModuleFederationPlugin({
name: "microFrontEnd2",
filename: "remoteEntry.js",
exposes: {
"./MicroFrontEnd2Index": "./src/index",
},
}),
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
};
主应用
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
mode: "development",
devServer: {
port: 8080,
},
plugins: [
new ModuleFederationPlugin({
name: "container",
filename: "remoteEntry.js",
remotes: {
microFrontEnd1: "microFrontEnd1@http://localhost:8081/remoteEntry.js",
microFrontEnd2: "microFrontEnd2@http://localhost:8082/remoteEntry.js",
},
}),
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
};
参考文档:模块联邦实现一个微前端
补充
1. SplitChunks原理
optimization: {
splitChunks: {
chunks: "all"
}
}
splitChunks的默认配置如下所示:
splitChunks: {
chunks: "async", //initial表示直接引入的模块,async表示按需引入的模块,all表示都包括
minSize: 30000, //最小包体积,这里的单位是byte,超过这个大小的包会被splitChunks优化
minChunks: 1, //模块的最小引用次数,如果引用次数低于这个值,将不会被优化
maxAsyncRequests: 5, //设置async chunks的最大并行请求数
maxInitialRequests: 3, //设置initial chunks的最大并行请求数
automaticNameDelimiter: '~', //产出chunks的文件名分割符
name: true, //true:根据提取chunk的名字自动生成,false:根据缓存组IdHint生成,string:生成文件名即为这个string
cacheGroups: { //缓存组,自定义拆包规则在此定义
vendors: { //默认配置,node_modules的chunk
test: /[\/]node_modules[\/]/,
priority: -10
},
default: { //业务代码的chunk
minChunks: 2,
priority: -20,
reuseExistingChunk: true //复用已存在的chunks
}
}
}
SplitChunks工作流程:
- 分析模块间的依赖关系
SplitChunksPlugin 会分析模块之间的依赖关系,并根据这些关系确定哪些模块可以组成一个共享块。这样可以确保代码被正确地分离,而不会出现意外的行为。
- 生成共享块
SplitChunksPlugin 根据配置项生成共享块。配置项包括 minSize(指定共享块大小的最小值)、maxSize(指定共享块大小的最大值)、minChunks(指定一个模块至少被使用的次数才会被拆分成共享块)等。
- 提取共享块
在分析和生成共享块后,SplitChunksPlugin 会将共享块提取出来,并创建新的 chunk(即打包后的文件),将这些共享块放入新的 chunk 中。这样,每个共享块只需被下载一次,而不必重复下载多次,从而提高了应用程序的加载速度。
- 缓存共享块
为了进一步提高性能,SplitChunksPlugin 会将共享块缓存起来,并在后续的构建中重复使用它们。这样,如果某个共享块已经存在于缓存中,就不必再重新生成它,从而节省了构建时间。