拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Vue多客户配置自动化:从目录结构到CI构建全攻略

Vue多客户配置自动化:从目录结构到CI构建全攻略

1. 多客户配置自动化究竟在解决什么问题

我做过多套白标项目,最怕的不是需求写得多变态,而是客户一多,配置就乱。同一个Vue底座,客户A要蓝色主题、客户B要绿色主题,客户A的接口走内网域名,客户B要带租户ID才能鉴权……如果每个客户都靠“复制一份项目再改代码”来交付,等维护到第十个客户的时候,一个按钮的改动要同步十份代码,早晚会出事故。

这篇是《Vue项目多客户配置自动化方案》的第二篇,默认你已经完成了基础的客户分析、配置字段梳理,也知道了哪些配置需要在编译时定死、哪些必须在运行时读取。上一篇更多是讲“思路”,这一篇直接落到工程实现:客户配置目录怎么建、环境变量怎么自动生成、构建脚本怎么写、怎么在CI里一次产出多个客户的产物,以及我在实际项目中踩过的坑。适合正在做SaaS化、私有化部署、多品牌站点或者多租户后台的前端同学,听完可以直接在自己的项目里改。

我先把方案的核心思路放这儿:自动化不等于把配置写进更多文件,而是让“换一个客户”从人工改代码变成一次参数传递。所有差异化信息集中管理,构建和运行时按需读取,把这个流程捋顺后,新增客户的工作量才会真正降下来。

1.1 先别急着写脚本,把客户差异拆成“维度”

刚开始做多客户配置时,很多人第一反应是写一堆if else:if (客户A) ... else if (客户B) ...。这个方案看着简单,实际上是把客户的差异全部耦合进了业务代码。每个组件都在判断客户,产品经理改一个需求,涉及的组件可能比单个客户版本还要多。

我建议先按两个维度拆:环境维度和品牌维度。环境维度指接口地址、上传域名、租户标识、权限码、登录方式这些“换了客户就要换的值”;品牌维度指主题色、Logo、版权文案、模块可见性、默认语言这些“换了客户就要换的观感”。这两个维度最大的区别在于:环境维度通常要在构建时就确定,品牌维度最好在运行时可以切换,尤其是你有预览环境或者需要多个客户共用一套部署的时候。

1.2 配置管理不是堆文件,而是约定

“集中管理”最忌讳的是做出来一个没人敢改的巨型配置中心。我比较推荐的做法是给每个客户建一个独立目录,比如config/customers/customerA/,里面分成env.json(环境配置)和brand.json(品牌配置)。然后建一个 base 基础配置,里面放所有客户公共的部分。客户配置文件只需要写差异字段,加载的时候做一次深合并。深合并的顺序有讲究:base 被客户配置覆盖,客户配置被命令行参数覆盖,这样谁最后出现谁权力最大,规则清晰,后面排查问题会省很多事。

2. 配置目录、加载器与环境变量注入

2.1 一套目录结构,让新客户五分钟就能接入

直接看我最终采用的目录结构吧:

vue-multi-customer/ ├── config/ │ ├── base/ │ │ ├── env.json │ │ └── brand.json │ └── customers/ │ ├── customerA/ │ │ ├── env.json │ │ └── brand.json │ └── customerB/ │ ├── env.json │ └── brand.json ├── scripts/ │ ├── build.mjs │ └── prepare-env.mjs ├── public/ │ └── app.config.sample.json └── src/ ├── config/ │ ├── index.js │ └── runtime.js ├── router/ ├── store/ └── main.js

每新增一个客户,就是config/customers/下多一个目录,里面放两个 JSON。不涉及代码修改,构建脚本会自动把这个目录里的数据读出来,生成对应的环境文件。关键点在于:目录结构本身就是文档,新人进来一看就知道往哪儿加东西。

2.2 运行时配置加载器要够快、够稳

