sudo-iのBlog

  • 🍟首页
  • 🍊目录
    • 技术分享
    • vps教程
    • 软件分享
    • 干货分享
  • 🍎链接
  • 🍓工具
    • 🌽IP路由追踪
    • 域名被墙检测
    • KMS激活
    • 域名whois查询
  • 🍕联系
  • 🍌登录
Sudo-i
关注互联网,生活,音乐,乐此不疲
  1. 首页
  2. 干货分享
  3. 正文

Vue 3 + TypeScript 实战:构建企业级前端应用

7 3 月, 2026 61点热度 0人点赞 0条评论

引言

Vue 3 的发布带来了 Composition API、Performance 提升等重磅更新,结合 TypeScript 的类型系统,可以构建出更加健壮、可维护的企业级应用。本文将通过实战项目,带你掌握 Vue 3 + TypeScript 的核心技术。

一、项目初始化

1.1 使用 Vite 创建项目

# 创建 Vue 3 + TypeScript 项目
npm create vite@latest my-app -- --template vue-ts

# 安装依赖
cd my-app
npm install

# 安装常用工具库
npm install vue-router pinia axios
npm install -D @types/node

1.2 项目结构规范

src/
├── api/              # API 接口
│   ├── modules/      # 按模块划分
│   └── index.ts      # 统一导出
├── assets/           # 静态资源
├── components/       # 公共组件
│   ├── base/         # 基础组件
│   └── business/     # 业务组件
├── composables/      # 组合式函数
├── hooks/            # 自定义 hooks
├── layouts/          # 布局组件
├── router/           # 路由配置
├── stores/           # Pinia 状态管理
├── styles/           # 全局样式
├── types/            # TypeScript 类型定义
├── utils/            # 工具函数
├── views/            # 页面组件
├── App.vue
└── main.ts

二、TypeScript 类型定义

2.1 基础类型定义

// types/user.ts
export interface User {
  id: number;
  username: string;
  email: string;
  role: "admin" | "user" | "guest";
  createdAt: Date;
  profile?: UserProfile;
}

export interface UserProfile {
  avatar?: string;
  bio?: string;
  phone?: string;
}

// types/api.ts
export interface ApiResponse {
  code: number;
  message: string;
  data: T;
}

export interface PageParams {
  page: number;
  pageSize: number;
}

export interface PageResult {
  list: T[];
  total: number;
  page: number;
  pageSize: number;
}

2.2 组件 Props 类型

<script setup lang="ts">
import type { User } from "@/types/user";

interface Props {
  userInfo: User;
  showAvatar?: boolean;
  maxLength?: number;
}

const props = withDefaults(defineProps<Props>(), {
  showAvatar: true,
  maxLength: 100
});

interface Emits {
  (e: "update", value: string): void;
  (e: "delete", id: number): void;
}

const emit = defineEmits<Emits>();
</script>

三、Composition API 实战

3.1 基础组合式函数

// composables/useLoading.ts
import { ref, type Ref } from "vue";

export function useLoading(initialValue = false) {
  const loading = ref(initialValue);

  const startLoading = () => {
    loading.value = true;
  };

  const stopLoading = () => {
    loading.value = false;
  };

  const withLoading = async <T>(fn: () => Promise<T>): Promise<T> => {
    startLoading();
    try {
      return await fn();
    } finally {
      stopLoading();
    }
  };

  return {
    loading,
    startLoading,
    stopLoading,
    withLoading
  };
}

3.2 数据请求组合式函数

// composables/useFetch.ts
import { ref, type Ref } from "vue";
import type { ApiResponse } from "@/types/api";

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

export function useFetch<T>(
  url: string,
  options: UseFetchOptions = {}
) {
  const { immediate = true, onError } = options;
  
  const data: Ref<T | null> = ref(null);
  const error: Ref<Error | null> = ref(null);
  const loading = ref(false);

  const execute = async () => {
    loading.value = true;
    error.value = null;
    
    try {
      const response = await fetch(url);
      const result: ApiResponse<T> = await response.json();
      
      if (result.code === 200) {
        data.value = result.data;
      } else {
        throw new Error(result.message);
      }
    } catch (e) {
      error.value = e as Error;
      onError?.(error.value);
    } finally {
      loading.value = false;
    }
  };

  if (immediate) {
    execute();
  }

  return {
    data,
    error,
    loading,
    execute,
    refresh: execute
  };
}

3.3 表单处理组合式函数

// composables/useForm.ts
import { ref, reactive, type Ref } from "vue";

interface ValidationRule {
  required?: boolean;
  pattern?: RegExp;
  minLength?: number;
  maxLength?: number;
  validator?: (value: any) => boolean | string;
}

interface FieldConfig {
  rules?: ValidationRule[];
}

