【前端】快速过一遍Vue3全家桶2025

#王者杯·14天创作挑战营·第1期#

这两天更新完基本内容,后续不定期更新,进阶部分长期更新,建议关注收藏点赞。

创建 Vue3 项目(Vite + TS)

npm create vite@latest my-vue-app -- --template vue-ts
# -- 告诉 npm 后面是要传给 vite 的参数,不是 npm 自己的参数(是一个分隔符)。
# --template vue-ts 指定使用 vue-ts 模板,表示该项目使用 Vue 3 + TypeScript 模板。
cd my-vue-app
npm install
npm run dev
  • 目录结构
目录/文件作用
main.ts入口文件,挂载 App.vue 到页面上
App.vue根组件,可以当作应用入口
components/放各种小组件
vite.config.ts项目配置(可以添加路径别名等)

样式体系 - Tailwind CSS 快速写样式

后续补充

模板语法与数据绑定

  • 插值表达式 {{variate}}在模版中使用
  • 属性绑定 v-bind(简写为 :
  • 事件绑定 @click
  • (表单)v-model 双向绑定内容
  • 条件渲染 v-if / v-else
  • 列表渲染 v-for
    :key 必写,用来唯一标识每项,有助于性能优化。

响应式系统(ref、reactive、computed、watch)

  • 响应式系统
    只要改数据,页面会自己更新,无需手动操作 DOM。
    Vue 3 通过 Composition API 中的 ref() 和 reactive() 等,建立了一个响应式系统 —— 任何响应式数据的变化,都会自动让相关的模板、计算、监听器等同步更新。
  • ref() 基本类型
    会在模版中自动解包,但在js中要搭配.value使用
    可以在模板中使用并能触发响应式更新。
import { ref } from 'vue';
const myArray = ref<string[]>([]);
/*
<string[]> 是泛型写法,告诉 TypeScript:这个 ref 中存放的是一个 字符串数组。
[]是初始值,表示这是一个空数组。
*/
  • reactive() 对象/数组(引用类型)
    注意:reactive 返回的是整个对象的代理,不能像 ref().value 那样操作。而是直接使用其key
  • computed():计算属性(带缓存)
    computed 会在依赖值变化时重新计算,并具备缓存能力。为什么说具备缓存能力,是因为它的值只会在依赖的响应式数据发生变化时才重新计算,否则会使用上次缓存的结果,避免重复运算,提高性能。
    const priceWithTax = computed(() => price.value * 1.13)
  • watch():监听变化,做副作用操作
    watch 在 Vue 3 中默认是“懒执行”的,也就是不会在创建时立即执行,而是等被监听的值首次变化后才执行回调函数。
    用途:如监听输入框、状态变化、定时器清理、接口触发等。
// 每次 keyword 改变都会触发回调
watch(keyword, (newVal, oldVal) => {
  console.log(`搜索关键字变了:${oldVal}${newVal}`)
  // 模拟:请求接口、过滤数据等操作
})
  • watchEffect():自动追踪依赖(高级)
    非懒执行,创建时立即执行 + 自动依赖追踪,不推荐滥用。
watchEffect(() => {
  console.log('当前搜索词是:', keyword.value)
})

组件系统(Props、事件、插槽)

组件是 Vue 项目的基本构建单元,每个小页面/小功能都是一个组件。

  • <script setup lang="ts">用来指定当前 <script> 标签中使用的编程语言是 TypeScript。如果你在 Vue 组件中使用 TypeScript,就需要加上这个属性。
  • <style scoped>用来指示当前样式是否只作用于当前组件的,它将样式限制在当前 Vue 组件中,不会影响其他组件。如果你希望样式只应用于当前组件,就需要加上这个属性。
  • <script setup> 是Vue3 的推荐写法
    优点:
    更简洁,不需要 export defaultdefineComponent
    更好支持类型推导(props/emits)
    性能更好,编译期优化更彻底
  • 组件基本结构+引入
<!-- HelloWorld.vue -->
<template>
  <h2>Hello, {{ name }}</h2>
</template>

<script setup lang="ts">
const name = 'Vue 学员'
</script>

<style scoped>
h2 {
  color: teal;
}
</style>

<!-- App.vue -->
<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'
</script>

<template>
  <HelloWorld />
</template>
  • Props:父传子(传值)
<!-- 子组件 Message.vue -->
<template>
  <p>消息内容:{{ content }}</p>
</template>

<script setup lang="ts">
defineProps<{
  content: string
}>()
/*
defineProps<{}>() 中的尖括号 <> 里必须是一个对象类型,不能直接写 string、number、boolean 等原始类型。
{} 是用来声明一个对象类型,这个对象指定了组件将会接收哪些 props 及其类型。
content: string:定义一个 prop,名为 content,它的类型是 string
*/
</script>

<!-- 父组件 App.vue -->
<script setup lang="ts">
import Message from './components/Message.vue'
</script>

<template>
  <Message content="你好,这是父组件发的消息" />
</template>
  • 插槽 Slot(父组件传“内容”给子组件)
<!-- 子组件 Card.vue -->
<template>
  <div class="card">
    <slot />
  </div>
</template>

<style scoped>
.card {
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 8px;
}
</style>
<!-- 父组件 App.vue -->
<script setup lang="ts">
import Card from './components/Card.vue'
</script>

<template>
  <Card>
    <h3>我是插槽内容</h3>
    <p>我将被放入子组件中</p>
  </Card>
</template>

和props都是父传子,那有什么区别?
props 是父传子【数据】,slot 是父传子【结构内容,即HTML/组件】。

  • Emit:子传父(事件)
<!-- 子组件 Counter.vue -->
<template>
  <button @click="emit('addOne')">点击 +1</button>
</template>

<script setup lang="ts">
const emit = defineEmits<{
  (e: 'addOne'): void
}>()
/*
Vue 3 <script setup> 中使用 TypeScript 为 emit 事件进行类型声明
通过 emit('addOne') 触发一个名为 'addOne' 的事件,并且这个事件 没有参数(返回值是 void)。

{}语法上它是一个 TypeScript 对象类型,但它不是普通的属性对象
而是一个函数签名的对象类型,这在 TypeScript 里叫做接口的函数重载定义结构。事件函数的类型签名.
()是函数类型定义。接受一个参数 e,值必须是 'addOne',而函数不返回任何东西(void)。
*/
</script>

<!-- 父组件 App.vue -->
<script setup lang="ts">
import Counter from './components/Counter.vue'

function handleAdd() {
  alert('子组件触发了事件')
}
</script>

<template>
  <Counter @addOne="handleAdd" />
</template>

Vue Router v4(多页面跳转、路由传参、嵌套页面)

所有路由页面放在 src/views/
所有组件放在 src/components/

npm install vue-router

创建路由配置文件src/router/index.ts

// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'
/*
@ 是一种配置别名,表示你的项目中某个目录的快捷路径。
在 Vue 项目中,@ 通常是通过构建工具(如 Vite 或 Webpack)配置的,用来简化模块路径的书写,避免使用复杂的相对路径。
在vite.config.ts 或者 webpack.config.js中配置alias
*/

const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About }
]

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

