【DS】vue3项目数据配置架构

【问】
开发一个vue3项目,如何配置兼顾以下需求:
1、dev环境下后端数据用mock配置,随时可调试
2、随时可以build,但build后后端数据采用本地静态数据。

【答】
针对你的需求,这里提供一个完整的 Vue3 项目配置方案。这个方案使用不同的环境变量来控制数据源,实现开发环境用 mock,生产环境用静态数据。

项目结构

project/
├── src/
│   ├── api/
│   │   ├── index.js           # 统一API入口
│   │   ├── mock/              # Mock数据
│   │   │   ├── user.js
│   │   │   └── product.js
│   │   ├── local/             # 本地静态数据
│   │   │   ├── user.js
│   │   │   └── product.js
│   │   └── services/          # 真实API服务(可选)
│   ├── plugins/
│   │   └── api.js            # API插件配置
│   └── main.js
├── public/
│   └── data/                 # 构建后的静态数据文件
├── .env.development
├── .env.production
├── .env.local
└── vue.config.js

1. 环境变量配置

.env.development (开发环境 - 使用 mock)

VUE_APP_API_SOURCE=mock
VUE_APP_API_BASE_URL=http://localhost:3000/api

.env.production (生产环境 - 使用静态数据)

VUE_APP_API_SOURCE=local
VUE_APP_API_BASE_URL=/data
NODE_ENV=production

.env.local (本地调试 - 可切换到真实API)

VUE_APP_API_SOURCE=real  # 可选:mock, local, real
VUE_APP_API_BASE_URL=http://real-api.com/api

2. 数据层配置

src/api/index.js - 统一API入口

import mockAPI from './mock'
import localAPI from './local'
import realAPI from './services'

const apiSources = {
  mock: mockAPI,
  local: localAPI,
  real: realAPI
}

// 根据环境变量选择数据源
const apiSource = process.env.VUE_APP_API_SOURCE || 'mock'
export default apiSources[apiSource]

src/api/mock/user.js - Mock 数据示例

import Mock from 'mockjs' // 可以使用 mockjs 或直接返回数据

export default {
  // 模拟用户列表
  getUsers() {
    return Promise.resolve({
      code: 200,
      data: [
        { id: 1, name: '张三', age: 25 },
        { id: 2, name: '李四', age: 30 }
      ]
    })
  },
  
  // 模拟用户详情
  getUserById(id) {
    return Promise.resolve({
      code: 200,
      data: { id, name: '模拟用户', age: 28 }
    })
  }
}

src/api/local/user.js - 本地静态数据

// 本地静态数据
const localData = {
  users: [
    { id: 1, name: '静态用户1', age: 25 },
    { id: 2, name: '静态用户2', age: 30 }
  ]
}

export default {
  // 获取用户列表
  getUsers() {
    return Promise.resolve({
      code: 200,
      data: localData.users
    })
  },
  
  // 获取用户详情
  getUserById(id) {
    const user = localData.users.find(u => u.id === id)
    return Promise.resolve({
      code: user ? 200 : 404,
      data: user || null
    })
  }
}

3. Vue 插件配置

src/plugins/api.js - API插件

import api from '../api'

export default {
  install(app) {
    // 全局注入API
    app.config.globalProperties.$api = api
    
    // 提供注入方式
    app.provide('api', api)
  }
}

src/main.js - 主入口

import { createApp } from 'vue'
import App from './App.vue'
import apiPlugin from './plugins/api'

const app = createApp(App)

// 注册API插件
app.use(apiPlugin)

app.mount('#app')

4. 组件中使用示例

src/components/UserList.vue

<template>
  <div>
    <h2>用户列表</h2>
    <ul>
      <li v-for="user in users" :key="user.id">
        {{ user.name }} - {{ user.age }}
      </li>
    </ul>
  </div>
</template>

<script setup>
import { ref, onMounted, inject } from 'vue'

const api = inject('api') // 使用依赖注入
const users = ref([])

onMounted(async () => {
  const response = await api.user.getUsers()
  if (response.code === 200) {
    users.value = response.data
  }
})
</script>

5. 构建配置

vue.config.js - Vue CLI配置

const path = require('path')

