什么是前端工程化
前端工程化是指将软件工程的方法和原则应用到前端开发中,通过系统化、规范化、标准化的方法提升前端开发效率和质量的一套完整实践体系。 前端工程化主要包含以下几个方面:
-
模块化开发
- JavaScript模块化(ES Module, CommonJS等)
- CSS模块化(CSS Modules, CSS-in-JS等)
- 组件化开发(React/Vue/Angular组件)
-
自动化构建
- 代码编译(如Babel转译)
- 打包工具(Webpack, Rollup, Vite等)
- 任务自动化(Gulp, Grunt等)
-
规范化体系
- 代码规范(ESLint, Stylelint)
- 提交规范(Git Commit Message)
- 目录结构规范
- 文档规范
-
性能优化
- 代码分割(Code Splitting)
- 按需加载
- 缓存策略
- 资源压缩
-
质量保障
- 单元测试(Jest, Mocha)
- E2E测试(Cypress, Playwright)
- 代码覆盖率
- 类型检查(TypeScript)
-
部署与监控
- CI/CD流程
- 错误监控(Sentry)
- 性能监控
- 日志收集
这次的重点在第二部分,
const glob = require("glob");
const path = require("path");
const webpack = require("webpack");
const { VueLoaderPlugin } = require("vue-loader");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CleanWebpackPlugin = require("clean-webpack-plugin");
// 动态构造entry和HtmlWebpackPlugin
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(
new HtmlWebpackPlugin({
filename: path.resolve(
process.cwd(),
`./app/public/dist/${entryName}.tpl`
), // 输出文件路径
template: path.resolve(process.cwd(), `./app/view/entry.tpl`), // 模板文件路径
chunks: [entryName], // 引入的chunk
})
);
});
/**
* webpack 配置文件基础配置
*/
module.exports = {
// 入口配置
entry: pageEntries,
// 模块解析配置(决定了要加载解析哪些模块,以及用什么方式解析)
module: {
rules: [
// 针对vue文件的解析规则
{
test: /\.vue$/,
use: {
loader: "vue-loader",
},
},
// 针对js文件的解析规则
{
test: /\.js$/,
include: [
// 只对业务代码进行babel转换,减少编译时间,加快打包速度
path.resolve(process.cwd(), "./app/pages"),
],
use: {
loader: "babel-loader",
},
},
// 针对图片文件的解析规则
{
test: /\.(png|jpg|gif|jpeg|webp)(\?.+)?$/,
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"],
},
],
},
// 产物输出路径,开发和生产环境的输出不一致,所以各自配置
output: {},
// 配置模块解析的具体行为(定义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"), // 状态管理目录
},
},
// 配置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", // 启用选项API
__VUE_PROD_DEVTOOLS__: "false", // 禁用Vue Devtools
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: "false", // 禁用生产环境显式’水合‘信息
}),
// 显式打包进度
new webpack.ProgressPlugin(),
// 每次build前,删除dist目录
new CleanWebpackPlugin(["public/dist"], {
root: path.resolve(process.cwd(), "./app/"),
exclude: [],
verbose: true, // 开启在控制台输出信息
dry: false, // 启用删除文件
}),
//构建最终渲染的页面模板
...HtmlWebpackPluginList,
],
// 配置打包输出优化(代码分割,模块合并,缓存,Tree Shaking,压缩等优化策略)
optimization: {
splitChunks: {
chunks: "all", //对同步和异步代码都进行分割
maxAsyncRequests: 10, // 每次异步加载的最大并行请求数量
maxInitialRequests: 10, // 初始加载的最大并行请求数量
cacheGroups: {
// 第三方依赖
vendor: {
test: /[\\/]node_modules[\\/]/, // 匹配node_modules目录下的模块
name: "vendor", // 打包后的文件名
priority: 20, // 优先级,数字越大,优先级越高
enforce: true, // 强制分割,即使模块没有被引用,也会被分割
reuseExistingChunk: true, // 复用已有的公共chunk
},
// 公共组件
common: {
name: "common", // 打包后的文件名
minChunks: 2, // 模块至少被引用的次数,被引用2次以上才会被分割
minSize: 1, // 模块的最小大小,单位是字节,小于1字节的模块不会被分割
priority: 10, // 优先级,数字越大,优先级越高
reuseExistingChunk: true, // 复用已有的公共chunk
},
},
},
runtimeChunk: true, // 运行时代码单独打包
},
};
这个文件实现的功能,首先会根据路径,读取到app/pages下所有的entry.*.js文件,作为入口文件,根据对应的规则,进行打包,打包出来的结果又会构建最终渲染成最终渲染的页面
new HtmlWebpackPlugin({
filename: path.resolve(
process.cwd(),
`./app/public/dist/${entryName}.tpl`
), // 输出文件路径
template: path.resolve(process.cwd(), `./app/view/entry.tpl`), // 模板文件路径
chunks: [entryName], // 引入的chunk
})
chunks是对打包出来的结果,template对应注入的模版文件,把这两个结合起来生成最终的文件,也就是这样
<script defer="defer" src="/dist/prod/js/runtime~entry.page1_ae875b58.bundle.js" crossorigin="anonymous"></script>
<script defer="defer" src="/dist/prod/js/vendor_7eb0dbae.bundle.js" crossorigin="anonymous"></script>
<script defer="defer" src="/dist/prod/js/entry.page1_dec2ddb7.bundle.js" crossorigin="anonymous"></script>
其中还引入了代码分包,这里采用的策略是,第三方依赖打个包,公共组件打个包,业务组件打个包,对应的实现是这里
// 配置打包输出优化(代码分割,模块合并,缓存,Tree Shaking,压缩等优化策略)
optimization: {
splitChunks: {
chunks: "all", //对同步和异步代码都进行分割
maxAsyncRequests: 10, // 每次异步加载的最大并行请求数量
maxInitialRequests: 10, // 初始加载的最大并行请求数量
cacheGroups: {
// 第三方依赖
vendor: {
test: /[\\/]node_modules[\\/]/, // 匹配node_modules目录下的模块
name: "vendor", // 打包后的文件名
priority: 20, // 优先级,数字越大,优先级越高
enforce: true, // 强制分割,即使模块没有被引用,也会被分割
reuseExistingChunk: true, // 复用已有的公共chunk
},
// 公共组件
common: {
name: "common", // 打包后的文件名
minChunks: 2, // 模块至少被引用的次数,被引用2次以上才会被分割
minSize: 1, // 模块的最小大小,单位是字节,小于1字节的模块不会被分割
priority: 10, // 优先级,数字越大,优先级越高
reuseExistingChunk: true, // 复用已有的公共chunk
},
},
},
runtimeChunk: true, // 运行时代码单独打包
},
这样就完成了生产环境的打包
我们都知道我们在开发环境中,不只是需要打包,我们还需要在开发环境的时候对代码进行调试,下面就涉及到了我们在开发环境下的热更新
// 本地开发启动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 { PORT, HRM_PATH } = DEV_SERVER_CONFIG;
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,PATH,OPTIONS",
"Access-Control-Allow-Headers":
"X-Requested-With,content-type,Authorization",
},
stats: {
colors: true,
},
})
);
// 引用hotMiddleware中间件 (实现热更新通讯)
app.use(
hotMiddleware(compiler, {
path: `/${HRM_PATH}`,
log: () => {},
})
);
consoler.info("请等待webpack初次构建完成提示...");
// 启动devServer
app.listen(PORT, () => {
console.log(`app listening on port ${PORT}`);
});
开发环境下的热更新,采取的策略是,模板打包,js,css部分,使用express启动一个服务,服务监听文件的改动,改动后实时更新服务上的打包结果,页面重新获取服务上的打包结果,实现文件一动 => 重新打包 => 页面重新获取