export default router

挂载路由到 Vue 应用,在 main.ts 中注册:

//main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
//.mount('#app'):把这个应用挂载到 HTML 页面中 id="app" 的那个元素上。

不要直接放在 router/index.ts 或 main.ts 里!
这两个地方是配置路由用的,不适合处理“跳转事件”逻辑。

  • 跳转页面方式
  1. 使用 <router-link>
    跳转路由时,不会导致浏览器整页刷新,只会在页面中“局部切换”内容。正是 单页应用(SPA) 的核心特性。
  2. 编程式导航(js 跳转)
    编程式导航也必须依赖 Vue Router 正常安装并配置好路由表。它不能跳转到未注册的路径,否则会报错或显示 404 页面。
    同样是点击按钮就会跳转,但页面不会刷新,仍然是 SPA 风格
<template>
  <nav>
    <router-link to="/">首页</router-link>
    <router-link to="/about">关于</router-link>
  </nav>
</template>
//放在需要跳转的组件
import { useRouter } from 'vue-router'
const router = useRouter()
router.push('/about')//跳转到 /about 页面
  • 传参方式
    动态路由参数 或者 query 参数(?key=value)
    • :id 是 动态路由参数,也叫做“路径参数”或“占位符”。Vue Router 会自动把 xxx 提取出来,作为参数传给你
{ path: '/user/:id', component: UserDetail }
//跳转 写在需要跳转的组件内
<router-link :to="`/user/${userId}`">查看用户</router-link>

