使用 Vite Mode 实现客户端与管理端的物理隔离

16 阅读5分钟

一、背景与目标

在我们的项目中,客户端管理端共享绝大部分的组件、工具和样式代码,但两者的登录入口业务路由完全不同。过去,我们将它们放在同一个Vue项目中,通过同一套路由混合管理。

目标

我们希望在不拆分代码仓库、保持公共代码高效复用的前提下,实现彻底的隔离:

  • 执行 pnpm dev:client 时,我们得到一个仅包含客户端路由和页面的纯净开发环境
  • 执行 pnpm dev:admin 时,我们得到一个仅包含管理端路由和页面的纯净开发环境
  • 在执行构建时,能产出两个互不包含对方代码的独立部署包。

实现这一目标的核心,便是利用 Vite 的 --mode 参数在构建时区分应用,并动态决定最终生效的路由配置。

二、核心机制:命令行参数驱动

整个方案的核心是利用 Vite 的 --mode 参数,在启动和构建时向应用注入一个“身份标识”。

  1. 定义启动与构建命令 (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", // 需要安装此依赖
      }
    }
    
  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' // 构建输出到不同目录
        }
        // ... 其他公共配置
      };
    });
    

三、核心难点与解决方案

  1. 全局常量替换的“坑”:为什么必须用 JSON.stringify

    简单来说:define的机制类似于在构建时执行了一段 new Function("return " + 你定义的值) 。你定义的值会被直接当作JavaScript代码(表达式)插入到你的源码位置。

  • 错误配置define: { APP_TYPE: 'admin' }

    • 效果相当于:把你的代码里的 APP_TYPE替换成 admin这个表达式。
    • 结果:console.log(APP_TYPE)变成了 console.log(admin)
    • 问题admin被当成一个变量,而它未定义,所以报错。
  • 正确配置define: { APP_TYPE: JSON.stringify('admin') }

    • JSON.stringify('admin')的结果是 '"admin"'(一个带引号的字符串)。
    • 效果相当于:把你的代码里的 APP_TYPE替换成 "admin"这个字符串字面量。
    • 结果:console.log(APP_TYPE)变成了 console.log("admin")
    • 正确"admin"就是一个合法的字符串值。

一句话总结define的值必须是能直接写在代码里的合法 JavaScript 表达式JSON.stringify()能自动帮你把任何值(字符串、对象等)转换成正确的表达式形式。

  1. 路由动态配置:通过 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 // 使用确定后的路由
    })
    
  2. TypeScript 支持:声明全局常量

    在项目文件中使用 APP_TYPE 时,TypeScript 会因找不到定义而报错。需在类型声明文件中声明此全局常量。

    // src/env.d.ts// 声明通过 define 注入的全局常量
    declare const APP_TYPE: 'client' | 'admin';
    

    此声明为 APP_TYPE 提供了类型支持,使其在代码中具备完整的类型提示与检查。

四、完整工作流程

  1. 安装依赖pnpm add -D concurrently

  2. 配置命令:按上文修改 package.json

  3. 配置Vite:按上文创建动态的 vite.config.js

  4. 声明类型:创建 src/env.d.ts文件声明 APP_TYPE

  5. 代码中区分逻辑:在任何需要区分两端的地方使用 APP_TYPE常量。

    // 例如在路由、组件、API配置中
    if (APP_TYPE === 'admin') {
      // 管理员端逻辑
    } else {
      // 客户端逻辑
    }
    
  6. 运行

    • pnpm dev:all一键启动两个端,分别访问 http://localhost:3000(client) 和 http://localhost:3001(admin)。
    • pnpm build:all一键构建两个端,产物分别输出到 dist-clientdist-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') {
  // 管理员端专属逻辑
}