export function useForm<T extends Record<string, any>>(
  initialValues: T,
  validationConfig: Record<keyof T, FieldConfig> = {}
) {
  const form = reactive({ ...initialValues });
  const errors: Ref<Record<keyof T, string>> = ref({} as any);
  const isSubmitting = ref(false);

  const validateField = (field: keyof T): boolean => {
    const rules = validationConfig[field]?.rules || [];
    const value = form[field];
    
    for (const rule of rules) {
      if (rule.required && !value) {
        errors.value[field] = "此字段为必填项";
        return false;
      }
      
      if (rule.pattern && !rule.pattern.test(value)) {
        errors.value[field] = "格式不正确";
        return false;
      }
      
      if (rule.minLength && value.length < rule.minLength) {
        errors.value[field] = `最少需要${rule.minLength}个字符`;
        return false;
      }
      
      if (rule.validator) {
        const result = rule.validator(value);
        if (result !== true) {
          errors.value[field] = typeof result === "string" ? result : "验证失败";
          return false;
        }
      }
    }
    
    errors.value[field] = "";
    return true;
  };

  const validate = (): boolean => {
    let isValid = true;
    Object.keys(form).forEach((key) => {
      if (!validateField(key as keyof T)) {
        isValid = false;
      }
    });
    return isValid;
  };

  const reset = () => {
    Object.assign(form, initialValues);
    errors.value = {} as any;
  };

  return {
    form,
    errors,
    isSubmitting,
    validate,
    validateField,
    reset
  };
}

四、Pinia 状态管理

4.1 Store 定义

// stores/user.ts
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import type { User } from "@/types/user";

export const useUserStore = defineStore("user", () => {
  // State
  const user = ref<User | null>(null);
  const token = ref<string>("");
  const isLoggedIn = computed(() => !!token.value);

  // Actions
  const login = async (username: string, password: string) => {
    const response = await fetch("/api/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ username, password })
    });
    
    const result = await response.json();
    if (result.code === 200) {
      token.value = result.data.token;
      user.value = result.data.user;
      localStorage.setItem("token", result.data.token);
    }
  };

  const logout = () => {
    user.value = null;
    token.value = "";
    localStorage.removeItem("token");
  };

  const fetchUserInfo = async () => {
    const response = await fetch("/api/user/info", {
      headers: { Authorization: `Bearer ${token.value}` }
    });
    const result = await response.json();
    user.value = result.data;
  };

  return {
    user,
    token,
    isLoggedIn,
    login,
    logout,
    fetchUserInfo
  };
});

4.2 在组件中使用

<script setup lang="ts">
import { useUserStore } from "@/stores/user";

const userStore = useUserStore();

// 访问 state
console.log(userStore.user);
console.log(userStore.isLoggedIn);

// 调用 action
await userStore.login("username", "password");
await userStore.fetchUserInfo();
</script>

五、路由配置

5.1 路由类型定义

// types/router.ts
import type { RouteLocationNormalized } from "vue-router";

export type RouteMeta = {
  title?: string;
  requiresAuth?: boolean;
  roles?: Array<"admin" | "user">;
  keepAlive?: boolean;
};

export type AppRouteRecord = RouteRecordRaw & {
  meta?: RouteMeta;
  children?: AppRouteRecord[];
};

5.2 路由守卫

// router/index.ts
import { createRouter, createWebHistory } from "vue-router";
import type { RouteMeta } from "@/types/router";
import { useUserStore } from "@/stores/user";

const routes: AppRouteRecord[] = [
  {
    path: "/",
    component: () => import("@/layouts/DefaultLayout.vue"),
    children: [
      {
        path: "",
        name: "Home",
        component: () => import("@/views/Home.vue"),
        meta: { title: "首页" }
      },
      {
        path: "dashboard",
        name: "Dashboard",
        component: () => import("@/views/Dashboard.vue"),
        meta: { title: "仪表盘", requiresAuth: true }
      }
    ]
  },
  {
    path: "/login",
    name: "Login",
    component: () => import("@/views/Login.vue"),
    meta: { title: "登录" }
  }
];

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

// 全局前置守卫
router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore();
  const meta = to.meta as RouteMeta;
  
  // 设置页面标题
  document.title = meta.title ? `${meta.title} - 应用名称` : "应用名称";
  
  // 检查是否需要登录
  if (meta.requiresAuth && !userStore.isLoggedIn) {
    next({ name: "Login", query: { redirect: to.fullPath } });
    return;
  }
  
  // 检查角色权限
  if (meta.roles && !meta.roles.includes(userStore.user?.role)) {
    next({ name: "403" });
    return;
  }
  
  next();
});

export default router;

六、API 请求封装

6.1 Axios 实例配置

// api/index.ts
import axios, { type AxiosInstance, type AxiosRequestConfig } from "axios";
import type { ApiResponse } from "@/types/api";
import { useUserStore } from "@/stores/user";

const baseURL = import.meta.env.VITE_API_BASE_URL || "/api";

const apiClient: AxiosInstance = axios.create({
  baseURL,
  timeout: 10000,
  headers: {
    "Content-Type": "application/json"
  }
});

