介绍一下 Vue 3 项目的最佳工程化实践。以下是推荐的项目结构和关键实践:
一、TS
| src/ |
| ├── main.ts |
| ├── App.vue |
| ├── env.d.ts |
| ├── assets/ |
| ├── components/ |
| │ ├── common/ |
| │ └── business/ |
| ├── composables/ |
| ├── configs/ |
| ├── plugins/ |
| │ ├── pinia.ts |
| │ ├── router.ts |
| │ └── index.ts |
| ├── router/ |
| │ ├── modules/ |
| │ └── index.ts |
| ├── stores/ |
| │ ├── modules/ |
| │ └── index.ts |
| ├── services/ |
| │ ├── api/ |
| │ └── http.ts |
| ├── styles/ |
| ├── types/ |
| ├── utils/ |
| └── views/ |
下面是各个关键部分的最佳实践示例:
1、插件配置 (src/plugins/index.ts
):
| import { type App } from 'vue' |
| import { setupPinia } from './pinia' |
| import { setupRouter } from './router' |
| |
| |
| export function setupPlugins(app: App) { |
| setupPinia(app) |
| setupRouter(app) |
| |
| } |
2、Pinia 配置 (src/plugins/pinia.ts
):
| import { type App } from 'vue' |
| import { createPinia } from 'pinia' |
| import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' |
| |
| export function setupPinia(app: App) { |
| const pinia = createPinia() |
| pinia.use(piniaPluginPersistedstate) |
| app.use(pinia) |
| } |
3、HTTP 服务配置 (src/services/http.ts
):
| import axios from 'axios' |
| import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' |
| |
| class HttpRequest { |
| private instance: AxiosInstance |
| |
| constructor(config: AxiosRequestConfig) { |
| this.instance = axios.create(config) |
| this.setupInterceptors() |
| } |
| |
| private setupInterceptors() { |
| this.instance.interceptors.request.use( |
| (config) => { |
| |
| return config |
| }, |
| (error) => { |
| return Promise.reject(error) |
| } |
| ) |
| |
| this.instance.interceptors.response.use( |
| (response) => { |
| |
| return response.data |
| }, |
| (error) => { |
| return Promise.reject(error) |
| } |
| ) |
| } |
| |
| request<T = any>(config: AxiosRequestConfig): Promise<T> { |
| return this.instance.request(config) |
| } |
| } |
| |
| export const http = new HttpRequest({ |
| baseURL: import.meta.env.VITE_API_BASE_URL, |
| timeout: 10000 |
| }) |
4、API 接口定义 (src/services/api/user.ts
):
| import { http } from '../http' |
| |
| export interface UserInfo { |
| id: number |
| name: string |
| |
| } |
| |
| export const userApi = { |
| getUserInfo(id: number) { |
| return http.request<UserInfo>({ |
| url: `/user/${id}`, |
| method: 'GET' |
| }) |
| } |
| } |
5、Store 模块 (src/stores/modules/user.ts
):
| import { defineStore } from 'pinia' |
| import type { UserInfo } from '@/services/api/user' |
| |
| export const useUserStore = defineStore('user', { |
| state: () => ({ |
| userInfo: null as UserInfo | null |
| }), |
| actions: { |
| async fetchUserInfo(id: number) { |
| const { userApi } = await import('@/services/api/user') |
| this.userInfo = await userApi.getUserInfo(id) |
| } |
| }, |
| persist: true |
| }) |
6、入口文件 (src/main.ts
):
| import { createApp } from 'vue' |
| import App from './App.vue' |
| import { setupPlugins } from './plugins' |
| import '@/styles/index.scss' |
| |
| const app = createApp(App) |
| setupPlugins(app) |
| app.mount('#app') |
7、环境变量配置 (.env.development
):
| VITE_API_BASE_URL=http://localhost:3000 |
8、组合式函数 (src/composables/useRequest.ts
):
| import { ref } from 'vue' |
| |
| export function useRequest<T, P extends any[]>( |
| api: (...args: P) => Promise<T> |
| ) { |
| const data = ref<T | null>(null) |
| const loading = ref(false) |
| const error = ref<Error | null>(null) |
| |
| const execute = async (...args: P) => { |
| loading.value = true |
| error.value = null |
| try { |
| data.value = await api(...args) |
| } catch (e) { |
| error.value = e as Error |
| } finally { |
| loading.value = false |
| } |
| } |
| |
| return { |
| data, |
| loading, |
| error, |
| execute |
| } |
| } |
这样的工程化实践有以下优点:
- 目录结构清晰,职责分明
- 模块化设计,便于维护和扩展
- 类型安全,使用 TypeScript
- 统一的错误处理和状态管理
- 可复用的组合式函数
- 环境配置分离
- API 接口统一管理
- 插件配置集中管理
此外,还建议:
- 使用 ESLint + Prettier 统一代码风格
- 使用 Husky + lint-staged 进行提交前检查
- 使用 Conventional Commits 规范提交信息
- 编写单元测试确保代码质量
- 使用 CI/CD 实现自动化部署
这样的工程化结构可以帮助团队更好地协作,提高代码质量和开发效率。
二、JS
对于 JavaScript 项目,我们同样可以实现良好的工程化。以下是基于 JavaScript 的最佳实践:
| src/ |
| ├── main.js |
| ├── App.vue |
| ├── assets/ |
| ├── components/ |
| │ ├── common/ |
| │ └── business/ |
| ├── hooks/ |
| ├── configs/ |
| ├── plugins/ |
| ├── router/ |
| ├── stores/ |
| ├── services/ |
| ├── styles/ |
| └── utils/ |
下面是各个核心文件的示例代码:
1、插件配置 (src/plugins/index.js
):
| import { setupPinia } from './pinia' |
| import { setupRouter } from './router' |
| |
| export function setupPlugins(app) { |
| setupPinia(app) |
| setupRouter(app) |
| } |
2、Pinia 配置 (src/plugins/pinia.js
):
| import { createPinia } from 'pinia' |
| import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' |
| |
| export function setupPinia(app) { |
| const pinia = createPinia() |
| pinia.use(piniaPluginPersistedstate) |
| app.use(pinia) |
| } |
3、HTTP 服务配置 (src/services/http.js
):
| import axios from 'axios' |
| |
| class HttpRequest { |
| constructor(config) { |
| this.instance = axios.create(config) |
| this.setupInterceptors() |
| } |
| |
| setupInterceptors() { |
| |
| this.instance.interceptors.request.use( |
| config => { |
| |
| const token = localStorage.getItem('token') |
| if (token) { |
| config.headers.Authorization = `Bearer ${token}` |
| } |
| return config |
| }, |
| error => { |
| return Promise.reject(error) |
| } |
| ) |
| |
| |
| this.instance.interceptors.response.use( |
| response => { |
| |
| const { code, data, message } = response.data |
| |
| if (code === 200) { |
| return data |
| } else { |
| |
| this.handleError(code, message) |
| return Promise.reject(new Error(message)) |
| } |
| }, |
| error => { |
| |
| const { status } = error.response |
| this.handleError(status) |
| return Promise.reject(error) |
| } |
| ) |
| } |
| |
| handleError(code, message) { |
| switch (code) { |
| case 401: |
| |
| break |
| case 403: |
| |
| break |
| case 404: |
| |
| break |
| case 500: |
| |
| break |
| default: |
| |
| break |
| } |
| } |
| |
| request(config) { |
| return this.instance.request(config) |
| } |
| } |
| |
| export const http = new HttpRequest({ |
| baseURL: import.meta.env.VITE_API_BASE_URL, |
| timeout: 10000 |
| }) |
4、API 接口定义 (src/services/api/user.js
):
| import { http } from '../http' |
| |
| export const userApi = { |
| |
| getUserInfo(id) { |
| return http.request({ |
| url: `/user/${id}`, |
| method: 'GET' |
| }) |
| }, |
| |
| |
| login(data) { |
| return http.request({ |
| url: '/user/login', |
| method: 'POST', |
| data |
| }) |
| } |
| } |
5、Store 模块 (src/stores/modules/user.js
):
| import { defineStore } from 'pinia' |
| import { userApi } from '@/services/api/user' |
| |
| export const useUserStore = defineStore('user', { |
| state: () => ({ |
| userInfo: null, |
| token: localStorage.getItem('token') || '' |
| }), |
| |
| getters: { |
| isLogin: state => !!state.token |
| }, |
| |
| actions: { |
| async login(credentials) { |
| try { |
| const { token, userInfo } = await userApi.login(credentials) |
| this.token = token |
| this.userInfo = userInfo |
| localStorage.setItem('token', token) |
| return true |
| } catch (error) { |
| return false |
| } |
| }, |
| |
| async getUserInfo(id) { |
| try { |
| this.userInfo = await userApi.getUserInfo(id) |
| } catch (error) { |
| console.error('获取用户信息失败:', error) |
| } |
| }, |
| |
| logout() { |
| this.token = '' |
| this.userInfo = null |
| localStorage.removeItem('token') |
| } |
| }, |
| |
| persist: { |
| key: 'user-store', |
| paths: ['token'] |
| } |
| }) |
6、组合式函数 (src/hooks/useRequest.js
):
| import { ref } from 'vue' |
| |
| export function useRequest(api) { |
| const data = ref(null) |
| const loading = ref(false) |
| const error = ref(null) |
| |
| const execute = async (...args) => { |
| loading.value = true |
| error.value = null |
| try { |
| data.value = await api(...args) |
| } catch (err) { |
| error.value = err |
| console.error('请求错误:', err) |
| } finally { |
| loading.value = false |
| } |
| } |
| |
| return { |
| data, |
| loading, |
| error, |
| execute |
| } |
| } |
7、入口文件 (src/main.js
):
| import { createApp } from 'vue' |
| import App from './App.vue' |
| import { setupPlugins } from './plugins' |
| import './styles/index.scss' |
| |
| const app = createApp(App) |
| setupPlugins(app) |
| app.mount('#app') |
8、工具函数 (src/utils/validate.js
):
| |
| export const validators = { |
| isEmail(value) { |
| return /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(value) |
| }, |
| |
| isPhone(value) { |
| return /^1[3-9]\d{9}$/.test(value) |
| }, |
| |
| isPassword(value) { |
| return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/.test(value) |
| } |
| } |
推荐的工程化实践:
1、使用 ESLint 和 Prettier:
| |
| module.exports = { |
| root: true, |
| env: { |
| node: true, |
| browser: true, |
| es2021: true |
| }, |
| extends: [ |
| 'plugin:vue/vue3-recommended', |
| 'eslint:recommended' |
| ], |
| rules: { |
| |
| } |
| } |
2、使用 husky 和 lint-staged 进行代码检查:
| { |
| "scripts": { |
| "lint": "eslint --ext .js,.vue src", |
| "lint:fix": "eslint --ext .js,.vue src --fix" |
| }, |
| "husky": { |
| "hooks": { |
| "pre-commit": "lint-staged" |
| } |
| }, |
| "lint-staged": { |
| "*.{js,vue}": [ |
| "eslint --fix", |
| "git add" |
| ] |
| } |
| } |
4、统一的错误处理:
| |
| export class BusinessError extends Error { |
| constructor(code, message) { |
| super(message) |
| this.code = code |
| } |
| } |
| |
| export const errorHandler = (error) => { |
| if (error instanceof BusinessError) { |
| |
| } else { |
| |
| } |
| } |
主要优势:
- 代码组织清晰,易于维护
- 统一的错误处理机制
- 可重用的工具函数和 hooks
- 规范的代码风格
- API 接口统一管理
- 状态管理清晰
- 自动化的代码检查和格式化
最后,建议:
- 合理使用注释,特别是复杂逻辑部分
- 遵循一致的命名规范
- 适当做性能优化
- 定期进行代码审查
- 保持依赖包的更新
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