Vue 3 作为当前主流前端框架之一,其生态系统已经发展成为一个功能完备的技术矩阵。我使用 Vue 3 开发过多个大型项目,深刻体会到这套技术栈的强大之处。不同于简单的框架升级,Vue 3 从底层架构到上层工具都进行了革命性的重构,为开发者提供了全新的开发体验。
Vue 3 最显著的变化是其模块化架构设计。与 Vue 2 的整体式架构不同,Vue 3 将各个功能模块解耦,使得框架更加灵活和可扩展。这种设计带来的直接好处是:
我在一个需要轻量级嵌入的项目中就受益于这种架构。通过只引入响应式系统和虚拟 DOM 核心,最终打包体积比完整版减少了近 40%。
组合式 API 是 Vue 3 最具革命性的特性。在开发复杂业务组件时,传统的选项式 API 会导致相关逻辑分散在不同选项中,而组合式 API 允许我们将逻辑按功能组织:
javascript复制// 用户认证逻辑组合
function useAuth() {
const user = ref(null)
const isLoggedIn = computed(() => !!user.value)
const login = async (credentials) => {
// 登录实现
}
const logout = () => {
// 登出实现
}
return { user, isLoggedIn, login, logout }
}
// 在组件中使用
export default {
setup() {
const { user, isLoggedIn, login } = useAuth()
// 其他逻辑...
return { user, isLoggedIn, login }
}
}
这种模式特别适合大型项目,我在一个后台管理系统项目中,将各种业务逻辑封装成 20 多个这样的组合函数,极大提高了代码复用率。
Vue 3 使用 ES6 Proxy 重构了响应式系统,这带来了几个关键改进:
Proxy 的工作原理示例:
javascript复制const reactive = (target) => {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key) // 依赖收集
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key) // 触发更新
return result
}
// 其他拦截器...
})
}
Vue 3 提供了一系列响应式工具函数,合理使用它们可以优化性能:
shallowRef:只对 .value 属性做响应式处理shallowReactive:只对根级别属性做响应式处理markRaw:标记对象永远不被转为响应式在开发一个大型表单时,我使用 shallowReactive 处理表单数据结构,避免了深层嵌套带来的性能损耗:
javascript复制const formData = shallowReactive({
basicInfo: { /* 大量嵌套数据 */ },
contactInfo: { /* 大量嵌套数据 */ }
})
// 手动控制需要响应式的字段
watch(() => formData.basicInfo.name, (newVal) => {
// 特定字段的响应逻辑
})
Vue Router 4 与组合式 API 深度集成,提供了更灵活的路由控制方式:
javascript复制import { useRouter, useRoute } from 'vue-router'
export default {
setup() {
const router = useRouter()
const route = useRoute()
// 编程式导航
const navigate = (path) => {
router.push(path)
}
// 监听路由变化
watch(() => route.params.id, (newId) => {
// 处理参数变化
})
return { navigate }
}
}
在实际项目中,我通常会实现以下路由优化:
<transition> 实现平滑切换Pinia 作为 Vue 的官方状态管理库,比 Vuex 更加简洁和类型友好。一个典型的 store 定义:
typescript复制// stores/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
history: [] as number[]
}),
getters: {
doubled: (state) => state.count * 2
},
actions: {
increment() {
this.history.push(this.count)
this.count++
},
// 异步 action
async fetchData() {
const data = await api.getData()
// 更新 state...
}
}
})
在组件中使用:
javascript复制import { useCounterStore } from '@/stores/counter'
export default {
setup() {
const counter = useCounterStore()
// 直接访问 state
console.log(counter.count)
// 调用 action
counter.increment()
// 使用 getter
console.log(counter.doubled)
return { counter }
}
}
Vue 3 的编译器做了多项优化:
这些优化在大型列表渲染时效果尤为明显。在一个数据可视化项目中,使用 Vue 3 渲染 1000+ 节点的树形结构,性能比 Vue 2 提升了近 3 倍。
合理使用 v-memo:缓存模板子树
html复制<div v-memo="[item.id]">
{{ item.content }}
</div>
避免不必要的响应式:对大型静态数据使用 markRaw
优化 watch 使用:
javascript复制// 不好的做法 - 深度监听大型对象
watch(() => state.largeObject, (newVal) => {
// ...
}, { deep: true })
// 好的做法 - 只监听需要的属性
watch(() => state.largeObject.importantField, (newVal) => {
// ...
})
经过多个项目实践,我总结出以下高效的项目结构:
code复制src/
├── assets/ # 静态资源
├── components/ # 通用组件
│ ├── ui/ # 基础UI组件
│ └── business/ # 业务组件
├── composables/ # 组合式函数
├── views/ # 页面组件
├── router/ # 路由配置
├── stores/ # Pinia状态管理
├── services/ # API服务
│ ├── api.ts # 基础请求封装
│ └── modules/ # 模块化API
├── utils/ # 工具函数
├── types/ # 类型定义
├── App.vue # 根组件
└── main.ts # 入口文件
企业后台系统通常需要完善的权限控制,我通常这样实现:
路由权限:通过路由守卫控制访问
javascript复制router.beforeEach(async (to) => {
const authStore = useAuthStore()
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
return '/login'
}
// 其他权限检查...
})
组件权限:自定义指令控制元素显示
javascript复制app.directive('permission', {
mounted(el, binding) {
const authStore = useAuthStore()
if (!authStore.hasPermission(binding.value)) {
el.parentNode?.removeChild(el)
}
}
})
API 权限:在请求拦截器中处理
javascript复制axios.interceptors.response.use(response => {
if (response.data.code === 403) {
// 处理权限不足
}
return response
})
在使用解构赋值时容易丢失响应式:
javascript复制// 错误做法 - 解构会丢失响应式
const { count } = useCounterStore()
// 正确做法1 - 使用 storeToRefs
import { storeToRefs } from 'pinia'
const { count } = storeToRefs(useCounterStore())
// 正确做法2 - 直接访问
const counter = useCounterStore()
console.log(counter.count) // 保持响应式
在开发复杂应用时需要注意:
清理事件监听器:
javascript复制onMounted(() => {
const handler = () => { /*...*/ }
window.addEventListener('resize', handler)
onUnmounted(() => {
window.removeEventListener('resize', handler)
})
})
取消未完成的异步操作:
javascript复制setup() {
let isActive = true
const fetchData = async () => {
const data = await api.getData()
if (isActive) {
// 更新状态
}
}
onUnmounted(() => {
isActive = false
})
}
javascript复制// vite.config.js
export default defineConfig({
plugins: [
vue(),
// 自动导入组件
Components({
resolvers: [ElementPlusResolver()],
dts: true // 生成类型声明文件
})
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
},
server: {
proxy: {
'/api': {
target: 'http://backend:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
build: {
rollupOptions: {
output: {
manualChunks: {
vue: ['vue', 'vue-router', 'pinia'],
element: ['element-plus']
}
}
}
}
})
组合式 API 调试:
javascript复制const count = ref(0)
// 在控制台调试
window.__state = { count }
性能分析:
javascript复制import { startMeasure, stopMeasure } from 'vue'
startMeasure('my-operation')
// 执行操作...
stopMeasure('my-operation')
自定义 DevTools 钩子:
javascript复制if (process.env.NODE_ENV === 'development') {
window.__VUE_DEVTOOLS_GLOBAL_HOOK__.emit('custom-event', data)
}
渐进式迁移:
@vue/compat 构建兼容版本自动化工具辅助:
bash复制vue-cli-service migrate --plugin composition-api
常见问题处理:
构建兼容性矩阵表:
| 库名称 | Vue 2 版本 | Vue 3 版本 | 迁移难度 |
|---|---|---|---|
| Vue Router | v3.x | v4.x | 中等 |
| Vuex | v3.x | Pinia | 高 |
| Element UI | v2.x | Element Plus | 高 |
| Axios | 无变化 | 无变化 | 低 |
使用 Vitest 进行高效测试:
javascript复制// vitest.config.js
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
coverage: {
reporter: ['text', 'json', 'html']
}
}
})
组件测试示例:
javascript复制import { mount } from '@vue/test-utils'
import Counter from '@/components/Counter.vue'
describe('Counter.vue', () => {
it('increments count when button is clicked', async () => {
const wrapper = mount(Counter)
await wrapper.find('button').trigger('click')
expect(wrapper.find('p').text()).toContain('Count: 1')
})
})
使用 Cypress 进行端到端测试:
javascript复制// cypress/e2e/counter.cy.js
describe('Counter', () => {
it('increments count', () => {
cy.visit('/counter')
cy.contains('button', 'Increment').click()
cy.contains('Count: 1')
})
})
配置 CI 集成:
yaml复制# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm install
- run: npm run test:unit
- run: npm run test:e2e
代码分割策略:
javascript复制// vite.config.js
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia'],
ui: ['element-plus'],
charts: ['echarts']
}
}
}
}
CDN 配置:
javascript复制build: {
rollupOptions: {
external: ['vue', 'vue-router'],
output: {
globals: {
vue: 'Vue',
'vue-router': 'VueRouter'
}
}
}
}
dockerfile复制# Dockerfile
FROM node:16 as builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Nginx 配置示例:
nginx复制# nginx.conf
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:3000;
}
}
javascript复制// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import federation from '@originjs/vite-plugin-federation'
export default defineConfig({
plugins: [
vue(),
federation({
name: 'host-app',
remotes: {
remote_app: 'http://localhost:5001/assets/remoteEntry.js'
},
shared: ['vue', 'pinia']
})
],
build: {
target: 'esnext'
}
})
主应用配置:
javascript复制import { registerMicroApps, start } from 'qiankun'
registerMicroApps([
{
name: 'vue3-app',
entry: '//localhost:7100',
container: '#subapp-container',
activeRule: '/vue3'
}
])
start()
子应用配置:
javascript复制// main.js
import { createApp } from 'vue'
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper'
let app
function render(props = {}) {
const { container } = props
app = createApp(App)
app.mount(container ? container.querySelector('#app') : '#app')
}
renderWithQiankun({
mount(props) {
render(props)
},
bootstrap() {},
unmount() {
app.unmount()
}
})
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
render()
}
javascript复制// main.js
import { createApp } from 'vue'
import Vant from 'vant'
import 'vant/lib/index.css'
const app = createApp(App)
app.use(Vant)
响应式适配方案:
javascript复制// postcss.config.js
module.exports = {
plugins: {
'postcss-px-to-viewport': {
viewportWidth: 375, // 设计稿宽度
unitPrecision: 5,
viewportUnit: 'vw',
selectorBlackList: ['.ignore'],
minPixelValue: 1,
mediaQuery: false
}
}
}
使用 @vueuse/gesture 处理手势:
javascript复制import { useGesture } from '@vueuse/gesture'
const bind = useGesture({
onDrag: ({ active, movement: [mx] }) => {
// 处理拖拽逻辑
},
onPinch: ({ origin: [ox, oy], movement: [ms] }) => {
// 处理缩放逻辑
}
})
// 在模板中使用
<div v-bind="bind()" />
动画性能优化技巧:
will-change 属性提示浏览器javascript复制// nuxt.config.ts
export default defineNuxtConfig({
modules: [
'@pinia/nuxt',
'@nuxtjs/tailwindcss'
],
vite: {
plugins: [
// 自定义 Vite 插件
]
},
runtimeConfig: {
public: {
apiBase: process.env.API_BASE
}
}
})
基于 Vite 的 SSR 配置:
javascript复制// server.js
import { createServer } from 'vite'
import { renderToString } from 'vue/server-renderer'
const server = await createServer({
server: { middlewareMode: 'ssr' }
})
app.use('*', async (req, res) => {
const url = req.originalUrl
const template = await server.transformIndexHtml(
url,
fs.readFileSync('index.html', 'utf-8')
)
const { app } = await server.ssrLoadModule('/src/entry-server.js')
const html = await renderToString(app)
res.end(template.replace('<!--app-html-->', html))
})
typescript复制// stores/modules/user.ts
export const useUserStore = defineStore('user', () => {
const profile = ref<UserProfile | null>(null)
const permissions = ref<string[]>([])
const fetchProfile = async () => {
profile.value = await api.getProfile()
}
return { profile, permissions, fetchProfile }
})
// stores/modules/app.ts
export const useAppStore = defineStore('app', () => {
const theme = ref('light')
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
return { theme, toggleTheme }
})
使用 pinia-plugin-persistedstate:
javascript复制import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
// 在 store 中使用
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
userInfo: null
}),
persist: {
paths: ['token'], // 只持久化 token
storage: localStorage
}
})
javascript复制// plugins/i18n.js
import { createI18n } from 'vue-i18n'
import messages from '@/locales'
export default createI18n({
locale: 'zh-CN',
fallbackLocale: 'en',
messages
})
// 在组件中使用
import { useI18n } from 'vue-i18n'
export default {
setup() {
const { t, locale } = useI18n()
const changeLanguage = (lang) => {
locale.value = lang
}
return { t, changeLanguage }
}
}
javascript复制// locales/index.js
export async function loadLocaleMessages(i18n, locale) {
if (i18n.global.availableLocales.includes(locale)) {
return
}
const messages = await import(`./${locale}.json`)
i18n.global.setLocaleMessage(locale, messages.default)
return nextTick()
}
内容安全策略 (CSP):
html复制<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
安全编码实践:
javascript复制// 使用 DOMPurify 净化 HTML
import DOMPurify from 'dompurify'
const clean = DOMPurify.sanitize(userInput)
Vue 内置防护:
v-html 时要特别小心SameSite Cookie:
javascript复制// 后端设置
Set-Cookie: sessionId=123; SameSite=Strict; Secure
CSRF Token:
javascript复制// Axios 拦截器
axios.interceptors.request.use(config => {
config.headers['X-CSRF-Token'] = getCSRFToken()
return config
})
使用 web-vitals 库:
javascript复制import { getCLS, getFID, getLCP } from 'web-vitals'
function sendToAnalytics(metric) {
// 发送到监控系统
}
getCLS(sendToAnalytics)
getFID(sendToAnalytics)
getLCP(sendToAnalytics)
javascript复制// 全局错误处理
app.config.errorHandler = (err, vm, info) => {
console.error('Vue error:', err)
// 发送到错误监控系统
}
// 未捕获的 Promise 错误
window.addEventListener('unhandledrejection', event => {
console.error('Unhandled rejection:', event.reason)
// 发送到错误监控系统
})
vue复制<!-- src/components/Button.vue -->
<template>
<button
:class="[
'btn',
`btn-${type}`,
{ 'btn-disabled': disabled }
]"
:disabled="disabled"
@click="handleClick"
>
<slot></slot>
</button>
</template>
<script setup>
defineProps({
type: {
type: String,
default: 'default',
validator: (value) => ['default', 'primary', 'danger'].includes(value)
},
disabled: Boolean
})
const emit = defineEmits(['click'])
const handleClick = (event) => {
if (!props.disabled) {
emit('click', event)
}
}
</script>
vue复制<template>
<div ref="chartRef" style="width: 600px; height: 400px;"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
const chartRef = ref(null)
let chartInstance = null
onMounted(() => {
chartInstance = echarts.init(chartRef.value)
const option = {
// ECharts 配置
}
chartInstance.setOption(option)
// 响应式调整
window.addEventListener('resize', resizeHandler)
})
onUnmounted(() => {
window.removeEventListener('resize', resizeHandler)
chartInstance?.dispose()
})
const resizeHandler = () => {
chartInstance?.resize()
}
</script>
vue复制<template>
<canvas ref="canvasRef"></canvas>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as THREE from 'three'
const canvasRef = ref(null)
onMounted(() => {
// 初始化场景、相机、渲染器
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
const renderer = new THREE.WebGLRenderer({ canvas: canvasRef.value })
// 添加物体
const geometry = new THREE.BoxGeometry()
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
const cube = new THREE.Mesh(geometry, material)
scene.add(cube)
camera.position.z = 5
// 动画循环
const animate = () => {
requestAnimationFrame(animate)
cube.rotation.x += 0.01
cube.rotation.y += 0.01
renderer.render(scene, camera)
}
animate()
// 响应式调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
})
})
onUnmounted(() => {
// 清理资源
})
</script>
在多个 Vue 3 项目实践中,我总结了以下关键经验:
特别在大型项目中,良好的架构设计可以显著降低维护成本。我通常会:
这些实践帮助我在多个复杂项目中保持代码质量和开发效率。