阅读视图

发现新文章,点击刷新页面。

小米Android 手机连接PC Local调试

手机设置

  1. 进入设置界面 => 打开 我的设备
  2. 进入 全部参数与信息 => 多次点击点击 OS版本(1.0.4xxx) ,直到显示**你已处于开发者模式*
  3. 返回主菜单,进入 更多设置, 点击**开发者选项 => 打开 USB调试

电脑使用

  1. chrome打开 chrome://inspect/#devices
  2. 手机chrome访问 www.baidu.com
  3. 可在电脑上查看到,devices的远程设备

image-2024-11-26_8-55-14.png

  1. 点击 inspect 选项,查看到手机访问的内容 image-2024-11-26_8-56-59.png

安装fiddler并开启,引起https网页打不开

  1. tools=> Options=> https => Decrypt HTTPS traffic  勾选
  2. 点击右侧Actions =>  Reset All Certificates 
  3. 参考blog.csdn.net/xiaona0523/…

手机连接local调试

1.设置wify 代理到本地

2.chrome中访问local 地址,即可开始调试

使用Cursor 完成 Vike + Vue 3 + Element Plus 管理后台 — 从 0 到 1 (实例与文档)

目录

  1. 项目概述
  2. 技术栈
  3. 项目初始化
  4. 目录结构
  5. 核心配置文件
  6. 服务端 — Express 服务器
  7. Vike 页面约定与 Hook 体系
  8. 状态管理 — Pinia
  9. 国际化 — Vue I18n
  10. API 层 — Alova + Axios
  11. Layout 系统
  12. Element Plus 集成(SSR 兼容)
  13. 权限系统
  14. 路由与导航
  15. 业务页面示例
  16. SSR 与 CSR 策略
  17. 关键踩坑与解决方案
  18. 开发与构建命令
  19. 生产部署

1. 项目概述

本项目是一个基于 Vike(前 vite-plugin-ssr)+ Vue 3 的企业级管理后台模板。核心思路是利用 Vike 框架的原生 Hook 体系(+config.ts+guard.ts+data.ts+Layout.vue+onCreateApp.ts)替代传统 Vue Router 的路由守卫和路由配置方式,实现:

  • SSR 首屏渲染 — 首屏数据通过 +data.ts 在服务端预取,直接输出到 HTML
  • 统一权限验证 — 通过 +guard.ts 在 SSR 阶段调用后端权限接口,无权限直接渲染 403 页面
  • 公共 Layout 可定制 — 每个页面可通过 Pinia Store 方法动态修改 Layout 标题、面包屑、顶部按钮等
  • 国际化 — Vue I18n 支持中英文切换,菜单、标题、错误页均支持多语言
  • UI 组件库 — Element Plus 全量引入,SSR 兼容

2. 技术栈

类别 技术 版本 说明
框架 Vue 3 ^3.5 Composition API
元框架 Vike ^0.4.252 SSR / 文件系统路由
Vue 适配 vike-vue ^0.9.10 Vike 的 Vue 3 适配器
UI 组件库 Element Plus ^2.9 管理后台 UI 组件
状态管理 Pinia ^3.0 Vue 3 官方状态管理
国际化 Vue I18n ^11.1 多语言支持
HTTP 请求 Alova + Axios ^3.2 / ^1.9 请求策略库 + HTTP 客户端
服务端 Express 5 ^5.2 Node.js HTTP 服务器
构建工具 Vite 7 ^7.3 开发服务器 + 打包
语言 TypeScript ^5.9 类型安全
CSS 预处理 SCSS ^1.87 样式预处理
代码规范 ESLint + typescript-eslint ^9.39 代码质量保障

3. 项目初始化

3.1 创建项目

# 创建目录
mkdir vike-zyh-test && cd vike-zyh-test

# 初始化 package.json
npm init -y

3.2 安装依赖

运行时依赖:

npm install vue vike vike-vue express compression cookie-parser sirv \
  pinia vue-i18n element-plus alova @alova/adapter-axios axios

开发依赖:

npm install -D vite @vitejs/plugin-vue typescript tsx sass \
  unplugin-auto-import unplugin-vue-components \
  @intlify/unplugin-vue-i18n cross-env \
  eslint @eslint/js eslint-plugin-vue typescript-eslint vue-eslint-parser globals \
  @types/express @types/compression @types/cookie-parser

3.3 设定 package.json Scripts

{
  "type": "module",
   "scripts": {
    "dev": "tsx server/server.ts",
    "build": "vike build",
    "preview": "vike build && cross-env NODE_ENV=production tsx server/server.ts",
    "lint": "eslint .",
    "fix": "eslint . --fix"
  },
}

关键点:开发模式使用 tsx 直接运行 TypeScript 编写的 Express 服务器,而非 vite dev。这允许我们完全掌控服务端中间件、Mock API 和渲染流程。


4. 目录结构

