Webpack 工程化
一、引言
Webpack 是一个强大的模块打包工具,在前端开发中,它能够帮助我们处理各种资源,如 JavaScript、CSS、图片等,还能进行代码分割、优化和压缩等操作,提升项目的开发效率和性能。本文将详细介绍今天实现的 Webpack 工程化开发相关功能。
功能概述
基于 Webpack 的工程化配置,主要包括以下内容:
- 开发环境与生产环境的分离:通过
webpack.base.js提供基础配置,分别创建webpack.dev.js和webpack.prod.js实现开发和生产环境的差异化配置。 - 热更新(HMR)支持:在开发环境中引入
webpack-hot-middleware,实现代码修改后的自动刷新功能。 - 模块化配置管理:通过
webpack-merge插件合并基础配置与环境特定配置,提升代码可维护性。
插件详解
html-webpack-plugin:是 Webpack 中最核心的插件之一,专门用于动态生成 HTML 文件并自动注入打包后的资源(CSS/JS)。happypack:基于 多进程并行处理 的 Webpack 插件,专为优化 JavaScript 打包性能设计。它通过将 Loader 的处理任务分配到多个子进程中执行,充分利用多核 CPU 资源,显著提升构建速度。mini-css-extract-plugin:用于 CSS 提取 的核心插件,专为生产环境优化设计。它将 CSS 代码从 JavaScript bundles 中分离到独立的 CSS 文件,提升缓存复用率并减少 JS 文件体积。clean-webpack-plugin:用于 自动清理构建目录 的核心插件,有效避免旧文件残留导致的部署错误或缓存问题。css-minimizer-webpack-plugin:用于 CSS 文件压缩优化 的核心插件,专为生产环境设计。它通过移除冗余代码、合并重复规则、压缩关键属性值等方式显著减小 CSS 文件体积,提升加载性能。html-webpack-inject-attributes-plugin:专注于 动态注入 HTML 属性 的 Webpack 插件,允许开发者在不修改模板文件的情况下,基于构建配置或环境变量向生成的 HTML 注入自定义属性(如data-*属性)。terser-webpack-plugin:用于 JavaScript 代码压缩与优化 的核心插件,通过并发和缓存提升压缩阶段性能,移除冗余代码,自动删除console.log、debugger、未使用函数等。
二、 核心配置详解
2.1 基础配置 (webpack.base.js)
基础配置文件包含所有环境通用的配置项,例如入口文件、输出路径、加载器(loaders)等。
// webpack.base.js
const glob = require("glob");
const path = require("path");
const webpack = require("webpack");
const { VueLoaderPlugin } = require("vue-loader");
const HtmlWebpackPlugin = require("html-webpack-plugin");
// 动态构造 pages 入口文件
const pageEntries = {};
// 动态构造 htmlWebpackPluginList 渲染模板
const htmlWebpackPluginList = [];
// 获取 app/pages 下所有的入口文件 (entry.xx.js)
const entryList = path.resolve(process.cwd(), "./app/pages/**/entry.*.js");
glob.sync(entryList).forEach((file) => {
const entryName = path.basename(file, ".js");
// 构造 entry
pageEntries[entryName] = file;
htmlWebpackPluginList.push(
// html-webpack-plugin 插件 辅助注入打包后的 boundle 文件到tpl文件中
new HtmlWebpackPlugin({
// 产物 最终模板 输出路径
filename: path.resolve(
process.cwd(),
"./app/public/dist/",
`${entryName}.tpl`
),
// 指定要使用的模板文件
template: path.resolve(process.cwd(), "./app/views/entry.tpl"),
// 注入的代码块
chunks: [entryName],
})
);
// 构造最终渲染的页面文件
});
/**
* webpack.base.js 基础配置
* */
module.exports = {
// 入口文件
entry: pageEntries,
// 出口文件 产物输出路径,开发和生产环境输出不一致,须在各自环境中配置
output: {},
// 模块加载解析器
module: {
rules: [
{
test: /\.vue$/,
use: "vue-loader",
},
{
test: /\.js$/,
include: [path.resolve(process.cwd(), "./app/pages")], //只针对 业务代码 进行babel转换 加快webpack打包速度
use: { loader: "babel-loader" },
},
{
test: /\.(png|jpe?g|gif)(\?.+)?$/,
use: {
loader: "url-loader",
options: {
limit: 1024, // 1024 以下的图片打包成 base64 格式
esModule: false,
},
},
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /\.less$/,
use: ["style-loader", "css-loader", "less-loader"],
},
{
test: /\.(eot|svg|ttf|woff|woff2)(\?\S*)?$/,
use: "file-loader",
},
],
},
// 模块解析具体行为
resolve: {
extensions: [".js", ".vue", ".less", ".css"],
alias: {
$pages: path.resolve(process.cwd(), "./app/pages"),
$common: path.resolve(process.cwd(), "./app/pages/common"),
$widgets: path.resolve(process.cwd(), "./app/pages/widgets"),
$store: path.resolve(process.cwd(), "./app/pages/store"),
},
},
// 配置 webpack 插件
plugins: [
// 配置 vue-loader 插件 处理.vue文件必须配置
// 将定义过的其他规则复制并应用到.vue文件里 /\.js$/ 他会将该规则定义到.vue中的<script>模块内
new VueLoaderPlugin(),
// 配置 ProvidePlugin 插件 将第三方库暴露到window全局 contenxt下
// 这样就可以在业务代码中直接使用 window.Vue 来使用Vue
new webpack.ProvidePlugin({ Vue: "vue" }),
// 配置 DefinePlugin 插件 定义全局变量
new webpack.DefinePlugin({
__VUE_OPTIONS_API__: "true", // 支持vue解析optionsApi
__VUE_PROD_DEVTOOLS__: "false", // 禁用Vue调试工具
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: "false", // 禁用Vue生产环境下显示 水合 信息
}),
...htmlWebpackPluginList,
],
// 配置 打包输出优化 (代码分割|模块合并|缓存|TreeShaking|压缩等优化策略)
optimization: {
/**
* 代码分割 配置
* 把 js 文件打包成3中类型
* 1. vendor: 第三方lib库,基本不会改动,除非依赖版本升级
* 2. common: 业务组件代码公共部分抽离,改动较少
* 3. entry.{page}: 不同页面 entry 里面业务组件代码差异部分,会经常改动
* 目的:把改动和引用频率不一样的 js 区分出来,以达到更好利用浏览器缓存效果
*/
splitChunks: {
chunks: "all", //对同步和异步模块都进行分割
maxAsyncRequests: 10, // 按需加载时最大并行请求数
maxInitialRequests: 10, // 一个入口最大并行请求数
cacheGroups: {
vendor: {
// 打包第三方依赖库
name: "vendor", // 模块名称
test: /[\\/]node_modules[\\/]/, //打包 node_modules下的模块
priority: 20, // 优先级,值越大优先级越高
enforce: true, // 强制打包
reuseExistingChunk: true, // 复用已存在的模块
},
common: {
// 公共模块
name: "common", // 模块名称
minChunks: 2, // 被引用次数大于等于2次的模块
minSize: 1, // 最小分割文件大小(1 byte)
priority: 10, // 优先级,值越大优先级越高
reuseExistingChunk: true, // 复用已存在的模块
},
},
},
// 将 webpack 运行时生产的代码打包到 runtime.js 中,减少 main.js 的体积
runtimeChunk: true,
},
};
2.2 开发环境配置 (webpack.dev.js)
基于 Express 的本地开发服务器,配合 Webpack 进行前端项目的开发。该服务器支持热更新功能,能够在代码修改后自动更新浏览器页面,提高开发效率。同时,它还配置了跨域请求的支持,方便前后端分离开发。
开发环境配置主要关注开发效率,包括热更新、Source Map 等功能。
// webpack.dev.js
const path = require("path");
const mrege = require("webpack-merge");
const webpack = require("webpack");
// 基类配置
const baseConfig = require("./webpack.base.js");
// dev-server 配置
const DEV_SERVER_CONFIG = {
HOST: "127.0.0.1",
PORT: 9002,
HMR_PATH: "__webpack_hmr", //官方规定配置
TIMEOUT: 20000,
};
// 开发阶段 entry 配置需要加入 hmr
Object.keys(baseConfig.entry).forEach((entryKey) => {
// 第三方包不作为 hmr入口
if (entryKey !== "vendor") {
baseConfig.entry[entryKey] = [
// 主入口文件
baseConfig.entry[entryKey],
// hmr 热更新入口文件 官方指定的 hmr 路径
`webpack-hot-middleware/client?path=http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/${DEV_SERVER_CONFIG.HMR_PATH}&timeout=${DEV_SERVER_CONFIG.TIMEOUT}&reload=true`,
];
}
});
// 开发环境配置
const webpackConfig = mrege.smart(baseConfig, {
mode: "development", // 指定开发环境
// source-map 配置 开发工具,呈现代码映射关系,便于开发环境的调试代码
devtool: "eval-cheap-module-source-map", // 开发环境 配置
output: {
// 输出 开发环境 配置
filename: "js/[name]_[chunkhash:8].bundle.js",
path: path.resolve(process.cwd(), "./app/public/dist/dev/"), //输出文件存储路径
publicPath: `http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/public/dist/dev/`, // 外部资源公共路径
globalObject: "this", // 全局对象
},
// 开发阶段插件
plugins: [
// 用于实现热模块替换 (Hot Module Replacement 简称 HMR)
// 模块热替换允许在应用程序运行时更新部分模块
// 极大的提升开发效率,能让应用程序一直保持运行状态
new webpack.HotModuleReplacementPlugin({
multiStep: true, // 热更新 多步模式
}), // 热更新插件
new webpack.DefinePlugin({
// 全局变量
__DEV__: true,
}),
],
});
module.exports = {
// webpackConfig 配置
webpackConfig,
// devServer 配置 暴露给dev.js使用
DEV_SERVER_CONFIG,
};
开发环境 启动文件 (dev.js)
// dev.js 本地开发启动 devServe
const express = require("express");
const path = require("path");
const consoler = require("consoler");
const webpack = require("webpack");
const devMiddleware = require("webpack-dev-middleware");
const hotMiddleware = require("webpack-hot-middleware");
// 通过 webpack.dev.js 获取 webpack配置 和 devServer配置
const { webpackConfig, DEV_SERVER_CONFIG } = require("./config/webpack.dev.js");
const app = express();
const compiler = webpack(webpackConfig);
// 指定静态文件目录
app.use(express.static(path.join(__dirname, "../public/dist")));
// 引用 devMiddleware 中间件(监控文件改动)
app.use(
devMiddleware(compiler, {
// 落地文件路径
writeToDisk: (filePath) => filePath.endsWith(".tpl"),
// 资源路径
publicPath: webpackConfig.output.publicPath,
// headers 配置
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
"Access-Control-Allow-Headers":
"X-Requested-With, content-type, Authorization",
},
// 日志配置
stats: {
colors: true,
},
})
);
// 引用 hotMiddleware 中间件(实现热更新通讯)
app.use(
hotMiddleware(compiler, {
path: `/${DEV_SERVER_CONFIG.HMR_PATH}`,
log: () => {},
})
);
consoler.info("请等待webpack初次编译构建完成提示...");
const port = DEV_SERVER_CONFIG.PORT;
// 启动 webpack-dev-middleware,将 webpack 编译打包后的产品文件挂在到 express 服务上
app.listen(port, () => {
console.log(`express as app listening on port ${port}`);
});
2.3 生产环境配置 (webpack.prod.js)
生产环境下使用 Webpack 对项目进行打包构建。它会读取 Webpack 生产环境配置文件,然后执行打包操作,并将打包过程中的统计信息输出到控制台,方便开发者了解打包情况。
生产环境配置注重性能优化,包括代码压缩、Tree Shaking 等。
// webpack.prod.js
const path = require("path");
const mrege = require("webpack-merge"); // 基类配置
const baseConfig = require("./webpack.base.js");
const os = require("os");
const HappyPack = require("happypack");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CleanWebpackPlugin = require("clean-webpack-plugin");
const CSSMinimizerPlugin = require("css-minimizer-webpack-plugin");
const HtmlWebpackInjectAttributesPlugin = require("html-webpack-inject-attributes-plugin");
const TerserWebpackPlugin = require("terser-webpack-plugin");
// 多线程打包
const happypackCommonConfig = {
debug: false,
threadPool: HappyPack.ThreadPool({ size: os.cpus().length }),
};
// 生产环境配置
const webpackConfig = mrege.smart(baseConfig, {
mode: "production", // 指定生产环境
output: {
filename: "js/[name]_[chunkhash:8].bundle.js",
path: path.join(process.cwd(), "./app/public/dist/prod"),
publicPath: "/dist/prod",
crossOriginLoading: "anonymous",
},
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "happypack/loader?id=css"],
},
{
test: /\.js$/,
include: [path.resolve(process.cwd(), "./app/pages")], //只针对 业务代码 进行babel转换 加快webpack打包速度
use: { loader: "happypack/loader?id=js" },
},
],
},
// webpack 插件 不会有大量 hints 信息 默认为 warning
performance: {
hints: false,
},
plugins: [
// 每次 build 前删除 pubilc/dist 目录
new CleanWebpackPlugin(["public.dist"], {
root: path.resolve(process.cwd(), "./app/"),
exclude: [],
verbose: true,
dry: false,
}),
// 提取 css 的公共部分 有效利用缓存
new MiniCssExtractPlugin({
chunkFilename: "css/[name]_[contenthash:8].bundle.css",
}),
// 优化并压缩 css 文件
new CSSMinimizerPlugin(),
// 多线程打包 js,提高打包速度
new HappyPack({
id: "js",
...happypackCommonConfig,
loaders: [
`babel-loader?${JSON.stringify({
presets: ["@babel/preset-env"],
plugins: ["@babel/plugin-transform-runtime"],
})}`,
],
}),
// 多线程打包 css,提高打包速度
new HappyPack({
id: "css",
...happypackCommonConfig,
loaders: [
{
path: "css-loader",
options: { importLoaders: 1 },
},
],
}),
// 浏览器在请求资源时不发送用户的身份凭证
new HtmlWebpackInjectAttributesPlugin({ crossorigin: "anonymous" }),
],
optimization: {
// 使用 TerserPlugin 的并发和缓存 提升压缩阶段性能
// 清除 console.log 等代码
minimize: true,
minimizer: [
new TerserWebpackPlugin({
parallel: true, // 利用多核cpu优化来加快压缩速度
cache: true, // 启用缓存加速构建过程
terserOptions: {
compress: {
drop_console: true, // 清除 console.log 等代码
},
},
}),
],
},
});
module.exports = webpackConfig;
生产环境 启动文件 (prod.js)
// prod.js
const webpack = require("webpack");
const webpackProdConfig = require("./config/webpack.prod.js");
console.log("now in produce building... ");
webpack(webpackProdConfig, (err, stats) => {
if (err) {
console.log(err);
return;
}
process.stdout.write(
`${stats.toString({
colors: true, // 增加控制台颜色信息
modules: false, // 不显示内置模块打包信息
children: false, // 不显示子级编译任务信息
chunks: false, // 不显示每个代码块的信息
chunkModules: true, // 显示代码块中模块信息
})}`
);
});
三、 关键点解析
3.1 热更新(HMR)
- 在开发环境中,通过
webpack-hot-middleware实现热更新功能。 - 配置中将 HMR 入口文件添加到主入口文件中,确保页面修改后能够实时刷新。
3.2 模块化配置管理
- 使用
webpack-merge插件合并基础配置与环境特定配置,避免重复代码。 - 通过
merge.smart方法智能合并数组和对象,确保配置项不会被覆盖。
3.3 性能优化
- 代码压缩:使用
TerserPlugin对生产环境代码进行压缩,减少文件体积。 - Tree Shaking:通过
splitChunks配置对第三方库进行单独打包,提升加载速度。
四、 配置启动脚本
在 package.json 中添加启动打包脚本:
package.json
{
"scripts": {
"build:dev": "node --max_old_space_size=4096 ./app/webpack/dev.js",
"build:prod": "node ./app/webpack/prod.js"
},
}
--max_old_space_size=4096:这是 Node.js 的一个选项,用于设置 Node.js 进程的最大堆内存大小。max_old_space_size表示 V8 引擎的老生代堆内存的最大容量,单位是 MB。这里将其设置为 4096MB,意味着 Node.js 进程在运行过程中,老生代堆内存最多可以使用 4GB 的空间。在处理大型项目或进行复杂的构建任务时,可能会消耗大量的内存,通过设置这个选项可以避免因内存不足而导致的程序崩溃。
4.1 启动打包编译开发环境
运行以下命令启动开发服务器:
npm run build:dev
npm run dev
4.2 启动打包编译生产环境
运行以下命令生成生产环境代码:
npm run build:prod
五、 总结
通过本次开发,我们实现了 Webpack 的工程化配置,涵盖了开发与生产环境的分离、热更新支持以及性能优化等内容,在 webpack.dev.js 所配置的开发环境中,热更新(Hot Module Replacement,简称 HMR)和打包是紧密相关且相互协作的,它们共同服务于提高开发效率这一目标。下面详细阐述二者的关系:
1. 打包是热更新的基础
- 资源整合与处理:Webpack 的核心功能是打包,它会从项目的入口文件开始,递归地分析项目中的所有依赖关系,将各种类型的资源(如 JavaScript、CSS、图片等)进行处理和整合。在
webpack.dev.js中,通过配置entry指定入口文件,output指定输出路径和文件名,以及module.rules配置不同类型文件的加载器,完成对项目资源的打包。例如:
webpack.dev.js
Apply
const path = require("path");
// ...
const webpackConfig = mrege.smart(baseConfig, {
// ...
entry: {
main: './src/index.js' // 入口文件
},
output: {
filename: "js/[name]_[chunkhash:8].bundle.js",
path: path.resolve(process.cwd(), "./app/public/dist/dev/"), // 输出路径
publicPath: `http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/public/dist/dev/` // 外部资源公共路径
},
module: {
rules: [
{
test: /.js$/,
use: 'babel-loader', // 处理 JavaScript 文件
exclude: /node_modules/
},
{
test: /.css$/,
use: ['style-loader', 'css-loader'] // 处理 CSS 文件
}
]
}
// ...
});
- 为热更新提供基础结构:打包过程会生成一个包含所有模块信息的文件结构,热更新正是基于这个结构来实现的。每个模块在打包后都有唯一的标识符,热更新利用这些标识符来识别和替换发生变化的模块。
2. 热更新是打包在开发环境的优化
- 避免全量打包:在开发过程中,如果每次修改代码都进行全量打包,会消耗大量的时间,降低开发效率。热更新机制允许在不重新加载整个页面的情况下,只更新发生变化的模块。例如,当修改了某个 JavaScript 模块或 CSS 文件时,Webpack 会监测到文件的变化,只对该模块进行重新打包,并将更新后的模块发送到浏览器,浏览器会自动替换旧的模块,从而实现页面的局部更新。
- 提高开发体验:热更新能够保留应用的当前状态,如表单输入、滚动位置等,让开发者可以更流畅地进行开发和调试。在
webpack.dev.js中,通过配置HotModuleReplacementPlugin来启用热更新功能:
webpack.dev.js
Apply
const webpack = require("webpack");
// ...
const webpackConfig = mrege.smart(baseConfig, {
// ...
plugins: [
new webpack.HotModuleReplacementPlugin({
multiStep: true // 热更新多步模式
})
]
// ...
});
3. 二者协同工作流程
- 启动开发服务器:运行
npm run build:dev启动 Webpack 开发服务器,服务器会根据webpack.dev.js的配置进行首次打包,并将打包后的文件输出到指定的目录。 - 监听文件变化:开发服务器会监听项目文件的变化,当检测到某个文件发生修改时,Webpack 会重新打包该文件及其依赖的模块。
- 发送更新信息:Webpack 会将更新后的模块信息发送到浏览器,浏览器根据这些信息找到对应的旧模块,并进行替换。
- 应用更新:浏览器应用更新后的模块,页面会自动更新,展示最新的代码效果。
综上所述,打包是热更新的基础,为热更新提供了必要的模块结构和资源整合;而热更新是打包在开发环境的优化,通过局部更新模块,避免了全量打包的时间消耗,提高了开发效率和体验。二者相互配合,共同为开发者提供了一个高效的开发环境。