这两天更新完基本内容,后续不定期更新,进阶部分长期更新,建议关注收藏点赞。
目录
- 创建 Vue3 项目(Vite + TS)
- 样式体系 - Tailwind CSS 快速写样式
- 模板语法与数据绑定
- 响应式系统(ref、reactive、computed、watch)
- 组件系统(Props、事件、插槽)
- Vue Router v4(多页面跳转、路由传参、嵌套页面)
- 状态管理(Vuex4)
- 状态管理(Pinia)
- 表单处理 + 校验 + Composition API 实战技巧
- 项目实战
- 网络请求 - Axios 封装与接口调用
- 权限控制 - 登录鉴权、路由拦截、用户角色控制
- 性能优化(懒加载、组件缓存、响应式优化)
- Vue 项目单元测试(Jest + Vue Test Utils)
- 项目部署 + 构建优化(Vite 构建分析、上线流程)
- 进阶- SSR(服务器端渲染)与 Nuxt.js
- 进阶- PWA(渐进式 Web 应用)与离线功能
- 进阶 - 微前端架构与模块化开发
创建 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 default
、defineComponent
更好支持类型推导(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 里!
这两个地方是配置路由用的,不适合处理“跳转事件”逻辑。
- 跳转页面方式
- 使用
<router-link>
跳转路由时,不会导致浏览器整页刷新,只会在页面中“局部切换”内容。正是 单页应用(SPA) 的核心特性。 - 编程式导航(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 下模块类型增强(可选)
定义模块的类型,来获得自动补全和类型检查。
该文件放的位置:
- 这个文件要放在项目中 TypeScript 能识别的位置,如 src/types/ 或根目录(types/vue-shim.d.ts 或 global.d.ts)。
要在 tsconfig.json 里保证包含了这个文件(通常默认会包含)
给所有组件实例 添加一个统一可用的全局属性
"include": ["src", "src/types"]
。 - 放在同一个模块文件旁(适合自定义模块增强)
比如你定义了一个模块 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>
- 响应式优化
- ref vs reactive
对于简单值(string、number)用 ref
对于对象、数组,用 reactive
不要对嵌套复杂结构乱用 reactive,避免性能损耗 - 避免不必要的响应式
- 如果只是想响应“顶层引用变化”,而不响应内部属性变化。使用 shallowRef, shallowReactive
- 虚拟列表(解决长列表性能问题)
当列表数据非常多时(1000+条),Vue 的渲染性能会明显下降。推荐虚拟滚动库vue-virtual-scroller
- 常见性能优化方案
问题 | 解决方案 |
---|---|
页面卡顿 | 懒加载、虚拟列表 |
列表渲染慢 | 使用 :key 提高 diff 效率 |
状态更新频繁 | 合理拆分组件 + watch 限制频率 |
响应式失控 | 精准使用 ref 和 reactive ,必要时使用 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.log | esbuild 内置支持 |
压缩体积 | 动态导入组件、按需引入库 |
减少依赖 | 替换大型库(如 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 是关键:让前端路由生效。
- Nginx 配置
server {
listen 80;
server_name your-domain.com;
root /path/to/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}