vike-zyh-test/
├── server/                          # Express 服务端
│   └── server.ts                    # 入口:中间件 + Mock API + Vike 渲染
├── src/
│   ├── api/                         # API 层
│   │   ├── alovaInstance.ts         # Alova 实例管理 + apiCreator 统一请求工厂
│   │   ├── createClientApi.ts       # 客户端 Alova 实例创建
│   │   ├── createServerApi.ts       # 服务端 Alova 实例创建(用于 +data.ts / +guard.ts)
│   │   ├── dashboardApi.ts          # Dashboard 业务 API
│   │   └── permissionApi.ts         # 权限业务 API
│   ├── composables/                 # 组合式函数
│   │   ├── useLayout.ts             # Layout 控制接口(setTitle / setBreadcrumbs / setHeaderActions ...)
│   │   ├── usePagination.ts         # 分页逻辑封装
│   │   └── usePermission.ts         # 权限检查(hasPermission)
│   ├── constants/                   # 常量
│   │   ├── constants.ts             # 通用常量(分页默认值、枚举等)
│   │   ├── menu.ts                  # 侧边栏菜单配置
│   │   └── permissionApis.ts        # 权限 API URL 常量(统一管理)
│   ├── directive/                   # 自定义指令
│   │   └── directive.ts             # 指令注册入口(如权限指令 v-permission)
│   ├── i18n/                        # 国际化
│   │   ├── i18n.ts                  # createI18n 工厂函数
│   │   ├── zh-CN.json               # 中文语言包
│   │   └── en-US.json               # 英文语言包
│   ├── layout/                      # Layout 组件
│   │   ├── AppSidebar.vue           # 侧边栏
│   │   └── AppHeader.vue            # 顶部导航栏
│   ├── pages/                       # Vike 文件系统路由 ★
│   │   ├── +config.ts               # 全局页面配置
│   │   ├── +onCreateApp.ts          # Vue App 创建钩子(注册 Pinia/I18n/ElementPlus)
│   │   ├── +guard.ts                # 全局路由守卫(权限验证)
│   │   ├── +Layout.vue              # 全局 Layout
│   │   ├── +Head.vue                # 全局 HTML <head>
│   │   ├── _error/                  # 错误页面(401/403/404/500)
│   │   │   └── +Page.vue
│   │   ├── index/                   # 首页 /
│   │   │   ├── +config.ts
│   │   │   ├── +data.ts             # SSR 数据预取
│   │   │   └── +Page.vue
│   │   └── permission/              # 权限管理模块
│   │       ├── +config.ts
│   │       ├── +data.ts             # SSR 数据预取(权限列表)
│   │       ├── +Page.vue            # 权限列表页
│   │       ├── add/                 # 新增权限 /permission/add
│   │       │   ├── +config.ts
│   │       │   ├── +data.ts         # 空 data,阻止继承父级
│   │       │   └── +Page.vue
│   │       └── @id/                 # 动态路由 /permission/:id
│   │           └── edit/            # 编辑权限 /permission/:id/edit
│   │               ├── +config.ts
│   │               ├── +data.ts     # 空 data,阻止继承父级
│   │               └── +Page.vue
│   ├── scss/                        # 全局样式
│   │   └── common.scss
│   ├── stores/                      # Pinia 状态管理
│   │   ├── global.ts                # 全局状态(env/lang/user)
│   │   └── layout.ts                # 布局状态(title/breadcrumbs/headerActions/sidebar)
│   └── viewComponents/              # 页面级可复用组件
│       └── permission/
│           └── PermissionForm.vue   # 权限表单组件(新增/编辑复用)
├── vite.config.ts                   # Vite 配置
├── tsconfig.json                    # TypeScript 根配置(引用子配置)
├── tsconfig.app.json                # 前端 TS 配置
├── tsconfig.node.json               # Vite 配置用 TS 配置
├── tsconfig.server.json             # 服务端 TS 配置
├── eslint.config.ts                 # ESLint 配置
└── package.json

约定说明pages/ 目录下以 + 开头的文件是 Vike 框架约定文件,分别承担配置、数据预取、守卫、布局、渲染等职责。@id 目录名表示动态路由参数。_error 为 Vike 约定的错误页面目录。


5. 核心配置文件

5.1 package.json

{
  "type": "module",
  "imports": {
    "#*": "./*",
    "#server/*": "./server/*"
  }
}
  • "type": "module" — 启用 ESM
  • "imports" — Node.js 原生子路径导入映射,配合 tsconfig.jsonpaths 实现统一的 # 前缀路径别名

5.2 vite.config.ts

import { fileURLToPath, URL } from 'node:url';
import { readdir } from 'node:fs/promises';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vike from 'vike/plugin';
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';

// 自动扫描 src/ 下的子目录,生成路径别名
const srcSubDirs = (
  await readdir(new URL('./src', import.meta.url), { withFileTypes: true })
)
  .filter((d) => d.isDirectory())
  .map(({ name }) => name);

export default defineConfig({
  plugins: [
    vue(),
    vike(),
    AutoImport({
      resolvers: [ElementPlusResolver({ importStyle: false })],
    }),
    Components({
      resolvers: [ElementPlusResolver({ importStyle: false })],
    }),
    VueI18nPlugin({ ssr: true, strictMessage: false }),
  ],
  resolve: {
    alias: {
      '#': fileURLToPath(new URL('./', import.meta.url)),
      '#src': fileURLToPath(new URL('./src', import.meta.url)),
      '#server': fileURLToPath(new URL('./server', import.meta.url)),
      // 自动生成: #api, #composables, #stores, #i18n, #layout, #pages ...
      ...Object.fromEntries(
        srcSubDirs.map((name) => [
          `#${name}`,
          fileURLToPath(new URL(`./src/${name}`, import.meta.url)),
        ]),
      ),
    },
  },
  build: { target: 'es2022' },
});

关键设计点:

配置项 说明
vike() 启用 Vike 插件,提供 SSR + 文件系统路由
ElementPlusResolver({ importStyle: false }) 禁用 样式自动导入,避免 SSR 中加载 CSS 文件报错。样式改为在 +Layout.vue 中手动 import 'element-plus/dist/index.css'
VueI18nPlugin({ ssr: true }) 开启 i18n 的 SSR 优化,编译时处理 <i18n>
路径别名自动扫描 自动读取 src/ 子目录,无需手动逐个配置别名

5.3 TypeScript 配置

项目采用三配置策略

文件 作用 module
tsconfig.app.json 前端源码 (src/) ES2022 / Bundler
tsconfig.node.json Vite 配置文件 ES2022 / Bundler
tsconfig.server.json 服务端代码 (server/) Node16 / Node16

tsconfig.app.json 中配置了所有 # 前缀的路径映射:

{
  "compilerOptions": {
    "paths": {
      "#*": ["./*"],
      "#src/*": ["./src/*"],
      "#api/*": ["./src/api/*"],
      "#stores/*": ["./src/stores/*"],
      "#i18n/*": ["./src/i18n/*"],
      "#layout/*": ["./src/layout/*"],
      "#composables/*": ["./src/composables/*"],
      "#constants/*": ["./src/constants/*"],
      "#directive/*": ["./src/directive/*"],
      "#viewComponents/*": ["./src/viewComponents/*"],
      "#server/*": ["./server/*"]
    }
  }
}

5.4 ESLint 配置

使用 ESLint 9 Flat Config,集成 typescript-eslinteslint-plugin-vue

// eslint.config.ts
import eslint from '@eslint/js';
import pluginVue from 'eslint-plugin-vue';
import tseslint from 'typescript-eslint';
import vueParser from 'vue-eslint-parser';

export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
  // Vue 文件使用 vue-eslint-parser 嵌套 typescript parser
  {
    files: ['**/*.vue'],
    languageOptions: {
      parser: vueParser,
      parserOptions: { parser: tseslint.parser },
    },
  },
  ...pluginVue.configs['flat/recommended'],
);

