作为一名长期奋战在一线的前端开发者,我见证了Vue.js从2.x到3.x的蜕变过程。Vue 3带来的Composition API、性能优化和TypeScript支持等特性,让它真正具备了构建企业级应用的能力。本文将分享我在多个大型项目中积累的Vue 3实战经验,涵盖从项目搭建到生产部署的全流程。
Vue 3相比Vue 2有几个关键改进:
目前主流有两种方式创建Vue 3项目:
bash复制# 官方推荐方式(基于Vite)
npm init vue@latest
# 传统方式(基于Webpack)
npm install -g @vue/cli
vue create my-project
Vite凭借其极速的启动和热更新能力,已经成为现代Vue项目的首选。我在实际项目中测试发现,Vite的冷启动速度比Webpack快5-10倍,热更新几乎瞬间完成。
一个企业级项目需要完善的工程化配置:
javascript复制// vite.config.js 基础配置示例
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')
}
},
server: {
port: 3000,
open: true
}
})
关键配置项说明:
alias:配置路径别名,简化导入路径plugins:Vite插件系统,支持丰富的扩展server:开发服务器配置ESLint + Prettier配置:
javascript复制// .eslintrc.js
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/typescript/recommended',
'@vue/prettier',
'@vue/prettier/@typescript-eslint'
],
rules: {
'vue/multi-word-component-names': 'off'
}
}
Git Hook配置:
json复制// package.json
{
"scripts": {
"prepare": "husky install"
},
"lint-staged": {
"*.{js,jsx,vue,ts,tsx}": [
"eslint --fix",
"prettier --write"
]
}
}
安装相关依赖:
bash复制npm install -D husky lint-staged
Composition API是Vue 3最重要的革新,它解决了Options API在大型组件中的几个痛点:
基础用法示例:
vue复制<script setup>
import { ref, computed, watch } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
watch(count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`)
})
</script>
Vue 3使用Proxy替代了Vue 2的Object.defineProperty,带来了几个优势:
响应式工具对比:
| 工具 | 适用场景 | 特点 |
|---|---|---|
| ref | 基本类型 | 需要通过.value访问 |
| reactive | 对象 | 直接访问属性 |
| shallowRef | 大型对象 | 只响应.value变化 |
| shallowReactive | 大型对象 | 只响应第一层属性 |
Pinia是Vue官方推荐的状态管理库,相比Vuex有这些优势:
typescript复制// stores/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
getters: {
double: (state) => state.count * 2
},
actions: {
increment() {
this.count++
},
async incrementAsync() {
await new Promise(resolve => setTimeout(resolve, 1000))
this.increment()
}
}
})
vue复制<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
</script>
<template>
<div>Count: {{ counter.count }}</div>
<div>Double: {{ counter.double }}</div>
<button @click="counter.increment()">Increment</button>
</template>
typescript复制// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '@/views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
meta: {
requiresAuth: true
}
},
{
path: '/about',
name: 'about',
component: () => import('@/views/AboutView.vue'),
props: route => ({ query: route.query.id })
}
]
})
typescript复制router.beforeEach((to, from, next) => {
const isAuthenticated = checkAuth() // 自定义认证检查
if (to.meta.requiresAuth && !isAuthenticated) {
next({ name: 'login', query: { redirect: to.fullPath } })
} else {
next()
}
})
code复制src/
├── assets/
├── components/
│ ├── ui/ # 基础UI组件
│ ├── business/ # 业务组件
│ └── layout/ # 布局组件
├── composables/ # 组合式函数
├── router/
├── stores/
├── types/
├── utils/
├── views/
├── App.vue
└── main.ts
vue复制<script setup lang="ts">
interface Props {
title: string
count?: number
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update:title', value: string): void
}>()
</script>
vue复制<script setup lang="ts">
interface User {
id: number
name: string
age?: number
}
const user = ref<User>({
id: 1,
name: 'John'
})
</script>
typescript复制// composables/useFetch.ts
import { ref } from 'vue'
interface UseFetchOptions {
immediate?: boolean
}
export function useFetch<T>(url: string, options: UseFetchOptions = {}) {
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
const loading = ref(false)
async function execute() {
// 实现fetch逻辑
}
if (options.immediate) {
execute()
}
return {
data,
error,
loading,
execute
}
}
typescript复制// 路由懒加载
const UserProfile = () => import('@/views/UserProfile.vue')
// 组件懒加载
const Modal = defineAsyncComponent(() => import('@/components/Modal.vue'))
vue复制<template>
<RecycleScroller
class="scroller"
:items="largeList"
:item-size="50"
key-field="id"
v-slot="{ item }"
>
<div class="item">{{ item.name }}</div>
</RecycleScroller>
</template>
javascript复制// vitest.config.js
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'jsdom'
}
})
typescript复制import { mount } from '@vue/test-utils'
import Counter from '@/components/Counter.vue'
describe('Counter', () => {
it('increments count when button is clicked', async () => {
const wrapper = mount(Counter)
await wrapper.find('button').trigger('click')
expect(wrapper.find('.count').text()).toBe('1')
})
})
bash复制# Vite构建命令
npm run build
# 输出目录结构
dist/
├── assets/
│ ├── index.[hash].js
│ └── index.[hash].css
└── index.html
yaml复制# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- run: npm ci
- run: npm run build
- run: npm run test
- uses: actions/upload-artifact@v2
with:
name: dist
path: dist
javascript复制// main.ts
import * as Sentry from '@sentry/vue'
const app = createApp(App)
Sentry.init({
app,
dsn: 'your-dsn',
integrations: [
new Sentry.BrowserTracing({
routingInstrumentation: Sentry.vueRouterInstrumentation(router)
})
],
tracesSampleRate: 1.0
})
typescript复制app.config.errorHandler = (err, vm, info) => {
console.error('Global error:', err, info)
// 发送错误到监控服务
Sentry.captureException(err, { extra: { component: vm?.$options.name, info } })
}
html复制<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
javascript复制// utils/rem.js
function setRem() {
const docEl = document.documentElement
const resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize'
const recalc = () => {
const clientWidth = docEl.clientWidth
if (!clientWidth) return
docEl.style.fontSize = `${clientWidth / 10}px`
}
window.addEventListener(resizeEvt, recalc, false)
document.addEventListener('DOMContentLoaded', recalc, false)
}
export default setRem
在一个电商项目中,我们通过以下优化手段将首屏加载时间从3.2秒降低到1.5秒:
<link rel="preload">问题1:Store变得过于庞大
解决方案:按业务模块拆分多个Store
问题2:循环引用
解决方案:避免在Store中直接导入其他Store
问题3:SSR兼容性问题
解决方案:使用pinia-plugin-persistedstate的SSR配置
@vue/compatVue.prototype改为app.config.globalProperties$on、$off被移除使用Conventional Commits规范:
code复制feat: 添加用户登录功能
fix: 修复首页加载异常
docs: 更新README文档
style: 调整按钮样式
refactor: 重构用户模块
test: 添加登录测试用例
chore: 更新依赖版本
code复制docs/
├── ARCHITECTURE.md # 架构设计
├── DEVELOPMENT.md # 开发指南
├── API-REFERENCE.md # API参考
├── DEPLOYMENT.md # 部署指南
└── CHANGELOG.md # 变更日志
javascript复制// 主应用配置
import { registerMicroApps, start } from 'qiankun'
registerMicroApps([
{
name: 'vue3-subapp',
entry: '//localhost:7101',
container: '#subapp-container',
activeRule: '/vue3'
}
])
start()
typescript复制// plugins/i18n.ts
import { createI18n } from 'vue-i18n'
const i18n = createI18n({
locale: 'zh-CN',
messages: {
'zh-CN': {
hello: '你好'
},
'en-US': {
hello: 'Hello'
}
}
})
export default i18n
typescript复制// 路由meta定义
{
path: '/admin',
meta: {
requiresAuth: true,
roles: ['admin']
}
}
// 路由守卫检查
router.beforeEach((to) => {
const userRoles = getUserRoles()
if (to.meta.roles && !to.meta.roles.some(role => userRoles.includes(role))) {
return '/forbidden'
}
})
vue复制<template>
<button v-if="hasPermission('create')">创建</button>
</template>
<script setup>
import { useAuthStore } from '@/stores/auth'
const auth = useAuthStore()
function hasPermission(permission) {
return auth.permissions.includes(permission)
}
</script>
vue复制<template>
<div ref="chartRef" style="width: 600px; height: 400px;"></div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import * as echarts from 'echarts'
const chartRef = ref()
onMounted(() => {
const chart = echarts.init(chartRef.value)
chart.setOption({
// ECharts配置项
})
})
</script>
vue复制<template>
<EditorContent :editor="editor" />
</template>
<script setup>
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
const editor = useEditor({
content: '<p>Hello World!</p>',
extensions: [
StarterKit
]
})
</script>
javascript复制import { Extension } from '@tiptap/core'
const CustomExtension = Extension.create({
addKeyboardShortcuts() {
return {
'Mod-b': () => this.editor.commands.toggleBold()
}
}
})
vue复制<template>
<draggable
v-model="list"
item-key="id"
@end="onDragEnd"
>
<template #item="{ element }">
<div>{{ element.name }}</div>
</template>
</draggable>
</template>
<script setup>
import draggable from 'vuedraggable'
const list = ref([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
])
function onDragEnd() {
console.log('排序变化:', list.value)
}
</script>
vue复制<template>
<button @click="show = !show">Toggle</button>
<Transition name="fade">
<p v-if="show">Hello</p>
</Transition>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
vue复制<script setup>
import { ref, onMounted } from 'vue'
import gsap from 'gsap'
const box = ref()
onMounted(() => {
gsap.to(box.value, {
x: 100,
duration: 1,
repeat: -1,
yoyo: true
})
})
</script>
<template>
<div ref="box" class="box"></div>
</template>
bash复制# 创建Nuxt项目
npx nuxi init my-project
javascript复制// server.js
import { createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'
import App from './App.vue'
async function render(url) {
const app = createSSRApp(App)
const html = await renderToString(app)
return `
<!DOCTYPE html>
<html>
<head>
<title>Vue SSR</title>
</head>
<body>
<div id="app">${html}</div>
<script src="/client.js"></script>
</body>
</html>
`
}
javascript复制// .vitepress/config.js
export default {
title: 'My Project',
description: 'Project documentation',
themeConfig: {
nav: [
{ text: 'Guide', link: '/guide' }
]
}
}
javascript复制// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue({
template: {
ssr: true
}
})
]
})
javascript复制// vite.config.js
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
VitePWA({
registerType: 'autoUpdate',
manifest: {
name: 'My App',
short_name: 'App',
theme_color: '#ffffff'
}
})
]
})
javascript复制// 自定义Service Worker
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request)
})
)
})
javascript复制// main.js
import { defineCustomElement } from 'vue'
import MyComponent from './MyComponent.ce.vue'
const MyElement = defineCustomElement(MyComponent)
customElements.define('my-element', MyElement)
html复制<!DOCTYPE html>
<html>
<body>
<my-element></my-element>
<script src="./main.js"></script>
</body>
</html>
bash复制# 创建Tauri项目
npm create tauri-app
javascript复制// src-tauri/tauri.conf.json
{
"build": {
"distDir": "../dist"
}
}
bash复制# 添加Capacitor
npm install @capacitor/core @capacitor/cli
npx cap init
npx cap add android
npx cap add ios
javascript复制import { Plugins } from '@capacitor/core'
const { Camera } = Plugins
async function takePhoto() {
const image = await Camera.getPhoto({
quality: 90,
allowEditing: true,
resultType: 'uri'
})
}
npm outdatednpm updatejavascript复制import { getCLS, getFID, getLCP } from 'web-vitals'
getCLS(console.log)
getFID(console.log)
getLCP(console.log)
javascript复制// 使用Chrome DevTools Memory面板
// 定期做堆快照比较
v-htmlDOMPurify处理用户输入bash复制# 使用npm audit检查漏洞
npm audit
# 使用snyk进行深度检查
npx snyk test
bash复制# 使用TypeDoc生成API文档
npx typedoc --out docs src
javascript复制// 使用JSDoc注释
/**
* @component
* @name MyButton
* @description 通用按钮组件
* @prop {String} type - 按钮类型
*/
export default {
props: {
type: String
}
}
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: {
remoteApp: 'http://localhost:5001/assets/remoteEntry.js'
},
shared: ['vue']
})
]
})
javascript复制// 使用Storybook + Chromatic
import { visualTest } from '@chromatic-com/storybook'
visualTest('Button', () => {
// 渲染组件并截图比较
})
code复制design-system/
├── packages/
│ ├── tokens/ # 设计变量
│ ├── components/ # Vue组件
│ └── docs/ # 文档
└── playground/ # 开发环境
javascript复制// 使用CSS变量
:root {
--primary-color: #42b983;
}
.dark {
--primary-color: #2c3e50;
}
vue复制<template>
<component
v-for="field in schema"
:key="field.name"
:is="field.component"
v-bind="field.props"
v-model="formData[field.name]"
/>
</template>
<script setup>
const schema = [
{
name: 'username',
component: 'InputText',
props: {
label: '用户名'
}
}
]
</script>
javascript复制// 使用JSON Schema定义组件
{
"type": "object",
"properties": {
"text": {
"type": "string",
"title": "文本内容"
}
}
}
javascript复制// 使用AI生成测试用例
describe('UserForm', () => {
it('should validate required fields', () => {
// AI生成的测试逻辑
})
})
经过多个Vue 3企业级项目的实践,我认为以下几点尤为关键:
Vue 3生态仍在快速发展,未来值得关注的趋势包括:
在实际项目中,建议根据团队规模和技术栈选择合适的架构方案,不必盲目追求最新技术,但也要保持对新技术的敏感度,适时引入能够真正提升开发效率和产品质量的工具和实践。