运行时配置这块,我把品牌相关、需要动态展示的内容放在/config/下,通过src/config/runtime.js加载。加载器的工作有三件事:先读当前客户 ID,再拉取运行时配置,最后把结果写入一个响应式 store,方便全局到处读取。

以 Vue 3 为例,我是这样写的:

// src/config/runtime.js import { reactive } from 'vue' const state = reactive({ customerId: '', brand: {}, features: {}, loaded: false }) export async function loadRuntimeConfig(customerId) { const response = await fetch(`/config/${customerId}/app-config.json`, { headers: { 'Cache-Control': 'no-cache' } }) const data = await response.json() state.customerId = customerId state.brand = data.brand state.features = data.features state.loaded = true return state } export function useRuntimeConfig() { return state }

在main.js里先await loadRuntimeConfig(customerId)再挂载应用,避免首屏渲染的时候品牌信息还没拿到,出现“先白屏再变色”的问题。需要注意,这条请求不能放在普通组件里发,否则每个页面都要等待配置请求完成,白屏时间会变成累计的漏斗。

2.3 编译期环境变量自动生成,别手写 .env

Vite 项目里有一堆.env.development、.env.production,多客户以后.env.customerA.production会越来越多。手动维护这些文件很快会被搞炸,因为客户每调整一次接口域名,你就要打开对应文件改一次,一旦改错,影响的是整个客户端的交付。我选择统一从配置文件生成环境文件。

scripts/prepare-env.mjs的核心逻辑大致是这样:

// scripts/prepare-env.mjs import { readFile, writeFile, mkdir } from 'node:fs/promises' import path from 'node:path' export async function generateEnvFile(customerId, mode) { const baseEnv = JSON.parse(await readFile('config/base/env.json', 'utf-8')) const customerEnvPath = `config/customers/${customerId}/env.json` let customerEnv = {} try { customerEnv = JSON.parse(await readFile(customerEnvPath, 'utf-8')) } catch (err) { console.warn(`[prepare-env] 客户 ${customerId} 缺少 env.json,将只使用 base 配置`) } const merged = { ...baseEnv, ...customerEnv } const entries = Object.entries(merged) .map(([key, value]) => `VITE_${key}=${value}`) .join('\n') const envDir = `.env/${customerId}` await mkdir(envDir, { recursive: true }) await writeFile(path.join(envDir, `${mode}.env`), entries) return merged }

然后构建命令就变成了:

node scripts/prepare-env.mjs --customer customerA --mode production vite build --mode production

Vite 加载环境变量时,会自动读取.env/${customerId}/${mode}.env吗?不会,默认只会读项目根目录。你需要在vite.config.js里指定envDir,或者在脚本里把它复制到根目录。我实际用的办法是给vite.config.js加一个loadEnv逻辑,按当前客户 ID 动态指定:

// vite.config.js import { defineConfig, loadEnv } from 'vite' export default defineConfig(({ mode }) => { const customerId = process.env.CUSTOMER_ID || 'default' const env = loadEnv(mode, path.resolve(process.cwd(), `.env/${customerId}`), '') return { define: { __APP_CUSTOMER_ID__: JSON.stringify(customerId) }, build: { outDir: `dist/${customerId}` } } })

这里__APP_CUSTOMER_ID__是编译期常量,相当于告诉整个应用“你现在在给谁干活”。接口地址这些通过import.meta.env.VITE_API_BASE读取,构建时就会被替换成具体值,不用担心运行时会读到 undefined。

3. 把“打几个包”变成一行命令

3.1 一个 build 脚本,串联全部流程

prepare-env只解决了环境文件的生成,真正要自动化还得有一个入口脚本,把“生成配置 → 打前端包 → 产物输出到指定目录 → 汇总报告”串起来。我写scripts/build.mjs的时候,让它支持三种调用方式:

  • node scripts/build.mjs --customer customerA:只构建一个客户,适合日常联调。
  • node scripts/build.mjs --all:读取 customers 目录下的所有客户,逐个构建。
  • node scripts/build.mjs --customer customerA --mode staging:指定非生产环境。

