简介:这是一份基于 webpack5 搭建 Vue3 开发与生产环境的实战示例工程,面向希望熟悉 webpack5 新特性、掌握前端构建优化的初中级开发者。资源以 JavaScript 为主,配合 Vue 单文件组件与 SCSS 样式,完整演示了从开发调试到生产打包的配置流程。压缩包共 23 个文件,包含 11 个 js 脚本、3 个 scss 样式、2 个 vue 组件、2 个 json 配置以及 html、babelrc 等辅助文件,整体约 160KB,体积轻量便于快速上手。工程重点覆盖 webpack5 的持久缓存、长期缓存算法、Tree Shaking 与 CommonJS 摇树优化、ES5/ES6 代码生成等改动,并借助 Babel 预设处理新语法转换。目录中区分了开发与生产两套配置,配合路由、状态管理与工具模块,读者可据此理解构建链路、对比不同环境差异并迁移到自己的项目。目前已有 864 人学习下载,适合作为构建工具进阶的参考范例。
1. 从零搭一套 Vue3 + Webpack5 + Element Plus 的工程模板,到底值不值得抄
如果你最近在接手一个需要长期维护的中后台项目,大概率会遇到一个纠结:用 Vite 起手快,但团队里有人坚持 Webpack 生态更稳;用 Element Plus 又不想只靠按需引入那点优化,还想把构建链路完全握在自己手里。这套vue3-webpack5-element-plus的组合,就是冲着这个场景来的——它不是一个能跑起来的玩具 Demo,而是一份把 Vue3 的响应式、Webpack5 的模块联邦与持久化缓存、Element Plus 的组件体系串成一条完整链路的工程模板。
它适合谁?适合那些不想被脚手架黑盒绑架、需要自己控制 loader 顺序和 splitChunks 策略的团队;也适合刚学完 Vue3 基础、想看看一个真实项目从package.json到webpack.config.js到底该怎么写的人。我拆过不少类似的包,很多模板只给你一个能跑的壳,但这份资源在配置分层、环境变量注入和样式处理上留了足够的扩展点。下面我就按自己复现的顺序,把关键配置和容易翻车的地方一条条拆开。
2. 拆开 package.json 与目录结构:先看清依赖边界再动手
拿到一个工程模板,我习惯先看package.json和目录树,而不是急着npm install。因为依赖版本之间的约束关系,往往决定了你后面要不要花半天去解决 peer dependency 冲突。这份模板把运行时依赖和构建时依赖分得很清楚,Vue3 和 Element Plus 在dependencies,Webpack 全家桶和各类 loader 在devDependencies,这个边界不能乱。
2.1 核心依赖版本与职责划分
先看一份我整理出来的关键依赖表,版本号以你实际拿到的资源为准,这里只说明每个包在链路里干什么:
| 包名 | 所在位置 | 在工程里的职责 |
|---|---|---|
| vue | dependencies | 运行时框架,提供createApp和响应式 API |
| element-plus | dependencies | UI 组件库,含样式与图标 |
| webpack | devDependencies | 核心构建器,负责模块打包 |
| webpack-cli | devDependencies | 命令行入口,执行 build 与 serve |
| webpack-dev-server | devDependencies | 本地开发服务器与 HMR |
| vue-loader | devDependencies | 解析.vue单文件组件 |
| @vue/compiler-sfc | devDependencies | 编译 SFC 中的 template 与 script |
| css-loader / style-loader | devDependencies | 处理 CSS 模块与注入 DOM |
| sass / sass-loader | devDependencies | 编译 Element Plus 的 scss 主题 |
| html-webpack-plugin | devDependencies | 生成挂载用的 index.html |
| mini-css-extract-plugin | devDependencies | 生产环境抽离 CSS 文件 |
这里有个容易忽略的点:vue-loader在 Vue3 下必须配合@vue/compiler-sfc,而且两者的主版本要对应。如果你拿到模板后手动升级了 vue,却没同步升vue-loader,编译时就会报Cannot find module 'vue/compiler-sfc'这类错。我一般会在package.json里把vue和vue-loader的版本范围写死到小版本,避免 CI 上因为^自动拉新导致构建失败。
2.2 目录结构里藏着的扩展点
一个健康的模板不会把所有配置塞进一个文件。常见的做法是拆成config/webpack.base.js、config/webpack.dev.js、config/webpack.prod.js,再用webpack-merge合并。目录大致长这样:
project-root/ ├── config/ │ ├── webpack.base.js # 公共配置:entry、resolve、module.rules │ ├── webpack.dev.js # 开发配置:devServer、HMR │ └── webpack.prod.js # 生产配置:压缩、抽离、缓存 ├── src/ │ ├── main.js # 应用入口,挂载 Vue 实例 │ ├── App.vue # 根组件 │ ├── router/ # 路由配置 │ ├── store/ # 状态管理 │ └── views/ # 页面级组件 ├── public/ │ └── index.html # html-webpack-plugin 模板 ├── .env.development # 开发环境变量 ├── .env.production # 生产环境变量 └── package.json为什么要拆config目录?因为开发环境需要devServer和热更新,生产环境需要MiniCssExtractPlugin和TerserPlugin,两者混在一起会让配置对象越来越臃肿。我见过有人把devServer写进 base 配置,结果生产构建时也去起服务,CI 直接卡死。拆开之后,webpack.base.js只保留两边都需要的规则,比如resolve.extensions和module.rules里的 vue、js、css 处理。
提示:如果你拿到的模板没有拆分配置,建议先手动拆一遍再改业务代码,不然后面加一个 loader 就要在几百行里找位置。
3. 配置 Webpack5 处理 Vue3 与 Element Plus:loader 顺序和样式隔离
这一章是整份模板的核心。Webpack5 相比 4 最大的变化是内置了持久化缓存和模块联邦,但处理 Vue3 SFC 和 Element Plus 样式的逻辑没变,依然是 loader 链的排列组合。顺序错了,要么样式不生效,要么编译报错。
3.1 vue-loader 与 compiler-sfc 的接入
在webpack.base.js里,处理.vue文件的规则通常这样写:
// config/webpack.base.js const { VueLoaderPlugin } = require('vue-loader'); module.exports = { module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', options: { // 开启编译时的模板优化,生产环境可关掉调试信息 compilerOptions: { whitespace: 'condense' } } }, { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'], cacheDirectory: true // 开启 babel 缓存,二次构建提速明显 } } } ] }, plugins: [ new VueLoaderPlugin() // 必须注册,否则 .vue 文件里的 template 无法编译 ], resolve: { extensions: ['.js', '.vue', '.json'], alias: { '@': path.resolve(__dirname, '../src') } } };逻辑说明:vue-loader负责把.vue文件拆成 template、script、style 三块,再分别交给对应的 loader。VueLoaderPlugin的作用是克隆你配置里的其他规则,让 SFC 里的<script>和<style>也能被 babel-loader、css-loader 处理。如果你忘了注册这个插件,最常见的报错是vue-loader was used without the corresponding plugin。
参数说明:compilerOptions.whitespace设为condense可以去掉模板里多余的空格文本节点,减小产物体积。cacheDirectory: true让 babel 把转译结果缓存到node_modules/.cache,第二次构建能快 30% 以上,但要注意 CI 环境如果每次都是干净目录,这个缓存反而增加写入开销,可以按需关掉。
3.2 Element Plus 按需引入与样式处理
Element Plus 支持完整引入和按需引入。完整引入简单,但会把所有组件和样式打进包;按需引入需要配合unplugin-vue-components和unplugin-auto-import,在 Webpack 下也能用。我一般推荐按需引入,因为中后台项目动辄几十个页面,完整引入的 CSS 体积很容易超过 500KB。
按需引入的配置分两步。第一步在webpack.base.js里加插件:
// config/webpack.base.js const AutoImport = require('unplugin-auto-import/webpack'); const Components = require('unplugin-vue-components/webpack'); const { ElementPlusResolver } = require('unplugin-vue-components/resolvers'); module.exports = { plugins: [ AutoImport({ resolvers: [ElementPlusResolver()], imports: ['vue', 'vue-router'], // 自动导入 vue 和 router 的 API dts: 'src/auto-imports.d.ts' // 生成类型声明,TS 项目需要 }), Components({ resolvers: [ElementPlusResolver()], dts: 'src/components.d.ts' }) ] };第二步,在main.js里不再全量app.use(ElementPlus),而是让插件自动按需注册。如果你需要定制主题色,还要在webpack.base.js的 scss 规则里注入变量:
{ test: /\.scss$/, use: [ 'style-loader', 'css-loader', { loader: 'sass-loader', options: { additionalData: `@use "@/styles/element-variables.scss" as *;` } } ] }逻辑说明:AutoImport负责自动导入ref、reactive这类 API,省去手写 import;Components负责在模板里遇到<el-button>时自动引入对应组件和样式。additionalData让每个 scss 文件编译前都先加载你的变量文件,这样 Element Plus 的主题色覆盖才能生效。
参数说明:dts路径要和你项目的 TS 配置对应,JS 项目可以去掉这行。additionalData里用@use而不是@import,是因为 sass 新版已经废弃@import,继续用会有弃用警告。这里有个坑:如果你同时用了style-loader和MiniCssExtractPlugin.loader,开发环境用前者,生产环境用后者,不能同时出现,否则样式会重复注入。
3.3 开发服务器与热更新配置
开发环境的webpack.dev.js主要配devServer:
// config/webpack.dev.js const { merge } = require('webpack-merge'); const base = require('./webpack.base'); module.exports = merge(base, { mode: 'development', devtool: 'eval-cheap-module-source-map', devServer: { port: 8080, hot: true, open: true, historyApiFallback: true, // 支持前端路由刷新不 404 client: { overlay: true // 编译错误时在浏览器上覆盖提示 } } });逻辑说明:hot: true开启模块热替换,改组件时只更新变化的模块,不刷新整页。historyApiFallback对 Vue Router 的 history 模式是必须的,否则刷新/user/list会返回 404。devtool选eval-cheap-module-source-map是开发环境速度和可读性的平衡点,生产环境要换成source-map或直接关掉。
参数说明:overlay建议开着,编译错误直接盖在页面上,比翻控制台快。port如果被占用,devServer 会自动换端口,但如果你在 CI 里依赖固定端口,就要加strictPort: true让它直接报错而不是静默换端口。
4. 构建产物优化与 splitChunks:别让首屏加载超过三秒
模板能跑起来只是第一步,真正拉开差距的是生产构建的产物质量。Webpack5 的splitChunks默认配置已经比 4 聪明很多,但中后台项目里 Element Plus 和业务代码混在一起,还是需要手动干预。
4.1 缓存策略与持久化缓存
Webpack5 内置了cache配置,开启文件系统缓存后,二次构建能快很多:
// config/webpack.prod.js module.exports = { cache: { type: 'filesystem', buildDependencies: { config: [__filename] // 配置文件变化时缓存失效 }, name: 'prod-cache' } };逻辑说明:type: 'filesystem'把模块编译结果写到磁盘,下次构建直接复用。buildDependencies指定配置文件本身作为依赖,改了 webpack 配置就自动清缓存,避免用了旧缓存导致配置不生效。name区分不同环境的缓存目录,开发和生产不要共用。
参数说明:缓存目录默认在node_modules/.cache/webpack,如果 CI 环境每次都是全新容器,这个缓存带不来收益,反而增加磁盘写入,可以设cache: false。本地开发强烈建议开着。
4.2 splitChunks 拆分 vendor 与业务代码
默认的splitChunks会把node_modules里的包拆成vendors,但 Element Plus 体积大,最好单独拆出来:
// config/webpack.prod.js module.exports = { optimization: { splitChunks: { chunks: 'all', cacheGroups: { elementPlus: { test: /[\\/]node_modules[\\/]element-plus[\\/]/, name: 'element-plus', priority: 20 // 优先级高于默认 vendors }, vendors: { test: /[\\/]node_modules[\\/]/, name: 'vendors', priority: 10 } } }, runtimeChunk: 'single' // 把 runtime 单独抽出来,避免业务 chunk 哈希频繁变化 } };逻辑说明:priority决定匹配顺序,Element Plus 的规则优先级更高,所以它会被单独打包,不会混进vendors。runtimeChunk: 'single'把 webpack 的运行时代码抽成一个独立文件,这样业务代码变更时,vendor 和 runtime 的哈希不变,浏览器缓存能持续命中。
参数说明:chunks: 'all'表示对同步和异步 chunk 都生效。如果你的项目有大量异步路由,可以再加一个async缓存组,把异步公共模块单独拆出来。注意name不要用中文或特殊字符,否则某些 CDN 会处理异常。
4.3 生产环境 CSS 抽离与压缩
生产环境要把 CSS 从 JS 里抽出来,避免闪屏:
// config/webpack.prod.js const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); module.exports = { module: { rules: [ { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'] }, { test: /\.scss$/, use: [ MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader' ] } ] }, plugins: [ new MiniCssExtractPlugin({ filename: 'css/[name].[contenthash:8].css' }) ], optimization: { minimizer: [ '...', // 保留默认的 JS 压缩 new CssMinimizerPlugin() ] } };逻辑说明:MiniCssExtractPlugin.loader替换掉style-loader,把 CSS 抽成独立文件。contenthash根据内容生成哈希,内容不变哈希不变,缓存友好。minimizer里的'...'是 webpack5 的语法,表示保留默认压缩器,再追加 CSS 压缩。
参数说明:filename里的[contenthash:8]取 8 位哈希,够用且文件名短。如果你用了 CDN,还要配publicPath,否则 CSS 里的图片路径会指向本地。
5. 避坑与排查:这五个问题我几乎每次搭环境都会遇到
5.1 现象:页面白屏,控制台报Failed to resolve component: el-button
原因:Element Plus 按需引入插件没生效,或者Components插件的resolvers配置写错。常见的是把ElementPlusResolver写成了ElementPlusResolver()但没传参,或者dts路径指向了不存在的目录导致插件静默失败。
解决:先检查webpack.base.js里Components插件是否注册,再确认resolvers数组里是ElementPlusResolver()的调用结果而不是函数本身。如果还不行,在main.js里临时全量引入ElementPlus验证是不是按需引入的问题,定位后再切回按需。
5.2 现象:修改 scss 变量后主题色没变化
原因:sass-loader的additionalData注入的变量文件里,变量定义顺序在 Element Plus 默认变量之后,被覆盖了。或者你用的是@import而不是@use,导致变量作用域不对。
解决:确保变量文件里用@use 'element-plus/theme-chalk/src/common/var.scss' with (...)的方式覆盖,而不是直接写$--color-primary: xxx。另外检查additionalData的路径别名是否被 sass 解析,必要时用相对路径。
5.3 现象:开发环境热更新失效,改代码后页面整页刷新
原因:devServer.hot开了,但vue-loader没有正确传递 HMR 信息,或者target配置成了node导致 HMR 客户端无法连接。
解决:确认webpack.dev.js里没有覆盖target,默认是web。再检查devServer.client.webSocketURL是否被错误配置。如果用了代理,确保 HMR 的 websocket 路径没被代理拦截。
5.4 现象:生产构建后图片 404,路径变成[object Module]
原因:Webpack5 里file-loader和url-loader已经被资源模块替代,如果模板里还在用旧 loader,或者type: 'asset'配置和esModule冲突,就会出这个问题。
解决:把图片规则改成type: 'asset',并设置parser.dataUrlCondition.maxSize。如果必须用file-loader,要加esModule: false。更推荐直接用 Webpack5 的资源模块:
{ test: /\.(png|jpe?g|gif|svg)$/i, type: 'asset', parser: { dataUrlCondition: { maxSize: 8 * 1024 // 小于 8KB 转 base64 } }, generator: { filename: 'images/[name].[hash:8][ext]' } }5.5 现象:npm run build内存溢出,报JavaScript heap out of memory
原因:项目大了之后,Webpack 构建时 Node 默认内存不够,尤其是开了 source-map 和持久化缓存。
解决:在package.json的 scripts 里加NODE_OPTIONS=--max_old_space_size=4096,或者用cross-env兼容 Windows。同时检查devtool生产环境是不是还开着eval类配置,生产环境应该用false或source-map,不要用eval。
6. 进阶:用模块联邦做微前端拆分,以及我验证配置的习惯
当项目从单体走向微前端时,Webpack5 的 Module Federation 是个绕不开的能力。这份模板虽然没直接配联邦,但它的分层配置结构让扩展变得容易。我一般会在webpack.prod.js里加一个ModuleFederationPlugin,把公共依赖和业务组件暴露出去:
const { ModuleFederationPlugin } = require('webpack').container; module.exports = { plugins: [ new ModuleFederationPlugin({ name: 'hostApp', filename: 'remoteEntry.js', remotes: { // 远程子应用名: '子应用名@远程地址/remoteEntry.js' remoteApp: 'remoteApp@http://localhost:8081/remoteEntry.js' }, shared: { vue: { singleton: true, requiredVersion: '^3.0.0' }, 'element-plus': { singleton: true } } }) ] };逻辑说明:name是当前应用对外暴露的标识,filename是生成的入口文件。remotes声明依赖哪些远程应用,shared里的singleton: true保证 Vue 和 Element Plus 只加载一份,避免多实例导致响应式失效。这里最常见的翻车是子应用和主应用的 Vue 版本不一致,singleton会直接报版本冲突,所以团队内要统一版本。
参数说明:requiredVersion写^3.0.0表示接受 3.x 的任何版本,但实际运行时如果主应用是 3.2、子应用是 3.4,singleton会以主应用为准,子应用可能用到不存在的 API。稳妥做法是锁死到具体小版本。
验证配置有没有生效,我有个固定习惯:构建完成后不急着部署,先在本地用npx serve dist起一个静态服务,打开 Network 面板看三个东西——首屏 JS 和 CSS 的 gzip 后体积、vendor 和业务 chunk 是否分离、有没有重复加载的 Element Plus 样式。如果 vendor 超过 1MB,就回去调splitChunks;如果 Element Plus 样式出现两份,就检查MiniCssExtractPlugin和按需引入是不是同时生效了。
从那以后我每次拿到新的工程模板,都会先跑一遍npm run build看产物报告,再跑npm run dev改一个组件确认 HMR,最后才动业务代码。这个顺序帮我省下了不少返工时间。希望帮到你。
本文还有配套的精品资源,点击获取