基于 webpack5 对工程化的理解
一、什么是前端工程化
前端工程化,是指将传统手动式的前端开发,转变为标准化、自动化、模块化、可维护、可扩展的,现代主流工程模式。它不再局限于编写页面,而是通过工具链、规范体系、构建流程、协作机制,将前端开发从编码 → 校验 → 构建 → 测试 → 部署 → 监控,形成完整自动化的流程
二、前端工程化的意义
传统开发模式 与 现代开发模式 对比:
| 开发流程 | 传统开发模式 | 现代开发模式 |
|---|---|---|
| 前期准备 | 无需安装额外工具,直接手写HTML、CSS、JS,无需配置,打开浏览器即可编写 | 需安装构建工具(Webpack/Vite)、代码校验工具,配置环境变量、依赖管理等 |
| 开发过程 | 无模块化,代码直接书写,公共部分复制粘贴;无热更新,修改后需手动刷新浏览器 | 模块化开发,组件可复用;支持热更新(HMR),修改代码实时生效,无需手动刷新 |
| 代码规范 | 无统一规范,代码风格、缩进、命名全凭个人习惯,多人协作易混乱 | 通过ESLint、Prettier、husky等工具,统一代码风格、命名规范、缩进格式,强制标准化 |
| 依赖管理 | 手动下载第三方库,手动引入,无版本管理,更新需替换文件 | 用npm/pnpm管理依赖,一键安装、更新、降级,自动处理依赖关系 |
| 上线优化 | 人工合并代码、压缩图片,无系统优化,全靠手动操作,易出错 | 构建工具自动完成代码压缩、分包、缓存优化,无需人工干预,性能更有保障 |
| 部署 | 纯人工部署,具体步骤: 1. 人工完成代码、图片压缩优化; 2. 下载FTP工具(如FlashFXP); 3. 输入服务器IP、账号、密码连接服务器; 4. 手动选中本地所有前端文件(HTML、CSS、JS、图片等); 5. 拖拽上传至服务器指定目录; 6. 上传完成后,手动在浏览器访问服务器地址,校验部署是否成功 | 自动化部署,依据CI/CD工作流程,步骤: 1. 代码提交至Git仓库; 2. 触发自动构建、自动测试; 3. 测试通过后,自动将构建后的dist文件部署至目标服务器; 4. 部署完成后自动校验,全程无需人工干预,可追溯部署记录 |
| 维护 | 只要涉及代码改动,都需要重新手动打包,重新走 123456 部署流程 | 上传代码后,靠构建工具,实现自动化部署,依据CI/CD工作流程 |
根据表格开发流程的对比,可想而知前端工程化的重要性,使用前端工程化开发流程的好处就是,提升了开发的效率,保证了代码的质量(统一性),上线流程的优化,降低维护成本等。
三、根据 webpack5 理解工程化
实现工程化建设的工具有很多,例如:Webpack、 Vite、Rollup、Parcel、esbuild 等。在这里,选择了 webpack 这个构建工具主要不并不是为了学习如何使用这个工具,而是 webpack 的功能全面 且 生态强大,可以更好地、更方便地让我们了解和学习 工程化的思想 与 搭建流程。
打包构建的工具,一切都是围绕三个方向去实现:解析编译模块、模块分包、压缩优化,想要达到这些效果就需要做不同配置去实现
基础配置
一个基础的 webpack 配置会有哪些 ?包括但不限于以下几点:
1、模式 mode
开发模式(development) 和 生产模式(production),但是一般不会在基础配置文件 webpack.base.js 中去指定具体的模式,而是分别在 webpack.dev.js 和 webpack.prod.js 中定义好,在不同的环境下使用时再合并进 webpack.base.js
这里只举一个例子:webpack.dev.js
// 开发环境 webpack 配置
const merge = require('webpack-merge')
const webpackConfig = merge.smart(baseConfig, {
// 指定开发环境模式
mode: 'development'
})
2、入口 entry
入口起点(entry point) 指示 webpack 应该使用哪个模块,来作为构建其内部的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
module.exports = {
// 入口配置:
entry: './index.js' (例子),
}
由于此次学习的项目中涉及到多页面:
- 找出所有的入口文件
- 利用 glob 遍历所有入口文件,提取文件名作为 入口名称
const glob = require('glob')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 获取所有 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'),
// 要注入的代码块(对应 entry 配置中的 key)
chunks: [entryName]
})
)
})
module.exports = {
// 入口配置:
entry: pageEntries,
}
3、模块解析 module
用于解析不同模块下的依赖,把它转成浏览器可识别的语言,例如:
- vue --> vue-loader
- less --> less-loader
- css --> css-loader
- js --> bebal-loader
- png --> file-loader
module.exports = {
// 模块解析配置(决定了要加载解析哪些模块,以及用什么方式去解析)
module: {
rules: [
{
test: /\.vue$/,
use: {
loader: 'vue-loader'
}
},
{
test: /\.js$/,
include: [
// 只对业务代码进行bebel,加快 webpack 打包速度
path.resolve(process.cwd(), './app/pages')
],
use: {
loader: 'babel-loader'
}
},
{
test: /\.(png|jpe?g|gif|svg)(\?.+)?$/,
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'
}
]
},
4、产物输出 output
output 是用来配置在不同环境下,产物输出的名称以及文件存放的路径,但是不同的环境下,所需要的内容又是不一样的,这一点需要我们注意!
开发环境 webpack.dev.js
const webpackConfig = merge.smart(baseConfig, {
// 开发环境 output 配置
output: {
filename: 'js/[name]_[chunkhash:8].bundle.js',
path: path.resolve(process.cwd(), './app/public/dist/dev/'), // 输出文件存储路径
publicPath: `http://${HOST}:${PORT}/public/dist/dev/`, // 外部资源公共路径
globalObject: 'this' // 全局对象,用于在浏览器中访问 webpack 打包后的代码
}
})
生产环境 webpack.prod.js
module.exports = {
output: {
// 输出文件名;[name] 文件名称,[chunkhash:8] 哈希值8位
filename: 'js/[name]_[chunkhash:8].bundle.js',
// 产物输出目录
path: path.join(process.cwd(), './app/public/dist/prod'),
// 静态资源路径
publicPath: '/dist/prod',
// 让跨域资源获得正确的跨域权限
crossOriginLoading: 'anonymous'
}
}
filename
其中 filename 是指定输出文件名的格式,一般利用哈希值8位来区分文件命名,有利于避免缓存问题,代码没有更新的情况。
path 和 publicPath
在 开发环境 中,path 是构建编译出开发环境产物输出目录,publicPath 是外部资源公共路径,便于 webpack-dev-middleware 中间件使用该路径作为静态资源路径,监听文件的改动,配合 webpack-hot-middleware 中间件 实现 HRM 热更新
在 生产环境 中,path 和 publicPath 仅是为了构建编译出生产环境产物输出目录,静态资源存放路径。
5、插件 plugins
对于我自己的理解,插件的作用是为了更好地去做一些扩展功能的配置,解决一些 module无法处理的工作,以及加快打包速度等。
基础的插件配置
涉及到对一些模块功能的解析,例如:一些特定的文件、第三方库等
下面的例子是对vue做一些相应的处理
module.exports = {
// 配置 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
],
}
开发环境
一般看需求而定,但是如果要实现HMR的话,就需要做一下配置:
// 开发环境 webpack 配置
const webpackConfig = merge.smart(baseConfig, {
// 开发阶段插件
plugins: [
// HotModuleReplacementPlugin 用于实现模块热替换(Hot Module Replacement 简称 HMR)
// 模块热替换允许在应用程序运行时替换
// 极大的提升开发效率,应为能在不刷新页面的情况下更新模块
new webpack.HotModuleReplacementPlugin({
multiStep: false // 是否启用 multiStep 模式,默认为 false,启用后会在编译完成后等待一段时间再进行下一轮编译,适用于大型项目
})
]
})
生产环境
需要确保每次打包后的文件都是新的,对一些功能模块进行额外的处理,例如提取css,加快代码构建速度,进行多线程打包等。因为插件都是返回一个方法,所以使用插件的时候,都需要 New 一下
- clean-webpack-plugin:每次 build 前清理 public/dist 目录
- mini-css-extract-plugin :提取 css 到单独的文件
- happypack:webpack4 多线程打包插件
- thread-loader:webpack5 多线程打包
6、打包优化 optimization
optimization 是用来配置一些打包优化的配置。通常需要对代码进行分块打包,这中间涉及一些第三方库的依赖(node_modules),公共模块等。并且,打生产包的时候,往往还需要对代码进行一个压缩(js、css),清除一些console.log 等内容:
optimization: {
----------------------------- 基本构建所需 -----------------------------
/**
* 把 js 文件 打包成 3种 类型
* 1. vendor: 第三方 lib 库, 基本不会改动,除非依赖版本升级
* 2. common: 业务组件代码的公共部分抽取出来,改动较少
* 3. entry.{page}: 不同页面 entry 里的业务组件代码的差异部分,会经常改动
* 目的:把改动和引用频率不一样的js 区分出来,以达到更好利用浏览器缓存的效果
*/
splitChunks: {
chunks: 'all', // 对不同和异步模块都进行分割
maxAsyncRequests: 10, // 每次异步加载的最大并行请求数
maxInitialRequests: 10, // 入口点的最大并行请求数
cacheGroups: {
// 第三方依赖库
vender: {
test: /[\\/]node_modules[\\/]/, // 打包 node_modules 中的文件
name: 'vendor', // 模块名称
priority: 20, // 优先级,数字越大,优先级越高
enforce: true, // 强制执行
reuseExistingChunk: true // 重用已存在的 chunk,避免重复打包
},
// 公共模块
common: {
name: 'common', // 模块名称
minChunks: 2, // 被两处引用即被归为公共(最小引用次数,超过次数才会被打包)
minSize: 1, // 最小分割文件大小 (1 byte)
priority: 10, // 优先级
reuseExistingChunk: true // 重用已存在的 chunk,避免重复打包
}
}
},
// 将 webpack运行时生成的代码打包到 runtime.js
runtimeChunk: true,
------------------------------ 生产所需 -------------------------------
// 开启压缩
minimize: true,
minimizer: [
// 压缩 js 资源,使用 TerserWebpackPlugin 的并发和缓存,提升压缩阶段的性能
new TerserWebpackPlugin({
parallel: true, // 启用多进程 利用 cpu 的优势来加快压缩速度
cache: true, // 启用缓存,提升压缩速度
terserOptions: {
compress: {
drop_console: true, // 清除 console.log
drop_debugger: true // 清除 debugger 语句
}
}
}),
// 优化并压缩 css 资源
new cssMinimizerPlugin({
parallel: true
})
]
}
7、优化类的模块解析命名 resolve
用来配置模块解析的具体行为,方便 webpack 在打包时,如何找到并解析具体模块的路径
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')
}
},
理解思路:通过定义好 入口 entry 和 出口 output,使用 module 对模块依赖进行解析,经过配置一系列辅助构建打包的插件 plugins 和 配置打包输出优化的optimization,从提高整体打包构建速度和质量
四、实现开发环境 HMR
此次学习,一个关键的点是学会了,如何在本地开发环境,构建HMR,实现代码修改,自动更新代码依赖,页面不需要刷新即可看见最新效果。
利用 Express 搭建本地开发 devServer,搭配 webpack-dev-middleware 和 webpack-hot-middleware 中间件 实现热更新HMR
webpack-dev-middleware(编译 + 静态服务)
- 实时编译:监听文件变化,自动重新编译 Webpack 代码
- 内存存储:把编译后的文件存在内存里,不写入硬盘(速度极快)
- 静态服务:把内存中的编译结果,作为静态资源暴露给浏览器访问
- 基础依赖:是热更新中间件的前置依赖,没有它就无法运行热更新
只负责**编译 + 提供文件 **,不负责热更新。
webpack-hot-middleware(热模块替换 HMR)
- 热更新通信:和浏览器建立 WebSocket 长连接,通知文件变化
- 热模块替换:不刷新整个页面,只替换修改的代码模块
- 状态保留:修改代码后,页面表单输入、组件状态不会丢失
负责 「热更新」 ,让你改代码后页面无需刷新
必须为客户端添加 hmr 热更新入口
Object.keys(baseConfig.entry).forEach(v => {
// 第三方包不作为 hmr 入口
if (v !== 'vendor') {
baseConfig.entry[v] = [
// 主入口文件
baseConfig.entry[v],
// hmr 更新入口,官方指定的 hmr 路径
`webpack-hot-middleware/client?path=http://${HOST}:${PORT}/${HMR_PATH}&timeout=${TIMEOUT}&reload=true`
]
}
})
同时,还有热更新插件
// 开发阶段插件
plugins: [
// HotModuleReplacementPlugin 用于实现模块热替换(Hot Module Replacement 简称 HMR)
// 模块热替换允许在应用程序运行时替换
// 极大的提升开发效率,应为能在不刷新页面的情况下更新模块
new webpack.HotModuleReplacementPlugin({
multiStep: false // 是否启用 multiStep 模式,默认为 false,启用后会在编译完成后等待一段时间再进行下一轮编译,适用于大型项目
})
]
五、总结
通过学习 webpack 不同的配置,加深了对项目工程化的理解。此次学习的重点,不仅仅是为了学习一个工具的配置和使用,而是在于思考如何进行对打包进行打包 和 构建打包时所需要的东西,同时对需要打包的(js、css)内容做对应的处理,在这个基础上,再去想办法如何优化打包的效率。值得注意的是,需要对不同的环境做不同的配置,实现不同的环境干不同的事!