module.exports = {
  // 构建时复制静态数据到public目录
  chainWebpack: config => {
    // 开发环境配置
    config.when(process.env.NODE_ENV === 'development', config => {
      config.plugin('define').tap(args => {
        args[0]['process.env'].VUE_APP_API_SOURCE = JSON.stringify('mock')
        return args
      })
    })
    
    // 生产环境配置
    config.when(process.env.NODE_ENV === 'production', config => {
      config.plugin('define').tap(args => {
        args[0]['process.env'].VUE_APP_API_SOURCE = JSON.stringify('local')
        return args
      })
    })
  },
  
  // 开发服务器代理配置(如果需要)
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        pathRewrite: {
          '^/api': ''
        }
      }
    },
    // 开发环境mock服务
    setupMiddlewares: (middlewares, devServer) => {
      if (!devServer) {
        throw new Error('webpack-dev-server is not defined')
      }
      
      // 可以在这里添加mock中间件
      // devServer.app.use('/api', mockMiddleware)
      
      return middlewares
    }
  }
}

6. 使用 Vite 的配置(如果使用 Vite)

vite.config.js

import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '')
  
  return {
    plugins: [vue()],
    
    // 环境变量配置
    define: {
      'process.env.VUE_APP_API_SOURCE': JSON.stringify(
        mode === 'production' ? 'local' : 'mock'
      )
    },
    
    server: {
      proxy: {
        '/api': {
          target: 'http://localhost:3000',
          changeOrigin: true
        }
      }
    },
    
    build: {
      // 构建时将静态数据复制到dist目录
      assetsDir: 'data',
      rollupOptions: {
        // 如果有额外的静态数据文件需要复制
        // input: {
        //   main: resolve(__dirname, 'index.html'),
        // },
      }
    }
  }
})

7. 快速切换数据源的脚本

package.json 中添加脚本

{
  "scripts": {
    "serve": "vue-cli-service serve",
    "serve:mock": "VUE_APP_API_SOURCE=mock vue-cli-service serve",
    "serve:local": "VUE_APP_API_SOURCE=local vue-cli-service serve",
    "serve:real": "VUE_APP_API_SOURCE=real vue-cli-service serve",
    "build": "vue-cli-service build",
    "build:mock": "VUE_APP_API_SOURCE=mock vue-cli-service build",
    "build:local": "VUE_APP_API_SOURCE=local vue-cli-service build"
  }
}

8. 高级功能:自动切换

如果需要更智能的切换,可以创建一个配置文件:

src/config/api.config.js

// 根据环境自动选择API源
const getApiSource = () => {
  if (import.meta.env?.PROD) {
    return 'local' // 生产环境用静态数据
  }
  
  // 开发环境可以根据需要配置
  const configSource = import.meta.env?.VUE_APP_API_SOURCE
  return configSource || 'mock' // 默认使用mock
}

export const API_SOURCE = getApiSource()
export const API_BASE_URL = import.meta.env?.VUE_APP_API_BASE_URL || ''

使用建议

  1. 开发时:使用 npm run servenpm run serve:mock 启动mock服务
  2. 调试静态数据:使用 npm run serve:local 验证静态数据
  3. 构建测试npm run build 会使用本地静态数据构建
  4. 特殊构建npm run build:mock 可以构建带mock数据的版本(用于某些测试场景)

这种架构的好处:

  • 清晰的关注点分离
  • 易于切换数据源
  • 便于测试和调试
  • 构建后的包不包含mock逻辑,体积更小
  • 可以根据需要轻松扩展新的数据源

【问】考虑到在远程服务器上为项目提供预览,如下考量和结构是最合理的吗?
【答】
你的结构基本合理,但针对远程服务器预览和静态数据部署的场景,我建议进行一些优化。以下是改进方案:

🔧 优化后的项目结构

