什么是前端工程化?
"前端工程化"是指将前端开发流程标准化、自动化、模块化、体系化的一种方法和思想,目的是提升开发效率、代码质量和项目的可维护性。
前端工程化可以分为几个核心方面:
- 模块化 将前端的代码(HTML、CSS、JavaScript)拆分成多个功能单一、相互独立的模块,方便维护和复用。常见模块化规范包括:
- CommonJS(Node.js)
- AMD / RequireJS
- ES6 Modules(import/export)
- 组件化
UI层面的模块化,把一个页面拆成若干可复用组件(如 Vue、React 中的组件),提升复用性和开发效率。
- 自动化
通过工具自动完成重复性任务,比如:
- 构建(构建工具如 webpack、Vite)
- 打包压缩(JS/CSS 压缩、图片压缩)
- 自动刷新(HMR)
- 自动部署(CI/CD)
- 规范化
通过代码规范(如 ESLint、Stylelint)、提交规范(如 Git Commit 规范)、接口规范(如 Swagger)来保障团队协作质量。
webpack和前端工程化的关系
前端工程化的核心是打包工具。webpack 是前端工程化中的 打包构建工具,它在工程化体系中扮演着非常重要的角色。
- 模块打包
webpack 能将各种模块(JS、CSS、图片等)统一打包成浏览器可以直接运行的资源。
- Loader 和 Plugin 系统
- Loader 让 webpack 能够处理非 JavaScript 的资源(如 SCSS、图片、字体等)。
- Plugin 用于处理构建过程中的各种任务,如压缩、注入变量、生成 HTML。
- 开发服务器和热更新
webpack-dev-server 提供本地开发服务器,支持热更新(HMR),大大提升开发效率。
- 生产环境优化
通过 tree-shaking、代码分割、压缩混淆等技术优化最终上线代码体积和性能。
webpack打包构建流程
- 初始化(读取配置)
webpack 会加载 webpack.config.js 配置文件,读取 entry、output、module、plugin 等配置。
- 确定入口(entry) 从配置中指定的入口文件(如 index.js)开始,作为构建的起点。在这里可以进行单页面入口,也可以进行多页面构建,配置多个入口文件即可,如果页面多,可以用 glob 动态读取目录。我们可以通过遍历循环方式动态生成entry配置,这样对我们的开发更加友好,后续无论我们需要多少入口文件都不需要关心。
js
const pageEntries = {};
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辅助注入打包后的bundle文件到tpl文件中
new HtmlWebpackPlugin({
//产物(最终模版)输出路径
filename: path.resolve(process.cwd(), './app/public/dist/', `${entryName}.tpl`),
//指定到使用的模版文件
template: path.resolve(process.cwd(), './app/view/entry.tpl'),
//要注入的代码块
chunks: [entryName]
}),
)
});
//入口配置
entry: pageEntries,
Webpack 打包多页面应用时,核心思想是:为每个页面配置独立的入口(entry)和对应的 HTML 模板。Webpack 会为每个页面打包出一份 JS 和 HTML 文件。单页面应用只有一个页面,通过前端路由控制页面跳转,适合交互复杂的应用。多页面应用有多个页面,通过浏览器跳转,每次跳转会刷新整个页面,适合页面多单交互不复杂的应用,各页面相互独立,对SEO友好
- 构建模块依赖图
从入口模块出发,递归解析其依赖(import 或 require)的模块,形成一棵依赖树(模块图)。
- 使用 Loader 加载模块
对非 JS 模块(如 .vue、.scss、图片等)使用对应的 loader 处理,转成 webpack 能识别的模块。
例如:
- 使用 babel-loader 转换 ES6+
- 使用 vue-loader 解析 .vue 文件
- 使用 css-loader 和 style-loader 处理 CSS
js
//模块解析配置(决定了要加载解析哪些模块,以及用什么方式去解析)
module: {
rules: [{
test: /\.vue$/,
use: {
loader: 'vue-loader'
}
}, {
test: /\.js$/,
include: [
//只对业务代码进行babel,加快webpack打包速度
path.resolve(process.cwd(), './app/pages')
],
use: {
loader: 'babel-loader'
}
}, {
test: /\.(png|jep?g|gif)(\?.+)?$/,
use: {
loader: 'url-loader',
options: {
limit: 300,
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'
}]
},
- 执行 Plugin 插件
插件在构建的各个阶段(如开始、生成资源前、完成等)都可以钩入,做各种处理:
- 生成 HTML:HtmlWebpackPlugin
- 清理输出目录:CleanWebpackPlugin
- 提取 CSS:MiniCssExtractPlugin
- 压缩优化:TerserPlugin
js
//配置webpack插件
plugins: [
//处理.vue文件,这个插件是必须的
//它的职能是将你定义过的其他规则复制并应用到.vue文件里。
//例如,如果有一条匹配规则/\.js$/的规则,那么它会应用到.vue文件中的<script>板块中
new VueLoaderPlugin(),
//把第三方库暴露到window.context下
new webpack.ProvidePlugin({
Vue: 'vue',
axios:'axios',
_:'lodash'
}),
//定义全局常量
new webpack.DefinePlugin({
_VUE_OPTIONS_API_: 'true',//支持vue解析optionsApi
_VUE_PROD_DEVTOOLS_: 'false',//禁用Vue调试工具
_VUE_PROD_HYDRATION_MISMATCH_DETAILS_: 'false'//禁用生产环境显示"水合"信息
}),
//构造最终渲染的页面模版
...HtmlWebpackPluginList,
],
- 输出文件
webpack 将处理后的各个模块整合打包成一个或多个 bundle 文件,并输出到 output 指定的目录。 在这个阶段,webpack准备输出 bundle 文件时,会根据 optimization.splitChunks 的配置,把大的 bundle 拆分成多个更小的 bundle,以提高性能(比如更好地缓存、并行加载等) optimization通过合理分割 JS 文件,让浏览器更好地缓存不经常变化的代码,提升页面加载性能。
- vendor: 第三方库(如 Vue、Lodash 等) → 改动少
- common: 业务中多个页面或组件共享的代码 → 偶尔改动
- entry.{page}: 每个页面的私有逻辑代码 → 经常改动
js
//配置打包输出优化(配置代码分割,模块合并,缓存,TreeShaking,压缩等优化策略)
optimization: {
/**
* 把js文件打包成3种类型
* 1.vendor:第三方lib库,基本不会改动,除非依赖版本升级
* 2.common:业务组件代码的公共部分抽取出来,改动较少
* 3.entry.{page}:不同页面entry里的业务组件代码的差异部分,会经常改动
* 目的:把改动和引用频率不一样的js区分出来,以达到更好利用浏览器缓存的效果
*/
splitChunks: {
chunks: 'all',//对同步和异步模块进行分割
maxAsyncRequests: 10,//每次异步加载的最大并行请求数
maxInitialRequests: 10,//入口点的最大并行请求数
cacheGroups: {
vendor: {//第三方依赖库
test: /[\\/]node_modules[\\/]/,//打包node_module中的文件
name: 'vendor',//模块名称
priority: 20,//优先级,数字越大,优先级越高
enforce: true,//强制执行
reuseExistingChunk: true,//复用已有的公共chunk
},
common: {//公共模块
name: 'common',//模块名称
minChunks: 2,//被两处引用即被归为公共模块
minSize: 1,//最小分割文件大小
enforce: true,//强制提取,不受 request 限制
priority: 10,//优先级
reuseExistingChunk: true,//复用已有的公共chunk
}
}
},
//将webpack运行时生成的代码打包到runtime.js
runtimeChunk: true
}
为什么要用 splitChunks?
- 所有代码打在一个文件中,文件太大,首屏加载慢
- 公共依赖每次都重新加载,缓存命中率低
- 多个页面有重复代码,浪费流量
小结一句话: splitChunks 是 webpack 在 生成输出资源(bundle)阶段的优化策略,它会根据模块的使用频率和来源,自动将代码进行拆分,从而提升性能。
这里有个问题,如果我有20个公共模块common,本来应该打包20个不同的文件,但是我设置了maxInitialRequests为10,这样webpack会怎么办?
Webpack 构建时: 它会遍历你的所有依赖和模块引用关系,然后:
- 一开始,发现可以提取出 20 个 common 模块
- 接着发现某个页面需要加载其中 15 个,而你配置了 maxInitialRequests: 10
- 它就会在打包阶段决定:→ "这 15 个太多了,咱们合成几个大的 chunk 出来,最终不超过 10 个 initial chunk"
总结
- cacheGroups 控制拆分,决定了最终打包的 chunk(比如将 vendor.js 和 common.js 提取出来)。
- maxInitialRequests 控制"请求数",如果拆分结果导致初始请求数超标,Webpack 会尝试合并部分 chunk。
所以,它们是相互协作的,并不是 maxInitialRequests 覆盖了 cacheGroups,而是 maxInitialRequests 会影响 cacheGroups 拆分之后的结果。 如果你的拆分策略不想受到maxInitialRequests的影响,也可以设置enforce为true,这样可以强制提取,不受 request 限制。
不同环境下的webpack配置需要做什么优化
开发环境
开发环境追求快速构建 + 好调试,一般要用到Source Map和热更新 首先在开发环境,如果每次修改一点代码就要重新打包,重新启动项目,这样会大大降低我们的开发效率 所以webpack引入了HMR(Hot Module Replacement,热模块替换)的概念 简单说: 热更新就是修改代码后,浏览器自动替换更新的模块,不刷新页面、不丢失状态。不需要重新手动打包运行项目 使用webpack的热更新也很简单,启动 Webpack Dev Server,并开启 hot: true。
热更新的原理:
js
// 本地开发启动devServer
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-Request-With,content-type,Authorization'
},
stats: {
colors: true
}
}));
//引用hotMiddleware 中间件(实现热更新通讯)
app.use(hotMiddleware(compiler, {
path: `/${DEV_SERVER_CONFIG.HMR_PATH}`,
log: () => { }
}));
consoler.info('请等待webpack初次构建完成提示......')
//启动devServer
const port = DEV_SERVER_CONFIG.PORT;
app.listen(port, () => {
console.log(`app listening on port ${port}`)
});
上述代码用Express 模拟了一个 webpack-dev-server 的功能,通过webpack-dev-middleware监控文件改动,当文件发生更改时,自动重新打包 +,更新内存中的输出文件,然后通过webpack-hot-middleware监听模块更新事件,再通过 socket 通知客户端页面更新组件。整个流程如下:
js
你修改了某个 JS/Vue/CSS 文件
↓
webpack-dev-middleware 自动检测变更
↓
Webpack 重新编译打包(在内存中)
↓
hot-middleware 检测编译完成 & 通知客户端热更新
↓
浏览器自动替换变更的模块,不刷新页面(HMR)
文件监听实现机制
核心在这段代码中:
js
const compiler = webpack(webpackConfig);
app.use(devMiddleware(compiler, {
// ...
}));
webpack-dev-middleware调用 Webpack 的 compiler.watch() 方法, 自动监听你项目里 entry 相关的文件, 当文件发生更改时:自动重新打包 + 更新内存中的输出文件。
热更新实现机制
核心代码:
js
app.use(hotMiddleware(compiler, {
path: `/${DEV_SERVER_CONFIG.HMR_PATH}`,
log: () => {}
}));
webpack-hot-middleware通过compiler监听 Webpack 的模块更新事件,通过 socket 通知客户端页面更新组件,从内存中请求新的代码。
source-map
在开发环境为了方便调试,我们需要使用source-map,它可以从打包压缩后的代码快速定位到源代码,方便开发调试。
常见的本地开发用cheap-module-eval-source-map,它优点是快 + 可定位源代码,调试舒服,还有source-map,这个是最完整的映射,但是构建速度慢一点。 还有hidden-source-map和nosources-source-map,这两个常用在生产环境,因为用户看不到源码,但是也能定位错误。
生产环境
生产环境追求体积小 + 性能好 + 安全性,一般要进行压缩代码,优化打包速度 生产环境要落地我们打包后的代码,所以主要要进行代码压缩,优化打包速度
首先为了优化打包速度,我们可以使用HappyPack 为什么叫 HappyPack? 因为它让构建过程"更快乐": Webpack 默认是单线程:一个文件一个文件顺序处理 HappyPack 会创建 worker 线程池,让多个 loader 并行工作,提升效率
除了HappyPack,也可以使用
- 1.thread-loader,Webpack 官方支持,可以多线程并行处理 loader
- 2.esbuild-loader号称构建速度最快的,go写的构建器
- 3.swc-loader,构建速度也很快,Rust 写的构建器
除了优化打包速度,我们也可以使用各种插件对我们的代码进行压缩 开启代码压缩(通常由 mode: 'production' 自动开启),Webpack 默认使用 TerserWebpackPlugin 来压缩 JS
js
optimization: {
//使用TerserPlugin的并发和缓存,提升压缩阶段的性能
//清除console.log
minimize: true,
minimizer: [
new TerserWebpackPlugin({
cache: true,//启用缓存来加速构建过程
parallel: true,//利用多核CPU的优势来加快压缩速度
terserOptions: {
compress: {
drop_console: true,//去掉console.log内容
}
}
})
]
}
CSS 压缩 + 抽离为单独文件,通常使用MiniCssExtractPlugin和CssMinimizerPlugin
js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
plugins: [
//提取CSS的公共部分,有效利用缓存
new MiniCssExtractPlugin({
chunkFilename: 'css/[name]_[contenthash:8].bundle.css',
}),
//优化并压缩css资源
new CSSMinimizerPlugin(),
],
};
小结
"前端工程化"就是将我们写的各种各样的代码最终编译打包为浏览器可执行的代码的过程,将这个过程通过各种工具和规范变得流程化,这样可以提升开发效率,增加前端项目的可维护性。
注:本文灵感来自抖音"哲玄前端"《大前端全栈实践》