Announcement

👇Official Account👇

Welcome to join the group & private message

Article first/tail QR code

Skip to content

Wails 教程系列 - 前端开发实战指南

💡 适合人群: 有一定前端基础的开发者,想学 Wails 桌面应用开发

📖 难度等级:

  • 🟢 新手: 会 Vue 3 基础语法就行
  • 🟡 进阶: 懂 TypeScript 和状态管理
  • 🔴 高手: 想搞架构设计和性能优化

前面几篇文章聊了 Wails 的环境搭建和项目创建,现在来聊聊前端开发这块。用户界面是应用的门面,做得好不好直接影响用户体验。

📋 目录

1. 快速上手 🚀

1.1 技术栈选择

现在 Wails 前端开发用的都是最新的技术栈,性能好、开发体验棒:

主要技术:

  • Vue 3.4+: 最新的组合式 API,性能提升明显
  • TypeScript 5.3+: 类型安全,减少 bug
  • Vite 5.0+: 构建速度快得飞起
  • Pinia 2.1+: 状态管理,比 Vuex 好用
  • VueUse: 一堆实用的工具函数,不用重复造轮子

1.2 项目初始化

bash
# 创建 Wails 项目
wails init -n my-app -t vue

# 进入项目目录
cd my-app

# 安装依赖
npm install

# 启动开发服务器
npm run dev

1.3 依赖配置

json
// package.json - 当前最新版本
{
  "name": "wails-frontend",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "preview": "vite preview",
    "type-check": "vue-tsc --noEmit",
    "test": "vitest",
    "test:coverage": "vitest --coverage"
  },
  "dependencies": {
    "vue": "^3.4.0",
    "vue-router": "^4.2.5",
    "pinia": "^2.1.7",
    "@vueuse/core": "^10.7.0",
    "element-plus": "^2.4.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.0.0",
    "@wailsapp/vite-plugin": "^1.0.0",
    "vite": "^5.0.0",
    "typescript": "^5.3.0",
    "vue-tsc": "^1.8.25",
    "@types/node": "^20.10.0",
    "vitest": "^1.0.0",
    "@vue/test-utils": "^2.4.0",
    "jsdom": "^23.0.0"
  }
}

2. 技术栈详解 💻

2.1 设计理念

Wails 前端开发有自己的一套设计理念,理解这些很重要:

核心原则:

  • 🔄 打包成一体: 前端代码最终会打包到 Go 程序里,变成一个文件
  • ⚡ 原生体验: 用 WebKit 渲染,不用担心浏览器兼容问题
  • 🚀 性能第一: 轻量设计,启动快、响应快
  • 🖥️ 桌面特性: 充分利用桌面应用的优势
  • 🛡️ 类型安全: 用 TypeScript 减少 bug
  • 🧪 好测试: 支持完整的测试流程

2.2 技术选择建议

技术适合什么项目优点需要注意什么
Vue 3.4+渐进式开发、组件化项目学习容易、生态丰富、性能好要会 Composition API 和 <script setup>
React 18+大型应用、复杂状态管理生态最全、并发特性强关注 Suspense 和并发渲染
Svelte 5对性能要求很高的应用编译优化、体积小、响应快社区小一点,资料相对少
原生 JS/TS轻量应用、学习练手完全控制、没有框架包袱要自己处理状态管理和路由

3. 开发实践 🔧

3.1 项目结构

项目结构组织得好,开发起来会更顺畅,建议这样安排:

frontend/
├── src/
│   ├── components/          # 可复用的组件
│   │   ├── ui/             # 基础 UI 组件(按钮、输入框等)
│   │   └── business/       # 业务组件(用户列表、订单等)
│   ├── views/              # 页面组件
│   ├── stores/             # 状态管理
│   ├── composables/        # 组合式函数
│   ├── utils/              # 工具函数
│   ├── types/              # 类型定义
│   ├── assets/             # 静态资源(图片、字体等)
│   ├── styles/             # 样式文件
│   ├── wailsjs/            # Wails 自动生成的代码
│   │   ├── go/             # Go 方法绑定
│   │   └── runtime/        # 运行时 API
│   ├── main.ts             # 应用入口
│   └── App.vue             # 根组件
├── public/                 # 公共资源
├── dist/                   # 构建输出
├── tests/                  # 测试文件
├── package.json            # 依赖配置
├── vite.config.ts          # Vite 配置
├── tsconfig.json           # TypeScript 配置
├── vitest.config.ts        # 测试配置
└── index.html              # HTML 模板