这个脚本内部用 Node 的child_process.spawn依次执行子任务。顺序很关键:先生成环境文件,再清空旧的dist/${customerId},然后执行 Vue 的类型检查和构建,最后把构建日志整理成一个简短的 JSON 报告,方便 CI 识别成功失败。

一次性构建多个客户时,最忌并行执行 Vite 构建,因为多个进程同时写同一个 node_modules/.vite 缓存目录,会出现诡异的“文件被占用”错误。我一开始图快,用Promise.all并行跑构建,结果十个客户里总有那么两三个随机失败,后来老老实实改成串行,或者给每个构建进程设置独立的cacheDir:

if (customers.length > 1) { await asyncForEach(customers, buildOneCustomer) } else { await buildOneCustomer(customers[0]) }

3.2 任何一个配置项都要有默认值

多客户自动化最怕“客户配置缺失时没人发现”。为了这套方案不变成新的故障源,我给所有需要读取的配置项都加了默认值和提醒。比如brand.json里没有primaryColor,就用 base 里的#1677FF,并在控制台打一条警告。宁可先用默认值顶住不让构建崩,也要通过告警把问题暴露出来。

3.3 CI 里怎么编排多客户构建

在 GitLab CI 或 GitHub Actions 里,我建议不要在一个 job 里跑完所有客户,而是拆成两个阶段:准备阶段生成客户列表,构建阶段用 matrix 并行。GitLab 的写法大概是这样:

generate-customer-list: script: - node scripts/list-customers.mjs > customers.txt artifacts: paths: [customers.txt] build: parallel: matrix script: - node scripts/build.mjs --customer $CUSTOMER_ID

这里的重点在于:客户列表要能自动发现。如果新增了一个config/customers/customerC/目录,CI 不需要改代码,下一轮构建自然就会多一个customerC的 job。这个体验非常关键,因为多客户项目里最值钱的就是“改配置不改流程”。

4. 常见问题与排查技巧实录

方案跑起来不难,难的是出了问题能快速定位。我把实际遇到过的典型问题整理成一个排查表,按发生频率排序。

问题现象根因排查思路解决方案
客户A的接口地址跑到了客户B的包里环境文件没有按客户隔离,或构建时读错 envDir检查dist/客户目录/assets/index.*.js里搜索 API 域名确认envDir按客户目录指向,构建前先打印关键变量
启动后首屏白屏几秒才显示品牌色运行时配置在挂载后才加载看 Network 面板,配置请求是否滞后在main.js顶部await loadRuntimeConfig()
新增客户后构建成功但页面是上一个客户的风格runtime 配置 JSON 被缓存浏览器或 CDN 对app-config.json做了强缓存请求加cache-control: no-cache,CDN 上设置不缓存
所有客户构建时随机失败并行构建共用 Vite 缓存看日志中是否出现.vite临时文件错误串行,或每个客户独立cacheDir
客户配置字段名写错但构建通过没有做 schema 校验看控制台警告加 JSON Schema 校验,或者用 zod 对配置做 parse

问题一:环境变量没有生效。这类问题十有八九是loadEnv的参数写错,或者是环境文件名不匹配。Vite 只会加载.env开头的文件,且默认只加载.env、.env.local、.env.[mode]和.env.[mode].local。如果你的环境文件放在.env/customerA/production.env,就必须显式设置envDir: '.env/customerA',并且文件名是production.env时 Vite 会当成自定义文件名,需要用loadEnv(mode, dir)的第三个参数把 prefix 传空,或者在文件名上做文章。我后来直接统一命名为.env.customerA.production,避免和标准命名搞混。

问题二:客户A的调试数据泄漏到生产环境。有些同事会在本地env.json里写好测试账号,构建时忘了切换,结果生产包带着测试数据。我在脚本里加了一道关卡:mode === 'production'时,如果检测到 apiBase 里包含test、dev、localhost,直接构建失败,并提示“疑似测试环境配置进入生产包”。这个看似粗暴的拦截,已经拦下了两三次发布事故。