6. 服务端 — Express 服务器

server/server.ts 是项目入口,使用 Express 5 搭建 HTTP 服务器:

import express from 'express';
import compression from 'compression';
import cookieParser from 'cookie-parser';
import { renderPage, createDevMiddleware } from 'vike/server';

async function startServer() {
  const app = express();

  // 1. 基础中间件
  app.use(compression());       // Gzip 压缩
  app.use(cookieParser());      // Cookie 解析
  app.disable('x-powered-by');  // 隐藏 Express 标识

  // 2. 静态文件 / Vite 开发中间件
  if (isProd) {
    app.use(sirv('dist/client'));  // 生产环境:静态文件
  } else {
    const { devMiddleware } = await createDevMiddleware({ root });
    app.use(devMiddleware);        // 开发环境:Vite HMR
  }

  // 3. Mock API(开发阶段可替换为真实后端代理)
  app.use(express.json());
  app.get('/api/v1/dashboard/stats', ...);
  app.get('/api/v1/permissions', ...);
  app.post('/api/v1/permission/check', ...);

  // 4. Vike 页面渲染 — 所有未匹配的 GET 请求
  app.get('/{*path}', async (req, res, next) => {
    const pageContext = await renderPage({
      urlOriginal: req.originalUrl,
      headersOriginal: req.headers,
      cookies: req.cookies,
    });

    if (!pageContext.httpResponse) return next();

    const { body, statusCode, headers } = pageContext.httpResponse;
    headers.forEach(([name, value]) => res.setHeader(name, value));
    res.status(statusCode).send(body);
  });

  app.listen(3000);
}

重点说明:

  1. Express 5 路由语法app.get('/{*path}', ...) — Express 5 使用命名通配符,不再支持 app.get('*', ...)
  2. pageContext 初始化headersOriginalcookies 被传入 pageContext,供 +guard.ts+data.ts 中的 SSR API 调用使用(转发原始请求头实现登录态传递)
  3. Mock API 位于 Vike 渲染之前:确保 API 请求不会被 Vike 拦截

7. Vike 页面约定与 Hook 体系

Vike 的核心理念:通过 + 前缀文件约定替代路由配置。每个约定文件承担特定职责,按以下顺序执行:

请求进入 → +guard.ts(权限验证)→ +data.ts(数据预取)→ +Page.vue(页面渲染)
                                                          ↑
                                              +Layout.vue 包裹
                                              +Head.vue 注入 <head>

7.1 +config.ts — 全局/页面级配置

全局配置 src/pages/+config.ts

import vikeVue from 'vike-vue/config';
import type { Config } from 'vike/types';

export default {
  extends: [vikeVue],   // 继承 vike-vue 默认行为
  title: 'Admin',
  passToClient: ['user', 'locale', 'permissionResult', 'routeName'],
  meta: {
    permissionUrls: {
      env: { server: true, client: true },  // 自定义配置项,服务端和客户端均可访问
    },
  },
} satisfies Config;
  • passToClient — 指定哪些 pageContext 属性传递到客户端(SSR → CSR 数据桥接)
  • meta.permissionUrls — 声明自定义页面配置项,用于权限验证

页面级配置 src/pages/permission/+config.ts

import { PERMISSION_APIS } from '../../constants/permissionApis';

export default {
  title: '权限列表',
  permissionUrls: [
    PERMISSION_APIS.LIST,
    PERMISSION_APIS.CREATE,
    PERMISSION_APIS.UPDATE,
    PERMISSION_APIS.DELETE,
  ],
};

每个页面的 +config.ts 中的 permissionUrls 会被 +guard.ts 读取,用于权限验证。权限 URL 常量统一定义在 src/constants/permissionApis.ts 中。

7.2 +onCreateApp.ts — Vue 应用创建钩子

每次渲染(SSR 和 CSR)都会执行此钩子,用于注册全局插件和指令:

import type { OnCreateAppSync } from 'vike-vue/types';
import { createPinia } from 'pinia';
import { ID_INJECTION_KEY, ZINDEX_INJECTION_KEY } from 'element-plus';
import { createI18n } from '#i18n/i18n';
import directives from '#directive/directive';

const onCreateApp: OnCreateAppSync = (pageContext) => {
  const { app } = pageContext;

  // 1. Pinia 状态管理
  app.use(createPinia());

  // 2. Vue I18n 国际化
  app.use(createI18n());

  // 3. Element Plus SSR 兼容 — 必须 provide ID 和 ZIndex
  app.provide(ID_INJECTION_KEY, { prefix: 1024, current: 0 });
  app.provide(ZINDEX_INJECTION_KEY, { current: 0 });

  // 4. 自定义指令
  Object.entries(directives).forEach(([name, directive]) => {
    app.directive(name, directive);
  });
};

export default onCreateApp;

7.3 +Layout.vue — 全局布局

公共 Layout 包裹所有页面,集成侧边栏、顶部导航、Element Plus 配置提供者:

<template>
  <el-config-provider :locale="elementLocale">
    <div class="app-layout">
      <aside v-if="layoutStore.showSidebar" :class="['app-sidebar', { collapsed: layoutStore.sidebarCollapsed }]">
        <AppSidebar :menus="defaultMenus" :collapsed="layoutStore.sidebarCollapsed" />
      </aside>
      <div class="app-main">
        <AppHeader
          v-if="layoutStore.showHeader"
          :breadcrumbs="layoutStore.breadcrumbs"
          :header-actions="layoutStore.headerActions"
          @toggle-sidebar="layoutStore.toggleSidebar()"
        />
        <main class="app-content">
          <slot />  <!-- 页面内容插入点 -->
        </main>
      </div>
    </div>
  </el-config-provider>
</template>

<script lang="ts" setup>
import 'element-plus/dist/index.css';   // 手动引入样式(SSR 兼容)
import '#scss/common.scss';

// ...组件引入与状态管理
</script>

7.4 +Head.vue — 全局 HTML Head

<template>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
</template>

7.5 +guard.ts — 路由守卫(权限验证)

核心权限验证机制,在 SSR 阶段拦截请求:

import type { GuardAsync } from 'vike/types';
import { render } from 'vike/abort';