3.2 配置文件

Vite 配置

typescript
// vite.config.ts - 构建配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import { WailsPlugin } from '@wailsapp/vite-plugin'

export default defineConfig({
  plugins: [
    vue({
      // 支持 Vue 3.4+ 的新特性
      script: {
        defineModel: true,
        propsDestructure: true
      }
    }),
    WailsPlugin()
  ],
  
  // 构建配置
  build: {
    outDir: '../dist',
    emptyOutDir: true,
    target: 'esnext',
    minify: 'esbuild',
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router'],
          'wails-runtime': ['../wailsjs/runtime/runtime'],
          'ui-vendor': ['element-plus'] // 如果用了 UI 框架
        }
      }
    }
  },
  
  // 开发服务器配置
  server: {
    port: 34115,
    strictPort: true,
    hmr: {
      port: 34115,
      overlay: false // 避免热重载错误弹窗
    }
  },
  
  // 路径别名
  resolve: {
    alias: {
      '@': resolve(__dirname, './src'),
      'wailsjs': resolve(__dirname, './wailsjs')
    }
  },
  
  // 环境变量
  define: {
    __VUE_OPTIONS_API__: false, // 不用 Options API
    __VUE_PROD_DEVTOOLS__: false // 生产环境关掉 devtools
  }
})

TypeScript 配置

json
// tsconfig.json - 类型检查配置
{
  "compilerOptions": {
    "target": "ES2022",
    "useDefineForClassFields": true,
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitReturns": true,
    "noImplicitOverride": true,
    "exactOptionalPropertyTypes": true,
    
    // 路径别名
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "wailsjs/*": ["wailsjs/*"]
    },
    
    // 类型定义
    "types": ["vite/client", "node"]
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.d.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "wailsjs/**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "dist"
  ]
}

3.3 Vue 3.4+ 实践

组件示例

vue
<!-- components/UserProfile.vue - 用 Vue 3.4+ 新特性 -->
<template>
  <div class="user-profile">
    <div v-if="loading" class="loading">
      <LoadingSpinner />
    </div>
    
    <div v-else-if="error" class="error">
      <ErrorMessage :message="error" @retry="loadUser" />
    </div>
    
    <div v-else-if="user" class="user-info">
      <h2>{{ user.name }}</h2>
      <p>{{ user.email }}</p>
      
      <!-- 用 defineModel (Vue 3.4+ 新特性) -->
      <input 
        v-model="editableName" 
        @blur="updateUserName"
        placeholder="输入姓名"
      />
      
      <!-- 用 Teleport 把模态框渲染到 body -->
      <Teleport to="body">
        <UserEditModal 
          v-if="showEditModal"
          :user="user"
          @close="showEditModal = false"
          @save="handleSave"
        />
      </Teleport>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import { useWailsApi } from '@/composables/useWailsApi'
import { GetUserInfo, UpdateUser } from '@/wailsjs/go/main/App'
import type { User } from '@/types/user'

// Props 和 Emits
interface Props {
  userId: string
}

interface Emits {
  (e: 'user-updated', user: User): void
}

const props = defineProps<Props>()
const emit = defineEmits<Emits>()

// 状态管理
const { data: user, loading, error, execute: loadUser } = useWailsApi(GetUserInfo)
const { execute: updateUser } = useWailsApi(UpdateUser)

const showEditModal = ref(false)
const editableName = ref('')

// 计算属性
const userName = computed(() => user.value?.name || '')

