阅读视图

发现新文章,点击刷新页面。

从零构建Vue项目的完全指南:手把手打造现代化前端工程

从零构建Vue项目的完全指南:手把手打造现代化前端工程

一、项目构建整体流程图

让我们先看看完整的项目构建流程:

deepseek_mermaid_20251207_d1ddc8.png

二、详细构建步骤

步骤1:环境准备与项目初始化

首先确保你的开发环境已准备好:

# 检查Node.js版本(建议18+)
node -v

# 检查npm版本
npm -v

# 安装Vue CLI(如果还没有)
npm install -g @vue/cli

# 创建新项目
vue create my-vue-project

# 选择配置(推荐手动选择)
? Please pick a preset: 
  Default ([Vue 2] babel, eslint)
  Default (Vue 3) ([Vue 3] babel, eslint)
❯ Manually select features

# 选择需要的功能
? Check the features needed for your project:
 ◉ Babel
 ◉ TypeScript
 ◉ Progressive Web App (PWA) Support
 ◉ Router
 ◉ Vuex
 ◉ CSS Pre-processors
❯◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing

步骤2:项目目录结构设计

一个良好的目录结构是项目成功的基础。这是我推荐的目录结构:

my-vue-project/
├── public/                    # 静态资源
│   ├── index.html
│   ├── favicon.ico
│   └── robots.txt
├── src/
│   ├── api/                  # API接口管理
│   │   ├── modules/         # 按模块划分的API
│   │   ├── index.ts         # API统一导出
│   │   └── request.ts       # 请求封装
│   ├── assets/              # 静态资源
│   │   ├── images/
│   │   ├── styles/
│   │   └── fonts/
│   ├── components/          # 公共组件
│   │   ├── common/         # 全局通用组件
│   │   ├── business/       # 业务组件
│   │   └── index.ts        # 组件自动注册
│   ├── composables/        # 组合式函数
│   │   ├── useFetch.ts
│   │   ├── useForm.ts
│   │   └── index.ts
│   ├── directives/         # 自定义指令
│   │   ├── permission.ts
│   │   └── index.ts
│   ├── layouts/            # 布局组件
│   │   ├── DefaultLayout.vue
│   │   └── AuthLayout.vue
│   ├── router/             # 路由配置
│   │   ├── modules/       # 路由模块
│   │   ├── index.ts
│   │   └── guard.ts      # 路由守卫
│   ├── store/              # Vuex/Pinia状态管理
│   │   ├── modules/       # 模块化store
│   │   └── index.ts
│   ├── utils/              # 工具函数
│   │   ├── auth.ts        # 权限相关
│   │   ├── validate.ts    # 验证函数
│   │   └── index.ts
│   ├── views/              # 页面组件
│   │   ├── Home/
│   │   ├── User/
│   │   └── About/
│   ├── types/              # TypeScript类型定义
│   │   ├── api.d.ts
│   │   ├── global.d.ts
│   │   └── index.d.ts
│   ├── App.vue
│   └── main.ts
├── tests/                   # 测试文件
├── .env.*                   # 环境变量
├── vite.config.ts          # Vite配置
├── tsconfig.json           # TypeScript配置
└── package.json

步骤3:核心配置详解

1. 配置Vite(vite.config.ts)
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@components': path.resolve(__dirname, 'src/components'),
      '@views': path.resolve(__dirname, 'src/views'),
    },
  },
  server: {
    host'0.0.0.0',
    port3000,
    proxy: {
      '/api': {
        target'http://localhost:8080',
        changeOrigintrue,
        rewrite: (path) => path.replace(/^/api/, ''),
      },
    },
  },
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/assets/styles/variables.scss";`,
      },
    },
  },
})
2. 路由配置(router/index.ts)
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'

const routesRouteRecordRaw[] = [
  {
    path'/',
    name'Home',
    component() => import('@views/Home/Home.vue'),
    meta: {
      title'首页',
      requiresAuthtrue,
    },
  },
  {
    path'/login',
    name'Login',
    component() => import('@views/Login/Login.vue'),
    meta: {
      title'登录',
    },
  },
  {
    path'/user/:id',
    name'User',
    component() => import('@views/User/User.vue'),
    propstrue,
  },
]

const router = createRouter({
  historycreateWebHistory(),
  routes,
})

// 路由守卫
router.beforeEach((to, from, next) => {
  document.title = to.meta.title as string || 'Vue项目'
  
  // 检查是否需要登录
  if (to.meta.requiresAuth && !localStorage.getItem('token')) {
    next('/login')
  } else {
    next()
  }
})

export default router
3. 状态管理(使用Pinia)
// store/user.ts
import { defineStore } from 'pinia'

interface UserState {
  userInfo: {
    namestring
    avatarstring
    rolesstring[]
  } | null
}

export const useUserStore = defineStore('user', {
  state: (): UserState => ({
    userInfonull,
  }),
  actions: {
    async login(credentials: { username: string; password: string }) {
      // 登录逻辑
      const response = await api.login(credentials)
      this.userInfo = response.data
      localStorage.setItem('token', response.token)
    },
    logout() {
      this.userInfo = null
      localStorage.removeItem('token')
    },
  },
  getters: {
    isLoggedIn(state) => !!state.userInfo,
    hasRole(state) => (role: string) => 
      state.userInfo?.roles.includes(role) || false,
  },
})

步骤4:核心工具库和插件选择

这是我在项目中推荐使用的库:

{
  "dependencies": {
    "vue""^3.3.0",
    "vue-router""^4.2.0",
    "pinia""^2.1.0",
    "axios""^1.4.0",
    "element-plus""^2.3.0",
    "lodash-es""^4.17.21",
    "dayjs""^1.11.0",
    "vxe-table""^4.0.0",
    "vue-i18n""^9.0.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue""^4.2.0",
    "@types/node""^20.0.0",
    "sass""^1.62.0",
    "eslint""^8.0.0",
    "prettier""^3.0.0",
    "husky""^8.0.0",
    "commitlint""^17.0.0",
    "vitest""^0.30.0",
    "unplugin-auto-import""^0.16.0",
    "unplugin-vue-components""^0.25.0"
  }
}

步骤5:实用的组件示例

1. 全局请求封装
// src/api/request.ts
import axios from 'axios'
import type { AxiosRequestConfigAxiosResponse } from 'axios'
import { ElMessage } from 'element-plus'

const service = axios.create({
  baseURLimport.meta.env.VITE_API_BASE_URL,
  timeout10000,
})

// 请求拦截器
service.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  (response: AxiosResponse) => {
    const { code, data, message } = response.data
    
    if (code === 200) {
      return data
    } else {
      ElMessage.error(message || '请求失败')
      return Promise.reject(new Error(message))
    }
  },
  (error) => {
    if (error.response?.status === 401) {
      // 未授权,跳转到登录页
      localStorage.removeItem('token')
      window.location.href = '/login'
    }
    ElMessage.error(error.message || '网络错误')
    return Promise.reject(error)
  }
)

export default service
2. 自动导入组件配置
// vite.config.ts 补充配置
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  plugins: [
    // 自动导入API
    AutoImport({
      imports: ['vue''vue-router''pinia'],
      dts'src/types/auto-imports.d.ts',
      resolvers: [ElementPlusResolver()],
    }),
    // 自动导入组件
    Components({
      dts'src/types/components.d.ts',
      resolvers: [ElementPlusResolver()],
      dirs: ['src/components'],
    }),
  ],
})
3. 实用的Vue 3组合式函数
// src/composables/useForm.ts
import { ref, reactive, computed } from 'vue'
import type { Ref } from 'vue'

export function useForm<T extends object>(initialData: T) {
  const formData = reactive({ ...initialData }) as T
  const errors = reactive<Record<stringstring>>({})
  const isSubmitting = ref(false)

  const validate = async (): Promise<boolean> => {
    // 这里可以集成具体的验证逻辑
    return true
  }

  const submit = async (submitFn: (data: T) => Promise<any>) => {
    if (!(await validate())) return
    
    isSubmitting.value = true
    try {
      const result = await submitFn(formData)
      return result
    } catch (error) {
      throw error
    } finally {
      isSubmitting.value = false
    }
  }

  const reset = () => {
    Object.assign(formData, initialData)
    Object.keys(errors).forEach(key => {
      errors[key] = ''
    })
  }

  return {
    formData,
    errors,
    isSubmittingcomputed(() => isSubmitting.value),
    validate,
    submit,
    reset,
  }
}

步骤6:开发规范与最佳实践

1. 代码提交规范
# 安装Git提交钩子
npx husky install
npm install -D @commitlint/config-conventional @commitlint/cli

# 创建commitlint配置
echo "module.exports = { extends: ['@commitlint/config-conventional'] }" > .commitlintrc.js

# 创建提交信息规范
# feat: 新功能
# fix: 修复bug
# docs: 文档更新
# style: 代码格式
# refactor: 重构
# test: 测试
# chore: 构建过程或辅助工具的变动
2. 环境变量配置
# .env.development
VITE_APP_TITLE=开发环境
VITE_API_BASE_URL=/api
VITE_USE_MOCK=true

# .env.production
VITE_APP_TITLE=生产环境
VITE_API_BASE_URL=https://api.example.com
VITE_USE_MOCK=false

步骤7:性能优化建议

// 路由懒加载优化
const routes = [
  {
    path'/dashboard',
    component() => import(/* webpackChunkName: "dashboard" */ '@/views/Dashboard.vue'),
  },
  {
    path'/settings',
    component() => import(/* webpackChunkName: "settings" */ '@/views/Settings.vue'),
  },
]

// 图片懒加载指令
// src/directives/lazyLoad.ts
import type { Directive } from 'vue'

const lazyLoadDirective = {
  mounted(el, binding) {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          el.src = binding.value
          observer.unobserve(el)
        }
      })
    })
    observer.observe(el)
  },
}

三、项目启动和常用命令

{
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "preview": "vite preview",
    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
    "format": "prettier --write src/",
    "prepare": "husky install",
    "test": "vitest",
    "test:coverage": "vitest --coverage"
  }
}

四、总结与建议

通过以上步骤,你已经拥有了一个现代化、可维护的Vue项目基础。记住几个关键点:

  1. 1. 保持一致性 - 无论是命名规范还是代码风格
  2. 2. 模块化设计 - 功能解耦,便于维护和测试
  3. 3. 类型安全 - 充分利用TypeScript的优势
  4. 4. 自动化 - 尽可能自动化重复工作
  5. 5. 渐进式 - 不要一开始就追求完美,根据项目需求逐步完善

项目代码就像一座大厦,良好的基础决定了它的稳固性和可扩展性。希望这篇指南能帮助你在Vue项目开发中少走弯路!

Vue Router 组件内路由钩子全解析

一、什么是组件内路由钩子?

在 Vue Router 中,组件内路由钩子(也称为导航守卫)是在路由变化时自动调用的特殊函数,它们允许我们在特定时机执行自定义逻辑,比如:

  • • 权限验证(是否登录)
  • • 数据预加载
  • • 页面离开确认
  • • 滚动行为控制
  • • 动画过渡处理
// 一个简单的示例
export default {
  name'UserProfile',
  beforeRouteEnter(to, from, next) {
    console.log('组件还未创建,但即将进入...')
    next()
  }
}

二、三大核心钩子函数详解

Vue Router 提供了三个主要的组件内路由钩子,它们组成了一个完整的导航生命周期:

1. beforeRouteEnter - 进入前的守卫

调用时机:在组件实例被创建之前调用,此时组件还未初始化。

特点

  • • 不能访问 this(因为组件实例还未创建)
  • • 可以通过回调函数访问组件实例
export default {
  beforeRouteEnter(to, from, next) {
    // ❌ 这里不能使用 this
    console.log('from'from.path// 可以访问来源路由
    
    // ✅ 通过 next 的回调访问组件实例
    next(vm => {
      console.log('组件实例:', vm)
      vm.loadData(to.params.id)
    })
  },
  
  methods: {
    loadData(id) {
      // 加载数据逻辑
    }
  }
}

适用场景

  • • 基于路由参数的权限验证
  • • 预加载必要数据
  • • 重定向到其他页面

2. beforeRouteUpdate - 路由更新守卫

调用时机:在当前路由改变,但组件被复用时调用。

常见情况

  • • 从 /user/1 导航到 /user/2
  • • 查询参数改变:/search?q=vue → /search?q=react
export default {
  data() {
    return {
      usernull
    }
  },
  
  beforeRouteUpdate(to, from, next) {
    // ✅ 可以访问 this
    console.log('路由参数变化:'from.params.id'→', to.params.id)
    
    // 重新加载数据
    this.fetchUserData(to.params.id)
    
    // 必须调用 next()
    next()
  },
  
  methods: {
    async fetchUserData(id) {
      const response = await fetch(`/api/users/${id}`)
      this.user = await response.json()
    }
  }
}

实用技巧:使用这个钩子可以避免重复渲染,提升性能。

3. beforeRouteLeave - 离开前的守卫

调用时机:在离开当前路由时调用。

重要特性

  • • 可以阻止导航
  • • 常用于保存草稿或确认离开
export default {
  data() {
    return {
      hasUnsavedChangesfalse,
      formData: {
        title'',
        content''
      }
    }
  },
  
  beforeRouteLeave(to, from, next) {
    if (this.hasUnsavedChanges) {
      const answer = window.confirm(
        '您有未保存的更改,确定要离开吗?'
      )
      
      if (answer) {
        next() // 允许离开
      } else {
        next(false// 取消导航
      }
    } else {
      next() // 直接离开
    }
  },
  
  methods: {
    onInput() {
      this.hasUnsavedChanges = true
    },
    
    save() {
      // 保存逻辑
      this.hasUnsavedChanges = false
    }
  }
}

三、完整导航流程图

让我们通过一个完整的流程图来理解这些钩子的执行顺序:

是

否

是

next

next false

beforeRouteEnter 特殊处理
无法访问 this通过 next 回调访问实例开始导航组件是否复用?调用 beforeRouteUpdate调用 beforeRouteEnter组件内部处理确认导航 next创建组件实例执行 beforeRouteEnter 的回调渲染组件用户停留页面用户触发新导航?调用 beforeRouteLeave允许离开?执行新导航停留在当前页面

四、实际项目中的应用案例

案例1:用户权限验证系统

// UserProfile.vue
export default {
  beforeRouteEnter(to, from, next) {
    // 检查用户是否登录
    const isAuthenticated = checkAuth()
    
    if (!isAuthenticated) {
      // 未登录,重定向到登录页
      next({
        path: '/login',
        query: { redirect: to.fullPath }
      })
    } else if (!hasPermission(to.params.id)) {
      // 没有权限,重定向到403页面
      next('/403')
    } else {
      // 允许访问
      next()
    }
  },
  
  beforeRouteLeave(to, from, next) {
    // 如果是管理员,记录操作日志
    if (this.user.role === 'admin') {
      logAdminAccess(from.fullPath, to.fullPath)
    }
    next()
  }
}

案例2:电商商品详情页优化

// ProductDetail.vue
export default {
  data() {
    return {
      productnull,
      relatedProducts: []
    }
  },
  
  beforeRouteEnter(to, from, next) {
    // 预加载商品基础信息
    preloadProduct(to.params.id)
      .then(product => {
        next(vm => {
          vm.product = product
          // 同时开始加载相关商品
          vm.loadRelatedProducts(product.category)
        })
      })
      .catch(() => {
        next('/404'// 商品不存在
      })
  },
  
  beforeRouteUpdate(to, from, next) {
    // 商品ID变化时,平滑过渡
    this.showLoading = true
    this.fetchProductData(to.params.id)
      .then(() => {
        this.showLoading = false
        next()
      })
      .catch(() => {
        next(false// 保持当前商品
      })
  },
  
  methods: {
    async fetchProductData(id) {
      const [product, related] = await Promise.all([
        api.getProduct(id),
        api.getRelatedProducts(id)
      ])
      this.product = product
      this.relatedProducts = related
    },
    
    loadRelatedProducts(category) {
      // 异步加载相关商品
    }
  }
}

五、高级技巧与最佳实践

1. 组合式API中的使用

import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

export default {
  setup() {
    const unsavedChanges = ref(false)
    
    // 使用组合式API守卫
    onBeforeRouteLeave((to, from) => {
      if (unsavedChanges.value) {
        return confirm('确定要离开吗?')
      }
    })
    
    onBeforeRouteUpdate(async (to, from) => {
      // 处理路由参数更新
      await loadData(to.params.id)
    })
    
    return { unsavedChanges }
  }
}

2. 异步操作的优雅处理

export default {
  beforeRouteEnter(tofromnext) {
    // 使用async/await
    const enterGuard = async () => {
      try {
        const isValid = await validateToken(to.query.token)
        if (isValid) {
          next()
        } else {
          next('/invalid-token')
        }
      } catch (error) {
        next('/error')
      }
    }
    
    enterGuard()
  }
}

3. 避免常见的坑

坑1:忘记调用 next()

// ❌ 错误示例 - 会导致导航挂起
beforeRouteEnter(to, from, next) {
  if (checkAuth()) {
    // 忘记调用 next()
  }
}

// ✅ 正确示例
beforeRouteEnter(to, from, next) {
  if (checkAuth()) {
    next()
  } else {
    next('/login')
  }
}

坑2:beforeRouteEnter 中直接修改数据

// ❌ 错误示例
beforeRouteEnter(to, from, next) {
  next(vm => {
    // 避免直接修改响应式数据
    vm.someData = 'value' // 可能导致响应式问题
  })
}

// ✅ 正确示例
beforeRouteEnter(to, from, next) {
  next(vm => {
    vm.$nextTick(() => {
      vm.someData = 'value' // 在下一个tick中修改
    })
  })
}

六、与其他导航守卫的配合

组件内守卫还可以与全局守卫、路由独享守卫配合使用:

// 全局前置守卫
router.beforeEach((to, from, next) => {
  console.log('全局守卫 → 组件守卫')
  next()
})

// 路由配置中的独享守卫
const routes = [
  {
    path'/user/:id',
    componentUserProfile,
    beforeEnter(to, from, next) => {
      console.log('路由独享守卫 → 组件守卫')
      next()
    }
  }
]

执行顺序

    1. 导航被触发
    1. 调用全局 beforeEach
    1. 调用路由配置中的 beforeEnter
    1. 调用组件内的 beforeRouteEnter
    1. 导航被确认
    1. 调用全局的 afterEach

七、性能优化建议

1. 懒加载守卫逻辑

export default {
  beforeRouteEnter(to, from, next) {
    // 按需加载验证模块
    import('@/utils/auth').then(module => {
      if (module.checkPermission(to.meta.requiredRole)) {
        next()
      } else {
        next('/forbidden')
      }
    })
  }
}

2. 缓存验证结果

let authCache = null

export default {
  beforeRouteEnter(to, from, next) {
    if (authCache === null) {
      // 首次验证
      checkAuth().then(result => {
        authCache = result
        handleNavigation(result, next)
      })
    } else {
      // 使用缓存结果
      handleNavigation(authCache, next)
    }
  }
}

总结

Vue Router 的组件内路由钩子为我们提供了强大的导航控制能力。通过合理使用这三个钩子函数,我们可以:

  1. 1. beforeRouteEnter:在组件创建前进行权限验证和数据预加载
  2. 2. beforeRouteUpdate:优化动态参数页面的用户体验
  3. 3. beforeRouteLeave:防止用户意外丢失未保存的数据

记住这些钩子的调用时机和限制,结合实际的业务需求,你就能构建出更加健壮、用户友好的单页应用。

❌