Vue 3 面试题 - TypeScript 与工程化

目录

  1. [TypeScript 集成](#TypeScript 集成)
  2. 项目配置
  3. 测试
  4. 构建工具
  5. 最佳实践

TypeScript 集成

Q1: 如何在 Vue 3 中使用 TypeScript?

详细解答:

1. 基础配置

安装依赖:

bash 复制代码
npm install -D typescript @vue/tsconfig

tsconfig.json 配置:

json 复制代码
{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "jsx": "preserve",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    },
    "types": ["vite/client"]
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.d.ts",
    "src/**/*.tsx",
    "src/**/*.vue"
  ],
  "exclude": ["node_modules"]
}

vite-env.d.ts:

typescript 复制代码
/// <reference types="vite/client" />

declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

2. 组件类型定义

基础组件:

vue 复制代码
<script setup lang="ts">
import { ref, computed, type Ref } from 'vue'

// 基本类型
const count: Ref<number> = ref(0)
const message: Ref<string> = ref('Hello')

// 对象类型
interface User {
  id: number
  name: string
  email: string
  age?: number
}

const user: Ref<User | null> = ref(null)

// 计算属性类型推导
const doubled = computed(() => count.value * 2) // 自动推导为 ComputedRef<number>

// 函数类型
function increment(): void {
  count.value++
}

async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`)
  return response.json()
}
</script>

Props 类型:

vue 复制代码
<script setup lang="ts">
// 方法 1: 运行时声明
const props = defineProps({
  name: {
    type: String,
    required: true
  },
  age: {
    type: Number,
    default: 0
  }
})

// 方法 2: 类型声明(推荐)
interface Props {
  name: string
  age?: number
  tags?: string[]
  user?: User
}

const props = defineProps<Props>()

// 方法 3: 带默认值的类型声明
interface PropsWithDefaults {
  name: string
  age?: number
  tags?: string[]
}

const props = withDefaults(defineProps<PropsWithDefaults>(), {
  age: 0,
  tags: () => []
})

// 方法 4: 复杂类型
interface ComplexProps {
  // 联合类型
  status: 'pending' | 'success' | 'error'
  
  // 函数类型
  onClick: (event: MouseEvent) => void
  
  // 泛型
  items: Array<{ id: number; name: string }>
  
  // 可选回调
  onUpdate?: (value: string) => void
}

const props = defineProps<ComplexProps>()
</script>

Emits 类型:

vue 复制代码
<script setup lang="ts">
// 方法 1: 运行时声明
const emit = defineEmits(['update', 'delete'])

// 方法 2: 类型声明(推荐)
interface Emits {
  (e: 'update', id: number): void
  (e: 'delete', id: number): void
  (e: 'change', value: string): void
}

const emit = defineEmits<Emits>()

// 使用
emit('update', 123)
emit('delete', 456)
emit('change', 'new value')

// 方法 3: 带验证的声明
const emit = defineEmits({
  update: (payload: number) => {
    return payload > 0
  },
  delete: (payload: number) => {
    return payload > 0
  }
})
</script>

完整组件示例:

vue 复制代码
<!-- UserProfile.vue -->
<template>
  <div class="user-profile">
    <h2>{{ user?.name ?? 'Loading...' }}</h2>
    <p v-if="user">Email: {{ user.email }}</p>
    <p v-if="user && user.age">Age: {{ user.age }}</p>
    
    <button @click="handleUpdate">更新</button>
    <button @click="handleDelete">删除</button>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, type Ref } from 'vue'

// 类型定义
interface User {
  id: number
  name: string
  email: string
  age?: number
}

interface Props {
  userId: number
  canEdit?: boolean
}

interface Emits {
  (e: 'update', user: User): void
  (e: 'delete', id: number): void
}

// Props & Emits
const props = withDefaults(defineProps<Props>(), {
  canEdit: false
})

const emit = defineEmits<Emits>()

// State
const user: Ref<User | null> = ref(null)
const loading: Ref<boolean> = ref(false)
const error: Ref<Error | null> = ref(null)

// Methods
async function fetchUser(): Promise<void> {
  loading.value = true
  error.value = null
  
  try {
    const response = await fetch(`/api/users/${props.userId}`)
    if (!response.ok) {
      throw new Error('Failed to fetch user')
    }
    user.value = await response.json()
  } catch (err) {
    error.value = err as Error
  } finally {
    loading.value = false
  }
}

function handleUpdate(): void {
  if (user.value) {
    emit('update', user.value)
  }
}

function handleDelete(): void {
  emit('delete', props.userId)
}

// Lifecycle
onMounted(() => {
  fetchUser()
})

// Expose
defineExpose({
  fetchUser,
  user
})
</script>

3. Refs 类型

vue 复制代码
<script setup lang="ts">
import { ref, type Ref } from 'vue'
import type { ComponentPublicInstance } from 'vue'
import ChildComponent from './ChildComponent.vue'

// DOM 元素 ref
const inputRef = ref<HTMLInputElement | null>(null)
const divRef = ref<HTMLDivElement | null>(null)

// 组件实例 ref
const childRef = ref<InstanceType<typeof ChildComponent> | null>(null)

// 或者使用 ComponentPublicInstance
const childRef2 = ref<ComponentPublicInstance | null>(null)

onMounted(() => {
  // 访问 DOM 元素
  inputRef.value?.focus()
  
  // 访问组件方法
  childRef.value?.someMethod()
})
</script>

<template>
  <input ref="inputRef" />
  <div ref="divRef"></div>
  <ChildComponent ref="childRef" />
</template>

4. Composable 类型

typescript 复制代码
// composables/useFetch.ts
import { ref, type Ref } from 'vue'

interface UseFetchOptions {
  immediate?: boolean
  onSuccess?: (data: any) => void
  onError?: (error: Error) => void
}

interface UseFetchReturn<T> {
  data: Ref<T | null>
  error: Ref<Error | null>
  loading: Ref<boolean>
  execute: () => Promise<void>
}

export function useFetch<T = any>(
  url: string,
  options: UseFetchOptions = {}
): UseFetchReturn<T> {
  const { immediate = true, onSuccess, onError } = options
  
  const data: Ref<T | null> = ref(null)
  const error: Ref<Error | null> = ref(null)
  const loading: Ref<boolean> = ref(false)
  
  async function execute(): Promise<void> {
    loading.value = true
    error.value = null
    
    try {
      const response = await fetch(url)
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
      const result = await response.json()
      data.value = result
      
      if (onSuccess) {
        onSuccess(result)
      }
    } catch (err) {
      error.value = err as Error
      
      if (onError) {
        onError(err as Error)
      }
    } finally {
      loading.value = false
    }
  }
  
  if (immediate) {
    execute()
  }
  
  return {
    data,
    error,
    loading,
    execute
  }
}

// 使用
interface User {
  id: number
  name: string
  email: string
}

const { data, loading, error } = useFetch<User[]>('/api/users')

5. Pinia Store 类型

typescript 复制代码
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed, type Ref, type ComputedRef } from 'vue'

export interface User {
  id: number
  name: string
  email: string
  role: 'admin' | 'user' | 'guest'
}

interface LoginCredentials {
  email: string
  password: string
}

export const useUserStore = defineStore('user', () => {
  // State
  const user: Ref<User | null> = ref(null)
  const token: Ref<string> = ref('')
  const loading: Ref<boolean> = ref(false)
  
  // Getters
  const isLoggedIn: ComputedRef<boolean> = computed(() => !!token.value)
  const isAdmin: ComputedRef<boolean> = computed(() => user.value?.role === 'admin')
  
  // Actions
  async function login(credentials: LoginCredentials): Promise<boolean> {
    loading.value = true
    
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials)
      })
      
      if (!response.ok) {
        throw new Error('Login failed')
      }
      
      const data = await response.json()
      token.value = data.token
      user.value = data.user
      
      return true
    } catch (error) {
      console.error('Login error:', error)
      return false
    } finally {
      loading.value = false
    }
  }
  
  function logout(): void {
    user.value = null
    token.value = ''
  }
  
  return {
    user,
    token,
    loading,
    isLoggedIn,
    isAdmin,
    login,
    logout
  }
})

// 在组件中使用
import { useUserStore } from '@/stores/user'
import type { User } from '@/stores/user'

const userStore = useUserStore()

// 类型推导
const user: User | null = userStore.user
const isLoggedIn: boolean = userStore.isLoggedIn

6. 路由类型

typescript 复制代码
// router/index.ts
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'

// 路由元信息类型
declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean
    roles?: string[]
    title?: string
  }
}

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue'),
    meta: {
      title: 'Home'
    }
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: {
      requiresAuth: true,
      roles: ['admin'],
      title: 'Dashboard'
    }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

// 导航守卫类型
router.beforeEach((to, from, next) => {
  // to, from, next 都有完整的类型推导
  if (to.meta.requiresAuth) {
    // 检查认证
  }
  next()
})

export default router

项目配置

Q2: Vue 3 项目的 Vite 配置有哪些重要选项?

详细解答:

1. 基础 Vite 配置

typescript 复制代码
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  // 插件
  plugins: [
    vue({
      // Vue 插件选项
      script: {
        defineModel: true,
        propsDestructure: true
      },
      template: {
        compilerOptions: {
          // 自定义元素
          isCustomElement: (tag) => tag.startsWith('ion-')
        }
      }
    })
  ],
  
  // 路径解析
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
      '@components': resolve(__dirname, 'src/components'),
      '@utils': resolve(__dirname, 'src/utils'),
      '@assets': resolve(__dirname, 'src/assets')
    },
    extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
  },
  
  // CSS 配置
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/styles/variables.scss";`
      }
    },
    modules: {
      localsConvention: 'camelCase'
    }
  },
  
  // 开发服务器
  server: {
    port: 3000,
    open: true,
    cors: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  },
  
  // 构建选项
  build: {
    target: 'es2015',
    outDir: 'dist',
    assetsDir: 'assets',
    sourcemap: false,
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    },
    rollupOptions: {
      output: {
        // 分包策略
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui-vendor': ['element-plus'],
          'utils': ['axios', 'lodash-es']
        },
        // 文件名格式
        chunkFileNames: 'js/[name]-[hash].js',
        entryFileNames: 'js/[name]-[hash].js',
        assetFileNames: '[ext]/[name]-[hash].[ext]'
      }
    },
    chunkSizeWarningLimit: 1000
  },
  
  // 环境变量
  envPrefix: 'VITE_',
  
  // 优化选项
  optimizeDeps: {
    include: ['vue', 'vue-router', 'pinia'],
    exclude: ['some-large-library']
  }
})

2. 环境变量配置

bash 复制代码
# .env.development
VITE_APP_TITLE=My App (Dev)
VITE_API_BASE_URL=http://localhost:8080/api
VITE_APP_ENV=development

# .env.production
VITE_APP_TITLE=My App
VITE_API_BASE_URL=https://api.example.com
VITE_APP_ENV=production

# .env.staging
VITE_APP_TITLE=My App (Staging)
VITE_API_BASE_URL=https://staging-api.example.com
VITE_APP_ENV=staging
typescript 复制代码
// env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_APP_TITLE: string
  readonly VITE_API_BASE_URL: string
  readonly VITE_APP_ENV: 'development' | 'production' | 'staging'
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}
typescript 复制代码
// 使用环境变量
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL
const appTitle = import.meta.env.VITE_APP_TITLE

3. 常用插件配置

typescript 复制代码
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { visualizer } from 'rollup-plugin-visualizer'
import viteCompression from 'vite-plugin-compression'

export default defineConfig({
  plugins: [
    vue(),
    
    // 自动导入 API
    AutoImport({
      imports: [
        'vue',
        'vue-router',
        'pinia',
        {
          '@/utils': ['formatDate', 'debounce']
        }
      ],
      dts: 'src/auto-imports.d.ts',
      eslintrc: {
        enabled: true
      }
    }),
    
    // 自动导入组件
    Components({
      resolvers: [ElementPlusResolver()],
      dts: 'src/components.d.ts',
      dirs: ['src/components'],
      extensions: ['vue']
    }),
    
    // Gzip 压缩
    viteCompression({
      verbose: true,
      disable: false,
      threshold: 10240,
      algorithm: 'gzip',
      ext: '.gz'
    }),
    
    // 打包分析
    visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true,
      filename: 'dist/stats.html'
    })
  ]
})

测试

Q3: 如何在 Vue 3 项目中进行单元测试和集成测试?

详细解答:

1. Vitest 配置

bash 复制代码
npm install -D vitest @vue/test-utils happy-dom
typescript 复制代码
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  test: {
    globals: true,
    environment: 'happy-dom',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'dist/',
        '**/*.spec.ts',
        '**/*.test.ts'
      ]
    }
  },
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  }
})

2. 组件测试

typescript 复制代码
// Counter.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from '@/components/Counter.vue'

describe('Counter.vue', () => {
  it('renders initial count', () => {
    const wrapper = mount(Counter, {
      props: {
        initialCount: 5
      }
    })
    
    expect(wrapper.text()).toContain('5')
  })
  
  it('increments count when button clicked', async () => {
    const wrapper = mount(Counter)
    
    await wrapper.find('button').trigger('click')
    
    expect(wrapper.text()).toContain('1')
  })
  
  it('emits update event', async () => {
    const wrapper = mount(Counter)
    
    await wrapper.find('button').trigger('click')
    
    expect(wrapper.emitted('update')).toBeTruthy()
    expect(wrapper.emitted('update')?.[0]).toEqual([1])
  })
})

3. Composable 测试

typescript 复制代码
// useFetch.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useFetch } from '@/composables/useFetch'

global.fetch = vi.fn()

describe('useFetch', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })
  
  it('fetches data successfully', async () => {
    const mockData = { id: 1, name: 'Test' }
    
    vi.mocked(fetch).mockResolvedValueOnce({
      ok: true,
      json: async () => mockData
    } as Response)
    
    const { data, loading, error, execute } = useFetch('/api/test')
    
    await execute()
    
    expect(data.value).toEqual(mockData)
    expect(loading.value).toBe(false)
    expect(error.value).toBeNull()
  })
  
  it('handles errors', async () => {
    vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'))
    
    const { data, error, execute } = useFetch('/api/test')
    
    await execute()
    
    expect(data.value).toBeNull()
    expect(error.value).toBeInstanceOf(Error)
  })
})

4. Pinia Store 测试

typescript 复制代码
// user.spec.ts
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '@/stores/user'

describe('User Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })
  
  it('logs in successfully', async () => {
    const store = useUserStore()
    
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: async () => ({
        token: 'test-token',
        user: { id: 1, name: 'Test User' }
      })
    })
    
    const result = await store.login({
      email: 'test@example.com',
      password: 'password'
    })
    
    expect(result).toBe(true)
    expect(store.isLoggedIn).toBe(true)
    expect(store.user).toEqual({ id: 1, name: 'Test User' })
  })
})

构建工具

Q4: 如何优化 Vue 3 项目的构建配置?

详细解答:

1. 代码分割策略

typescript 复制代码
// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: (id) => {
          // node_modules 分包
          if (id.includes('node_modules')) {
            // Vue 全家桶单独打包
            if (id.includes('vue') || id.includes('pinia') || id.includes('vue-router')) {
              return 'vue-vendor'
            }
            
            // UI 框架单独打包
            if (id.includes('element-plus')) {
              return 'element-plus'
            }
            
            // 图表库单独打包
            if (id.includes('echarts')) {
              return 'echarts'
            }
            
            // 其他第三方库
            return 'vendor'
          }
          
          // 业务代码分包
          if (id.includes('src/views')) {
            const paths = id.split('/')
            const viewName = paths[paths.indexOf('views') + 1]
            return `view-${viewName}`
          }
        }
      }
    }
  }
})

2. 性能优化配置

typescript 复制代码
export default defineConfig({
  build: {
    // 启用 CSS 代码分割
    cssCodeSplit: true,
    
    // 生成 sourcemap
    sourcemap: process.env.NODE_ENV === 'development',
    
    // 压缩配置
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
        pure_funcs: ['console.log']
      }
    },
    
    // 警告阈值
    chunkSizeWarningLimit: 1000,
    
    // 关闭 brotli 压缩大小报告
    reportCompressedSize: false
  },
  
  // 依赖优化
  optimizeDeps: {
    // 强制包含
    include: ['vue', 'vue-router', 'pinia'],
    
    // 排除不需要优化的包
    exclude: ['some-esm-package']
  }
})

总结:

TypeScript 与工程化的关键点:

  1. 类型系统:完善的类型定义和类型推导
  2. 项目配置:合理的 Vite 配置和环境变量管理
  3. 代码质量:完善的测试覆盖和代码规范
  4. 构建优化:有效的代码分割和打包策略
  5. 开发效率:自动导入、热更新等开发体验优化
相关推荐
Cao_Ron2 分钟前
用 SVG image symbol 画 ECharts graph 圆形节点
前端
hxy06017 分钟前
React-Call让你的React组件像异步函数一样调用
前端·react.js
Hilaku8 分钟前
为什么 AI 写前端时,总是优先选择 React,而不是 Vue?
前端·javascript·程序员
京东云开发者10 分钟前
GPT-5.6、ChatGPT Work 与 Codex 更新食用指南
前端
Cao_Ron21 分钟前
用 ECharts graph 画出卡片式矩形节点
前端
skiyee29 分钟前
换个底座的 Wot Starter 居然让 AI 更懂项目!
前端·ai编程
anyup36 分钟前
这一套绝了!AI 大模型写故事,星云 SDK 驱动 3D 数字人实时演绎
前端·人工智能·aigc
耀耀切克闹灬1 小时前
短链跳转
前端
孟陬1 小时前
高质量全方位的 AI 工具 sse-stuntman — Mock SSE 接口
前端·node.js·openai
Cao_Ron1 小时前
给 ECharts 6 graph 节点叠加角标:从原理到性能优化
前端