// 方法
const updateUserName = async () => {
  if (!user.value || editableName.value === user.value.name) return
  
  const result = await updateUser({
    ...user.value,
    name: editableName.value
  })
  
  if (result.success) {
    emit('user-updated', result.data!)
  }
}

const handleSave = (updatedUser: User) => {
  user.value = updatedUser
  showEditModal.value = false
  emit('user-updated', updatedUser)
}

// 生命周期
loadUser(props.userId)
</script>

4. 前后端通信 🔄

4.1 API 封装

类型安全的 API 封装

typescript
// composables/useWailsApi.ts - 类型安全的 API 封装
import { ref, computed } from 'vue'
import type { Ref } from 'vue'

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

interface ApiState<T> {
  data: Ref<T | null>
  loading: Ref<boolean>
  error: Ref<string | null>
}

// API 状态管理
export function useApiState<T>() {
  const data = ref<T | null>(null) as Ref<T | null>
  const loading = ref(false)
  const error = ref<string | null>(null)

  return {
    data,
    loading,
    error,
    reset: () => {
      data.value = null
      loading.value = false
      error.value = null
    }
  }
}

// 安全的 API 调用
export async function safeApiCall<T>(
  apiFunction: (...args: any[]) => Promise<T>,
  ...args: any[]
): Promise<ApiResponse<T>> {
  try {
    const result = await apiFunction(...args)
    return { success: true, data: result }
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : '未知错误'
    return { 
      success: false, 
      error: errorMessage,
      code: (error as any)?.code || 'UNKNOWN_ERROR'
    }
  }
}

// Wails API 组合式函数
export function useWailsApi<T>(apiFunction: (...args: any[]) => Promise<T>) {
  const state = useApiState<T>()

  const execute = async (...args: any[]) => {
    state.loading.value = true
    state.error.value = null

    const result = await safeApiCall(apiFunction, ...args)
    
    if (result.success && result.data) {
      state.data.value = result.data
    } else {
      state.error.value = result.error || '操作失败'
    }
    
    state.loading.value = false
    return result
  }

  return {
    ...state,
    execute
  }
}

// 具体 API 实现
export function useUserApi() {
  const { GetUserInfo, ProcessLargeFile, BatchProcess } = await import('@/wailsjs/go/main/App')
  
  const userInfo = useWailsApi(GetUserInfo)
  const fileProcessor = useWailsApi(ProcessLargeFile)
  const batchProcessor = useWailsApi(BatchProcess)

  return {
    userInfo,
    fileProcessor,
    batchProcessor
  }
}

4.2 事件管理

事件管理器

typescript
// utils/eventManager.ts - 事件管理器
import { EventsOn, EventsOff, EventsEmit } from '@/wailsjs/runtime/runtime'

class EventManager {
  private handlers = new Map<string, Set<Function>>()
  private onceHandlers = new Map<Function, Function>()

  // 注册事件监听
  on(eventName: string, handler: Function) {
    if (!this.handlers.has(eventName)) {
      this.handlers.set(eventName, new Set())
    }
    this.handlers.get(eventName)!.add(handler)
    EventsOn(eventName, handler)
  }

  // 注册一次性事件监听
  once(eventName: string, handler: Function) {
    const wrappedHandler = (...args: any[]) => {
      handler(...args)
      this.off(eventName, wrappedHandler)
    }
    this.onceHandlers.set(handler, wrappedHandler)
    this.on(eventName, wrappedHandler)
  }

  // 移除事件监听
  off(eventName: string, handler: Function) {
    const handlers = this.handlers.get(eventName)
    if (handlers) {
      handlers.delete(handler)
      EventsOff(eventName, handler)
    }
    
    // 清理一次性处理器
    const wrappedHandler = this.onceHandlers.get(handler)
    if (wrappedHandler) {
      EventsOff(eventName, wrappedHandler)
      this.onceHandlers.delete(handler)
    }
  }

  // 移除所有事件监听
  offAll(eventName: string) {
    const handlers = this.handlers.get(eventName)
    if (handlers) {
      handlers.forEach(handler => {
        EventsOff(eventName, handler)
      })
      handlers.clear()
    }
  }