//读取参数 写在需要读取参数的组件内
import { useRoute } from 'vue-router'
const route = useRoute()
console.log(route.params.id)

//query参数
//跳转
router.push({ path: '/search', query: { keyword: 'vue' } })
//读取
console.log(route.query.keyword)
  • 嵌套路由(子页面)
//配套
{
  path: '/layout',
  component: Layout,
  children: [
    { path: 'a', component: PageA },// 当路径是 '/layout/a' 时显示 PageA
    { path: 'b', component: PageB }// 当路径是 '/layout/b' 时显示 PageB
  ]
}
<!-- Layout.vue -->
<template>
  <h2>我是布局</h2>
  <router-view /> <!-- 子路由的页面将渲染到这里 -->
</template>
  • 重定向与 404 页面
{ path: '/', redirect: '/home' },
{ path: '/:pathMatch(.*)*', component: NotFound }
/*
:pathMatch 是一个动态路由参数,它会捕获所有路径。
(.*) 是一个正则表达式,表示匹配任何字符(包括 /)。
* 是 Vue Router 特有的修饰符,表示它可以匹配任意深度的路径。
这条路由会匹配所有不符合其他路由规则的路径,并且将路径部分存储在pathMatch参数中
直到用户进入新的路由为止,会更新成新的路径
*/

状态管理(Vuex4)

Vuex 是 Vue 的状态管理库,统一管理多个组件之间共享的状态。Vuex 就是一个 专门用来存数据的“公共仓库”,所有组件都可以从这个仓库读数据、改数据。
适用于:
多个组件需要共享数据
组件之间通信复杂
页面刷新后数据丢失的情况(结合持久化)

概念作用
state存储数据
getters类似 computed,派生状态
mutations修改 state 的唯一方法(同步),不能放异步代码如 setTimeout、axios
actions异步操作,比如请求数据
modules模块化拆分 store
组件 —— 读取数据:store.state.xxx
组件 —— 改数据:store.commit('mutation名称')

复杂情况(如异步):
组件 —— store.dispatch('action名称') —— mutation —— state
actions不能直接改state,通过commit调mutation

dispatch 调 actions,commit 调 mutations
  • 安装
npm install vuex@4
  • 创建store文件 如store/index.ts
import { createStore } from 'vuex'

const store = createStore({
  state() {
    return {
      count: 0//全局数据
    }
  },
  mutations: {//专门用来“同步修改”数据(必须通过它来改)
    increment(state) {
      state.count++
    },
    add(state, payload) {
      state.count += payload
    }
  },
  actions: {
    asyncIncrement({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 1000)
    }
  },
  getters: {//类似computed计算属性
    doubleCount(state) {
      return state.count * 2
    }
  }
})

export default store
  • 在main.ts注册store
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'// 引入刚才写的 store
//默认引入的是文件夹 store 下的 index.ts 文件。
//自动去找该文件夹里的 index.ts 或 index.js 文件

createApp(App).use(store).mount('#app')
  • 在组件中使用
<script setup lang="ts">
import { useStore } from 'vuex'
import { computed } from 'vue'

const store = useStore()

// 读取数据
const count = computed(() => store.state.count)

// 读取 getter
const double = computed(() => store.getters.doubleCount)

// 触发 mutation
const increment = () => store.commit('increment')

// 触发 action
const asyncAdd = () => store.dispatch('asyncIncrement')
</script>

<template>
  <h2>{{ count }}</h2>
  <h2>{{ double }}</h2>
  <button @click="increment">+</button>
  <button @click="asyncAdd">异步+</button>