问题三:品牌主题在切换客户时出现闪烁。这是因为 CSS 变量在样式表加载完成后才被 JS 修改。我的处理是在 HTML 的head里内联一段极简的配置脚本,读取一段非常小的 bootstrap JSON,把主题色和背景色先设置到document.documentElement.style。这样即使主 JS 还没跑完,首屏也已经有了正确的底色,视觉上就不会闪。

5. 自动化之外,还要管好人的操作习惯

工具只是把流程固化下来,真正让多客户方案稳定运行的,是团队的操作约定。我踩过几次坑之后,有几个具体建议:

第一,所有客户配置必须走 git 评审,不允许任何人直接在生产服务器上改 JSON。原因是服务器上的改动不会经过构建流程,很容易出现“本地是这么回事、线上是另一回事”的配置漂移。配置漂移一旦发生,排查成本会成倍增长。

第二,客户 ID 命名要统一且不可变。上线之后客户的目录名不要随便改,因为产物目录、CDN 路径、后端日志里的租户标识可能都跟这个 ID 绑定。尽量用域名缩写或系统代号,不要用“华东区一期”“二期”这种会变的名字。

第三,每个客户至少留一个只展示自己配置的预览环境模板。做这个不是为了让老板爽,而是给你自己留一个“安全演练场”。上线前先把新客户的配置在预览环境里跑一遍,看图片路径、下载链接、导出文件名这些容易被忽略的细节是不是都带上了客户特有前缀。

第四,离线的配置能力也要覆盖。有些交付场景是内网环境,客户服务器没有外网,构建时无法拉取远程配置,所以我们的配置必须是纯本地文件,构建产物把配置打进包里,部署时不需要额外联网。方案设计之初要是没考虑离线,后面补会非常痛苦。

6. 最后再讲三个容易被忽略的细节

这里就不再重复前面的原理了,只说三个我复查代码时发现的、细节但影响很大的地方。

第一个是 favicon 和公开资源路径。每个客户的主机名可能不同,部署路径也可能带着子路径,比如https://xxx.example.com/customerA/。如果图片、字体、favicon 用的是相对路径,在子路径部署下问题不大;但如果你用了/assets/xxx.png这种以根路由开头的绝对路径,部署在子路径下就会全部 404。需要检查base配置能不能跟随客户上下文变化,最好是统一在构建脚本里给vite.config.js的base参数赋值。

第二个是语言和时区。多客户项目经常遇到客户要英文、繁体、阿拉伯语的场景。自动化方案一般只配置文案的位置,不负责翻译质量,但别忽略日期格式、货币符号这些偏运行时逻辑的差异化内容。我一般会把locale、timezone放进env.json,加载运行时配置后立刻设置 dayjs / date-fns 的 locale,让整个应用的日期展示从一开始就统一。

第三个是后端鉴权字段的传递方式。不同客户对接的鉴权体系可能差异极大,有些客户走 header 传 token,有些走 cookie,还有些要求每个请求带上X-Tenant-Id。这类逻辑如果散落在各个请求函数里,自动化方案就形同虚设。最好把所有和客户相关的请求增强逻辑集中到一个httpClient实例里,拦截器从运行时配置里读取需要的客户标识,避免业务代码脱离上下文。

可以说,做完这套自动化方案之后,我最大的体会是:多客户配置自动化解决的从来不只是“多打几个包”的问题,它逼着你把客户差异显式化、把构建过程可观测化,也让团队逐渐养成“改配置不要改代码”的共识。后续如果再扩展新客户,只需要按照既有约定补充两个 JSON,跑一遍构建脚本,整个流程就能闭环。如果你也在做类似方案,可以把你们在配置管理和构建编排上的经验在留言区一起聊聊,互相补补坑。

返回列表