  // 发送事件到后端
  emit(eventName: string, data: any) {
    EventsEmit(eventName, data)
  }
}

export const eventManager = new EventManager()

5. 性能优化 ⚡

5.1 虚拟滚动

vue
<!-- components/VirtualList.vue - 高性能虚拟滚动 -->
<template>
  <div 
    ref="containerRef" 
    class="virtual-list"
    :style="{ height: containerHeight + 'px' }"
    @scroll="handleScroll"
  >
    <div 
      class="virtual-list-content"
      :style="{ height: totalHeight + 'px' }"
    >
      <div 
        class="virtual-list-items"
        :style="{ transform: `translateY(${offsetY}px)` }"
      >
        <div 
          v-for="item in visibleItems" 
          :key="item.id"
          class="virtual-list-item"
          :style="{ height: itemHeight + 'px' }"
        >
          <slot :item="item" :index="getItemIndex(item)" />
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { useThrottleFn } from '@vueuse/core'

interface Props {
  items: any[]
  itemHeight: number
  containerHeight: number
  overscan?: number // 预渲染的项目数量
}

const props = withDefaults(defineProps<Props>(), {
  overscan: 5
})

const containerRef = ref<HTMLElement>()
const scrollTop = ref(0)

// 计算属性
const totalHeight = computed(() => props.items.length * props.itemHeight)
const visibleCount = computed(() => 
  Math.ceil(props.containerHeight / props.itemHeight) + props.overscan * 2
)
const startIndex = computed(() => 
  Math.max(0, Math.floor(scrollTop.value / props.itemHeight) - props.overscan)
)
const endIndex = computed(() => 
  Math.min(
    startIndex.value + visibleCount.value, 
    props.items.length
  )
)
const offsetY = computed(() => startIndex.value * props.itemHeight)

const visibleItems = computed(() => 
  props.items.slice(startIndex.value, endIndex.value)
)

// 防抖滚动处理
const handleScroll = useThrottleFn((event: Event) => {
  const target = event.target as HTMLElement
  scrollTop.value = target.scrollTop
}, 16) // 60fps

// 获取项目索引
const getItemIndex = (item: any) => {
  return props.items.findIndex(i => i.id === item.id)
}

// 滚动到指定项目
const scrollToItem = async (index: number) => {
  if (!containerRef.value) return
  
  const targetScrollTop = index * props.itemHeight
  containerRef.value.scrollTop = targetScrollTop
  
  await nextTick()
}

// 暴露方法
defineExpose({
  scrollToItem
})
</script>

<style scoped>
.virtual-list {
  overflow-y: auto;
  position: relative;
}

.virtual-list-content {
  position: relative;
}

.virtual-list-items {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
}

.virtual-list-item {
  box-sizing: border-box;
}
</style>

5.2 内存管理

typescript
// utils/memoryManager.ts - 内存管理器
class MemoryManager {
  private cache = new Map<string, any>()
  private maxCacheSize = 100
  private cleanupInterval = 5 * 60 * 1000 // 5分钟清理一次

  constructor() {
    this.startCleanup()
  }

  // 缓存数据
  set(key: string, value: any, ttl = 5 * 60 * 1000) { // 默认5分钟过期
    const item = {
      value,
      timestamp: Date.now(),
      ttl
    }
    
    this.cache.set(key, item)
    
    // 检查缓存大小
    if (this.cache.size > this.maxCacheSize) {
      this.cleanup()
    }
  }

  // 获取缓存数据
  get(key: string) {
    const item = this.cache.get(key)
    if (!item) return null
    
    // 检查是否过期
    if (Date.now() - item.timestamp > item.ttl) {
      this.cache.delete(key)
      return null
    }
    
    return item.value
  }

  // 清理过期数据
  cleanup() {
    const now = Date.now()
    for (const [key, item] of this.cache.entries()) {
      if (now - item.timestamp > item.ttl) {
        this.cache.delete(key)
      }
    }
  }

