使用 Vite Mode 实现客户端与管理端的物理隔离
一、背景与目标
在我们的项目中,客户端与管理端共享绝大部分的组件、工具和样式代码,但两者的登录入口和业务路由完全不同。过去,我们将它们放在同一个Vue项目中,通过同一套路由混合管理。
目标
我们希望在不拆分代码仓库、保持公共代码高效复用的前提下,实现彻底的隔离:
- 执行
pnpm dev:client时,我们得到一个仅包含客户端路由和页面的纯净开发环境。 - 执行
pnpm dev:admin时,我们得到一个仅包含管理端路由和页面的纯净开发环境。 - 在执行构建时,能产出两个互不包含对方代码的独立部署包。
实现这一目标的核心,便是利用 Vite 的 --mode 参数在构建时区分应用,并动态决定最终生效的路由配置。
二、核心机制:命令行参数驱动
整个方案的核心是利用 Vite 的 --mode 参数,在启动和构建时向应用注入一个“身份标识”。
-
定义启动与构建命令 (
package.json)我们通过不同命令传递不同的
mode值。为了能在单个终端窗口同时启动或构建两个应用,我们使用concurrently工具。{ "scripts": { // 1. 独立操作命令 "dev:client": "vite --mode client", "dev:admin": "vite --mode admin", "build:client": "vite build --mode client", "build:admin": "vite build --mode admin", // 2. 核心效率工具:使用 concurrently 一键操作两端 "dev:all": "concurrently "pnpm dev:client" "pnpm dev:admin"", "build:all": "concurrently "pnpm build:client" "pnpm build:admin"" }, "devDependencies": { "concurrently": "^9.1.2", // 需要安装此依赖 } } -
动态Vite配置 (
vite.config.js)Vite 配置函数能接收到
mode参数,我们据此动态设置所有差异化配置。import { defineConfig } from 'vite'; export default defineConfig(({ mode }) => { const isAdmin = mode === 'admin'; const appType = isAdmin ? 'admin' : 'client'; return { // 核心:将应用类型注入为全局常量 APP_TYPE define: { APP_TYPE: JSON.stringify(appType) // 关键:必须用JSON.stringify }, server: { port: isAdmin ? 3001 : 3000, // 为不同端分配不同端口,避免冲突 open: true }, build: { outDir: isAdmin ? 'dist-admin' : 'dist-client' // 构建输出到不同目录 } // ... 其他公共配置 }; });
三、核心难点与解决方案
-
全局常量替换的“坑”:为什么必须用
JSON.stringify?Vite 的
define配置本质是字符串级别的查找替换,并非 JavaScript 的变量赋值。理解“替换成什么文本”是关键。-
错误配置:
define: { APP_TYPE: 'admin' }- 替换过程:代码中
'console.log(APP_TYPE)'里的APP_TYPE会被直接替换为文本'admin'。 - 结果代码:
'console.log(admin)'。 -
问题:
admin没有引号,会被 JavaScript 引擎当作一个变量名,导致报错admin is not defined。
- 替换过程:代码中
-
正确配置:
define: { APP_TYPE: JSON.stringify('admin') }-
JSON.stringify('admin')的执行结果是字符串'"admin"'(包含双引号)。 - 替换过程:代码中的
APP_TYPE会被替换为文本"admin"。 - 结果代码:
console.log("admin")。 -
正确:
"admin"是一个字符串值,能正确打印。
-
结论:
JSON.stringify的作用是把值转换成其对应的、有效的 JSON 字符串表示形式,确保替换后的代码语法正确。 -
-
路由动态配置:通过
APP_TYPE分离两套路由这是本方案的核心应用之一。在路由定义文件中,我们准备两套完全独立的路由数组,并通过
APP_TYPE全局常量来决定最终使用哪一套。// src/router/index.js import { createRouter, createWebHistory } from 'vue-router' // 1. 定义客户端路由 const clientRoutes = [ { path: '/', component: () => import('@/views/client/Home.vue') }, { path: '/profile', component: () => import('@/views/client/Profile.vue') } ] // 2. 定义管理员端路由 const adminRoutes = [ { path: '/admin', component: () => import('@/views/admin/Dashboard.vue') }, { path: '/admin/users', component: () => import('@/views/admin/UserList.vue') } ] // 3. 根据 APP_TYPE 动态选择路由 const routes = APP_TYPE === 'client' ? clientRoutes : adminRoutes export default createRouter({ history: createWebHistory(), routes // 使用确定后的路由 }) -
TypeScript 支持:声明全局常量
在项目文件中使用
APP_TYPE时,TypeScript 会因找不到定义而报错。需在类型声明文件中声明此全局常量。// src/env.d.ts // 声明通过 define 注入的全局常量 declare const APP_TYPE: 'client' | 'admin';此声明为
APP_TYPE提供了类型支持,使其在代码中具备完整的类型提示与检查。
四、完整工作流程
-
安装依赖:
pnpm add -D concurrently。 -
配置命令:按上文修改
package.json。 -
配置Vite:按上文创建动态的
vite.config.js。 -
声明类型:创建
src/env.d.ts文件声明APP_TYPE。 -
代码中区分逻辑:在任何需要区分两端的地方使用
APP_TYPE常量。// 例如在路由、组件、API配置中 if (APP_TYPE === 'admin') { // 管理员端逻辑 } else { // 客户端逻辑 } -
运行:
-
pnpm dev:all:一键启动两个端,分别访问http://localhost:3000(client) 和http://localhost:3001(admin)。 -
pnpm build:all:一键构建两个端,产物分别输出到dist-client和dist-admin目录。
-
五、方案对比与更优方案
在理解了通过 define 配置全局常量的原理后,你会发现 Vite 本身就提供了更简洁的内置方案。
更优方案:直接使用 import.meta.env.MODE
Vite 会自动将 --mode参数的值注入到 import.meta.env.MODE这个内置环境变量中。这意味着你可以完全省略配置 define 和声明 .d.ts 文件的步骤,直接使用它。
在代码的任何地方,直接判断 import.meta.env.MODE即可。
// 路由配置中直接判断
const routes = import.meta.env.MODE === 'client' ? clientRoutes : adminRoutes;
// 在组件或逻辑中
if (import.meta.env.MODE === 'admin') {
// 管理员端专属逻辑
}