mcp-swagger-server/docs/mcp-swagger-ui-development-...

16 KiB
Raw Blame History

MCP Swagger UI 开发指南

快速开始

环境要求

  • Node.js: 18.0+ (推荐使用 LTS 版本)
  • npm/pnpm: 最新版本
  • TypeScript: 5.0+
  • 现代浏览器: Chrome 90+, Firefox 88+, Safari 14+

安装和启动

# 1. 进入项目目录
cd packages/mcp-swagger-ui

# 2. 安装依赖
npm install

# 3. 启动开发服务器
npm run dev

# 4. 在浏览器中访问
# http://localhost:3000

项目脚本

npm run dev        # 启动开发服务器
npm run build      # 构建生产版本
npm run preview    # 预览生产构建
npm run type-check # TypeScript 类型检查
npm run lint       # ESLint 代码检查
npm run lint:fix   # 自动修复 ESLint 问题

开发环境配置

VS Code 推荐扩展

创建 .vscode/extensions.json:

{
  "recommendations": [
    "Vue.volar",                    // Vue 3 语言支持
    "Vue.vscode-typescript-vue-plugin", // Vue TypeScript 支持
    "bradlc.vscode-tailwindcss",    // Tailwind CSS 支持
    "esbenp.prettier-vscode",       // 代码格式化
    "dbaeumer.vscode-eslint",       // ESLint 集成
    "ms-vscode.vscode-typescript-next", // TypeScript 支持
    "formulahendry.auto-rename-tag", // 自动重命名标签
    "christian-kohler.path-intellisense" // 路径智能提示
  ]
}

VS Code 工作区设置

创建 .vscode/settings.json:

{
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "[vue]": {
    "editor.defaultFormatter": "Vue.volar"
  },
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "typescript.preferences.importModuleSpecifier": "relative",
  "volar.completion.autoImportComponent": true
}

项目结构详解

目录组织原则

src/
├── components/     # 可复用组件
│   ├── common/     # 通用组件
│   ├── forms/      # 表单组件
│   └── display/    # 展示组件
├── views/          # 页面组件
├── stores/         # Pinia 状态管理
├── utils/          # 工具函数
├── types/          # TypeScript 类型定义
├── assets/         # 静态资源
├── styles/         # 全局样式
└── router/         # 路由配置

命名规范

  • 组件文件: PascalCase (如 ApiPreview.vue)
  • 工具文件: camelCase (如 apiUtils.ts)
  • 类型文件: PascalCase 接口 + camelCase 文件 (如 types/index.ts)
  • 样式类: kebab-case (如 .api-preview)

核心开发模式

1. 组件开发模式

单文件组件结构

<template>
  <!-- 模板使用 Element Plus 组件 -->
</template>

<script setup lang="ts">
// Composition APITypeScript 优先
import { ref, computed, watch } from 'vue'
import type { ApiEndpoint } from '@/types'

// Props 定义
interface Props {
  endpoints: ApiEndpoint[]
  loading?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  loading: false
})

// Emits 定义
interface Emits {
  update: [endpoints: ApiEndpoint[]]
  error: [message: string]
}

const emit = defineEmits<Emits>()

// 响应式状态
const selectedEndpoint = ref<ApiEndpoint | null>(null)

// 计算属性
const filteredEndpoints = computed(() => {
  return props.endpoints.filter(ep => !ep.deprecated)
})

// 方法
const handleSelect = (endpoint: ApiEndpoint) => {
  selectedEndpoint.value = endpoint
  emit('update', [endpoint])
}
</script>

<style scoped>
/* 组件作用域样式 */
.api-endpoint {
  @apply p-4 rounded-lg border border-gray-200;
}
</style>

组件测试

// components/__tests__/ApiPreview.test.ts
import { mount } from '@vue/test-utils'
import ApiPreview from '../ApiPreview.vue'
import type { ApiEndpoint } from '@/types'

describe('ApiPreview', () => {
  const mockEndpoints: ApiEndpoint[] = [
    {
      method: 'GET',
      path: '/users',
      summary: 'Get users'
    }
  ]

  it('renders endpoints correctly', () => {
    const wrapper = mount(ApiPreview, {
      props: { endpoints: mockEndpoints }
    })
    
    expect(wrapper.text()).toContain('Get users')
  })
})

2. 状态管理模式

Store 结构

// stores/feature.ts
import { defineStore } from 'pinia'

export const useFeatureStore = defineStore('feature', {
  state: () => ({
    data: [] as FeatureItem[],
    loading: false,
    error: null as string | null
  }),

  getters: {
    itemCount: (state) => state.data.length,
    hasError: (state) => !!state.error
  },

  actions: {
    async fetchData() {
      this.loading = true
      this.error = null
      
      try {
        const response = await api.getData()
        this.data = response.data
      } catch (error) {
        this.error = error.message
      } finally {
        this.loading = false
      }
    },

    updateItem(id: string, updates: Partial<FeatureItem>) {
      const index = this.data.findIndex(item => item.id === id)
      if (index !== -1) {
        this.data[index] = { ...this.data[index], ...updates }
      }
    }
  }
})

在组件中使用 Store

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useFeatureStore } from '@/stores/feature'

const featureStore = useFeatureStore()
const { data, loading, error } = storeToRefs(featureStore)

// 响应式地使用 store 数据
const handleRefresh = () => {
  featureStore.fetchData()
}
</script>

3. API 服务模式

API 服务结构

// services/api.ts
import axios from 'axios'
import type { ApiResponse, PaginatedResponse } from '@/types'

class ApiService {
  private readonly baseURL: string
  private readonly timeout: number

  constructor(baseURL: string, timeout = 10000) {
    this.baseURL = baseURL
    this.timeout = timeout
  }

  // 通用请求方法
  private async request<T>(
    method: 'GET' | 'POST' | 'PUT' | 'DELETE',
    url: string,
    data?: any
  ): Promise<ApiResponse<T>> {
    try {
      const response = await axios({
        method,
        url: `${this.baseURL}${url}`,
        data,
        timeout: this.timeout,
        headers: {
          'Content-Type': 'application/json'
        }
      })

      return {
        success: true,
        data: response.data
      }
    } catch (error) {
      return {
        success: false,
        error: error.message
      }
    }
  }

  // 具体 API 方法
  async validateOpenApi(spec: string): Promise<ApiResponse<ValidationResult>> {
    return this.request('POST', '/validate', { spec })
  }

  async convertToMcp(config: ConvertConfig): Promise<ApiResponse<McpResult>> {
    return this.request('POST', '/convert', config)
  }
}

export const apiService = new ApiService(
  import.meta.env.VITE_API_BASE_URL || '/api'
)

4. 类型定义模式

类型组织

// types/api.ts - API 相关类型
export interface ApiResponse<T = any> {
  success: boolean
  data?: T
  error?: string
  message?: string
}

export interface PaginatedResponse<T> extends ApiResponse<T[]> {
  pagination: {
    page: number
    pageSize: number
    total: number
    totalPages: number
  }
}

// types/domain.ts - 业务领域类型
export interface User {
  id: string
  name: string
  email: string
  createdAt: Date
  updatedAt: Date
}

export interface CreateUserRequest {
  name: string
  email: string
}

// types/ui.ts - UI 相关类型
export interface TabItem {
  key: string
  label: string
  icon?: string
  disabled?: boolean
}

export interface FormState {
  values: Record<string, any>
  errors: Record<string, string>
  touched: Record<string, boolean>
}

样式开发指南

Apple 风格设计系统

颜色系统

// styles/variables.scss
:root {
  // 主色调
  --color-primary: #667eea;
  --color-primary-dark: #5a67d8;
  --color-primary-light: #7c3aed;

  // 功能色
  --color-success: #10b981;
  --color-warning: #f59e0b;
  --color-error: #ef4444;
  --color-info: #3b82f6;

  // 中性色
  --color-gray-50: #f9fafb;
  --color-gray-100: #f3f4f6;
  --color-gray-200: #e5e7eb;
  --color-gray-300: #d1d5db;
  --color-gray-400: #9ca3af;
  --color-gray-500: #6b7280;
  --color-gray-600: #4b5563;
  --color-gray-700: #374151;
  --color-gray-800: #1f2937;
  --color-gray-900: #111827;

  // 渐变
  --gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  --gradient-surface: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%);
}

组件样式模式

// styles/components.scss
.card {
  background: var(--gradient-surface);
  border-radius: 12px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
  border: 1px solid var(--color-gray-200);
  transition: all 0.3s ease;

  &:hover {
    box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
    transform: translateY(-2px);
  }

  &__header {
    padding: 20px 24px;
    border-bottom: 1px solid var(--color-gray-200);
    font-weight: 600;
    color: var(--color-gray-800);
  }

  &__body {
    padding: 24px;
  }
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 12px 24px;
  border-radius: 8px;
  font-weight: 500;
  transition: all 0.2s ease;
  cursor: pointer;
  border: none;

  &--primary {
    background: var(--gradient-primary);
    color: white;

    &:hover {
      transform: translateY(-1px);
      box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
    }
  }

  &--secondary {
    background: white;
    color: var(--color-gray-700);
    border: 1px solid var(--color-gray-300);

    &:hover {
      background: var(--color-gray-50);
      border-color: var(--color-gray-400);
    }
  }
}

响应式设计

// styles/responsive.scss
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 20px;

  @media (max-width: 768px) {
    padding: 0 16px;
  }
}

.grid {
  display: grid;
  gap: 24px;

  &--auto-fit {
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  }

  &--responsive {
    grid-template-columns: repeat(3, 1fr);

    @media (max-width: 1024px) {
      grid-template-columns: repeat(2, 1fr);
    }

    @media (max-width: 768px) {
      grid-template-columns: 1fr;
    }
  }
}

调试和测试

开发调试

Vue DevTools

// 在开发环境中启用 Vue DevTools
if (import.meta.env.DEV) {
  const { createApp } = await import('vue')
  const { createPinia } = await import('pinia')
  
  const app = createApp(App)
  app.use(createPinia())
  
  // 启用 Vue DevTools
  app.config.devtools = true
}

控制台调试

// utils/debug.ts
export const debug = {
  log: (message: string, data?: any) => {
    if (import.meta.env.DEV) {
      console.log(`[DEBUG] ${message}`, data)
    }
  },
  
  error: (message: string, error?: any) => {
    if (import.meta.env.DEV) {
      console.error(`[ERROR] ${message}`, error)
    }
  },
  
  table: (data: any) => {
    if (import.meta.env.DEV) {
      console.table(data)
    }
  }
}

// 在组件中使用
import { debug } from '@/utils/debug'

const handleApiCall = async () => {
  debug.log('Starting API call', { endpoint: '/api/convert' })
  try {
    const result = await apiService.convert(config)
    debug.log('API call successful', result)
  } catch (error) {
    debug.error('API call failed', error)
  }
}

单元测试

测试配置

// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    globals: true
  },
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  }
})

测试示例

// src/components/__tests__/ApiPreview.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import ApiPreview from '../ApiPreview.vue'

describe('ApiPreview', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })

  it('displays API information correctly', () => {
    const props = {
      apiInfo: {
        title: 'Test API',
        version: '1.0.0',
        description: 'Test description'
      }
    }

    const wrapper = mount(ApiPreview, { props })

    expect(wrapper.find('[data-testid="api-title"]').text()).toBe('Test API')
    expect(wrapper.find('[data-testid="api-version"]').text()).toBe('1.0.0')
  })

  it('emits events correctly', async () => {
    const wrapper = mount(ApiPreview)
    
    await wrapper.find('[data-testid="refresh-button"]').trigger('click')
    
    expect(wrapper.emitted('refresh')).toHaveLength(1)
  })
})

性能优化

代码分割

// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue'), // 懒加载
  },
  {
    path: '/settings',
    name: 'Settings',
    component: () => import('@/views/Settings.vue'),
  }
]

组件懒加载

<!-- 在组件中使用 Suspense -->
<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div class="loading">Loading...</div>
    </template>
  </Suspense>
</template>

<script setup lang="ts">
import { defineAsyncComponent } from 'vue'

const AsyncComponent = defineAsyncComponent(
  () => import('@/components/HeavyComponent.vue')
)
</script>

虚拟滚动

<!-- 对于大列表使用虚拟滚动 -->
<template>
  <el-virtual-list
    :data="largeDataset"
    :height="400"
    :item-size="50"
  >
    <template #default="{ item }">
      <div class="list-item">{{ item.name }}</div>
    </template>
  </el-virtual-list>
</template>

部署指南

环境变量配置

# .env.production
VITE_APP_TITLE=MCP Swagger Server
VITE_API_BASE_URL=https://api.example.com
VITE_ENABLE_DEMO_MODE=false

Docker 部署

# Dockerfile
FROM node:18-alpine as builder

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

静态部署

# 构建生产版本
npm run build

# 部署到静态文件服务器
# dist/ 目录包含所有需要的文件

常见问题和解决方案

1. TypeScript 类型错误

// 使用类型断言
const element = document.getElementById('app') as HTMLElement

// 使用可选链
const value = response.data?.user?.name || 'Unknown'

// 定义完整的类型
interface ApiResponse<T> {
  success: boolean
  data?: T
  error?: string
}

2. Element Plus 样式问题

<!-- 使用 :deep() 修改第三方组件样式 -->
<style scoped>
:deep(.el-button) {
  border-radius: 8px;
}

:deep(.el-input__inner) {
  border-color: var(--color-primary);
}
</style>

3. 路由导航问题

// 使用 router.push 进行编程式导航
import { useRouter } from 'vue-router'

const router = useRouter()

const navigateToHome = () => {
  router.push({ name: 'Home' })
}

4. 状态持久化

// 使用 localStorage 持久化状态
import { defineStore } from 'pinia'

export const useAppStore = defineStore('app', {
  state: () => ({
    userPreferences: {}
  }),
  
  actions: {
    loadFromStorage() {
      const saved = localStorage.getItem('app-state')
      if (saved) {
        this.$patch(JSON.parse(saved))
      }
    },
    
    saveToStorage() {
      localStorage.setItem('app-state', JSON.stringify(this.$state))
    }
  }
})

这个开发指南提供了完整的开发流程、最佳实践和常见问题解决方案,帮助开发者快速上手并高效地开发 MCP Swagger UI 项目。