  // 启动定期清理
  private startCleanup() {
    setInterval(() => {
      this.cleanup()
    }, this.cleanupInterval)
  }

  // 清空所有缓存
  clear() {
    this.cache.clear()
  }
}

export const memoryManager = new MemoryManager()

6. 测试 🧪

6.1 测试配置

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: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['./tests/setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'dist/',
        'tests/',
        '**/*.d.ts',
        'wailsjs/'
      ]
    }
  },
  resolve: {
    alias: {
      '@': resolve(__dirname, './src'),
      'wailsjs': resolve(__dirname, './wailsjs')
    }
  }
})

6.2 组件测试

typescript
// tests/components/UserProfile.test.ts
import { mount } from '@vue/test-utils'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import UserProfile from '@/components/UserProfile.vue'
import { useAppStore } from '@/stores/app'

// Mock Wails 运行时
vi.mock('@/wailsjs/runtime/runtime', () => ({
  EventsOn: vi.fn(),
  EventsOff: vi.fn(),
  EventsEmit: vi.fn()
}))

// Mock Wails API
vi.mock('@/wailsjs/go/main/App', () => ({
  GetUserInfo: vi.fn(),
  UpdateUser: vi.fn()
}))

describe('UserProfile', () => {
  let pinia: any

  beforeEach(() => {
    pinia = createPinia()
    setActivePinia(pinia)
  })

  it('初始显示加载状态', () => {
    const wrapper = mount(UserProfile, {
      props: { userId: 'test-user' },
      global: { plugins: [pinia] }
    })
    
    expect(wrapper.find('.loading').exists()).toBe(true)
  })

  it('加载完成后显示用户信息', async () => {
    const store = useAppStore()
    store.setUser({ id: '1', name: '张三', email: 'zhangsan@example.com' })
    
    const wrapper = mount(UserProfile, {
      props: { userId: '1' },
      global: { plugins: [pinia] }
    })
    
    await wrapper.vm.$nextTick()
    
    expect(wrapper.find('.user-name').text()).toBe('张三')
    expect(wrapper.find('.user-email').text()).toBe('zhangsan@example.com')
  })

  it('用户更新时触发事件', async () => {
    const wrapper = mount(UserProfile, {
      props: { userId: '1' },
      global: { plugins: [pinia] }
    })
    
    const store = useAppStore()
    store.setUser({ id: '1', name: '张三', email: 'zhangsan@example.com' })
    
    await wrapper.vm.$nextTick()
    
    // 模拟用户更新
    await wrapper.vm.updateUserName()
    
    expect(wrapper.emitted('user-updated')).toBeTruthy()
  })
})

7. 安全性 🛡️

7.1 内容安全策略

html
<!-- index.html - 内容安全策略 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="Content-Security-Policy" 
        content="default-src 'self'; 
                 script-src 'self' 'unsafe-inline' 'unsafe-eval'; 
                 style-src 'self' 'unsafe-inline'; 
                 img-src 'self' data: https:; 
                 connect-src 'self' ws: wss:;">
  <title>Wails App</title>
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/src/main.ts"></script>
</body>
</html>

7.2 输入验证

typescript
// utils/validation.ts - 输入验证工具
import { z } from 'zod'

// 用户输入验证
export const userSchema = z.object({
  name: z.string().min(1, '姓名不能为空').max(50, '姓名不能超过50个字符'),
  email: z.string().email('邮箱格式不正确'),
  age: z.number().min(0, '年龄不能为负数').max(150, '年龄不能超过150'),
  bio: z.string().max(500, '个人简介不能超过500个字符').optional()
})

// 文件上传验证
export const fileUploadSchema = z.object({
  file: z.instanceof(File),
  maxSize: z.number().default(10 * 1024 * 1024), // 10MB
  allowedTypes: z.array(z.string()).default(['image/jpeg', 'image/png', 'image/gif'])
})