project/
├── src/
│   ├── api/
│   │   ├── index.ts          # API入口:根据环境选择数据源
│   │   ├── types/            # TypeScript类型定义
│   │   │   ├── user.ts
│   │   │   └── product.ts
│   │   ├── adapters/         # 数据适配器(核心)
│   │   │   ├── mock.adapter.ts    # Mock适配器
│   │   │   ├── static.adapter.ts  # 静态数据适配器
│   │   │   └── real.adapter.ts    # 真实API适配器
│   │   └── services/         # API函数定义
│   │       ├── user.service.ts
│   │       └── product.service.ts
│   ├── plugins/
│   │   └── api.ts           # API插件
│   └── main.ts
├── public/
│   ├── static-data/         # 构建后的静态JSON文件(自动生成)
│   │   ├── users.json
│   │   └── products.json
│   └── api-mock/            # 用于预览的Mock API(可选的纯静态方案)
│       ├── users/
│       │   ├── 1.json
│       │   └── index.json
│       └── products/
│           └── index.json
├── scripts/
│   ├── generate-static-data.js  # 自动生成静态数据
│   └── start-preview.js         # 启动预览服务器
├── .env
├── .env.development
├── .env.preview                # 新增:预览环境
├── .env.production
└── vite.config.ts

🎯 关键改进点

1. 环境配置细化

.env.preview(预览环境)

# 预览环境配置
VITE_APP_ENV=preview
VITE_APP_TITLE=预览环境
VITE_API_SOURCE=static     # 预览时使用静态数据
VITE_API_BASE_URL=./static-data  # 相对路径加载JSON
VITE_USE_MOCK=false
NODE_ENV=production

2. 数据适配器模式

src/api/adapters/static.adapter.ts - 静态数据适配器

import type { ApiAdapter } from './base.adapter'

export class StaticAdapter implements ApiAdapter {
  private baseUrl: string
  
  constructor(baseUrl: string = './static-data') {
    this.baseUrl = baseUrl
  }
  
  async request<T>(endpoint: string, options?: RequestInit): Promise<T> {
    // 静态数据适配逻辑
    const response = await fetch(`${this.baseUrl}/${endpoint}.json`)
    
    if (!response.ok) {
      throw new Error(`静态数据加载失败: ${endpoint}`)
    }
    
    return response.json()
  }
  
  // 特殊处理:分页、查询等
  async getUsers(params?: any) {
    const data = await this.request<{ users: any[] }>('users')
    // 在这里实现简单的分页/过滤逻辑
    return this.applyFilters(data.users, params)
  }
  
  private applyFilters(data: any[], params: any) {
    // 简单的客户端过滤
    let result = [...data]
    
    if (params?.page && params?.pageSize) {
      const start = (params.page - 1) * params.pageSize
      result = result.slice(start, start + params.pageSize)
    }
    
    if (params?.name) {
      result = result.filter(item => 
        item.name.includes(params.name)
      )
    }
    
    return result
  }
}

3. 构建时生成静态数据

scripts/generate-static-data.js

const fs = require('fs-extra')
const path = require('path')

async function generateStaticData() {
  const mockDir = path.join(__dirname, '../src/api/mock')
  const outputDir = path.join(__dirname, '../public/static-data')
  
  // 清空并创建目录
  await fs.emptyDir(outputDir)
  
  // 从mock文件生成静态数据
  const users = require(path.join(mockDir, 'user.js')).default
  const products = require(path.join(mockDir, 'product.js')).default
  
  // 生成JSON文件
  await fs.writeJson(
    path.join(outputDir, 'users.json'),
    await users.getUsers(),
    { spaces: 2 }
  )
  
  await fs.writeJson(
    path.join(outputDir, 'products.json'),
    await products.getProducts(),
    { spaces: 2 }
  )
  
  console.log('✅ 静态数据生成完成!')
}

// package.json中添加脚本
// "scripts": {
//   "build:preview": "node scripts/generate-static-data.js && vite build --mode preview"
// }

4. 预览服务器配置

scripts/start-preview.js(可选,用于纯静态预览)

const express = require('express')
const serveStatic = require('serve-static')
const path = require('path')

const app = express()
const port = 4173

// 静态文件服务
app.use(serveStatic(path.join(__dirname, '../dist'), {
  index: ['index.html']
}))

// Mock API路由(如果需要)
app.get('/api/users', (req, res) => {
  const data = require(path.join(__dirname, '../public/api-mock/users/index.json'))
  res.json(data)
})

app.listen(port, () => {
  console.log(`🚀 预览服务器运行在: http://localhost:${port}`)
})

5. 智能API选择器

src/api/index.ts - 核心选择逻辑

import { MockAdapter } from './adapters/mock.adapter'
import { StaticAdapter } from './adapters/static.adapter'
import { RealAdapter } from './adapters/real.adapter'
import type { ApiAdapter } from './adapters/base.adapter'

// 环境检测
const getAdapter = (): ApiAdapter => {
  const env = import.meta.env
  
  // 1. 命令行参数优先
  const urlParams = new URLSearchParams(window.location.search)
  if (urlParams.has('mock')) {
    console.log('🎭 使用Mock模式(URL参数)')
    return new MockAdapter()
  }
  
  // 2. 环境变量
  const apiSource = env.VITE_API_SOURCE
  
  switch (apiSource) {
    case 'mock':
      console.log('🎭 使用Mock模式')
      return new MockAdapter()
    
    case 'static':
      console.log('📄 使用静态数据模式')
      return new StaticAdapter(env.VITE_API_BASE_URL)
    
    case 'real':
      console.log('🌐 使用真实API模式')
      return new RealAdapter(env.VITE_API_BASE_URL)
    
    default:
      // 3. 自动检测
      if (env.DEV) {
        console.log('⚡ 开发环境,使用Mock模式')
        return new MockAdapter()
      } else {
        console.log('📦 生产环境,使用静态数据模式')
        return new StaticAdapter()
      }
  }
}

// 导出适配后的API
const adapter = getAdapter()
export const api = {
  user: {
    getUsers: (params) => adapter.getUsers(params),
    getUserById: (id) => adapter.get(`users/${id}`)
  },
  product: {
    getProducts: (params) => adapter.getProducts(params)
  }
}

6. Vite配置优化

vite.config.ts

import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import { viteMockServe } from 'vite-plugin-mock'

export default defineConfig(({ mode, command }) => {
  const env = loadEnv(mode, process.cwd(), '')
  
  return {
    plugins: [
      vue(),
      // 只在开发环境启用mock
      viteMockServe({
        enable: command === 'serve' && mode === 'development',
        mockPath: 'src/api/mock'
      })
    ],
    
    // 预览环境特殊配置
    build: {
      outDir: 'dist',
      assetsDir: 'assets',
      // 预览环境不压缩,便于调试
      minify: mode === 'preview' ? false : 'terser',
      sourcemap: mode === 'preview',
      rollupOptions: {
        output: {
          // 静态数据文件单独目录
          assetFileNames: (assetInfo) => {
            if (assetInfo.name?.endsWith('.json')) {
              return 'static-data/[name][extname]'
            }
            return 'assets/[name]-[hash][extname]'
          }
        }
      }
    },
    
    server: {
      port: 5173,
      proxy: mode === 'development' && env.VITE_API_SOURCE === 'real' ? {
        '/api': {
          target: env.VITE_API_BASE_URL,
          changeOrigin: true
        }
      } : undefined
    }
  }
})

7. package.json脚本

{
  "scripts": {
    "dev": "vite --mode development",
    "dev:mock": "vite --mode development",
    "dev:real": "vite --mode development --env-file .env.local",
    "build": "vite build --mode production",
    "build:preview": "npm run generate-static && vite build --mode preview",
    "preview": "vite preview --port 4173",
    "preview:static": "node scripts/start-preview.js",
    "generate-static": "node scripts/generate-static-data.js",
    "deploy:preview": "npm run build:preview && scp -r dist/* user@server:/path/to/preview"
  }
}

🚀 远程服务器部署流程

# 1. 生成预览版本(包含静态数据)
npm run build:preview

# 2. 部署到服务器
npm run deploy:preview

# 3. 或者使用更简单的部署(生成zip包)
npm run build:preview && zip -r preview.zip dist/

📊 不同环境的配置总结

环境 数据源 构建体积 部署方式 用途
开发 Mock API 本地 快速开发调试
预览 静态JSON 中等 远程服务器 给客户/产品预览
生产 真实API 最小 CDN 正式上线

你的结构分析及改进建议

你的结构优势:

  • ✅ 清晰的目录分离
  • ✅ 环境变量控制切换
  • ✅ 支持多种数据源