</template>
  • 模块化写法
src/
├─ store/
│  ├─ index.ts        ← 主入口
│  └─ modules/
│     └─ user.ts      ← 用户模块
│     └─ counter.ts      ← 计数器模块
// store/modules/user.ts
const userModule = {
  namespaced: true, // 开启命名空间,防止命名冲突
  state: () => ({
    name: '小明',
    token: ''
  }),
  mutations: {
    setName(state, newName: string) {
      state.name = newName
    }
  },
  actions: {
    login({ commit }, username) {
      // 假装登录成功
      commit('setName', username)
    }
  },
  getters: {
    welcome(state) {
      return `欢迎你,${state.name}`
    }
  }
}
export default userModule


//store/modules/counter.ts
export default {
  namespaced: true,//开启命名空间,防止命名冲突
  state: () => ({ count: 0 }),
  mutations: {
    increment(state) {
      state.count++
    }
  }
}

//store/index.ts
import { createStore } from 'vuex'
import counter from './modules/counter'
import user from './modules/user'
export default createStore({
  modules: {
    counter,
    user
  }
})

//组件中
const store = useStore()

// 获取用户名称(需要指定模块名)
const username = computed(() => store.state.user.name)
const welcomeText = computed(() => store.getters['user/welcome'])

// 提交 mutations(模块名/方法名)
store.commit('user/setName', '张三')
// 调用 actions
store.dispatch('user/login', '李四')

Vuex 的高级玩法

  • 状态持久化(持久存储在 localStorage / sessionStorage)
    Vuex 状态默认存在内存中,刷新就没了。解决办法是:把状态持久化保存到本地存储(localStorage)。
    推荐插件:vuex-persistedstate
npm install vuex-persistedstate
// store/index.ts 
import { createStore } from 'vuex'
import createPersistedState from 'vuex-persistedstate'

const store = createStore({
  state() {
    return {
      token: ''
    }
  },
  mutations: {
    setToken(state, token: string) {
      state.token = token
    }
  },
  plugins: [//安装插件,配置 plugins
    createPersistedState({
    //默认使用 localStorage,也支持改成 sessionStorage
      key: 'my-app',
      paths: ['token'] // 指定要持久化的字段
    })
  ]
})
export default store
  • 在组合式 API 中更优雅地使用 store(配合 storeToRefs)
    Vuex 中也可以配合 storeToRefs
import { storeToRefs } from 'pinia' // 或 Vuex 兼容 API
const { name } = storeToRefs(useStore()) // 响应式拆包
//解构出来的变量仍然是响应式的
  • 动态注册模块
    当某个模块只在特定路由或场景才用到时,可以动态注册它。如果不需要了,还可以注销。
store.registerModule('myModule', {
  state: () => ({ value: 123 }),
  mutations: { setValue(state, v) { state.value = v } }
})
store.unregisterModule('myModule')
  • 监听 Vuex 状态变化(订阅 store)(调试 / 日志)
store.subscribe((mutation, state) => {
  console.log('Mutation:', mutation.type)
  console.log('Payload:', mutation.payload)
})
  • TypeScript 下模块类型增强(可选)
    定义模块的类型,来获得自动补全和类型检查。
    该文件放的位置:
  1. 这个文件要放在项目中 TypeScript 能识别的位置,如 src/types/ 或根目录(types/vue-shim.d.ts 或 global.d.ts)。
    要在 tsconfig.json 里保证包含了这个文件(通常默认会包含)
    给所有组件实例 添加一个统一可用的全局属性
    "include": ["src", "src/types"]
  2. 放在同一个模块文件旁(适合自定义模块增强)
    比如你定义了一个模块 my-module.ts,你想增强它的类型,也可以写一个同名 .d.ts 文件
// src/types/vue.d.ts
import 'vue'
declare module 'vue' {
  interface ComponentCustomProperties {
    $myGlobalProp: string
  }
}
/*
在 Vue 中,$ 符号有约定俗成的用法,用于标识一些 全局的、特殊的属性
*/

//src/main.ts
//挂载这个全局属性
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.config.globalProperties.$myGlobalProp = 'Hello, this is global!'
app.mount('#app')

//在组件中使用 this.$myGlobalProp
//HelloWorld.vue
<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  mounted() {
    console.log(this.$myGlobalProp) // ✅ 有类型提示和正确值
  }
})
</script>

状态管理(Pinia)

  • 为什么需要状态管理?
    当项目变复杂时:
    多个组件需要共享同一份数据
    修改这些数据要有统一方式
    数据变化要能驱动所有依赖它的组件重新渲染
  • 安装、配置
npm install pinia
//main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'

const app = createApp(App)
app.use(createPinia())
app.mount('#app')
  • 创建一个store(状态模块)定义 Pinia store
    创建 src/stores/counter.ts
// src/stores/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment() {
      this.count++
    }
  }
})
  • 在组件中使用 Store
    支持多个模块,只需要所有 store 统一放在 src/stores/ 下。
<!-- src/components/Counter.vue -->
<template>
  <h2>当前计数:{{ counter.count }}</h2>
  <button @click="counter.increment">+1</button>
</template>

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()
//所有使用了这个 store 的组件,会自动响应数据变化!
</script>
  • 使用 computed + watch 也可以实现数据变化响应
    对 count 使用 watch() 或 <input v-model="count" />
Pinia 中的 store 默认是响应式的,但直接从 store 解构属性会失去响应性。
使用 storeToRefs() 可以保留响应性。
const { count } = counter // ❌错误方式:失去响应性

<!-- Counter.vue -->
<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="counter.increment">+1</button>
  </div>
</template>

<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()
const { count } = storeToRefs(counter)
</script>
  • 持久化存储(如保存登录状态)
npm install pinia-plugin-persistedstate
// main.ts
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
pinia.use(piniaPluginPersistedstate)

//在store文件中使用
export const useAuthStore = defineStore('auth', {
  state: () => ({
    token: ''
  }),
  persist: true
})

表单处理 + 校验 + Composition API 实战技巧

  • 基础表单绑定 v-model
  • 基础校验(自定义)
<template>
  <input v-model="email" placeholder="请输入邮箱" type="email"/>
  <p v-if="!isValidEmail">请输入正确的邮箱格式</p>
</template>

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

const email = ref('')
const isValidEmail = computed(() => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value))
//^放在字符集开头时,表示“取反” 
//不在字符集表示为以^开头
</script>
  • 表单封装技巧(组件封装 v-model)
<!-- components/MyInput.vue -->
<template>
  <input :value="modelValue" @input="updateValue" />
</template>

<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

function updateValue(event) {
  emit('update:modelValue', event.target.value)
}
</script>

<!-- 其父组件 -->
<template>
  <ChildInput v-model="name" />
  <p>你输入的是:{{ name }}</p>
</template>

<script setup>
import { ref } from 'vue'
import ChildInput from './components/ChildInput.vue'
const name = ref('张三')
</script>

$emit 和emit有啥区别
$emit 和 emit 虽然都用来触发事件,但它们用在不同的上下文中,不是一个东西。

名称用于哪里来源
$emit传统组件写法(options API)中Vue 实例上的方法(自动可用)
emit<script setup>需要手动通过 defineEmits() 获取
  • 表单校验库推荐:vee-validate
    暂略

项目实战

#标准vue项目
src/
├── assets/         # 静态资源(图片、字体等)
├── components/     # 公共组件(UI 组件)
├── views/          # 页面视图组件
├── router/         # 路由配置
├── stores/         # 状态管理(Pinia)
├── api/            # 接口请求
├── types/          # TypeScript 类型定义
├── styles/         # 公共样式
├── utils/          # 工具函数
└── App.vue         # 根组件
└── main.ts         # 入口文件

components/ 目录中,组件名使用 PascalCase(首字母大写,驼峰式命名),文件名应与组件名一致。模版中用kebab-case命名
views/ 目录,页面名也应使用 PascalCase。存放页面级组件,通常与路由绑定
router/ 目录通常一个 index.ts 用来集中配置路由
api/ 目录 管理接口请求,每个功能模块一个文件

  • 开发规范
    尽量使用 Composition API:ref(), reactive(), computed(),避免使用 data()、methods() 等选项 API。
    状态管理集中管理:全局状态使用 Pinia,不要在组件中直接使用 props、emit 等传递状态。

网络请求 - Axios 封装与接口调用

npm install axios
  • 封装 Axios 请求
    在 src/api/ 目录下创建一个 axios.ts 文件
// src/api/axios.ts
import axios from 'axios'

// 创建一个 axios 实例
const instance = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
})

// 请求拦截器:可以做登录验证、添加 token 等操作
instance.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token')
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// 响应拦截器:处理成功响应,或者全局错误处理
instance.interceptors.response.use(
  (response) => {
    // 根据响应状态进行处理(例如:200 -> 返回数据,其他则提示错误)
    if (response.status === 200) {
      return response.data
    } else {
      return Promise.reject('接口错误')
    }
  },
  (error) => {
    console.error('请求错误:', error)
    return Promise.reject(error)
  }
)

export default instance
  • 在 src/api/ 中每个模块单独创建接口调用函数,调用封装好的 Axios 实例
// src/api/user.ts
import axios from './axios'

export const getUserInfo = async () => {
  try {
    const response = await axios.get('/user')
    return response
  } catch (error) {
    console.error('获取用户信息失败', error)
    throw error
  }
}
  • 使用接口函数
// src/views/Profile.vue
<template>
  <div>
    <h1>{{ userInfo.name }}</h1>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getUserInfo } from '@/api/user'

const userInfo = ref({ name: '' })

onMounted(async () => {
  try {
    const data = await getUserInfo()
    userInfo.value = data
  } catch (error) {
    console.error('数据获取失败')
  }
})
</script>

权限控制 - 登录鉴权、路由拦截、用户角色控制

  • 登录鉴权(Token 验证)
    登录后将返回的 Token 存入 localStorage 或 sessionStorage
    在每次请求时,通过拦截器将 Token 添加到请求头
// 登录接口(模拟)
export const login = async (username: string, password: string) => {
  const response = await axios.post('/login', { username, password })
  const token = response.token
  localStorage.setItem('token', token)  // 存储 Token
}

instance.interceptors.request.use((config) => {
  const token = localStorage.getItem('token')
  if (token) {
    config.headers['Authorization'] = `Bearer ${token}`
  }
  return config
})
  • 路由拦截(鉴权)
    在 router/index.ts 中配置路由守卫
    在 userStore 中控制 isAuthenticated 的状态
//router/index.ts 
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/user'
import Home from '@/views/Home.vue'
import Login from '@/views/Login.vue'

const routes = [
  { path: '/', component: Home, meta: { requiresAuth: true } },
  { path: '/login', component: Login },
]

const router = createRouter({
  history: createWebHistory(),
  routes,
})
/*
Vue Router 中的 全局导航守卫,
它会在每次路由跳转之前执行,用来控制页面的访问权限。
它的作用是:在进入某个需要认证的页面时,判断用户是否已认证,
如果没有,则跳转到登录页。
*/
router.beforeEach((to, from) => {
  const userStore = useUserStore()
  if (to.meta.requiresAuth && !userStore.isAuthenticated) {
    return { path: '/login' }
  }
})
export default router

// src/stores/user.ts
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
  state: () => ({
    isAuthenticated: !!localStorage.getItem('token'),
    //!! 是一个双重取反操作符,用来将任意值转换为 布尔值(true 或 false)。
    role: 'guest', // 用户角色
  }),
  actions: {
    login(role) {
      this.isAuthenticated = true
      this.role = role
    },
    logout() {
      this.isAuthenticated = false
      this.role = 'guest'
    },
  },
})
  • 用户角色控制
    在路由中可以添加 meta 字段来控制角色权限
// src/router/index.ts
const routes = [
  {
    path: '/admin',
    component: AdminPage,
    meta: { requiresAuth: true, roles: ['admin'] },
  },
]

router.beforeEach((to, from) => {
  const userStore = useUserStore()
  if (to.meta.requiresAuth && !userStore.isAuthenticated) {
    return { path: '/login' }
  }
  if (to.meta.roles && !to.meta.roles.includes(userStore.role)) {
    return { path: '/403' }
  }
})

性能优化(懒加载、组件缓存、响应式优化)

  • 路由懒加载(分包)
    减少首屏加载体积,只加载当前访问的页面组件。
    利用动态引入,Vue + Vite 会自动把不同页面组件拆分成独立的 JS 文件,按需加载。
// src/router/index.ts
const routes = [
  {
    path: '/',
    component: () => import('@/views/Home.vue')  // 懒加载
  },
  {
    path: '/about',
    component: () => import('@/views/About.vue') // 懒加载
  }
]
  • 组件缓存 <KeepAlive>
    用于缓存切换过的组件状态(比如 Tab 页切换,保持表单数据不丢失)。
    <KeepAlive>包裹模版中的组件以及路由页面中的组件
<template>
 <KeepAlive>
   <component :is="activeTab" />
 </KeepAlive>
</template>

<script setup lang="ts">
import TabA from './TabA.vue'
import TabB from './TabB.vue'

const activeTab = ref('TabA')

const tabs = {
 TabA,
 TabB
}
</script>

<!-- or -->
<template>
 <router-view v-slot="{ Component }">
 <!--
v-slot 是用来「接收子组件传出的数据」的。

-->
   <KeepAlive>
     <component :is="Component" /> <!--动态渲染不同组件-->
   </KeepAlive>
 </router-view>
</template>
  • 响应式优化
  1. ref vs reactive
    对于简单值(string、number)用 ref
    对于对象、数组,用 reactive
    不要对嵌套复杂结构乱用 reactive,避免性能损耗
  2. 避免不必要的响应式
  3. 如果只是想响应“顶层引用变化”,而不响应内部属性变化。使用 shallowRef, shallowReactive
  4. 虚拟列表(解决长列表性能问题)
    当列表数据非常多时(1000+条),Vue 的渲染性能会明显下降。推荐虚拟滚动库vue-virtual-scroller
  • 常见性能优化方案
问题解决方案
页面卡顿懒加载、虚拟列表
列表渲染慢使用 :key 提高 diff 效率
状态更新频繁合理拆分组件 + watch 限制频率
响应式失控精准使用 refreactive,必要时使用 shallow 系列

Vue 项目单元测试(Jest + Vue Test Utils)

暂略

项目部署 + 构建优化(Vite 构建分析、上线流程)

  • Vite 构建打包命令
    默认使用的是 vite.config.ts 中的配置,会生成一个 dist/ 目录:
    index.html:入口 HTML
    assets/:压缩后的 JS、CSS、图片等资源
    非常小:经过 Tree Shaking、代码压缩、懒加载等优化
    npm run build
  • Vite 构建体积分析
    构建后自动打开浏览器展示体积构成图:哪些包最大、哪些可以懒加载、是否有用不到的代码
# 安装分析插件
npm install -D rollup-plugin-visualizer
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [vue(), visualizer({ open: true })]
})
  • 构建优化
优化点做法
删除 console.logesbuild 内置支持
压缩体积动态导入组件、按需引入库
减少依赖替换大型库(如 lodash)为函数导入
CDN 加速第三方库通过 CDN 引入,不打包进项目
// vite.config.ts
import path from 'path'
export default defineConfig({
  esbuild: {
    drop: ['console', 'debugger']
  },
    resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')//别名
    }
  }
})
  • 部署到服务器
    npm run build
    将 dist/ 文件夹上传到你的服务器
    • Nginx 配置
      try_files 是关键:让前端路由生效。
server {
  listen 80;
  server_name your-domain.com;

  root /path/to/dist;
  index index.html;

  location / {
    try_files $uri $uri/ /index.html;
  }
}

进阶- SSR(服务器端渲染)与 Nuxt.js

进阶- PWA(渐进式 Web 应用)与离线功能

进阶 - 微前端架构与模块化开发

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

七灵微

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值