// 验证函数
export function validateInput<T>(schema: z.ZodSchema<T>, data: unknown): T {
  try {
    return schema.parse(data)
  } catch (error) {
    if (error instanceof z.ZodError) {
      throw new Error(error.errors.map(e => e.message).join(', '))
    }
    throw error
  }
}

// 数据清理
export function sanitizeInput(input: string): string {
  return input
    .replace(/[<>]/g, '') // 移除 HTML 标签
    .trim()
    .slice(0, 1000) // 限制长度
}

8. 实战应用 🎯

8.1 完整项目示例

状态管理 (Pinia)

typescript
// stores/app.ts - 状态管理
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useWailsApi } from '@/composables/useWailsApi'
import { GetUserInfo, GetSettings } from '@/wailsjs/go/main/App'
import type { User, Settings } from '@/types'

export const useAppStore = defineStore('app', () => {
  // 状态
  const user = ref<User | null>(null)
  const settings = ref<Settings | null>(null)
  const theme = ref<'light' | 'dark'>('light')
  const sidebarCollapsed = ref(false)

  // API 状态
  const { loading: userLoading, error: userError, execute: loadUser } = useWailsApi(GetUserInfo)
  const { loading: settingsLoading, execute: loadSettings } = useWailsApi(GetSettings)

  // 计算属性
  const isAuthenticated = computed(() => user.value !== null)
  const userName = computed(() => user.value?.name || '访客')
  const isDarkMode = computed(() => theme.value === 'dark')

  // 动作
  const initializeApp = async () => {
    await Promise.all([
      loadUser(),
      loadSettings()
    ])
  }

  const setUser = (newUser: User) => {
    user.value = newUser
  }

  const clearUser = () => {
    user.value = null
  }

  const toggleTheme = () => {
    theme.value = theme.value === 'light' ? 'dark' : 'light'
    // 保存到本地存储
    localStorage.setItem('theme', theme.value)
  }

  const toggleSidebar = () => {
    sidebarCollapsed.value = !sidebarCollapsed.value
  }

  // 持久化
  const loadPersistedState = () => {
    const savedTheme = localStorage.getItem('theme') as 'light' | 'dark'
    if (savedTheme) {
      theme.value = savedTheme
    }
  }

  return {
    // 状态
    user,
    settings,
    theme,
    sidebarCollapsed,
    
    // 加载状态
    userLoading,
    userError,
    settingsLoading,
    
    // 计算属性
    isAuthenticated,
    userName,
    isDarkMode,
    
    // 动作
    initializeApp,
    setUser,
    clearUser,
    toggleTheme,
    toggleSidebar,
    loadPersistedState
  }
})

8.2 错误处理

typescript
// utils/errorHandler.ts - 错误处理
import { systemIntegration } from './systemIntegration'

interface ErrorInfo {
  message: string
  stack?: string
  context: string
  timestamp: string
  userAgent: string
  url: string
  componentName?: string
  props?: Record<string, any>
}

class ErrorHandler {
  private errorQueue: ErrorInfo[] = []
  private maxQueueSize = 50
  private isReporting = false

  constructor() {
    this.setupGlobalHandlers()
  }

  private setupGlobalHandlers() {
    // 未处理的 Promise 错误
    window.addEventListener('unhandledrejection', (event) => {
      this.handleError(event.reason, '未处理的 Promise 错误')
    })

    // 全局错误
    window.addEventListener('error', (event) => {
      this.handleError(event.error, '全局错误')
    })

    // Vue 错误处理
    if (window.Vue) {
      window.Vue.config.errorHandler = (error: Error, vm: any, info: string) => {
        this.handleError(error, `Vue 错误: ${info}`, {
          componentName: vm?.$options?.name,
          props: vm?.$props
        })
      }
    }
  }

  private handleError(error: Error | string, context: string, metadata?: any) {
    const errorInfo: ErrorInfo = {
      message: error instanceof Error ? error.message : error,
      stack: error instanceof Error ? error.stack : undefined,
      context,
      timestamp: new Date().toISOString(),
      userAgent: navigator.userAgent,
      url: window.location.href,
      ...metadata
    }

    // 记录错误
    systemIntegration.error(`[${context}] ${errorInfo.message}`)
    console.error('错误详情:', errorInfo)

    // 添加到队列
    this.addToQueue(errorInfo)

    // 显示用户友好的错误信息
    this.showUserFriendlyError(errorInfo)
  }