const guard: GuardAsync = async (pageContext) => {
  const permissionUrls = (pageContext.config as any).permissionUrls;

  // 没有配置权限 URL 的页面,直接放行
  if (!permissionUrls || permissionUrls.length === 0) return;

  // SSR 时调用后台权限验证接口
  if (typeof window === 'undefined') {
    try {
      const { createDefaultAPI } = await import('#api/createServerApi');
      const port = process.env.PORT || 3000;
      const alova = createDefaultAPI({
        baseURL: `http://localhost:${port}/api/v1`,
        headers: (pageContext as any).headersOriginal,  // 转发原始请求头
      });

      const result = await alova.Post('/permission/check', {
        urls: permissionUrls,
        pagePath: pageContext.urlPathname,
      });

      if (!result?.data?.allowed) {
        throw render(403);  // 渲染 403 错误页
      }

      // 权限结果存入 pageContext,传到客户端
      (pageContext as any).permissionResult = result.data;
    } catch (error) {
      if ((error as any)?.isAbort) throw error;  // 已是 abort 直接抛出
      throw render(403);  // 异常也视为无权限
    }
  }
};

7.6 +data.ts — SSR 数据预取

在服务端获取数据,通过 useData() 在页面组件中使用:

// src/pages/index/+data.ts
import type { PageContextServer } from 'vike/types';
import { createDefaultAPI } from '#api/createServerApi';

const SSR_API_BASE = `http://localhost:${process.env.PORT || 3000}/api/v1`;

export type Data = DashboardStats;

export async function data(_pageContext: PageContextServer): Promise<Data> {
  const alova = createDefaultAPI({
    baseURL: SSR_API_BASE,
    headers: (_pageContext as any).headersOriginal,
  });

  const res = await alova.Get('/dashboard/stats');
  return res.data;
}

注意+data.ts 的继承问题 — 子路由会继承父目录的 +data.ts。如果子页面不需要父级数据,需要创建空的 +data.ts 来阻止继承:

// src/pages/permission/add/+data.ts
export type Data = Record<string, never>;
export async function data() { return {}; }

7.7 +Page.vue — 页面组件

每个目录下的 +Page.vue 即该路由对应的页面组件。通过 useData() 获取 SSR 预取数据:

<script lang="ts" setup>
import { useData } from 'vike-vue/useData';
import type { Data } from './+data';

const data = useData<Data>();  // 类型安全地获取 SSR 数据
</script>

7.8 _error/+Page.vue — 错误页面

统一的错误页面,支持 401/403/404/500:

<script lang="ts" setup>
import { usePageContext } from 'vike-vue/usePageContext';

const pageContext = usePageContext();

const errorCode = computed(() => {
  return pageContext.is404 ? 404 : (pageContext.abortStatusCode || 500);
});
</script>

+guard.tsthrow render(403) 时,Vike 会自动渲染 _error/+Page.vue 并传递 abortStatusCode: 403


8. 状态管理 — Pinia

8.1 全局状态 (global.ts)

// src/stores/global.ts
import { defineStore } from 'pinia';

export const useGlobalStore = defineStore('global', {
  state: () => ({
    env: '',
    lang: 'zh-CN',
    user: null as null | { name: string; role: string },
  }),
  actions: {
    updateEnv(env: string) { this.env = env; },
    updateLang(lang: string) { this.lang = lang; },
    updateUser(user: { name: string; role: string } | null) { this.user = user; },
  },
});

8.2 布局状态 (layout.ts)

// src/stores/layout.ts
export const useLayoutStore = defineStore('layout', {
  state: () => ({
    title: '',
    breadcrumbs: [] as BreadcrumbItem[],
    sidebarMenus: [] as MenuItem[],
    showSidebar: true,
    showHeader: true,
    sidebarCollapsed: false,
    headerActions: [] as HeaderAction[],
  }),
  actions: {
    setTitle(title: string) { this.title = title; },
    setHeaderActions(actions: HeaderAction[]) { this.headerActions = actions; },
    clearHeaderActions() { this.headerActions = []; },
    setBreadcrumbs(items: BreadcrumbItem[]) { this.breadcrumbs = items; },
    toggleSidebar() { this.sidebarCollapsed = !this.sidebarCollapsed; },
    resetLayout() {
      this.title = '';
      this.breadcrumbs = [];
      this.headerActions = [];
      this.showSidebar = true;
      this.showHeader = true;
    },
  },
});

类型定义:

export interface BreadcrumbItem {
  label: string;
  path?: string;
}

export interface MenuItem {
  label: string;
  path: string;
  icon?: string;
  children?: MenuItem[];
}

export interface HeaderAction {
  key: string;
  label: string;
  icon?: string;
  type?: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'default';
  handler: () => void;
}

9. 国际化 — Vue I18n

9.1 创建 I18n 实例

// src/i18n/i18n.ts
import { createI18n as _createI18n } from 'vue-i18n';
import zhCN from '#i18n/zh-CN.json';
import enUS from '#i18n/en-US.json';

export const LANGUAGE = {
  ZH_CN: 'zh-CN',
  EN_US: 'en-US',
} as const;

export function createI18n() {
  return _createI18n({
    legacy: false,          // 使用 Composition API
    locale: LANGUAGE.ZH_CN, // 默认中文
    fallbackLocale: LANGUAGE.ZH_CN,
    messages: {
      [LANGUAGE.ZH_CN]: zhCN,
      [LANGUAGE.EN_US]: enUS,
    },
  });
}

9.2 语言包结构

// zh-CN.json
{
  "app": { "title": "管理后台" },
  "error": {
    "unauthorized": "登录已过期,请重新登录",
    "forbidden": "暂无权限访问此页面",
    "notFound": "页面不存在",
    "serverError": "服务器内部错误,请稍后重试"
  },
  "menu": {
    "home": "首页",
    "permission": "权限管理",
    "permissionList": "权限列表",
    "permissionAdd": "新增权限"
  },
  "common": { "add": "新增", "edit": "编辑", "delete": "删除", ... },
  "permission": { "name": "权限名称", "code": "权限编码", ... },
  "dashboard": { "totalPermissions": "总权限数", ... }
}

9.3 在组件中使用

<script setup>
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
</script>

<template>
  <span>{{ t('app.title') }}</span>
  <span>{{ t('menu.home') }}</span>
</template>

9.4 菜单配置与 i18n

菜单的 label 字段使用 i18n key,在渲染时通过 t() 翻译:

// src/constants/menu.ts
export const SIDEBAR_MENUS: MenuItem[] = [
  { label: 'menu.home', path: '/', icon: 'House' },
  {
    label: 'menu.permission', path: '/permission', icon: 'Lock',
    children: [
      { label: 'menu.permissionList', path: '/permission' },
      { label: 'menu.permissionAdd', path: '/permission/add' },
    ],
  },
];

10. API 层 — Alova + Axios

项目使用 Alova 作为请求策略层,底层适配 Axios。分为客户端和服务端两套实例。

10.1 核心实例管理 (alovaInstance.ts)

// src/api/alovaInstance.ts

// API 类型枚举
export const API_TYPE = { DEFAULT: 'default', LOCAL: 'local' } as const;

// 基础 URL 映射
export const API_BASE_URL = {
  [API_TYPE.DEFAULT]: '/api/v1',
  [API_TYPE.LOCAL]: '/local-api',
};

// 统一请求工厂
export function apiCreator(options: ApiOption, data?: any, customInstances?: AlovaInstances) {
  const { method = 'get', type = API_TYPE.DEFAULT, pathVariable, ...restOptions } = options;
  const instance = getAlovaInstance(customInstances, type);

  let { url = '' } = restOptions;
  if (pathVariable) url = templateUrl(url, pathVariable);  // URL 模板变量替换

  const methodName = method.charAt(0).toUpperCase() + method.slice(1);

  if (['Post', 'Put', 'Patch', 'Delete'].includes(methodName)) {
    return instance[methodName](url, data, restOptions);
  }
  return instance[methodName](url, { params: data, ...restOptions });
}

10.2 客户端 API (createClientApi.ts)

import { createAlova } from 'alova';
import VueHook from 'alova/vue';
import { axiosRequestAdapter } from '@alova/adapter-axios';

export function createClientAlova({ baseURL, timeout = 30000 }) {
  return createAlova({
    baseURL,
    timeout,
    cacheFor: null,        // 禁用缓存
    statesHook: VueHook,   // 绑定 Vue 响应式
    requestAdapter: axiosRequestAdapter(),
    responded: {
      onSuccess: async (response) => response.data,  // 自动解包 Axios 响应
      onError: (error) => { throw error; },
    },
  });
}

10.3 服务端 API (createServerApi.ts)

export function createServerAlova({ baseURL, headers, timeout = 30000 }) {
  return createAlova({
    baseURL,
    timeout,
    cacheFor: null,
    statesHook: VueHook,
    requestAdapter: axiosRequestAdapter(),
    beforeRequest(method) {
      // 转发原始请求头(携带 Cookie/Authorization 等)
      if (headers) {
        Object.assign(method.config, {
          headers: { ...method.config.headers, ...headers },
        });
      }
    },
    responded: {
      onSuccess: async (response) => response.data,
      onError: (error) => { throw error; },
    },
  });
}

客户端 vs 服务端的关键差异:服务端实例在 beforeRequest 中转发原始请求头(headersOriginal),用于传递登录态(Cookie、Token)。服务端还需要使用绝对 URLhttp://localhost:3000/api/v1)而非相对路径。

10.4 业务 API 定义

业务 API 通过 apiCreator 统一创建,例如权限 API:

// src/api/permissionApi.ts
import { apiCreator, API_TYPE } from '#api/alovaInstance';

export function fetchPermissionList(params, options?, customInstances?) {
  return apiCreator(
    { ...options, method: 'get', url: '/permissions', type: API_TYPE.DEFAULT },
    params, customInstances,
  );
}

export function createPermission(data, options?, customInstances?) {
  return apiCreator(
    { ...options, method: 'post', url: '/permissions', type: API_TYPE.DEFAULT },
    data, customInstances,
  );
}

11. Layout 系统

11.1 公共布局与页面自定义

设计理念:Layout 是全局公共的,但每个页面可以通过 Pinia Store 暴露的方法来修改布局状态。

+Layout.vue(全局布局)
    ├── AppSidebar(侧边栏 — 读取 layoutStore.sidebarMenus)
    ├── AppHeader(顶部栏 — 读取 layoutStore.breadcrumbs / headerActions)
    └── <slot />(页面内容)
            ↑
    页面在 onMounted 中调用 useLayout() 设置标题、面包屑、按钮等

11.2 useLayout 组合式函数

// src/composables/useLayout.ts
export function useLayout() {
  const layoutStore = useLayoutStore();

  onMounted(() => {
    layoutStore.resetLayout();  // 每次页面挂载时重置布局状态
  });

  return {
    setTitle(title: string) { layoutStore.setTitle(title); },
    setBreadcrumbs(items: BreadcrumbItem[]) { layoutStore.setBreadcrumbs(items); },
    setHeaderActions(actions: HeaderAction[]) { layoutStore.setHeaderActions(actions); },
    setShowSidebar(show: boolean) { layoutStore.setShowSidebar(show); },
    setShowHeader(show: boolean) { layoutStore.setShowHeader(show); },
    toggleSidebar() { layoutStore.toggleSidebar(); },
    clearHeaderActions() { layoutStore.clearHeaderActions(); },
  };
}

页面中使用示例:

<script lang="ts" setup>
import { onMounted } from 'vue';
import { useLayout } from '#composables/useLayout';
import { useI18n } from 'vue-i18n';

const { t } = useI18n();
const layout = useLayout();

onMounted(() => {
  layout.setTitle(t('menu.home'));
  layout.setBreadcrumbs([{ label: t('menu.home') }]);
  layout.setHeaderActions([
    { key: 'refresh', label: '刷新', type: 'primary', handler: () => loadData() },
  ]);
});
</script>

11.3 AppSidebar 组件

<!-- src/layout/AppSidebar.vue -->
<template>
  <div class="sidebar-menu">
    <div class="logo">
      <span class="logo-text">{{ t('app.title') }}</span>
    </div>
    <el-menu :default-active="activePath" :collapse="collapsed" @select="handleSelect">
      <template v-for="item in menus" :key="item.path">
        <el-sub-menu v-if="item.children?.length" :index="item.path">
          <template #title>
            <el-icon v-if="item.icon"><component :is="item.icon" /></el-icon>
            <span>{{ t(item.label) }}</span>
          </template>
          <el-menu-item v-for="child in item.children" :key="child.path" :index="child.path">
            {{ t(child.label) }}
          </el-menu-item>
        </el-sub-menu>
        <el-menu-item v-else :index="item.path">
          <el-icon v-if="item.icon"><component :is="item.icon" /></el-icon>
          <span>{{ t(item.label) }}</span>
        </el-menu-item>
      </template>
    </el-menu>
  </div>