// 请求拦截器
apiClient.interceptors.request.use(
  (config) => {
    const userStore = useUserStore();
    if (userStore.token) {
      config.headers.Authorization = `Bearer ${userStore.token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// 响应拦截器
apiClient.interceptors.response.use(
  (response) => {
    const { data } = response;
    if (data.code !== 200) {
      return Promise.reject(new Error(data.message));
    }
    return data;
  },
  (error) => {
    if (error.response?.status === 401) {
      const userStore = useUserStore();
      userStore.logout();
      window.location.href = "/login";
    }
    return Promise.reject(error);
  }
);

// 封装请求方法
export const request = <T = any>(
  config: AxiosRequestConfig
): Promise<ApiResponse<T>> => {
  return apiClient(config);
};

export const get = <T = any>(url: string, config?: AxiosRequestConfig) =>
  request<T>({ method: "GET", url, ...config });

export const post = <T = any>(url: string, data?: any, config?: AxiosRequestConfig) =>
  request<T>({ method: "POST", url, data, ...config });

export const put = <T = any>(url: string, data?: any, config?: AxiosRequestConfig) =>
  request<T>({ method: "PUT", url, data, ...config });

export const del = <T = any>(url: string, config?: AxiosRequestConfig) =>
  request<T>({ method: "DELETE", url, ...config });

6.2 API 模块示例

// api/modules/user.ts
import { get, post, put, del } from "@/api";
import type { User, PageParams, PageResult } from "@/types";

export const userApi = {
  // 获取用户列表
  getList: (params: PageParams) => 
    get<PageResult<User>>("/users", { params }),
  
  // 获取用户详情
  getDetail: (id: number) => 
    get<User>(`/users/${id}`),
  
  // 创建用户
  create: (data: Omit<User, "id" | "createdAt">) => 
    post<User>("/users", data),
  
  // 更新用户
  update: (id: number, data: Partial<User>) => 
    put<User>(`/users/${id}`, data),
  
  // 删除用户
  delete: (id: number) => 
    del(`/users/${id}`)
};

七、组件开发最佳实践

7.1 基础组件示例

<!-- components/base/AppButton.vue -->
<template>
  <button
    :class="[
      "app-button",
      `app-button--${type}`,
      `app-button--${size}`,
      { "app-button--loading": loading }
    ]"
    :disabled="disabled || loading"
    @click="handleClick"
  >
    <span v-if="loading" class="app-button__spinner"></span>
    <slot></slot>
  </button>
</template>

<script setup lang="ts">
interface Props {
  type?: "primary" | "success" | "warning" | "danger" | "default";
  size?: "small" | "medium" | "large";
  disabled?: boolean;
  loading?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  type: "default",
  size: "medium",
  disabled: false,
  loading: false
});

const emit = defineEmits<{
  click: [event: MouseEvent];
}>();

const handleClick = (event: MouseEvent) => {
  if (!props.disabled && !props.loading) {
    emit("click", event);
  }
};
</script>

<style scoped>
.app-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.2s;
}

.app-button--primary {
  background-color: #409eff;
  color: white;
}

.app-button--medium {
  padding: 8px 16px;
  font-size: 14px;
}

.app-button--loading {
  opacity: 0.7;
  cursor: not-allowed;
}
</style>

八、性能优化

8.1 组件懒加载

// 路由懒加载
const Dashboard = () => import("@/views/Dashboard.vue");

// 组件异步加载
const HeavyComponent = defineAsyncComponent(() =>
  import("@/components/business/HeavyComponent.vue")
);

8.2 列表虚拟滚动

<script setup lang="ts">
import { computed, ref } from "vue";

interface Props {
  items: any[];
  itemHeight?: number;
}

const props = withDefaults(defineProps<Props>(), {
  itemHeight: 50
});

const containerRef = ref<HTMLElement | null>(null);
const scrollTop = ref(0);
const visibleCount = ref(10);

const visibleItems = computed(() => {
  const start = Math.floor(scrollTop.value / props.itemHeight);
  const end = start + visibleCount.value;
  return props.items.slice(start, end);
});

const totalHeight = computed(() => {
  return props.items.length * props.itemHeight;
});
</script>

总结

Vue 3 + TypeScript 的组合为企业级应用开发提供了强大的工具链。通过本文的实战指南,你掌握了:

  1. 项目架构:规范化的目录结构和类型定义
  2. Composition API:可复用的组合式函数
  3. 状态管理:Pinia Store 的最佳实践
  4. 路由系统:类型安全的路由配置和守卫
  5. API 封装:统一的请求拦截和错误处理
  6. 组件开发:可复用的基础组件

记住:类型系统是你的朋友,不是敌人。投入时间完善类型定义,会在后期维护中获得巨大回报。持续学习 Vue 3 的新特性,保持代码的简洁和可维护性,是成为前端高手的关键。

无关联文章

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: 暂无
最后更新:7 3 月, 2026

李炫炫

这个人很懒,什么都没留下

点赞
< 上一篇
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2025 sudo-iのBlog. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang

鲁ICP备2024054662号

鲁公网安备37108102000450号