  private addToQueue(errorInfo: ErrorInfo) {
    this.errorQueue.push(errorInfo)
    
    if (this.errorQueue.length > this.maxQueueSize) {
      this.errorQueue.shift()
    }

    // 批量报告错误
    this.reportErrors()
  }

  private async reportErrors() {
    if (this.isReporting || this.errorQueue.length === 0) return

    this.isReporting = true
    
    try {
      const errorsToReport = [...this.errorQueue]
      this.errorQueue = []

      // 发送到后端
      await this.sendErrorsToBackend(errorsToReport)
    } catch (error) {
      console.error('报告错误失败:', error)
      // 将错误重新加入队列
      this.errorQueue.unshift(...this.errorQueue)
    } finally {
      this.isReporting = false
    }
  }

  private async sendErrorsToBackend(errors: ErrorInfo[]) {
    const { EventsEmit } = await import('@/wailsjs/runtime/runtime')
    EventsEmit('error-report', { errors, timestamp: new Date().toISOString() })
  }

  private showUserFriendlyError(errorInfo: ErrorInfo) {
    const message = this.getUserFriendlyMessage(errorInfo)
    
    // 显示错误通知
    this.showNotification('error', message)
  }

  private getUserFriendlyMessage(errorInfo: ErrorInfo): string {
    const { message } = errorInfo
    
    if (message.includes('network') || message.includes('fetch')) {
      return '网络连接失败,请检查网络设置后重试'
    }
    if (message.includes('permission') || message.includes('access')) {
      return '权限不足,请检查文件或目录权限'
    }
    if (message.includes('timeout')) {
      return '操作超时,请稍后重试'
    }
    
    return '操作失败,请稍后重试。如果问题持续存在,请联系技术支持。'
  }

  private showNotification(type: 'error' | 'warning' | 'info', message: string) {
    // 实现通知系统
    console.warn(`[${type.toUpperCase()}] ${message}`)
  }
}

export const errorHandler = new ErrorHandler()

总结 🎉

这篇文章介绍了 Wails 前端开发的主要内容:

🎯 重点内容

  1. 技术栈:

    • Vue 3.4+ 新特性和性能提升
    • TypeScript 5.3+ 类型安全
    • Vite 5.0+ 快速构建
    • Pinia 2.1+ 状态管理
  2. 开发技巧:

    • 组合式函数和 API 封装
    • 虚拟滚动和性能优化
    • 完整的测试方案
    • 安全性考虑
  3. 架构设计:

    • 类型安全的通信
    • 清晰的项目结构
    • 错误处理和监控
    • 内存管理

🚀 实践要点

  • 类型安全: 用 TypeScript 减少 bug
  • 性能优化: 虚拟滚动、代码分割、内存管理
  • 错误处理: 统一的错误处理,用户友好的提示
  • 测试覆盖: 完整的单元测试和集成测试
  • 安全性: CSP 配置、输入验证、数据清理

⚠️ 常见问题和解决方案

根据最新的 Wails 版本和社区反馈:

Windows 平台

  • 拖拽功能: 正确配置 CSS 拖拽区域
  • 窗口隐藏: 用 Minimised: true 替代 Hidden: true
  • DPI 感知: 检查 DPI 设置

macOS 平台

  • 毛玻璃效果: 用 Backdrop: 'transparent' 替代复杂配置
  • 透明窗口: 避免重复设置 NSGlassEffectViewStyle

Linux 平台

  • 系统托盘: 手动处理窗口显示
  • WebKit 检测: 验证依赖状态

🔗 相关资源

这些技术为构建高质量的 Wails 应用提供了完整的前端开发方案。下一篇文章我们会聊聊后端开发技术栈和系统集成。

上次更新于: