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. 开发效率:自动导入、热更新等开发体验优化
相关推荐
Ulyanov2 小时前
基于Impress.js的3D概念地图设计与实现
开发语言·前端·javascript·3d·ecmascript
小白菜学前端2 小时前
Git 推送 Vue 项目到远程仓库完整流程
前端·git
A南方故人2 小时前
一个用于实时检测 web 应用更新的 JavaScript 库
开发语言·前端·javascript
悟能不能悟2 小时前
postman怎么获取上一个接口执行完后的参数
前端·javascript·postman
小程故事多_802 小时前
穿透 AI 智能面纱:三大高危漏洞(RCE/SSRF/XSS)的攻防博弈与全生命周期防护
前端·人工智能·aigc·xss
koiy.cc2 小时前
新建 vue3 项目
前端·vue.js
虹科网络安全2 小时前
艾体宝新闻 | NPM 生态系统陷入困境:自我传播恶意软件在大规模供应链攻击中感染了 187 个软件包
前端·npm·node.js
qq_12498707532 小时前
基于springboot+vue的家乡特色旅游宣传推荐系统(源码+论文+部署+安装)
java·前端·vue.js·spring boot·毕业设计·计算机毕设·计算机毕业设计
pas1362 小时前
38-mini-vue 实现解析 element
前端·javascript·vue.js