可改进点:

  1. 静态数据管理:建议使用构建脚本生成,而非手动维护
  2. 预览环境配置:需要专门的预览环境配置
  3. 数据适配器模式:统一接口,便于扩展
  4. 构建优化:静态数据单独处理,不污染构建包
  5. 远程部署:考虑静态服务器的路由问题(SPA需要处理404)

🎯 最终建议

对于远程预览服务器,最佳实践是:

# 1. 使用专门的预览环境
VITE_API_SOURCE=static

# 2. 构建时自动生成静态数据
npm run build:preview

# 3. 部署到支持SPA的静态服务器(如Nginx)
# Nginx配置添加:
# try_files $uri $uri/ /index.html;

# 4. 或者使用Vercel/Netlify等现代部署平台
# 它们自动处理SPA路由问题

这样配置后,你的项目可以:

  • 🚀 快速开发调试(Mock模式)
  • 🌐 随时构建预览版本(静态数据)
  • 📦 一键部署到远程服务器
  • 🔧 无缝切换到真实API

【补充:不懂就问】不懂就问,既然有了services层,为啥又要adapters层呢,是为了给services的api请求添加额外功能吗?
【答】
问得非常好!这是一个关于架构设计的核心问题。让我详细解释这两层的区别和设计目的。

📊 核心区别:关注点分离

Services 层:做什么 (What to do)

// services/user.service.ts
class UserService {
  // 定义业务逻辑接口
  async getUsers(params?: PaginationParams): Promise<User[]> {
    // 这里不关心数据从哪里来
    // 只定义需要获取用户数据
  }
  
  async createUser(userData: CreateUserDto): Promise<User> {
    // 定义创建用户的业务逻辑
  }
}

职责:

  • 定义业务接口(函数签名)
  • 声明需要的数据和参数
  • 业务逻辑的抽象定义

Adapters 层:怎么做 (How to do it)

// adapters/mock.adapter.ts
class MockUserAdapter implements UserAdapter {
  async getUsers(params?: PaginationParams): Promise<User[]> {
    // 具体实现:从Mock数据获取
    return mockData.users
  }
}

// adapters/real.adapter.ts  
class RealUserAdapter implements UserAdapter {
  async getUsers(params?: PaginationParams): Promise<User[]> {
    // 具体实现:从真实API获取
    return axios.get('/api/users', { params })
  }
}

职责:

  • 具体的数据获取实现
  • 处理不同数据源的差异
  • 数据格式转换

🎯 为什么需要两层?

场景1:切换数据源时

// ❌ 不好的设计:业务逻辑和数据获取耦合
class UserService {
  async getUsers() {
    // 直接调用axios
    if (process.env.USE_MOCK) {
      return mockData // Mock逻辑
    } else {
      return axios.get('/api/users') // 真实API逻辑
    }
  }
}
// ✅ 好的设计:分离关注点
// Service只定义接口
interface IUserService {
  getUsers(): Promise<User[]>
}

// Adapter实现具体逻辑
class MockUserService implements IUserService {
  async getUsers() { return mockData }
}

class RealUserService implements IUserService {
  async getUsers() { return axios.get('/api/users') }
}

// 使用时根据环境选择
const userService = isMock ? new MockUserService() : new RealUserService()

场景2:添加额外功能

// adapters/cache.adapter.ts
class CachedUserAdapter implements UserAdapter {
  constructor(private adapter: UserAdapter) {}
  
  async getUsers(params?: PaginationParams): Promise<User[]> {
    const cacheKey = `users-${JSON.stringify(params)}`
    
    // 1. 先检查缓存
    if (localStorage.getItem(cacheKey)) {
      return JSON.parse(localStorage.getItem(cacheKey)!)
    }
    
    // 2. 实际获取数据
    const data = await this.adapter.getUsers(params)
    
    // 3. 设置缓存
    localStorage.setItem(cacheKey, JSON.stringify(data))
    
    return data
  }
}

// 使用装饰器模式叠加功能
const adapter = new CachedUserAdapter(
  new LoggingUserAdapter(
    new RealUserAdapter()
  )
)

🔄 完整的数据流示例

// 1. 定义接口(Service层)
interface IUserService {
  getUsers(page: number, size: number): Promise<User[]>
  getUserById(id: number): Promise<User>
}

