vite原理与webpack原理
webpack5原理学习笔记
工作原理
Webpack 实现文件打包的过程是一个复杂而精细的流程,涉及多个步骤和概念。__webpack_require__ 是 Webpack 运行时的一部分,确实在模块加载中扮演了重要角色,但它并不是打包过程的全部。以下是 Webpack 打包的核心步骤和组件:
-
入口起点(Entry Point): Webpack 从配置好的入口文件开始分析,找出所有依赖的模块。
-
依赖图构建(Dependency Graph): Webpack 递归地分析每个模块的依赖关系,构建一个依赖图。(可以想象成多叉树)
-
Loaders: Webpack 内置了一个 loader 系统,用于处理不同类型的文件。例如,
.js文件、.css文件、图片等,都会通过相应的 loader 进行转换。核心 -
编译(Compilation): Webpack 使用编译器(如 Babel)对源代码进行转换,将现代 JavaScript 代码转换为兼容目标浏览器的代码。
-
代码分割(Code Splitting): Webpack 可以自动或手动进行代码分割,将代码拆分成多个包(chunks),以实现按需加载。
-
优化(Optimization): Webpack 包含多种优化措施,如压缩代码、提取公共模块、限制包的大小等。
-
输出(Output): Webpack 将编译和优化后的模块打包成最终的文件,输出到配置的
output.path目录。 -
运行时(Runtime): Webpack 包含一个运行时系统,用于在运行时加载和管理模块。
__webpack_require__就是运行时的一部分,它负责模块的加载和执行。 -
插件(Plugins): Webpack 允许使用插件来扩展其功能。插件可以在打包过程中的特定时机注入代码或执行其他任务。
-
缓存(Caching): Webpack 使用缓存来提高构建性能,避免不必要的重新构建。
-
HMR(Hot Module Replacement): Webpack 支持热模块替换,可以在开发过程中快速更新模块,而无需刷新页面。
-
环境分离(Environment Separation): Webpack 可以根据不同环境(开发、生产)应用不同的配置。
其中__webpack_require__ 函数是 Webpack 运行时(runtime)的核心,但它主要负责在运行时加载和执行模块。Webpack 的打包过程是一个涉及文件解析、依赖管理、编译、优化、分割和输出的综合过程,__webpack_require__ 只是这个过程的最终产物之一。
Webpack 的打包流程可以简化为以下步骤:
- 初始化编译过程。
- 从入口文件开始递归地解析依赖。
- 使用 loader 转换不同类型的模块。
- 应用插件和优化措施。
- 生成最终的代码包和源映射文件。
- 输出到指定目录。
在整个过程中,Webpack 的配置文件(通常是 webpack.config.js)扮演了至关重要的角色,它定义了如何加载文件、如何应用 loader 和插件、如何优化和输出结果等。
模块导入标准兼容
js中加载模块资源,可以通过以下方式
- 遵循ES module标准的import声明
- 遵循CommonJs标准的require函数
- 遵循AMD标准的define函数和require函数
- 部分 loader 加载的资源中一些用法也会触发资源模块加载
- 例如:css文件中的@import,url(图片链接)
- 例如:html文件中的img src属性,a href属性
核心概念
-
entry::入口模块文件路径
-
output:输出bundle文件路径
-
module:模块,webpack构建对象
-
bundle:输出文件,webpack构建产物
-
chunk:中间文件,webpack构建的中间产物
-
loader:文件转换器
-
Plugin:插件,执行特定任务
核心思想
入口文件一般为js文件,整个页面根据Js代码的需要去动态地引入对应的所有资源
- 逻辑合理,js逻辑确实需要这些资源文件
- 确保上线资源不缺失,这是必要的
配置示例
生产环境
const os = require("os");
const path = require("path"); // nodejs核心模块,专门用来处理路径问题
const ESLintPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserWebpackPlugin = require("terser-webpack-plugin");
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin");
const WorkboxPlugin = require("workbox-webpack-plugin");
const threads = os.cpus().length; // cpu核数
// 用来获取处理样式的loader
function getStyleLoader(pre) {
return [
MiniCssExtractPlugin.loader, // 提取css成单独文件
"css-loader", // 将css资源编译成commonjs的模块到js中
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
"postcss-preset-env", // 能解决大多数样式兼容性问题
],
},
},
},
pre,
].filter(Boolean);
}
module.exports = {
// 入口
entry: "./src/main.js", // 相对路径
// 输出
output: {
// 所有文件的输出路径
// __dirname nodejs的变量,代表当前文件的文件夹目录
path: path.resolve(__dirname, "../dist"), // 绝对路径
// 入口文件打包输出文件名
filename: "static/js/[name].[contenthash:10].js",
// 给打包输出的其他文件命名
chunkFilename: "static/js/[name].chunk.[contenthash:10].js",
// 图片、字体等通过type:asset处理资源命名方式
assetModuleFilename: "static/media/[hash:10][ext][query]",
// 自动清空上次打包的内容
// 原理:在打包前,将path整个目录内容清空,再进行打包
clean: true,
},
// 加载器
module: {
rules: [
// loader的配置
{
oneOf: [
{
test: /\.css$/, // 只检测.css文件
use: getStyleLoader(), // 执行顺序:从右到左(从下到上)
},
{
test: /\.less$/,
// loader: 'xxx', // 只能使用1个loader
use: getStyleLoader("less-loader"),
},
{
test: /\.s[ac]ss$/,
use: getStyleLoader("sass-loader"),
},
{
test: /\.styl$/,
use: getStyleLoader("stylus-loader"),
},
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: "asset",
parser: {
dataUrlCondition: {
// 小于10kb的图片转base64
// 优点:减少请求数量 缺点:体积会更大
maxSize: 10 * 1024, // 10kb
},
},
// generator: {
// // 输出图片名称
// // [hash:10] hash值取前10位
// filename: "static/images/[hash:10][ext][query]",
// },
},
{
test: /\.(ttf|woff2?|map3|map4|avi)$/,
type: "asset/resource",
// generator: {
// // 输出名称
// filename: "static/media/[hash:10][ext][query]",
// },
},
{
test: /\.js$/,
// exclude: /node_modules/, // 排除node_modules下的文件,其他文件都处理
include: path.resolve(__dirname, "../src"), // 只处理src下的文件,其他文件不处理
use: [
{
loader: "thread-loader", // 开启多进程
options: {
works: threads, // 进程数量
},
},
{
loader: "babel-loader",
options: {
// presets: ["@babel/preset-env"],
cacheDirectory: true, // 开启babel缓存
cacheCompression: false, // 关闭缓存文件压缩
plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积
},
},
],
},
],
},
],
},
// 插件
plugins: [
// plugin的配置
new ESLintPlugin({
// 检测哪些文件
context: path.resolve(__dirname, "../src"),
exclude: "node_modules", // 默认值
cache: true, // 开启缓存
cacheLocation: path.resolve(__dirname, "../node_modules/.cache/eslintcache"),
threads, // 开启多进程和设置进程数量
}),
new HtmlWebpackPlugin({
// 模板:以public/index.html文件创建新的html文件
// 新的html文件特点:1. 结构和原来一致 2. 自动引入打包输出的资源
template: path.resolve(__dirname, "../public/index.html"),
}),
new MiniCssExtractPlugin({
filename: "static/css/[name].[contenthash:10].css",
chunkFilename: "static/css/[name].chunk.[contenthash:10].css",
}),
// new CssMinimizerPlugin(),
// new TerserWebpackPlugin({
// parallel: threads, // 开启多进程和设置进程数量
// }),
new PreloadWebpackPlugin({
// rel: "preload",
// as: "script",
rel: "prefetch",
}),
new WorkboxPlugin.GenerateSW({
// 这些选项帮助快速启用 ServiceWorkers
// 不允许遗留任何“旧的” ServiceWorkers
clientsClaim: true,
skipWaiting: true,
}),
],
optimization: {
// 压缩的操作
minimizer: [
// 压缩css
new CssMinimizerPlugin(),
// 压缩js
new TerserWebpackPlugin({
parallel: threads, // 开启多进程和设置进程数量
}),
// 压缩图片
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",
},
},
],
},
],
],
},
},
}),
],
// 代码分割配置
splitChunks: {
chunks: "all",
// 其他都用默认值
},
runtimeChunk: {
name: (entrypoint) => `runtime~${entrypoint.name}.js`,
},
},
// 模式
mode: "production",
devtool: "source-map",
};
开发环境
const os = require("os");
const path = require("path"); // nodejs核心模块,专门用来处理路径问题
const ESLintPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const threads = os.cpus().length; // cpu核数
module.exports = {
// 入口
entry: "./src/main.js", // 相对路径
// 输出
output: {
// 所有文件的输出路径
// 开发模式没有输出
path: undefined,
// 入口文件打包输出文件名
filename: "static/js/[name].js",
// 给打包输出的其他文件命名
chunkFilename: "static/js/[name].chunk.js",
// 图片、字体等通过type:asset处理资源命名方式
assetModuleFilename: "static/media/[hash:10][ext][query]",
},
// 加载器
module: {
rules: [
// loader的配置
{
// 每个文件只能被其中一个loader配置处理
oneOf: [
{
test: /\.css$/, // 只检测.css文件
use: [
// 执行顺序:从右到左(从下到上)
"style-loader", // 将js中css通过创建style标签添加html文件中生效
"css-loader", // 将css资源编译成commonjs的模块到js中
],
},
{
test: /\.less$/,
// loader: 'xxx', // 只能使用1个loader
use: [
// 使用多个loader
"style-loader",
"css-loader",
"less-loader", // 将less编译成css文件
],
},
{
test: /\.s[ac]ss$/,
use: [
"style-loader",
"css-loader",
"sass-loader", // 将sass编译成css文件
],
},
{
test: /\.styl$/,
use: [
"style-loader",
"css-loader",
"stylus-loader", // 将stylus编译成css文件
],
},
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: "asset",
parser: {
dataUrlCondition: {
// 小于10kb的图片转base64
// 优点:减少请求数量 缺点:体积会更大
maxSize: 10 * 1024, // 10kb
},
},
// generator: {
// // 输出图片名称
// // [hash:10] hash值取前10位
// filename: "static/images/[hash:10][ext][query]",
// },
},
{
test: /\.(ttf|woff2?|map3|map4|avi)$/,
type: "asset/resource",
// generator: {
// // 输出名称
// filename: "static/media/[hash:10][ext][query]",
// },
},
{
test: /\.js$/,
// exclude: /node_modules/, // 排除node_modules下的文件,其他文件都处理
include: path.resolve(__dirname, "../src"), // 只处理src下的文件,其他文件不处理
use: [
{
loader: "thread-loader", // 开启多进程
options: {
works: threads, // 进程数量
},
},
{
loader: "babel-loader",
options: {
// presets: ["@babel/preset-env"],
cacheDirectory: true, // 开启babel缓存
cacheCompression: false, // 关闭缓存文件压缩
plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积
},
},
],
},
],
},
],
},
// 插件
plugins: [
// plugin的配置
new ESLintPlugin({
// 检测哪些文件
context: path.resolve(__dirname, "../src"),
exclude: "node_modules", // 默认值
cache: true, // 开启缓存
cacheLocation: path.resolve(
__dirname,
"../node_modules/.cache/eslintcache"
),
threads, // 开启多进程和设置进程数量
}),
new HtmlWebpackPlugin({
// 模板:以public/index.html文件创建新的html文件
// 新的html文件特点:1. 结构和原来一致 2. 自动引入打包输出的资源
template: path.resolve(__dirname, "../public/index.html"),
}),
],
// 开发服务器: 不会输出资源,在内存中编译打包的
devServer: {
host: "localhost", // 启动服务器域名
port: "3000", // 启动服务器端口号
open: true, // 是否自动打开浏览器
hot: true, // 开启HMR(默认值)
},
optimization: {
// 开发模式下不需要压缩
// 代码分割配置
splitChunks: {
chunks: "all",
// 其他都用默认值
},
},
// 模式
mode: "development",
devtool: "cheap-module-source-map",
};
Vue项目
const path = require("path");
const EslintWebpackPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerWebpackPlugin = require("css-minimizer-webpack-plugin");
const TerserWebpackPlugin = require("terser-webpack-plugin");
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");
const { VueLoaderPlugin } = require("vue-loader");
const { DefinePlugin } = require("webpack");
// 返回处理样式loader函数
const getStyleLoaders = (pre) => {
return [
MiniCssExtractPlugin.loader,
"css-loader",
{
// 处理css兼容性问题
// 配合package.json中browserslist来指定兼容性
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: ["postcss-preset-env"],
},
},
},
pre,
].filter(Boolean);
};
module.exports = {
entry: "./src/main.js",
output: {
path: path.resolve(__dirname, "../dist"),
filename: "static/js/[name].[contenthash:10].js",
chunkFilename: "static/js/[name].[contenthash:10].chunk.js",
assetModuleFilename: "static/media/[hash:10][ext][query]",
clean: true,
},
module: {
rules: [
// 处理css
{
test: /\.css$/,
use: getStyleLoaders(),
},
{
test: /\.less$/,
use: getStyleLoaders("less-loader"),
},
{
test: /\.s[ac]ss$/,
use: getStyleLoaders("sass-loader"),
},
{
test: /\.styl$/,
use: getStyleLoaders("stylus-loader"),
},
// 处理图片
{
test: /\.(jpe?g|png|gif|webp|svg)$/,
type: "asset",
parser: {
dataUrlCondition: {
maxSize: 10 * 1024,
},
},
},
// 处理其他资源
{
test: /\.(woff2?|ttf)$/,
type: "asset/resource",
},
// 处理js
{
test: /\.js$/,
include: path.resolve(__dirname, "../src"),
loader: "babel-loader",
options: {
cacheDirectory: true,
cacheCompression: false,
},
},
{
test: /\.vue$/,
loader: "vue-loader",
},
],
},
// 处理html
plugins: [
new EslintWebpackPlugin({
context: path.resolve(__dirname, "../src"),
exclude: "node_modules",
cache: true,
cacheLocation: path.resolve(__dirname, "../node_modules/.cache/.eslintcache"),
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../public/index.html"),
}),
new MiniCssExtractPlugin({
filename: "static/css/[name].[contenthash:10].css",
chunkFilename: "static/css/[name].[contenthash:10].chunk.css",
}),
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, "../public"),
to: path.resolve(__dirname, "../dist"),
globOptions: {
// 忽略index.html文件
ignore: ["**/index.html"],
},
},
],
}),
new VueLoaderPlugin(),
// cross-env定义的环境变量给打包工具使用
// DefinePlugin定义环境变量给源代码使用,从而解决vue3页面警告的问题
new DefinePlugin({
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false,
}),
],
mode: "production",
devtool: "source-map",
optimization: {
splitChunks: {
chunks: "all",
},
runtimeChunk: {
name: (entrypoint) => `runtime~${entrypoint.name}.js`,
},
minimizer: [
new CssMinimizerWebpackPlugin(),
new TerserWebpackPlugin(),
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",
},
},
],
},
],
],
},
},
}),
],
},
// webpack解析模块加载选项
resolve: {
// 自动补全文件扩展名
extensions: [".vue", ".js", ".json"],
},
};
React项目
const path = require("path");
const EslintWebpackPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerWebpackPlugin = require("css-minimizer-webpack-plugin");
const TerserWebpackPlugin = require("terser-webpack-plugin");
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
// 获取cross-env定义的环境变量
const isProduction = process.env.NODE_ENV === "production";
// 返回处理样式loader函数
const getStyleLoaders = (pre) => {
return [
isProduction ? MiniCssExtractPlugin.loader : "style-loader",
"css-loader",
{
// 处理css兼容性问题
// 配合package.json中browserslist来指定兼容性
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: ["postcss-preset-env"],
},
},
},
pre && {
loader: pre,
options:
pre === "less-loader"
? {
// antd自定义主题配置
// 主题色文档:https://ant.design/docs/react/customize-theme-cn#Ant-Design-%E7%9A%84%E6%A0%B7%E5%BC%8F%E5%8F%98%E9%87%8F
lessOptions: {
modifyVars: { "@primary-color": "#1DA57A" },
javascriptEnabled: true,
},
}
: {},
},
].filter(Boolean);
};
module.exports = {
entry: "./src/main.js",
output: {
path: isProduction ? path.resolve(__dirname, "../dist") : undefined,
filename: isProduction ? "static/js/[name].[contenthash:10].js" : "static/js/[name].js",
chunkFilename: isProduction ? "static/js/[name].[contenthash:10].chunk.js" : "static/js/[name].chunk.js",
assetModuleFilename: "static/media/[hash:10][ext][query]",
clean: true,
},
module: {
rules: [
// 处理css
{
test: /\.css$/,
use: getStyleLoaders(),
},
{
test: /\.less$/,
use: getStyleLoaders("less-loader"),
},
{
test: /\.s[ac]ss$/,
use: getStyleLoaders("sass-loader"),
},
{
test: /\.styl$/,
use: getStyleLoaders("stylus-loader"),
},
// 处理图片
{
test: /\.(jpe?g|png|gif|webp|svg)$/,
type: "asset",
parser: {
dataUrlCondition: {
maxSize: 10 * 1024,
},
},
},
// 处理其他资源
{
test: /\.(woff2?|ttf)$/,
type: "asset/resource",
},
// 处理js
{
test: /\.jsx?$/,
include: path.resolve(__dirname, "../src"),
loader: "babel-loader",
options: {
cacheDirectory: true,
cacheCompression: false,
plugins: [
!isProduction && "react-refresh/babel", // 激活js的HMR
].filter(Boolean),
},
},
],
},
// 处理html
plugins: [
new EslintWebpackPlugin({
context: path.resolve(__dirname, "../src"),
exclude: "node_modules",
cache: true,
cacheLocation: path.resolve(__dirname, "../node_modules/.cache/.eslintcache"),
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../public/index.html"),
}),
isProduction &&
new MiniCssExtractPlugin({
filename: "static/css/[name].[contenthash:10].css",
chunkFilename: "static/css/[name].[contenthash:10].chunk.css",
}),
isProduction &&
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, "../public"),
to: path.resolve(__dirname, "../dist"),
globOptions: {
// 忽略index.html文件
ignore: ["**/index.html"],
},
},
],
}),
!isProduction && new ReactRefreshWebpackPlugin(),
].filter(Boolean),
mode: isProduction ? "production" : "development",
devtool: isProduction ? "source-map" : "cheap-module-source-map",
optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
// react react-dom react-router-dom 一起打包成一个js文件
react: {
test: /[\\/]node_modules[\\/]react(.*)?[\\/]/,
name: "chunk-react",
priority: 40,
},
// antd 单独打包
antd: {
test: /[\\/]node_modules[\\/]antd[\\/]/,
name: "chunk-antd",
priority: 30,
},
// 剩下node_modules单独打包
libs: {
test: /[\\/]node_modules[\\/]/,
name: "chunk-libs",
priority: 20,
},
},
},
runtimeChunk: {
name: (entrypoint) => `runtime~${entrypoint.name}.js`,
},
// 是否需要进行压缩
minimize: isProduction,
minimizer: [
new CssMinimizerWebpackPlugin(),
new TerserWebpackPlugin(),
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",
},
},
],
},
],
],
},
},
}),
],
},
// webpack解析模块加载选项
resolve: {
// 自动补全文件扩展名
extensions: [".jsx", ".js", ".json"],
},
devServer: {
host: "localhost",
port: 3000,
open: true,
hot: true, // 开启HMR
historyApiFallback: true, // 解决前端路由刷新404问题
},
performance: false, // 关闭性能分析,提升打包速度
};
loader
作用:负责资源文件从输入到输出之间的转换,对于同一个资源文件,可使用多个loader。
执行顺序:由下到上,从右到左
常见Loader
编译转换类
-
babel-loader
将es6转译为es5
-
css-loader
把css代码转为css module
-
style-loader
把css module动态转化为style标签插入页面
操作文件类
-
file-loader
拷贝对应文件到打包目录内,并export文件访问路径
-
url-loader
把文件转为file协议的字符串,并export出去(一般用于图片)
-
html-loader
html-loader可以将 HTML 文件加载为一个 JavaScript 模块,返回文件内容的字符串。
代码检查类
-
eslint-loader
eslint规范
自己实现一个Loader
loader传入的参数
在 Webpack 5 中,每个 loader 接收到的参数(通常是一个对象)包含了丰富的信息,这些信息可以用来定制 loader 的行为。以下是一些常见的参数:
-
this:- 代表当前 loader 的上下文,提供了一些方法和属性,例如
this.async()用于创建一个异步回调,this.cacheable()用于标识模块是否应该被缓存。
- 代表当前 loader 的上下文,提供了一些方法和属性,例如
-
resource:- 被处理文件的绝对路径。
-
resourcePath:- 同
resource,表示当前文件的绝对路径。
- 同
-
context:- 当前文件所在的上下文,通常是相对于配置文件的路径。
-
loaderIndex:- 当前 loader 在 loader 链中的索引。
-
loaders:- 当前 loader 链中的所有 loader 及其对应的配置。
-
query:- 传递给当前 loader 的查询参数,可以是字符串或对象。
-
data:- 包含了 Webpack 配置中的
module部分,允许 loader 访问配置信息。
- 包含了 Webpack 配置中的
-
options:- 包含了 Webpack 的完整配置对象。
-
emitFile:- 一个函数,允许 loader 向输出目录中添加文件。
-
fs和fileSystem:- 提供了文件系统操作的能力,允许 loader 读取或写入文件。
-
sourceMap:- 一个布尔值,指示是否需要生成源映射(source map)。
-
target:- Webpack 构建的目标平台(如
web、node等)。
- Webpack 构建的目标平台(如
-
webpack和compiler:- 允许 loader 访问 Webpack 的 API,例如添加插件或修改配置。
-
mode:- 当前 Webpack 的模式(
development或production)。
- 当前 Webpack 的模式(
-
env:- 当前的环境变量,例如
env.NODE_ENV。
- 当前的环境变量,例如
这些参数通过 loader 的调用函数传递,loader 开发者可以根据需要使用这些参数来实现特定的逻辑。例如,可以根据 query 参数来定制 loader 的行为,或者使用 this.async() 来实现异步处理。
以下是一个简单的 loader 示例,展示了如何使用这些参数:
module.exports = function(source) {
const callback = this.async(); // 使用 this.async() 创建异步处理
const query = this.query; // 获取查询参数
const resourcePath = this.resourcePath; // 获取资源路径
// 处理 source 代码...
callback(null, transformedSource); // 使用回调返回处理后的代码
};
在这个示例中,source 是 loader 接收到的文件内容,callback 用于异步返回处理结果,query 和 resourcePath 是从参数中获取的信息。
markdown-loader
把md文件的内容加载进来
const marked = require('marked')
module.exports = source => {
// console.log(source)
// return 'console.log("hello ~")'
const html = marked(source)
// return html
// return `module.exports = "${html}"`
// return `export default ${JSON.stringify(html)}`
// 返回 html 字符串交给下一个 loader 处理
return html
}
webpack.config.js配置
记住:执行顺序是从右往左,从后往前
{
module: {
rules: [
{
test: /.md$/,
use: [
'html-loader',
'./markdown-loader'
]
}
]
}
}
Plugin
plugin 的作用是 Webpack 扩展功能。loader 可以理解为转换器,用于处理模块之间的转换,plugin 则用于执行更广泛的任务,它可以访问 Webpack 的生命周期,在合适的时机执行插件的功能。
一般情况下,plugin更多地用于执行一些自动化的任务。
常见Plugin
-
html-webpack-plugin
我想在打包目录生成html文件,用于访问打包的js文件,我们可以手动创建,但是我们不建议直接操作
dist打包目录,我们可以通过plugin自动生成该文件。 -
copy-webpack-plugin:
在 Webpack 构建过程中,将指定的文件或目录复制到输出目录中。这个插件对于需要将静态资源(如图片、字体、HTML 文件等)包含在最终打包文件中的情况非常有用。(开发阶段一般不用)
-
clean-webpack-plugin
每次打包前清空输出目录,不过现在webpack5已经内置有output.clean的配置了
自己实现一个Plugin
核心:通过在打包生命周期中的钩子中挂载函数实现拓展。
以下是创建一个基本 Webpack 插件的步骤:
-
定义插件类: Webpack 插件是一个 JavaScript 函数,它接收一个
compiler对象作为参数。可以通过这个对象访问 Webpack 的钩子(hooks)。 -
访问钩子(Hooks): 使用
compiler.hooks访问 Webpack 的各种钩子,这些钩子在构建过程中的不同阶段被调用。 -
实现应用逻辑: 在钩子的回调函数中实现你的插件逻辑。
-
使用
tapable钩子: Webpack 的tapable库提供了钩子的注册和监听功能。 -
编译时和运行时: 根据需要在编译时(
compilation)或运行时(runtime)应用你的插件逻辑。 -
处理异步操作: 如果你的插件需要执行异步操作,确保正确处理 Promises 或使用
async/await。 -
导出插件: 将插件函数导出,以便在 Webpack 配置中使用。
下面是一个简单的 Webpack 插件示例,该插件在每次构建开始时记录一条消息:
const { Compilation } = require('webpack');
class MyCustomWebpackPlugin {
// 插件构造函数可以接收选项参数
constructor(options) {
this.options = options;
}
// Webpack 会调用这个函数来应用插件
apply(compiler) {
// compiler 是一个 Webpack 编译对象的引用
compiler.hooks.run.tap('MyCustomWebpackPlugin', (compilation) => {
// compilation 对象是当前编译过程的引用
console.log('Webpack build started!');
// 可以访问 compilation.hooks 来进一步扩展编译过程
});
}
}
module.exports = MyCustomWebpackPlugin;
要在 Webpack 配置中使用这个插件,可以将其添加到 plugins 数组中:
const MyCustomWebpackPlugin = require('./path/to/MyCustomWebpackPlugin');
module.exports = {
// ... 其他 Webpack 配置 ...
plugins: [
new MyCustomWebpackPlugin({ /* 插件选项 */ }),
],
};
这个示例展示了一个非常基础的插件结构。Webpack 插件可以执行更复杂的任务,包括但不限于:
- 操作
entry点。 - 替换或修改模块内容。
- 向输出的文件或目录添加内容。
- 改变资源的加载方式。
- 集成外部的构建工具或服务。
创建更高级的插件可能需要更深入地了解 Webpack 的工作原理和 API,以及 tapable 库的使用方法。记得在开发插件时,要考虑到插件的性能影响和潜在的错误处理。
compiler.hooks
在 Webpack 中,compiler.hooks 提供了一系列的钩子(hooks),允许插件在 Webpack 构建流程的特定点执行代码。以下是一些常用的 compiler 钩子:
-
run: 当 Webpack 开始编译流程时触发。 -
watchRun: 当使用--watch选项,每次开始监控文件变化时触发。 -
beforeCompile: 在每次编译之前触发,提供了compilation参数。 -
compile: 当创建一个新的compilation对象时触发。 -
make: 在compilation对象的make方法被调用时触发。 -
buildModule: 当模块开始构建时触发。 -
normalModuleFactory: 当创建正常的模块工厂时触发。 -
contextModuleFactory: 当创建上下文模块工厂时触发。 -
beforeResolve: 在解析模块请求之前触发。 -
afterResolve: 在解析模块请求之后触发。 -
resolve: 在模块请求解析时触发。 -
finishMake: 当make方法完成时触发。 -
seal: 当compilation对象被密封,准备输出时触发。 -
optimize: 在优化阶段触发,例如在调用optimizeChunks或optimizeTree时。 -
afterSeal: 在compilation对象密封之后触发。 -
emit: 在compilation对象准备输出资源到文件系统时触发。 -
assetEmitted: 当资源被发出时触发。 -
afterEmit: 在emit过程结束后触发。 -
done: 在编译完成之后触发。 -
failed: 当编译失败时触发。 -
invalid: 当编译无效时触发,通常用于--watch模式下文件更改。 -
watchClose: 当--watch模式下的监视被关闭时触发。
这些钩子允许插件进行各种操作,如修改配置、访问或修改模块、资源和依赖关系,以及在构建过程中进行自定义逻辑处理。
要使用这些钩子,你需要在插件中注册相应的监听器。例如:
class MyWebpackPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation, callback) => {
// 在 emit 阶段执行自定义逻辑
callback();
});
}
}
在这个例子中,tapAsync 方法用于注册一个异步监听器到 emit 钩子。compilation 参数提供了对当前编译过程的访问,callback 是一个必须被调用的回调函数,以确保 Webpack 继续执行后续的钩子。
记得在实际使用中,根据你的插件需求选择合适的钩子,并在其中实现相应的逻辑。
接下来是一个demo
实现插件:删除打包出来的js的注释
class MyPlugin {
apply(compiler) {
console.log('MyPlugin 启动')
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation => 可以理解为此次打包的上下文
for (const name in compilation.assets) {
// console.log(name)
// console.log(compilation.assets[name].source())
if (name.endsWith('.js')) {
const contents = compilation.assets[name].source()
const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
compilation.assets[name] = {
source: () => withoutComments,
size: () => withoutComments.length
}
}
}
})
}
}
实现插件:将script转为html内联的形式
作用:将 HTML 文件中通过 html-webpack-plugin 插入的某些 <script> 标签的外部文件资源转换为内联脚本(inline script)。具体来说,该插件会查找特定的 <script> 标签,并将它们引用的外部 JavaScript 文件内容直接嵌入到 HTML 中,而不是通过 src 属性引入外部文件。
const HtmlWebpackPlugin = require("safe-require")("html-webpack-plugin");
class InlineChunkWebpackPlugin {
constructor(tests) {
this.tests = tests;
}
apply(compiler) {
compiler.hooks.compilation.tap("InlineChunkWebpackPlugin", (compilation) => {
// 1. 获取html-webpack-plugin的hooks
const hooks = HtmlWebpackPlugin.getHooks(compilation);
// 2. 注册 html-webpack-plugin的hooks -> alterAssetTagGroups
hooks.alterAssetTagGroups.tap("InlineChunkWebpackPlugin", (assets) => {
// 3. 从里面将script的runtime文件,变成inline script
assets.headTags = this.getInlineChunk(assets.headTags, compilation.assets);
assets.bodyTags = this.getInlineChunk(assets.bodyTags, compilation.assets);
});
// 删除runtime文件
hooks.afterEmit.tap("InlineChunkWebpackPlugin", () => {
// 3. 从里面将script的runtime文件,变成inline script
Object.keys(compilation.assets).forEach((filepath) => {
if (this.tests.some((test) => test.test(filepath))) {
delete compilation.assets[filepath];
}
});
});
});
}
getInlineChunk(tags, assets) {
/*
目前:[
{
tagName: 'script',
voidTag: false,
meta: { plugin: 'html-webpack-plugin' },
attributes: { defer: true, type: undefined, src: 'js/runtime~main.js.js' }
},
]
修改为:
[
{
tagName: 'script',
innerHTML: runtime文件的内容
closeTag: true
},
]
*/
return tags.map((tag) => {
if (tag.tagName !== "script") return tag;
// 获取文件资源路径
const filepath = tag.attributes.src;
if (!filepath) return tag;
if (!this.tests.some((test) => test.test(filepath))) return tag;
return {
tagName: "script",
innerHTML: assets[filepath].source(),
closeTag: true,
};
});
}
}
module.exports = InlineChunkWebpackPlugin;
__webpack_require__方法
在Webpack中,__webpack_require__ 是一个在构建过程中自动注入的全局函数,它用于模块的加载和解析。这个函数是Webpack特有的,它在最终的打包文件中提供模块间的依赖关系解析。以下是__webpack_require__函数的一些主要作用:
-
模块加载:
__webpack_require__函数用于加载和执行模块。在Webpack打包后的代码中,当需要引入一个模块时,实际上是通过调用__webpack_require__(moduleId)来实现的,其中moduleId是模块的唯一标识符。 -
缓存机制:
__webpack_require__实现了模块的缓存机制。一旦模块被加载,它就会被缓存起来。后续如果再次请求相同的模块,__webpack_require__会直接从缓存中返回模块,而不是重新加载,这提高了性能。 -
依赖管理:Webpack使用
__webpack_require__来解析模块之间的依赖关系。当一个模块依赖于其他模块时,Webpack会分析这些依赖并在打包过程中生成相应的__webpack_require__调用来加载所需的依赖。 -
异步加载:Webpack支持异步模块加载,
__webpack_require__.e(/* chunkId, moduleId */) 可以用来异步加载指定的模块或代码块。 -
错误处理:
__webpack_require__还处理模块加载过程中的错误。如果模块加载失败,它会抛出一个错误。 -
热模块替换(HMR):在开发过程中,
__webpack_require__支持热模块替换,允许在不刷新页面的情况下更新模块。 -
环境无关性:
__webpack_require__抽象了模块加载的细节,使得Webpack可以在不同的环境中工作,如浏览器和Node.js。 -
与CommonJS/AMD/ESM的兼容性:Webpack可以处理不同模块格式的代码,
__webpack_require__在内部转换这些模块格式,以确保它们可以在Webpack的打包结果中正常工作。
在Webpack打包的最终输出中,__webpack_require__函数是核心的运行时部分,它确保了模块能够按照Webpack的逻辑被正确加载和执行。开发者通常不需要直接使用__webpack_require__,因为它会自动处理模块的导入和导出。
sourceMap策略
sourceMap主要解决实际运行代码和开发源代码不一致的问题。
详情可参考官方文档:webpack.docschina.org/configurati…
常见策略如下:
Source Map 是一种映射文件,它将压缩或编译后的代码映射回原始源代码,使得开发者可以更容易地调试和维护生产环境中的代码。Webpack 在构建过程中可以生成 Source Map,并通过不同的策略来控制它们的生成和使用。以下是一些常见的 Webpack Source Map 策略:
-
false:- 不生成 Source Map。
-
"eval":- 使用
eval()函数来加载模块,每个模块都会通过一个eval调用执行,并且生成一个包含 Source Map 的数据 URI。
- 使用
- 只能知道是哪个文件的报错,无法具体到哪行哪列
-
"cheap":- 生成 Source Map,但只包含行信息,不包含列信息或原始源代码。这种方式生成的 Source Map 文件较小,但调试时可能不够详细。
-
"cheap-module-source-map":- 生成 Source Map,包含行和列信息,但不包含原始源代码。这种方式比
"cheap"提供了更详细的调试信息,但仍然不包含完整的源代码。
- 生成 Source Map,包含行和列信息,但不包含原始源代码。这种方式比
- 只能定位到行
"cheap-module-eval-source-map":- 类似于
"cheap-module-source-map",但使用eval来加载模块。
- 类似于
- 并且对应的是自己写的源代码,不是被loader处理过的代码
-
"source-map":- 生成完整的 Source Map 文件,包含所有信息(行、列和原始源代码)。这种方式生成的 Source Map 文件较大,但提供了最完整的调试信息。
-
"inline-cheap":- 将 Source Map 作为数据 URI 内联到生成的文件中,不生成单独的 Source Map 文件。
- 基本不会用这个
"inline-cheap-module":- 类似于
"inline-cheap",但包含模块信息。
- 类似于
- 基本不会用这个
"inline-source-map":- 将完整的 Source Map 作为数据 URI 内联到生成的文件中,不生成单独的 Source Map 文件。
- 基本不会用这个
"hidden-source-map":- 生成 Source Map 文件,但不会在构建输出中包含 Source Map 的注释。
- 第三方库可能会用到
"nosources-source-map":- 生成 Source Map 文件,包含行和列信息,但不会包含源代码内容。
对比差异
const HtmlWebpackPlugin = require('html-webpack-plugin')
const allModes = [
'eval',
'cheap-eval-source-map',
'cheap-module-eval-source-map',
'eval-source-map',
'cheap-source-map',
'cheap-module-source-map',
'inline-cheap-source-map',
'inline-cheap-module-source-map',
'source-map',
'inline-source-map',
'hidden-source-map',
'nosources-source-map'
]
module.exports = allModes.map(item => {
return {
devtool: item,
mode: 'none',
entry: './src/main.js',
output: {
filename: `js/${item}.js`
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
filename: `${item}.html`
})
]
}
})
// module.exports = [
// {
// entry: './src/main.js',
// output: {
// filename: 'a.js'
// }
// },
// {
// entry: './src/main.js',
// output: {
// filename: 'b.js'
// }
// }
// ]
其中几个主要的差异点在于是否eval、是否cheap、是否inline等,个人认为eval这块不太好理解。
在 Webpack 中使用 eval 相关的 Source Map 策略,如 "eval"、"cheap-module-eval-source-map" 或 "inline-cheap-module-eval-source-map",主要是出于以下原因和好处:
-
无额外文件:
- 使用
eval可以在内存中执行模块,而不需要生成额外的文件。这意味着构建过程不需要写入文件到磁盘,可以减少 I/O 操作,从而提高性能。
- 使用
-
快速更新:
- 当源代码发生变化时,使用
eval的 HMR(热模块替换)可以实现快速更新。因为模块已经在内存中,所以更新时不需要重新加载文件。
- 当源代码发生变化时,使用
-
Source Map 内联:
eval相关的 Source Map 策略允许将 Source Map 作为数据 URI 内联到浏览器中,这样浏览器可以直接使用 Source Map,而不需要额外的 HTTP 请求来获取.map文件。
-
调试体验:
- 内联 Source Map 可以提供更好的调试体验,因为它们允许开发者工具直接映射压缩或编译后的代码到原始源代码。
-
避免文件碎片:
- 在开发服务器上,使用
eval可以避免生成大量的小文件,这些小文件可能会在某些情况下减慢构建和更新速度。
- 在开发服务器上,使用
-
兼容性:
- 对于一些特定的构建场景或旧的浏览器环境,使用
eval可以提供更好的兼容性。
- 对于一些特定的构建场景或旧的浏览器环境,使用
-
开发体验:
- 在开发过程中,
eval可以提供快速的模块加载和更新,从而提供更流畅的开发体验。
- 在开发过程中,
然而,使用 eval 也有一些潜在的缺点:
-
安全性:
eval可以执行字符串作为代码,这可能带来安全风险,尤其是在处理不受信任的代码时。
-
性能影响:
- 在某些情况下,使用
eval可能会对性能产生负面影响,尤其是在处理大型模块或复杂应用时。
- 在某些情况下,使用
-
调试限制:
- 使用
eval可能会限制浏览器的调试能力,因为eval源码不会被计入常规的源码列表中。
- 使用
示例配置
在 Webpack 配置文件中,可以通过 devtool 选项来设置 Source Map 策略:
module.exports = {
// ...
devtool: 'source-map', // 完整的 Source Map
// 或者
devtool: 'inline-source-map', // 内联 Source Map
// ...
};
选择策略时的考虑因素
- 调试需求:如果需要详细调试,选择包含源代码的 Source Map 策略(如
"source-map")。 - 构建速度:生成 Source Map 可能会影响构建速度,因此在生产环境中可能需要权衡。
- 文件大小:内联 Source Map 会增加最终打包文件的大小,但可以避免生成单独的文件。
- 隐私:包含源代码的 Source Map 可能会泄露敏感信息,需要谨慎处理。
通过合理选择 Source Map 策略,可以在保证调试便利性的同时,控制构建输出的大小和性能。
以个人实际开发经验来看:
开发环境下:cheap-module-eval-source-map (或者直接source-map)
- 代码一般一行不会超过80个字符
- 代码经过Loader转换过后的差异比较大
- 首次打包速度慢可以接受,重写打包速度相对较快(eval模式下,模块是在内存里存好的)
生产环境:none
-
避免隐患
-
如果是内部系统,可以考虑使用nosouces-source-map,用于排查一些很难解决的线上问题
模块热替换HMR
-
全称:Hot Modules Replacement
-
热替换只会将有更新的模块进行热插拔替换
-
使用
const webpack = require('webpack') // 然后在plugin里添加 new webpack.HotModuleReplacementPlugin() -
如果没有loader或自己没有手动处理该模块的热替换逻辑,webpack默认采取的是刷新整个页面
-
hotOnly: true(或者hot: only) // 只使用 HMR,不会 fallback 到 live reloading,可以保留之前的报错信息
-
打包时,热替换相关的代码会被自动删除
-
React Hot Loader:实时调整 react 组件。
-
Vue Loader:此 loader 支持 vue 组件的 HMR,提供开箱即用体验。
尝试自己手动实现热替换
import createEditor from './editor'
import background from './better.png'
import './global.css'
const editor = createEditor()
document.body.appendChild(editor)
const img = new Image()
img.src = background
document.body.appendChild(img)
// ============ 以下用于处理 HMR,与业务代码无关 ============
if (module.hot) {
let lastEditor = editor
module.hot.accept('./editor', () => {
// console.log('editor 模块更新了,需要这里手动处理热替换逻辑')
// console.log(createEditor)
const value = lastEditor.innerHTML
document.body.removeChild(lastEditor)
const newEditor = createEditor()
newEditor.innerHTML = value
document.body.appendChild(newEditor)
lastEditor = newEditor
})
module.hot.accept('./better.png', () => {
img.src = background
console.log(background)
})
}
工作原理总结
Webpack 的热模块替换(Hot Module Replacement,简称 HMR)是一种在应用程序运行时替换、添加或删除模块,而无需重新加载整个页面的功能。HMR 可以在开发过程中显著提高效率,因为它允许即时反馈修改效果,无需手动刷新浏览器。
运作过程可以总结为:Webpack在开发模式下监测到源文件变更后,通过HMR客户端与服务器通信,仅替换和更新浏览器中受影响的模块,而无需重新加载整个页面。
以下是 Webpack 实现 HMR 的主要机制:
-
模块热更新 API(Module Hot Accept):
- Webpack 允许模块通过
module.hot对象访问 HMR API。 module.hot.accept方法用于接受依赖模块的更新,当依赖模块更新时,可以执行一些自定义逻辑。
- Webpack 允许模块通过
-
热更新事件监听:
- Webpack 运行时会监听来自开发服务器的 HMR 事件,这些事件通知客户端有模块需要更新。
-
模块标识:
- 每个模块在打包时都会被分配一个唯一的标识符(通常是数字或字符串),用于 HMR 过程中识别模块。
-
更新检查:
- 当 HMR 事件发生时,Webpack 运行时会检查是否有模块需要更新,并决定是否需要重新加载模块。
-
模块替换:
- 如果确定需要更新模块,Webpack 运行时会从服务器获取更新的模块代码,并替换当前的模块。
-
状态保留:
- 在替换模块时,HMR 机制会尽量保留应用程序的状态,避免因重新加载模块而丢失用户操作。
-
依赖图更新:
- 更新模块后,Webpack 运行时会更新依赖图,确保所有依赖关系正确。
-
运行时代码注入:
- Webpack 在打包时会注入额外的运行时代码,用于处理 HMR 逻辑。
-
开发服务器:
- Webpack 开发服务器(如 webpack-dev-server)支持 HMR,它会在检测到文件更改时发送 HMR 事件。
-
浏览器端实现:
- 浏览器端的 HMR 客户端会处理从服务器接收到的 HMR 事件,并与 Webpack 运行时交互,完成模块的更新。
-
错误处理:
- 如果 HMR 更新失败,Webpack 运行时会提供错误处理机制,确保应用程序的稳定性。
-
配置选项:
- Webpack 的 HMR 功能可以通过配置文件中的
hot选项进行配置,例如hot: true启用 HMR。
- Webpack 的 HMR 功能可以通过配置文件中的
示例代码:
if (module.hot) {
module.hot.accept('./module', () => {
console.log('Module has been hot updated!');
});
}
在这个示例中,module.hot.accept 用于接受特定模块的更新。当该模块更新时,回调函数会被执行。
通过这种方式,Webpack 的 HMR 功能可以在开发过程中提供即时反馈,极大地提高了开发效率。
TreeShaking
前提:是使用Es Module实现的模块化
简单示例
- usedExports:负责标记哪些是枯树叶(无用的代码)
- minimize: 摇掉枯树叶
- concatenateModules: 尽可能合并每一个模块到一个函数中(scope hoisting作用域提升:减小函数数,提高运行效率)
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
optimization: {
// 模块只导出被使用的成员
usedExports: true,
// 尽可能合并每一个模块到一个函数中
concatenateModules: true,
// 压缩输出结果
// minimize: true
}
}
工作原理
Webpack 的 Tree Shaking 是一个在前端工程化中广泛使用的优化技术,其目的是移除代码库中未被引用的部分,从而减少最终打包文件的大小。以下是 Webpack 实现 Tree Shaking 的主要步骤和原理:
-
ES6 模块支持: Tree Shaking 依赖于 ES6 模块的静态结构,因为 ES6 模块的
import和export是静态可分析的。 -
静态分析: Webpack 通过静态分析源代码来确定哪些模块或模块的一部分未被引用。
-
确定引用: Webpack 检查模块的导出(exports)和导入(imports),确定哪些导出是被引用的。
-
排除未引用代码: 在打包过程中,Webpack 将排除那些没有被引用的模块或代码块。
-
副作用评估: Webpack 会评估模块是否有副作用。如果模块被标记为具有副作用,即使没有被引用,它们也不会被排除。
-
配置选项: Webpack 提供了配置选项来启用或禁用 Tree Shaking。例如,可以通过设置
treeShaking: true来显式启用 Tree Shaking。 -
副作用标记: 使用
sideEffects: false在package.json中标记整个包没有副作用,这会告诉 Webpack 该包中的所有模块都可以进行 Tree Shaking。 -
纯注释: 开发者可以在代码中使用
/* @__PURE__ */注释来告诉 Webpack 某个位置的函数调用是纯函数,即使它们在静态分析中未被引用。 -
构建优化: Webpack 在构建过程中应用 Tree Shaking 优化,移除未被引用的代码,减少输出文件的大小。
-
输出结果: 经过 Tree Shaking 后,Webpack 生成的最终打包文件只包含应用程序实际使用的代码,从而减少文件大小并提高加载性能。
Tree Shaking 是一个在现代前端构建工具中常见的优化特性,它通过静态分析和配置选项,帮助开发者移除未使用的代码,优化最终的打包结果。在 Webpack 中,Tree Shaking 通常在生产环境构建时自动应用,以确保最终的部署包尽可能精简。
sideEffects
默认值为true,即认为每个模块都有可能是包含副作用,具体情况具体分析。
在项目中正确设置 Webpack 的 sideEffects 属性,可以遵循以下步骤:
-
理解副作用: 确定代码或依赖的库中的哪些部分具有副作用。这包括修改全局状态、进行网络请求、读写文件等。
-
更新 package.json: 在
package.json文件中,添加或更新sideEffects属性。你可以将其设置为false,如果你确定包中所有模块都没有副作用。{ "name": "your-package-name", "version": "1.0.0", "sideEffects": false } -
使用通配符: 如果你只想标记包中的特定文件或文件夹没有副作用,可以使用通配符。例如,如果源代码在
src文件夹中,可以这样设置:{ "sideEffects": ["*.css", "!*.js"] }这表示所有 CSS 文件都有副作用,但 JavaScript 文件没有。
-
谨慎使用: 如果包确实包含有副作用的代码,不要将
sideEffects设置为false,因为这可能导致 Webpack 错误地移除必要的代码。 -
模块级别的副作用: 如果包中有的模块有副作用,有的没有,你需要更细粒度地控制 Tree Shaking。你可以通过在文件顶部添加
/*#__PURE__*/注释来标记这些模块,告诉 Webpack 这些模块没有副作用。 -
构建配置: 在 Webpack 配置文件中,可以使用
sideEffects属性来控制整个项目的 Tree Shaking 行为:module.exports = { optimization: { usedExports: true, // 尝试确定哪些导出是被使用的 providedExports: true, // 尝试确定哪些导出是被提供的 sideEffects: true, // 尝试确定哪些模块有副作用 }, // 其他配置... }; -
测试和验证: 更改
sideEffects设置后,进行彻底的测试以确保没有引入任何问题。验证打包后的代码是否按预期工作,并且没有删除掉重要的代码。 -
文档和维护: 更新项目文档,记录
sideEffects的设置和原因。这对于维护和未来的代码审查非常重要。 -
依赖管理: 检查依赖项,确保它们的
sideEffects属性设置正确。如果依赖项不正确地声明了副作用,可能会影响项目的 Tree Shaking 效果。
通过正确设置 sideEffects 属性,可以提高 Webpack 构建的性能和输出文件的效率,同时确保代码的正确性。
代码分割(分包、按需加载)
背景:打包的时候,所有代码都会被打包到一起
目的:优化代码颗粒度
多入口打包
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'none',
entry: {
index: './src/index.js',
album: './src/album.js'
},
output: {
filename: '[name].bundle.js'
},
optimization: {
splitChunks: {
// 自动提取所有公共模块到单独 bundle
chunks: 'all'
}
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html',
chunks: ['index']
}),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html',
chunks: ['album']
})
]
}
按需加载
js模块按需加载
// import posts from './posts/posts'
// import album from './album/album'
const render = () => {
const hash = window.location.hash || '#posts'
const mainElement = document.querySelector('.main')
mainElement.innerHTML = ''
if (hash === '#posts') {
// mainElement.appendChild(posts())
import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => {
mainElement.appendChild(posts())
})
} else if (hash === '#album') {
// mainElement.appendChild(album())
import(/* webpackChunkName: 'components' */'./album/album').then(({ default: album }) => {
mainElement.appendChild(album())
})
}
}
render()
window.addEventListener('hashchange', render)
css模块按需加载
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
mode: 'none',
entry: {
main: './src/index.js'
},
output: {
filename: '[name].bundle.js'
},
optimization: {
minimizer: [
// 内置的js压缩
new TerserWebpackPlugin(),
// css压缩
new OptimizeCssAssetsWebpackPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
// 'style-loader', // 将样式通过 style 标签注入
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Dynamic import',
template: './src/index.html',
filename: 'index.html'
}),
new MiniCssExtractPlugin()
]
}
原理总结
Webpack 5 通过以下几个步骤实现代码分割(Code Splitting):
-
入口起点(Entry Points): Webpack 从配置的入口起点开始分析,确定最初的模块依赖。
-
依赖图构建: Webpack 递归地分析所有依赖项,构建一个依赖图,包括动态导入和静态导入。
-
分割点识别: Webpack 识别代码中的分割点,如动态
import()表达式,这些点可以作为分割代码的入口。 -
分割策略: Webpack 使用不同的策略来决定如何分割代码。它可以自动分割代码,也可以通过配置来自定义分割逻辑。
-
生成 Chunk: 基于依赖图和分割策略,Webpack 生成多个 Chunk,每个 Chunk 包含一组模块。
-
Chunk 命名: Webpack 为每个 Chunk 生成唯一的名称,以便在最终的输出中引用。
-
异步加载: 对于异步导入的模块,Webpack 生成异步加载的 Chunk,并在主文件中通过
__webpack_require__.e来请求加载。 -
预加载和预获取: Webpack 支持预加载(prefetch)和预获取(preload),允许开发者提前加载可能需要的 Chunk。
-
输出文件: Webpack 将每个 Chunk 输出为单独的文件,并将这些文件引用到主文件或其他 Chunk 中。
-
运行时更新: Webpack 的运行时包含逻辑来处理 Chunk 的加载,包括如何处理异步加载和按需加载。
-
HMR 集成: Webpack 的热模块替换(HMR)与代码分割集成,允许在开发过程中动态更新 Chunk。
-
配置选项: Webpack 提供了多个配置选项,如
optimization.splitChunks,用于自定义代码分割的行为。 -
模块联邦(Module Federation): Webpack 5 引入了模块联邦,允许多个 Webpack 构建之间共享模块,这也是一种代码分割的形式。
-
浏览器支持: 生成的代码分割逻辑兼容主流浏览器,确保分割的代码可以在浏览器中正确加载和执行。
通过这些步骤,Webpack 5 能够将大型应用程序拆分成更小的、可管理的代码块,这些代码块可以在运行时按需加载,从而提高应用程序的加载性能和用户体验。
文件缓存
生产模式下的缓存
生产模式下,文件模式使用hash
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
mode: 'none',
entry: {
main: './src/index.js'
},
output: {
filename: '[name]-[contenthash:8].bundle.js'
},
optimization: {
minimizer: [
new TerserWebpackPlugin(),
new OptimizeCssAssetsWebpackPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
// 'style-loader', // 将样式通过 style 标签注入
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Dynamic import',
template: './src/index.html',
filename: 'index.html'
}),
new MiniCssExtractPlugin({
filename: '[name]-[contenthash:8].bundle.css'
})
]
}
运行时缓存
在 Webpack 中,runtimeChunk 配置选项用于控制运行时代码(runtime code)的生成和打包方式。运行时代码是指 Webpack 用来处理模块加载、热模块替换(HMR)和其他功能的代码。以下是 runtimeChunk 的主要作用:
-
分离运行时代码:
runtimeChunk允许将运行时代码从应用程序的主要bundle中分离出来,单独打包成一个或多个独立的chunk。 -
优化性能: 将运行时代码分离到单独的chunk可以减少主bundle的大小,从而提高页面加载性能和速度。
-
便于缓存: 由于运行时代码通常不会频繁更改,将其分离到单独的chunk可以使得浏览器缓存更有效,因为只有更改的部分需要被更新。
-
配置灵活性:
runtimeChunk可以配置为一个名称或一个配置对象,以便更细致地控制生成的chunk的名称、位置和行为。 -
支持多入口点: 当应用程序有多个入口点时,
runtimeChunk可以确保每个入口点使用独立的运行时代码,避免不同入口点之间的运行时代码冲突。 -
自定义运行时代码: 通过
runtimeChunk,可以自定义运行时代码的生成逻辑,例如,可以插入自定义的模块加载逻辑或错误处理逻辑。
Webpack 5 中 runtimeChunk 的配置示例:
module.exports = {
// ...
optimization: {
runtimeChunk: {
name: (entrypoint)=> `runtime~${entrypoint.name}-.js`
},
},
// ...
};
还有可以配置文件缓存的策略
module.exports = {
// ...
cache: {
type: 'filesystem', // 使用文件系统缓存
cacheDirectory: path.resolve(__dirname, 'cache'), // 指定缓存目录
buildDependencies: {
config: [__filename], // 依赖的配置文件,当配置文件改变时缓存将失效
},
},
// ...
};
开发模式和生产模式区分
- mode: 'production'或'develop'
公共配置抽离和合并
webpack.common.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/main.js',
output: {
filename: 'js/bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: 'file-loader',
options: {
outputPath: 'img',
name: '[name].[ext]'
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Tutorial',
template: './src/index.html'
})
]
}
webpack.dev.js
const webpack = require('webpack')
const merge = require('webpack-merge')
const common = require('./webpack.common')
module.exports = merge(common, {
mode: 'development',
devtool: 'cheap-eval-module-source-map',
devServer: {
hot: true,
contentBase: 'public'
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
})
webpack.prod.js
const merge = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const common = require('./webpack.common')
module.exports = merge(common, {
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
})
全局变量注入
使用DefinePlugin
const webpack = require('webpack')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js'
},
plugins: [
new webpack.DefinePlugin({
// 值要求的是一个代码片段
API_BASE_URL: JSON.stringify('https://api.example.com')
})
]
}
或者搭配使用cross-env
模块联邦
Webpack 5 引入了模块联邦(Module Federation)功能,这是一种新的代码拆分和动态模块加载的方式,它允许多个独立的 Webpack 构建之间共享模块。模块联邦的实现基于以下关键概念:
-
远程资源:
- 模块联邦允许你从另一个 Webpack 构建中动态导入远程资源。
-
共享依赖:
- 通过模块联邦,不同的构建之间可以共享公共依赖。
-
动态导入:
- 使用 JavaScript 的动态
import()语法来导入远程资源。
- 使用 JavaScript 的动态
-
构建配置:
- 在 Webpack 配置中,使用
ModuleFederationPlugin插件来配置模块联邦。
- 在 Webpack 配置中,使用
-
远程配置:
- 在
ModuleFederationPlugin配置中,定义了远程依赖的名称和远程构建的入口点。
- 在
-
版本协商:
- 模块联邦处理不同构建之间的版本协商,确保即使依赖项更新,远程依赖仍然可以正确解析。
-
环境抽象:
- 模块联邦抽象了运行时环境,允许在不同环境中运行,例如浏览器或 Node.js。
-
热模块替换(HMR):
- 模块联邦与 Webpack 的热模块替换集成,允许在开发过程中动态更新模块。
-
构建隔离:
- 每个模块联邦成员(federated member)都是独立的构建,它们可以独立更新和部署。
-
远程通信:
- 模块联邦使用 HTTP 请求或其他通信机制来加载远程资源。
-
依赖图:
- Webpack 构建的依赖图包括本地和远程依赖。
-
代码分割:
- 模块联邦支持代码分割,允许按需加载远程模块。
-
服务端渲染(SSR):
- 模块联邦可以在服务端渲染中工作,支持在服务器端解析和渲染远程模块。
-
开发体验:
- 模块联邦提供了更好的开发体验,允许开发者在不同的项目之间共享组件或库。
配置示例:
// webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin';
module.exports = {
// ...其他配置...
plugins: [
new ModuleFederationPlugin({
name: 'app', // 暴露的公共名称
filename: 'remoteEntry.js', // 远程入口文件
exposes: {
'./App': './src/App', // 暴露的模块和它们的路径
},
shared: ['react', 'react-dom'], // 共享依赖
}),
],
};
在这个示例中,ModuleFederationPlugin 被配置为暴露一个名为 app 的远程入口点,它将 ./src/App 组件暴露给其他联邦成员使用。同时,react 和 react-dom 被配置为共享依赖,这意味着所有使用这些依赖的联邦成员不需要再次打包它们。
通过这种方式,Webpack 5 的模块联邦功能允许开发者构建一个微前端架构,其中不同的应用程序或应用程序的部分可以独立开发、部署和维护,同时仍然能够共享和重用代码。
Manifest
一旦你的应用在浏览器中以 index.html 文件的形式被打开,一些 bundle 和应用需要的各种资源都需要用某种方式被加载与链接起来。在经过打包、压缩、为延迟加载而拆分为细小的 chunk 这些 webpack 优化 之后,你精心安排的 /src 目录的文件结构都已经不再存在。所以 webpack 如何管理所有所需模块之间的交互呢?这就是 manifest 数据用途的由来……
当 compiler 开始执行、解析和映射应用程序时,它会保留所有模块的详细要点。这个数据集合称为 "manifest",当完成打包并发送到浏览器时,runtime 会通过 manifest 来解析和加载模块。无论你选择哪种 模块语法,那些 import 或 require 语句现在都已经转换为 __webpack_require__ 方法,此方法指向模块标识符(module identifier)。通过使用 manifest 中的数据,runtime 将能够检索这些标识符,找出每个标识符背后对应的模块。
更多API和特性
参考官方网站:webpack.docschina.org/
面试常问
请简述一下Webpack的工作原理
参考上文
如何实现一个Webpack插件
参考上文
如何实现一个Webpack Loader
参考上文
Webpack HRM的原理
参考上文
为什么我们平时开发的时候并没有写热更新逻辑,但是依旧可以实现热更新
在实际开发中,即使没有显式编写有关 module.hot 的逻辑,React组件依旧能够实现热更新,这主要是因为以下几个原因:
-
React 的内置支持:
- React 自身提供了对 HMR 的内置支持。当使用诸如
React.lazy进行代码分割或在index.js中直接导入组件时,React 能够处理这些模块的热更新。
- React 自身提供了对 HMR 的内置支持。当使用诸如
-
Webpack 的自动 HMR 支持:
- Webpack 5 引入了对 ES 模块的自动 HMR 支持。如果项目使用了 ES 模块,Webpack 能够在不使用
module.hot显式逻辑的情况下,自动更新模块。
- Webpack 5 引入了对 ES 模块的自动 HMR 支持。如果项目使用了 ES 模块,Webpack 能够在不使用
-
Babel 插件:
- 如果构建配置中使用了 Babel,
babel-plugin-react-refresh插件可以为 React 组件提供快速刷新支持。这个插件会自动修改代码,以支持 React Fast Refresh。
- 如果构建配置中使用了 Babel,
-
样式和资源的 HMR:
- 对于 CSS 和图片等资源,Webpack 有对应的 loader(如
style-loader、css-loader等)来处理 HMR。当这些资源文件发生变化时,Webpack 可以替换掉旧的样式和资源,而不需要重新加载整个页面。
- 对于 CSS 和图片等资源,Webpack 有对应的 loader(如
-
框架或库的支持:
- 如果你使用的是 Create React App、Next.js 等框架或库,它们已经内置了 HMR 支持。不需要手动配置或编写 HMR 相关代码。
-
React Fast Refresh:
- React Fast Refresh 是 React 的一个特性,它通过保留应用程序的状态和避免不必要的重新渲染,提供了快速的开发体验。Fast Refresh 通常与 Webpack HMR 集成使用,但不需要开发者手动编写 HMR 逻辑。
-
模块替换逻辑:
- 在某些情况下,即使没有
module.hot,Webpack 也可能通过其他方式实现模块的替换。例如,使用import()动态导入的模块可以被 Webpack 替换而不需要刷新页面。
- 在某些情况下,即使没有
-
开发服务器:
- 使用 Webpack Dev Server 或类似的开发服务器,它们支持 HMR 并能够提供热更新。
因此,即使没有显式编写 module.hot 逻辑,React 应用仍然可以实现热更新,这得益于 React、Webpack 和其他工具的内置支持。这些工具在幕后处理了大部分 HMR 相关的工作,使得开发者可以更专注于编写业务逻辑代码。
vite和webpack有什么区别
主要区别在于开发阶段。
webpack会先打包,然后启动开发服务器,请求服务器时直接给予打包结果。 而vite是直接启动开发服务器,请求哪个模块再对该模块进行实时编译。 由于现代浏览器本身就支持ES Module,会自动向依赖的Module发出请求。vite充分利用这一点,将开发环境下的模块文件,就作为浏览器要执行的文件,而不是像webpack那样进行打包合并。 由于vite在启动的时候不需要打包,也就意味着不需要分析模块的依赖、不需要编译,因此启动速度非常快。当浏览器请求某个模块时,再根据需要对模块内容进行编译。这种按需动态编译的方式,极大的缩减了编译时间,项目越复杂、模块越多,vite的优势越明显。 在HMR方面,当改动了一个模块后,仅需让浏览器重新请求该模块即可,不像webpack那样需要把该模块的相关依赖模块全部编译一次,效率更高。 当需要打包到生产环境时,vite使用传统的rollup进行打包,因此,vite的主要优势在开发阶段。另外,由于vite利用的是ES Module,因此在代码中不可以使用CommonJS
更多可参考这篇文章
webpack4和webpack5有什么区别
可以参考这篇文章
webpack怎么设置在打包的时候单独处理一些第三方库
在 Webpack 中,你可以使用多种方法来单独处理第三方库,以优化构建性能和输出结果。以下是一些常用的方法:
-
外部化(Externals): 通过配置
externals,你可以指定某些库在最终打包文件中不被包含,而是在运行时从外部获取。// webpack.config.js module.exports = { // ... externals: { 'react': 'React', 'react-dom': 'ReactDOM', }, }; -
代码分割(Code Splitting): 使用
SplitChunksPlugin来自动或手动分割代码。你可以配置插件来分离第三方库。// webpack.config.js plugins: [ new SplitChunksPlugin({ chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendor', chunks: 'all', }, }, }), ]; -
动态导入(Dynamic Imports): 使用动态
import()语法来按需加载第三方库,实现代码分割。// 某个组件或模块中 if (condition) { import('some-library').then((lib) => { lib.doSomething(); }); } -
预加载和预取(Preload and Prefetch): 使用
PreloadPlugin或PrefetchPlugin来预加载或预取第三方库。 -
全局变量(Global Variables): 如果你不想处理某些全局可用的库(例如,通过
<script>标签全局加载的库),可以在 Webpack 配置中声明它们。// webpack.config.js plugins: [ new webpack.ProvidePlugin({ jQuery: 'jquery', }), ]; -
忽略(Ignore Plugin): 使用
IgnorePlugin来忽略某些特定的模块,不将它们包含在最终的打包文件中。 -
模块替换(Module Replacement): 使用
NormalModuleReplacementPlugin或ModuleReplacementPlugin来替换某些模块。 -
DedupePlugin: 使用
DedupePlugin来避免将重复的库打包多次。 -
库的自定义别名(Custom Aliases for Libraries): 为第三方库设置别名,通过
resolve.alias配置,可以控制库的导入方式。 -
优化配置(Optimization Configuration): 在 Webpack 的
optimization配置中,可以对库进行特定的优化处理。
这些方法可以帮助你更好地管理和优化第三方库的使用,根据项目需求和构建目标选择合适的方法。在实际应用中,可能需要结合多种方法来达到最佳效果。
你知道DLL相关的插件吗
DLL(Dynamic Link Library)插件在 Webpack 中用于优化构建性能,特别是对于大型项目。它的主要目的是将一些不经常变化的库或模块预先打包好,这样在后续的构建中就可以避免重复编译这些模块,从而加快构建速度。
以下是 DLL 插件的一些关键点:
-
预先打包: 使用 DLL 插件可以创建一个包含多个模块的独立包,这些模块通常是第三方库,它们不经常变化。
-
避免重复编译: 当项目中的其他代码发生变化时,只需要重新编译变化的部分,而预先打包的库不需要重新编译。
-
加快构建速度: 由于 DLL 插件减少了需要处理的模块数量,因此可以显著加快构建速度。
-
配置 DLL 插件: 在 Webpack 配置中配置 DLL 插件,指定要预先打包的库或模块。
-
DLL 引用: 在项目的主应用配置中引用 DLL 包,确保在运行时可以正确加载预先打包的库。
-
DLL 缓存: DLL 插件生成的包可以被缓存,这样在开发过程中或在部署后,这些库不需要被重新打包。
-
插件配合: DLL 插件通常与
DllReferencePlugin插件一起使用,后者用于在主应用的 Webpack 配置中引用 DLL 包。 -
多页面应用(MPA): 在多页面应用中,DLL 插件可以为每个页面或应用的共享库创建单独的包,进一步优化构建过程。
-
适用于生产环境: 虽然 DLL 插件在开发过程中也可以使用,但它更适用于生产环境,因为生产环境中对构建速度的要求更高。
-
示例配置: 使用 DLL 插件和 DllReferencePlugin 的基本配置示例:
// webpack.dll.config.js const path = require('path'); const webpack = require('webpack'); module.exports = { entry: { vendor: ['library-a', 'library-b'], // 预先打包的库 }, output: { path: path.join(__dirname, 'dist'), filename: '[name].dll.js', library: '[name]_[hash]', }, plugins: [ new webpack.DllPlugin({ path: path.join(__dirname, 'dist', '[name]-manifest.json'), name: '[name]_[hash]', }), ], }; // webpack.config.js module.exports = { // ... plugins: [ new webpack.DllReferencePlugin({ context: __dirname, manifest: require('./dist/vendor-manifest.json'), }), ], // ... };
通过这种方式,DLL 插件可以显著提高大型项目的构建性能,尤其是在开发过程中库的变动不频繁时。