</template>

<script lang="ts" setup>
import { navigate } from 'vike/client/router';

function handleSelect(index: string) {
  navigate(index);  // 使用 Vike 的 navigate 进行客户端路由跳转
}
</script>

重要:不能使用 Element Plus 的 router prop,因为它依赖 Vue Router。Vike 项目中应使用 @select 事件 + navigate() 手动导航。

11.4 AppHeader 组件

<!-- src/layout/AppHeader.vue -->
<template>
  <div class="app-header">
    <div class="header-left">
      <el-icon class="toggle-btn" @click="emit('toggle-sidebar')">
        <Fold v-if="!collapsed" /><Expand v-else />
      </el-icon>
      <el-breadcrumb separator="/">
        <el-breadcrumb-item v-for="item in breadcrumbs" :key="item.label" :to="item.path">
          {{ item.label }}
        </el-breadcrumb-item>
      </el-breadcrumb>
    </div>
    <div class="header-right">
      <!-- 页面自定义按钮区域 -->
      <el-button v-for="action in headerActions" :key="action.key" :type="action.type" @click="action.handler">
        {{ action.label }}
      </el-button>
      <!-- 用户信息 -->
      <el-dropdown>
        <span class="user-info">
          <el-icon><User /></el-icon> {{ user?.name || '未登录' }}
        </span>
      </el-dropdown>
    </div>
  </div>
</template>

12. Element Plus 集成(SSR 兼容)

在 SSR 项目中集成 Element Plus 需要解决三个问题:

12.1 CSS 加载问题

问题unplugin-vue-components 默认会自动导入组件对应的 CSS 文件,但 SSR 时 Node.js 无法处理 .css 文件。

解决方案

// vite.config.ts
Components({
  resolvers: [ElementPlusResolver({ importStyle: false })],  // 禁用自动导入样式
}),
<!-- +Layout.vue 中手动全量引入 -->
<script setup>
import 'element-plus/dist/index.css';
</script>

12.2 ID 注入问题

问题ElementPlusError: [IdInjection] Looks like you are using server rendering, you must provide a id provider

解决方案

// +onCreateApp.ts
import { ID_INJECTION_KEY, ZINDEX_INJECTION_KEY } from 'element-plus';

app.provide(ID_INJECTION_KEY, { prefix: 1024, current: 0 });
app.provide(ZINDEX_INJECTION_KEY, { current: 0 });

12.3 Locale 国际化

<!-- +Layout.vue -->
<template>
  <el-config-provider :locale="elementLocale">
    <!-- ... -->
  </el-config-provider>
</template>

<script setup>
import zhCN from 'element-plus/es/locale/lang/zh-cn';
import enUS from 'element-plus/es/locale/lang/en';

const elementLocale = computed(() => locale.value === 'en-US' ? enUS : zhCN);
</script>

13. 权限系统

13.1 权限 URL 统一管理

所有需要权限验证的 API URL 统一在 src/constants/permissionApis.ts 中管理:

// src/constants/permissionApis.ts
export const PERMISSION_APIS = {
  /** 查询权限列表 */
  LIST: 'GET /api/v1/permissions',
  /** 新增权限 */
  CREATE: 'POST /api/v1/permissions',
  /** 编辑权限 */
  UPDATE: 'PUT /api/v1/permissions',
  /** 删除权限 */
  DELETE: 'DELETE /api/v1/permissions',
} as const;