// 2. 适配器接口(Adapter层)
interface IUserAdapter extends IUserService {
  // 可能添加一些适配器特有的方法
  initialize?(): Promise<void>
}

// 3. 具体实现
// Mock适配器
class MockUserAdapter implements IUserAdapter {
  async getUsers(page: number, size: number) {
    // 从Mock.js生成数据
    return Mock.mock({
      'list|10': [{
        'id|+1': 1,
        'name': '@cname',
        'email': '@email'
      }]
    }).list
  }
}

// API适配器  
class ApiUserAdapter implements IUserAdapter {
  private client: AxiosInstance
  
  constructor(baseURL: string) {
    this.client = axios.create({ baseURL })
  }
  
  async getUsers(page: number, size: number) {
    const response = await this.client.get('/users', {
      params: { page, size }
    })
    return response.data
  }
}

// 4. 工厂函数创建实例
function createUserService(): IUserService {
  const env = import.meta.env
  
  let adapter: IUserAdapter
  
  if (env.VITE_API_SOURCE === 'mock') {
    adapter = new MockUserAdapter()
  } else if (env.VITE_API_SOURCE === 'static') {
    adapter = new StaticUserAdapter(env.VITE_API_BASE_URL)
  } else {
    adapter = new ApiUserAdapter(env.VITE_API_BASE_URL)
  }
  
  // 可以在这里添加统一的拦截器/装饰器
  if (env.VITE_ENABLE_CACHE) {
    return new CachedUserService(adapter)
  }
  
  return adapter
}

// 5. 在组件中使用
const userService = createUserService()

// 组件不关心数据来源
const users = await userService.getUsers(1, 10)

🎨 更直观的类比

概念 类比 说明
Service 餐厅菜单 定义有什么菜(接口)
Adapter 厨房/外卖 怎么做菜(实现)
组件使用 顾客点餐 只关心吃什么,不关心怎么做

📁 实际项目结构建议

src/
├── api/
│   ├── services/           # 定义业务接口
│   │   ├── user.service.ts    # interface IUserService
│   │   ├── product.service.ts # interface IProductService  
│   │   └── index.ts           # 导出所有接口
│   │
│   ├── adapters/           # 具体实现
│   │   ├── mock/              # Mock实现
│   │   │   ├── user.adapter.ts
│   │   │   └── product.adapter.ts
│   │   ├── http/              # HTTP API实现
│   │   │   ├── user.adapter.ts
│   │   │   └── product.adapter.ts
│   │   ├── static/            # 静态数据实现
│   │   │   ├── user.adapter.ts
│   │   │   └── product.adapter.ts
│   │   └── index.ts           # 适配器工厂
│   │
│   ├── types/              # TypeScript类型
│   │   ├── user.ts
│   │   ├── product.ts
│   │   └── common.ts
│   │
│   └── index.ts           # 统一入口
│
├── composables/           # 组合式函数
│   └── useUserApi.ts     # 封装Service使用
└── components/
    └── UserList.vue

🚀 简化的实际方案

如果你的项目不是特别复杂,可以简化:

// 简化版:直接在Service中根据环境切换
// src/api/services/user.service.ts
class UserService {
  private getAdapter() {
    if (import.meta.env.VITE_API_SOURCE === 'mock') {
      return {
        getUsers: () => mockData,
        getUserById: (id) => mockData.find(u => u.id === id)
      }
    } else {
      return {
        getUsers: (params) => axios.get('/api/users', { params }),
        getUserById: (id) => axios.get(`/api/users/${id}`)
      }
    }
  }
  
  async getUsers(params?: any) {
    const adapter = this.getAdapter()
    return adapter.getUsers(params)
  }
}

// 组件中使用
const userService = new UserService()
const users = await userService.getUsers()

总结:何时需要两层?

场景 建议 说明
小型项目 直接在Service中切换 快速开发,不必过度设计
中型项目 简单的Adapter模式 数据源可能变化
大型/复杂项目 完整的Service+Adapter 多数据源、需要装饰器模式、需要测试隔离

你的需求场景:需要Mock和静态数据切换,推荐使用简化Adapter模式,既能保持灵活性,又不会过度复杂化。

posted @ 2025-12-04 09:07  充实地生活着  阅读(2)  评论(0)    收藏  举报