Vue + Vite 一套代码打包多项目:工程化实践与踩坑指南

发布时间:2026/10/21 17:00:11
Vue + Vite 一套代码打包多项目:工程化实践与踩坑指南 1. 先搞清楚“一套代码打包多项目”到底解决什么问题这两年我前后手过好几个类似业务线某公司要做十几个落地页、某团队要维护一套后台同时输出几个客户品牌版本、还有电商大促期间那种几十个活动页面。一开始大家都很默契地复制粘贴建一个文件夹存一份代码改需求的时候定位到对应文件夹改一改看起来也没毛病直到需求开始交叉、UI开始统一、组件反复修同一处 Bug才发现维护成本和定时炸弹一样。“vue一套代码打包多项目”说白了就是同一个 Vue 工程通过构建参数和目录约定的变化打出多个不同名字、不同入口、不同样式甚至不同接口域的独立产物。这不是一次复制代码的行为而是让“一个仓库 一份主代码 N 份配置”成为构建事实从而在需求收敛时避免到处同步在需求分叉时又保证各有各的出口。这个方案特别适合下面几类情况内容近似但品牌标识不同的站点集群由同一套管理后台派生出多租户版本需要独立部署、独立发布的 H5 活动集Monorepo 里的多个前端子应用但共享大量基础组件和请求逻辑很多开发一听“打包多项目”就联想到微前端或者复杂的 Node 脚本实际上如果你的核心诉求就是“同一批页面代码构建时生成不同的项目桉数”那用 Vite 的轻量多入口机制配合环境变量就足够完全不需要上重型方案。至于什么时候必须升级到微前端我会在后面的章节单独讲。有一点要提前说明这套方案的难点不在“技术”而在“划分”。你得先想清楚哪些是公共底座哪些是项目差异层。划分错了后面每个配置都在打补丁代码最后会变得很难看。2. 前置准备先把目录和环境变量设计好2.1 Monorepo 式目录怎么组织最高效一套代码打多项目仓库组织我强烈建议按“公共层 项目层 构建配置”三个区域来规划。下面这个目录结构是我跑过几个项目后沉淀下来比较顺手的形态├── apps │ ├── projectA │ │ ├── index.html │ │ └── project.config.ts │ └── projectB │ ├── index.html │ └── project.config.ts ├── src │ ├── components │ ├── layouts │ ├── router │ ├── stores │ ├── styles │ ├── utils │ └── views │ ├── common │ └── projectA │ └── home.vue ├── scripts │ ├── build.ts │ └── resolveProject.ts ├── package.json └── vite.config.ts每个项目目录里放一个project.config.ts这个文件是整个多项目机制的核心。它负责描述该项目的标识、名称、接口域、路由白名单、主题配置等。构建的时候Vite 会先去解析当前要打包的是哪个项目然后把对应的配置合并进全局构建上下文。这种组织方式有几个好处公共代码在src下天然就是一个独立单元项目差异通过配置表达而不是在代码里写满if (project A)新增一个项目时不需要动主代码只要新建一个项目目录和配置我见过有人把项目配置散落在src/config下各自建文件也能跑但项目一多就很难搞清楚“哪些配置属于公共、哪些属于某个项目”最终会变成谁都不敢删的灾区。2.2 区分环境与项目环境变量别一股脑都用很多刚入门的朋友容易把.env.development、.env.production这类环境级别的文件和多项目级别的变量搞混。其实它们是两个维度环境维度开发、测试、预发、生产项目维度项目 A、项目 B、项目 C正确做法是环境变量管公共运行参数项目配置管差异项。我通常把公共运行参数直接写在.env开头的文件里项目差异项放在project.config.ts中通过loadEnv和自定义解析在构建时合并。举个例子接口地址这种就属于环境变量# .env.production VITE_API_BASE_URLhttps://api.example.com VITE_PUBLIC_PATH/而项目名称、主题色这种属于项目配置// apps/projectA/project.config.ts export default { projectId: projectA, title: 品牌A官网, apiPrefix: /a-api, theme: { primary: #1677ff, }, routes: [home, about, contact], }两者在构建脚本里合并成一个完整的config对象再交给 Vite 插件去替换和注入。这样你可以在代码里放心写useProjectConfig()而不是直接在业务代码里去读import.meta.env否则后期项目配置一旦复杂起来维护成本会直线上升。2.3 项目配置的类型安全怎么保证项目配置靠纯 JS/TS 对象传递最大的风险是字段写错。我第一版跑多项目时就在一个配置里把apiPrefix写成了apiPerfix构建不报错但登录接口全打出 404排查花了大半天。后来我规范了所有配置的类型定义// types/project-config.ts export interface ProjectConfig { projectId: string title: string apiPrefix?: string routes: string[] theme?: Recordstring, string outputDir: string publicPath?: string }每个project.config.ts都用satisfies ProjectConfig来声明提醒自己别漏字段、别写错类型。这个习惯帮我避免了很多低级错误尤其在项目数量上来以后。3. 核心实现用 Vite 环境变量驱动多项目构建3.1 先从 package.json 的脚本设计说起构建脚本的体验决定这套方案能不能从“能用”到“好用”。我建议把脚本设计成“项目 环境”的组合形式。package.json 看起来是这样的{ scripts: { dev:a: vite --mode development --project projectA, dev:b: vite --mode development --project projectB, build:a: npm run build:app -- projectA, build:b: npm run build:app -- projectB, build:app: node scripts/build.mjs, build:prod: node scripts/build.mjs --env production } }这里的--project参数是用来告诉构建工具“这次我打的是哪个项目”。开发模式下你可以分别起不同端口的 dev server生产模式下构建脚本会动态生成对应的构建入口。你可能想问为什么不直接在命令里写死--mode projectA当然可以但那样会丢失“环境”这个维度。把两个维度拆开才能支持“A 项目打测试环境”“B 项目打生产环境”这种灵活组合。3.2 Vite 配置如何动态生成多项目入口Vite 默认单页入口是index.html要支持一仓多项目就需要在vite.config.ts里用函数方式动态指定入口。关键代码大概是这样的// vite.config.ts import { defineConfig, loadEnv } from vite import vue from vitejs/plugin-vue import { resolve } from path import { parseProjectArg } from ./scripts/resolveProject export default defineConfig(({ mode }) { const env loadEnv(mode, process.cwd(), ) const project parseProjectArg(process.argv) || env.VITE_APP_PROJECT const pConfig loadProjectConfig(project) return { plugins: [vue()], define: { __PROJECT_CONFIG__: JSON.stringify(pConfig), }, build: { outDir: pConfig.outputDir || dist/${project}, emptyOutDir: true, rollupOptions: { input: { main: resolve(__dirname, apps/${project}/index.html), }, }, }, server: { port: 8080 projects.indexOf(project), }, } })这里的关键思路是__PROJECT_CONFIG__这个全局常量会在构建前被替换成该项目的完整配置对象业务代码里可以通过const projectConfig (window as any).__PROJECT_CONFIG__或者直接globalThis.__PROJECT_CONFIG__拿到。outDir写进项目配置里每个项目构建产物自然落到不同目录rollupOptions.input指向对应项目的 html 文件loadEnv处理环境级变量3.3 按项目的 index.html 和资源路径多项目场景里最容易踩的坑是资源路径。两个项目如果部署在不同的子路径下base必须跟着项目变。好在 Vite 支持在配置里动态设置basebase: pConfig.publicPath || /另外不同项目的index.html会有不同的标题、meta 描述、favicon。我通常把通用的公共部分放在根目录一个模板里然后每个项目自己的 html 通过vite-html-plugin或者简单的 html 字符串替换来注入差异字段。如果你不想为每个项目单独维护一个 html 文件也可以用transformIndexHtml钩子重写但这会稍微增加理解成本。我的经验是每个项目一个小 html 文件反而更直观。项目多了以后复制一个模板改几个字符的成本很低但可读性提升非常大。3.4 路由复用与分叉的平衡方案路由是“一套代码多项目”里最容易纠结的地方。项目 A 有 10 个页面项目 B 只需要其中 6 个项目 C 可能还要加一个独立的页面。我第一版做法是靠if判断动态过滤路由比如const allRoutes [...] const routes allRoutes.filter(r projectConfig.routes.includes(r.name))这种做法能跑但业务组件里也会出现“该不该渲染”的判断逻辑分布很散。后面我改成路由表本身就从项目配置里生成。具体来说在src/views下维护页面组件目录每个项目通过配置来决定加载哪些页面模块。这样改页面归属时只需要动配置文件不需要动组件内部代码。配合 Vite 的import.meta.glob可以实现按项目按需打包只把需要的视图包进产物减小构建体积const viewModules import.meta.glob(../views/**/*.vue) function buildRoutes(projectConfig: ProjectConfig) { return projectConfig.routes.map(name { const loader viewModules[../views/${name}.vue] return { path: /${name}, component: loader, } }) }这里有个性能小技巧懒加载一定要配好 chunk 分包否则多个项目之间共享的组件会被重复打进不同包里产物体积会翻倍增长。后面我会专门写这个问题。3.5 按项目区分请求层与状态管理请求层是另一个容易被“一套代码”思路坑到的环节。不同项目往往对应不同的接口前缀、鉴权方式甚至域名。如果只用一套 axios 实例很容易出现“A 项目上线了但 B 项目接口全 401”的问题。我的做法是封装一个createRequest(config)工厂函数在项目启动时用当前项目的接口配置生成对应的实例// api/request.ts export function createRequest(projectConfig: ProjectConfig) { return axios.create({ baseURL: projectConfig.apiPrefix, timeout: 15000, headers: { X-App-Id: projectConfig.projectId, }, }) }然后在入口处初始化一次存入全局状态。业务组件永远只 import 这个已经初始化好的实例不关心自己属于哪个项目。这个模式让切换项目时不需要改任何业务代码只改配置。状态管理也是同理。所有 store 模块应该是干净的业务逻辑不要在里面写死项目 ID 或路由白名单这些都要从配置里注入。如果某个 store 只属于某一个项目我更推荐放到apps/projectX下而不是塞到src/stores里。4. 产物策略与部署联通4.1 构建产物目录如何设计才能让部署不混乱多项目构建模式下产物目录建议统一约定为dist/projectId例如dist ├── projectA │ ├── index.html │ └── assets └── projectB ├── index.html └── assets然后把dist当作一个静态目录发布由 Nginx 或者网关层按路径转发到对应子目录。比如# nginx 配置示意 location /a/ { alias /srv/html/projectA/; try_files $uri $uri/ /a/index.html; } location /b/ { alias /srv/html/projectB/; try_files $uri $uri/ /b/index.html; }这种约定有两个好处新增项目不需要动发布脚本构建后自然多出一个子目录前端路由的 history 模式 fallback 时不同项目之间互不干扰如果你用容器化发布也可以把对应项目的产物目录单独打成镜像但我目前经验里直接把整个 dist 目录发布到一个静态服务上更省事因为构建是一次性产出所有项目而不是一次发布一个。4.2 懒加载分包与公共模块的复用优化多项目打包容易遇到的一个问题是多个项目共享同一个公共组件库但每个项目的产物里都打包了一份。按照 Vite 默认行为如果你在入口文件里直接import所有公共代码那每个入口都会打包进这部分代码。解决思路是手动配置rollupOptions.output.manualChunks。把公共依赖比如 vue、vue-router、pinia、axios和你的公共业务代码比如 layouts、常用组件拆成单独的 chunk这样构建后公共代码就只存在一份可以被各项目共享。常见的配置manualChunks(id) { if (id.includes(node_modules)) { if (id.includes(vue)) return vendor-vue if (id.includes(axios)) return vendor-axios return vendor } if (id.includes(/src/components/)) return shared-components }当然HTTP 静态服务本身会做文件缓存即使公共代码没有物理拆分只要文件名不变浏览器也会缓存。但对于大项目第一次访问 A 项目时能把缓存带上第二次访问 B 项目时就更快体验差别还是明显的。4.3 CI 的构建模板怎么写多项目构建流程接入 CI 时我建议把“构建哪个项目”作为流水线参数而不是写死命令。以常见的流水线为例npm install npm run build:app -- --project $PROJECT_ID --env $DEPLOY_ENV其中$PROJECT_ID由触发流水线时选择$DEPLOY_ENV决定使用哪套环境变量。构建后流水线再把产物目录传到目标服务器或者对象存储。如果你希望“一次构建、全部项目同时发布”那就直接执行npm run build:all这个脚本内部循环调用 build 脚本每个项目构建一次。我个人的建议是日常开发中按需构建只有发布到全量环境时才跑 all。5. 踩坑合集多项目方案中最常见的五类问题5.1base路径不统一导致部署后白屏多项目方案里第一高发的坑就是资源路径。如果你的某个项目部署在二级路径下而 vite.config 里的base没跟着项目配置走构建后 index.html 里引用的资源全是绝对路径/assets/xxx部署后就会全部 404。解决办法很简单每个项目的publicPath写清楚构建时用这个值覆盖 Vite 的base。同时注意router的createWebHistory(base)里的 base 也要保持一致否则页面能打开但路由跳转会出问题。5.2 接口请求仍打到默认域名项目切换后接口没跟着切换往往是请求层没有用项目配置初始化 baseURL。排查方式很简单在请求工具里打日志看当前请求 URL 和项目 ID 是否匹配。常见原因就是业务代码里头部写死了接口前缀绕过了工厂函数。我从第二版开始禁止在业务代码里直接写axios.get(/api/...)必须走 appRequest 实例。5.3 路由懒加载的文件名命名冲突用import.meta.glob按项目加载页面模块时如果你有两个页面文件夹下存在同名文件构建产物里就会出现覆盖或冲突问题。解决办法是保持视图目录里的文件名全局唯一或者在构建 chunk 支持自定义命名时使用包含路径的形式const viewModules import.meta.glob(../views/**/*.vue, { query: ?vuetyperoute, import: default, })5.4 开发模式下热更新慢或失效开发模式同时挂着多个项目时Vite 的文件监听默认是全局的可能出现 A 项目开发时B 项目文件的改动也触发 A 项目重新编译。这个问题低频率出现但一旦出现很闹心。我的做法是开发时按需指定项目另外在server.watch里加 ignore 规则排除非当前项目的页面和配置目录热更新会清爽很多。5.5 配置字段传递变成了字符串跨项目注入配置时如果你的自定义插件里JSON.stringify(pConfig)没处理干净业务代码中拿到的会是字符串而不是对象projectConfig.routes就会是undefined。我排查时发现这个问题的典型特征是启动不报错但页面空白控制台打印配置能看到引号。确保注入的全局变量是合法的对象字面量建议在代码里加一层防御性判断const projectConfig typeof window ! undefined ? JSON.parse(window.__PROJECT_CONFIG__ || {}) : ({} as ProjectConfig)6. 常见问题速查表与我的最终建议问题现象常见原因解决建议部署后静态资源 404base未按项目配置覆盖每个项目配置publicPath构建期动态设置base接口请求打错域名请求层未使用项目配置初始化统一走createRequest工厂禁止直接写死 baseURL页面白屏且控制台报undefined配置全局配置注入格式不对用JSON.parse解析确认字段名与类型一致多项目构建产物体积过大公共依赖被重复打包配置manualChunks拆分公共依赖热更新互相干扰Vite watch 范围未隔离用server.watch.ignored排除非当前项目目录路由 lazy 加载冲突视图目录同名文件覆盖保持视图文件全局唯一或自定义 chunk 名称这套方案能做到的是在一个仓库、一份主代码的前提下让多个项目并行演进与独立发布。但它也不是万能的。如果你遇到的是多个团队同时高频开发、需要各自独立部署和灰度发布的大型系统那就不能再靠这一套配置打天下应该把共享核心抽成真正的公共包同时引入微前端或独立应用级的模块联邦方案。模块联邦与这套方案的结合方式其实很自然你可以把公共组件和工具层作为 Local Remote让 A 项目和 B 项目在运行时拉取公共模块而不是构建时打包这样公共代码可以做到共享和热更新。前提是项目数量多到一定规模公共代码占到业务代码较大比重同时你愿意承担部分运行时的稳定性风险。我用这套方案重构过大概三四个项目集群从最早一团乱麻的复制粘贴到后来上百个页面、几十个项目一套代码同时构建最大的收获不是代码量的减少而是需求变更时终于只需要改一个地方了。以前修一个公共组件 Bug 要在七八个目录里重复操作还常常漏掉几个现在一次修改所有项目同时生效。最后再提醒一句这个方案不是“配置越多越好”而是“能统一的尽量统一该分叉的用配置表达”。我见过有人把每个按钮颜色都写进项目配置最后配置文件比业务代码还长完全失去了“一套代码打多项目”的初衷。把握好公共层与差异层的边界你才能真正享受到这套架构带来的红利。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询