注意+config.ts 文件由 vike 的 esbuild 插件编译,不支持 Vite 路径别名(#constants/...)。因此在 +config.ts 中必须使用相对路径导入常量,而在 +Page.vue 中可正常使用 # 别名。

各页面 +config.ts 中按需声明所需的权限 URL:

// src/pages/permission/+config.ts(列表页 — 需要所有操作权限)
import { PERMISSION_APIS } from '../../constants/permissionApis';

export default {
  title: '权限列表',
  permissionUrls: [
    PERMISSION_APIS.LIST,
    PERMISSION_APIS.CREATE,
    PERMISSION_APIS.UPDATE,
    PERMISSION_APIS.DELETE,
  ],
};
// src/pages/permission/add/+config.ts(新增页 — 只需 CREATE 权限)
import { PERMISSION_APIS } from '../../../constants/permissionApis';

export default {
  title: '新增权限',
  permissionUrls: [PERMISSION_APIS.CREATE],
};

13.2 页面级权限 — +guard.ts

流程:

  1. 页面在 +config.ts 中声明 permissionUrls(引用统一常量)
  2. +guard.ts 读取该配置,在 SSR 阶段调用后端 POST /api/v1/permission/check
  3. 后端返回 { allowed: true/false, urlPermissions: { [url]: boolean } }
  4. allowed: falsethrow render(403),整页渲染错误页(如新增权限页无 CREATE 权限)
  5. allowed: true 时将 urlPermissions 写入 pageContext.permissionResult,通过 passToClient 传到客户端

13.3 按钮级权限 — usePermission

通过 usePermission() 组合式函数在组件中检查单个 URL 的权限,控制按钮 disabled 状态:

// src/composables/usePermission.ts
export function usePermission() {
  const pageContext = usePageContext();
  const permissionResult = computed(() => (pageContext as any).permissionResult || {});

  function hasPermission(url: string): boolean {
    return permissionResult.value?.urlPermissions?.[url] ?? true;
  }

  return { permissionResult, hasPermission };
}

列表页使用示例(控制添加/编辑/删除按钮):

<script setup>
import { usePermission } from '#composables/usePermission';
import { PERMISSION_APIS } from '#constants/permissionApis';

const { hasPermission } = usePermission();
const canCreate = hasPermission(PERMISSION_APIS.CREATE);
const canUpdate = hasPermission(PERMISSION_APIS.UPDATE);
const canDelete = hasPermission(PERMISSION_APIS.DELETE);
</script>

<template>
  <el-button type="primary" :disabled="!canCreate" @click="handleAdd">新增</el-button>
  <!-- 表格操作列 -->
  <el-button :disabled="!canUpdate" @click="handleEdit(row)">编辑</el-button>
  <el-button :disabled="!canDelete" @click="handleDelete(row)">删除</el-button>
</template>

编辑页使用示例(通过 canSubmit prop 控制表单保存按钮):

<PermissionForm
  :initial-data="detail"
  :is-sending="isSending"
  :can-submit="canUpdate"
  @submit="submit"
  @cancel="goBack"
/>

PermissionForm.vue 中保存按钮根据 canSubmit 属性禁用:

<el-button type="primary" :loading="isSending" :disabled="canSubmit === false" @click="submit">
  {{ t('common.save') }}
</el-button>

13.4 Mock 权限验证(server/server.ts)

开发阶段通过 Mock 接口模拟权限检查:

// 模拟无权限的 URL 列表
const DENIED_URLS = new Set([
  'POST /api/v1/permissions',   // 新增权限
  'PUT /api/v1/permissions',    // 编辑权限
]);

// pagePath + URL 命中时整页拒绝(403)
const PAGE_BLOCKED_RULES = [
  { pathPattern: /^\/permission\/add$/, url: 'POST /api/v1/permissions' },
];

app.post('/api/v1/permission/check', (req, res) => {
  const { urls = [], pagePath = '' } = req.body;
  const urlPermissions = {};
  urls.forEach((url) => { urlPermissions[url] = !DENIED_URLS.has(url); });

  // 命中 PAGE_BLOCKED_RULES 则整页拒绝
  const allowed = !PAGE_BLOCKED_RULES.some(
    (rule) => rule.pathPattern.test(pagePath) && urls.includes(rule.url) && DENIED_URLS.has(rule.url),
  );

  res.json({ code: 0, data: { allowed, urlPermissions } });
});
  • DENIED_URLS — 控制哪些 URL 返回无权限(按钮 disabled)
  • PAGE_BLOCKED_RULES — 当特定页面路径命中被拒绝的 URL 时,整页返回 403

13.5 权限流程图

用户请求页面
    │
    ▼
+guard.ts 读取 +config.ts 中的 permissionUrls(引用 PERMISSION_APIS 常量)
    │
    ├── 未配置 → 直接放行
    │
    └── 已配置 → SSR 调用 POST /api/v1/permission/check { urls, pagePath }
                    │
                    ├── allowed: false → throw render(403) → 渲染 _error/+Page.vue
                    │   (如: /permission/add 页面无 CREATE 权限 → 整页 403)
                    │
                    └── allowed: true → permissionResult 存入 pageContext
                            │
                            └── 组件中通过 usePermission().hasPermission(url) 判断
                                    │
                                    ├── true  → 按钮正常可用
                                    └── false → 按钮 disabled
                                        (如: 编辑页无 UPDATE 权限 → 保存按钮禁用)

14. 路由与导航

14.1 文件系统路由

Vike 根据 src/pages/ 目录结构自动生成路由:

目录结构 路由路径 说明
pages/index/+Page.vue / 首页
pages/permission/+Page.vue /permission 权限列表
pages/permission/add/+Page.vue /permission/add 新增权限
pages/permission/@id/edit/+Page.vue /permission/:id/edit 编辑权限(动态路由)
pages/_error/+Page.vue 错误页面 401/403/404/500

@id 是 Vike 的动态路由语法,等效于 Vue Router 的 :id。通过 pageContext.routeParams.id 获取。

14.2 客户端导航

Vike 提供 navigate 函数实现客户端路由跳转(无刷新):

import { navigate } from 'vike/client/router';

// 跳转到指定页面
navigate('/permission');

// 跳转并替换历史记录
navigate('/permission', { overwriteLastHistoryEntry: true });

+config.ts 中已设置 clientRouting: true(由 vike-vue 默认配置),启用客户端路由。


15. 业务页面示例

15.1 Dashboard 首页

文件src/pages/index/

文件 作用
+config.ts 配置标题 '首页'
+data.ts SSR 调用 /api/v1/dashboard/stats 预取统计数据
+Page.vue 通过 useData() 获取数据,展示统计卡片和操作日志表格
<script setup>
const data = useData<Data>();  // SSR 预取的数据,无需 onMounted 加载
const statCards = computed(() => [
  { key: 'total', label: t('dashboard.totalPermissions'), value: data.totalPermissions },
  // ...
]);
</script>

15.2 权限列表页

文件src/pages/permission/

文件 作用
+config.ts 配置标题 + permissionUrls(启用权限验证)
+data.ts SSR 预取第一页权限列表
+Page.vue 展示列表 + 搜索 + 分页

SSR + CSR 混合:首页数据通过 SSR 预取,后续翻页/搜索通过客户端 Alova 调用。

15.3 新增权限页

文件src/pages/permission/add/

文件 作用
+config.ts 配置标题 + permissionUrls
+data.ts 空 data 文件(阻止继承父级的 +data.ts)
+Page.vue 使用 PermissionForm 组件

关键:必须创建空的 +data.ts,否则会继承 permission/+data.ts 的数据加载逻辑,导致不需要的 API 调用甚至报错。

15.4 编辑权限页

文件src/pages/permission/@id/edit/

与新增页类似,额外通过 pageContext.routeParams.id 获取路由参数,在 onMounted 中加载详情数据:

<script setup>
const pageContext = usePageContext();
const routeParams = pageContext.routeParams as { id: string };

onMounted(() => {
  fetchDetail(routeParams.id);
});
</script>

15.5 可复用组件 — PermissionForm

src/viewComponents/permission/PermissionForm.vue 同时服务于新增和编辑页面:

<script setup>
const props = defineProps<{
  initialData?: Record<string, any>;  // 编辑时传入已有数据
  isSending?: boolean;                // 提交中状态
  canSubmit?: boolean;                // 是否有提交权限(false 时禁用保存按钮)
}>();

const emit = defineEmits<{
  submit: [data: Record<string, any>];
  cancel: [];
}>();

// 表单验证规则
const rules: FormRules = {
  name: [{ required: true, message: '请输入权限名称', trigger: 'blur' }],
  code: [{ required: true, message: '请输入权限编码', trigger: 'blur' }],
  type: [{ required: true, message: '请选择权限类型', trigger: 'change' }],
};
</script>

16. SSR 与 CSR 策略

场景 策略 实现方式
首屏数据 SSR +data.tsuseData()
权限验证 SSR +guard.tsthrow render(403)
翻页/搜索 CSR 组件内直接使用客户端 Alova
表单提交 CSR 组件内调用 API 后 navigate()
页面跳转 CSR navigate() 客户端路由
初始页面加载 SSR Express → renderPage() → HTML

数据流:

SSR 阶段:
  Express → renderPage() → +guard.ts → +data.ts → +Layout.vue + +Page.vue → HTML

CSR 阶段 (客户端路由):
  navigate() → +guard.ts (client) → +data.ts → 组件更新

17. 关键踩坑与解决方案

17.1 Express 5 路由语法变更

问题app.get('*', ...) 报错 Missing parameter name

原因:Express 5 使用新版 path-to-regexp,不再支持裸通配符

解决:改为命名通配符 app.get('/{*path}', ...)

17.2 Element Plus CSS SSR 加载失败

问题TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".css"

原因:Node.js SSR 环境无法处理 CSS 文件

解决

  • ElementPlusResolver({ importStyle: false }) 禁用自动导入样式
  • +Layout.vueimport 'element-plus/dist/index.css'(Vite 会正确处理)

17.3 Element Plus SSR ID/ZIndex 注入

问题:Hydration 失败,控制台报 IdInjectionZIndexInjection 错误

解决:在 +onCreateApp.tsapp.provide(ID_INJECTION_KEY, ...)app.provide(ZINDEX_INJECTION_KEY, ...)

17.4 服务端 API 调用使用相对 URL

问题+data.ts+guard.ts 中使用 /api/v1/xxx 相对路径在 SSR 中无法工作

原因:Node.js 中没有浏览器的 location.origin,相对 URL 无法解析

解决:SSR 中使用绝对 URL http://localhost:${process.env.PORT || 3000}/api/v1

17.5 +data.ts 的继承问题

问题/permission/add 页面继承了 /permission/+data.ts 的数据加载,导致不必要的 API 调用

原因:Vike 的 +data.ts 会沿目录树向上继承

解决:在子目录创建空的 +data.ts

export type Data = Record<string, never>;
export async function data() { return {}; }

17.6 El-Menu 的 router prop 不兼容 Vike

问题:侧边栏菜单点击无反应或报错

原因:Element Plus 的 el-menu router prop 依赖 Vue Router,Vike 项目不使用 Vue Router

解决:移除 router prop,使用 @select 事件 + navigate():

<el-menu @select="handleSelect">
  <!-- ... -->
</el-menu>

<script setup>
import { navigate } from 'vike/client/router';
function handleSelect(index: string) {
  navigate(index);
}
</script>

17.7 process.env 在客户端不可用

问题ReferenceError: process is not defined

原因+guard.ts 在客户端也会执行,但 process.env 仅在 Node.js 中可用

解决:将 process.env 访问放在 if (typeof window === 'undefined') 分支内


18. 开发与构建命令

# 开发(启动 Express + Vite HMR)
npm run dev

# 构建(生成 dist/client + dist/server)
npm run build

# 生产预览
npm run preview

# 代码检查
npm run lint

# 自动修复
npm run fix

开发环境tsx server/server.ts → Express 启动 → createDevMiddleware 注入 Vite HMR → 访问 http://localhost:3000

生产构建vike build → 输出 dist/client(静态资源)+ dist/server(SSR Bundle)


19. 生产部署

19.1 构建产物结构

执行 npm run build(即 vike build)后生成 dist/ 目录:

dist/
├── assets.json                    # 资源映射文件(Vike 内部使用)
├── client/                        # 静态资源(浏览器端)
│   └── assets/
│       ├── chunks/                # JS 代码分割块
│       ├── entries/               # 各页面入口 JS
│       └── static/               # CSS 文件
└── server/                        # SSR 服务端代码
    ├── entry.mjs                  # SSR 入口(Vike renderPage 用)
    ├── entries/                   # 各页面的 SSR 渲染逻辑
    ├── chunks/                    # 服务端公共模块
    └── package.json               # { "type": "module" }

19.2 部署方式

本项目使用 Express 作为生产服务器server/server.ts 同时处理静态文件托管和 SSR 渲染。部署步骤:

1. 构建

npm run build

2. 部署所需文件

将以下文件/目录上传到服务器:

dist/                # 构建产物(client + server)
server/server.ts     # Express 服务器入口
package.json         # 依赖声明
node_modules/        # 或在服务器上 npm install

3. 启动服务

# 方式一:直接用 tsx 运行 TypeScript(需安装 tsx)
cross-env NODE_ENV=production tsx server/server.ts

# 方式二:用 PM2 管理进程(推荐)
pm2 start "cross-env NODE_ENV=production tsx server/server.ts" --name vike-admin

# 自定义端口
cross-env NODE_ENV=production PORT=8080 tsx server/server.ts

运行原理: server/server.ts 中根据 NODE_ENV 自动切换行为:

if (isProd) {
  // 生产环境:sirv 托管 dist/client 静态文件
  const sirv = (await import('sirv')).default;
  app.use(sirv(`${root}/dist/client`));
} else {
  // 开发环境:Vite HMR 开发中间件
  const { devMiddleware } = await createDevMiddleware({ root });
  app.use(devMiddleware);
}

Vike 的 renderPage() 在生产环境会自动加载 dist/server/entry.mjs 进行 SSR 渲染。

19.3 Nginx 反向代理(可选)

如果需要通过 Nginx 暴露服务:

server {
    listen 80;
    server_name your-domain.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

19.4 Docker 部署(可选)

FROM node:20-alpine
WORKDIR /app

COPY package.json yarn.lock ./
RUN yarn install --production=false

COPY . .
RUN yarn build

ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000

CMD ["npx", "tsx", "server/server.ts"]
docker build -t vike-admin .
docker run -d -p 3000:3000 vike-admin

19.5 注意事项

事项 说明
NODE_ENV 必须设为 production,否则会尝试启动 Vite 开发中间件
Mock API 生产环境应替换为真实后端 API 代理,移除 Mock 路由
tsx 生产环境仍需 tsx 来运行 TypeScript 的 server.ts,也可预编译为 JS
端口 默认 3000,可通过 PORT 环境变量修改
dist/ 路径 server.ts 通过 __dirname + '/.. 定位 dist,部署时保持目录相对关系

本文档对应项目版本:2026-02-12 · Vike 0.4.252 · Vue 3.5 · Element Plus 2.9 · Express 5.2

❌