作为一名长期奋战在前端开发一线的工程师,我见证了Vue.js从2.x到3.x的演进历程。今天我将系统梳理Vue.js的基础语法体系,帮助开发者快速掌握这个优雅的前端框架。本文将从工程化配置到核心语法,再到最佳实践,全方位解析Vue.js的开发要点。
Vite作为新一代前端构建工具,彻底改变了传统开发体验。它的核心优势体现在:
极速启动:基于原生ES模块,省去了传统打包工具的打包过程。在我的实际项目中,一个包含200+组件的大型应用启动时间从原来的45秒缩短到不到1秒。
快速热更新:采用按需编译策略。最近开发的一个后台管理系统,修改单个组件后的热更新仅需约50ms,相比Webpack的3-5秒有质的飞跃。
开箱即用的现代化支持:默认支持TypeScript、JSX、CSS预处理器等,省去了繁琐的配置过程。
| 特性 | Vite | Webpack |
|---|---|---|
| 构建原理 | 原生ESM + Rollup | 打包 + 插件系统 |
| 开发体验 | 即时启动,按需编译 | 全量打包,等待时间长 |
| 生产构建 | Rollup打包,高度优化 | 成熟的代码分割和优化 |
| 配置复杂度 | 约定优于配置 | 高度可配置但复杂 |
| 适用场景 | 现代浏览器项目 | 需要广泛兼容性的项目 |
bash复制# 使用pnpm创建Vue项目
pnpm create vite my-vue-app --template vue
# 进入项目目录
cd my-vue-app
# 安装依赖
pnpm install
# 启动开发服务器
pnpm dev
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'),
'components': path.resolve(__dirname, './src/components')
}
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://your-api-server.com',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`
}
}
}
})
在实际项目中,我通常会添加以下优化配置:
Vue组件的生命周期可以分为四个主要阶段:
javascript复制import { onMounted, onUpdated, onUnmounted } from 'vue'
export default {
setup() {
onMounted(() => {
console.log('组件已挂载')
// 适合进行DOM操作或数据请求
})
onUpdated(() => {
console.log('组件已更新')
// 谨慎使用,可能导致无限循环
})
onUnmounted(() => {
console.log('组件已卸载')
// 清理定时器、取消事件监听等
})
}
}
| 生命周期钩子 | 最佳实践场景 | 注意事项 |
|---|---|---|
| created | 初始化非响应式数据 | 此时DOM还未生成 |
| mounted | DOM操作、数据请求 | 确保元素已存在 |
| updated | 基于DOM状态的操作 | 避免直接修改数据 |
| unmounted | 清理副作用 | 取消定时器/事件监听 |
在实际项目中,我发现很多开发者容易在mounted钩子中过度加载数据,导致组件渲染延迟。更好的做法是:
Pinia作为Vue官方推荐的状态管理库,相比Vuex有以下优势:
javascript复制// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
lastChanged: null
}),
getters: {
doubleCount: (state) => state.count * 2,
formattedLastChanged: (state) => {
return state.lastChanged ? new Date(state.lastChanged).toLocaleString() : 'Never'
}
},
actions: {
increment() {
this.count++
this.lastChanged = new Date().toISOString()
},
async fetchCount() {
const response = await fetch('/api/count')
this.count = await response.json()
}
}
})
javascript复制<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
// 直接访问state
console.log(counter.count)
// 使用getter
console.log(counter.doubleCount)
// 调用action
counter.increment()
</script>
<template>
<div>
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.doubleCount }}</p>
<button @click="counter.increment">+1</button>
</div>
</template>
在实际项目中,我通常按功能模块划分Store,例如:
| 通信方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Props/Events | 父子组件简单通信 | 直观明确 | 深层嵌套时繁琐 |
| v-model | 表单组件双向绑定 | 语法简洁 | 仅适用于特定场景 |
| Provide/Inject | 跨层级组件通信 | 避免逐层传递 | 不利于追踪数据源 |
| Event Bus | 任意组件间通信 | 灵活 | 难以维护,不推荐 |
| Pinia | 全局状态共享 | 集中管理,响应式 | 小型项目可能过重 |
javascript复制// 父组件
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const message = ref('Hello from parent')
const count = ref(0)
function handleUpdate(newValue) {
count.value = newValue
}
</script>
<template>
<Child
:message="message"
@update="handleUpdate"
v-model:count="count"
/>
</template>
// 子组件
<script setup>
defineProps(['message'])
defineEmits(['update'])
const { modelValue } = defineModel('count')
</script>
javascript复制// 祖先组件
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme)
</script>
// 后代组件
<script setup>
import { inject } from 'vue'
const theme = inject('theme', 'light') // 默认值light
</script>
在大型项目中,我通常会建立通信规范:
Vue3使用Proxy实现响应式,相比Vue2的defineProperty有显著优势:
javascript复制const reactiveHandler = {
get(target, key) {
track(target, key) // 依赖收集
return Reflect.get(target, key)
},
set(target, key, value) {
const result = Reflect.set(target, key, value)
trigger(target, key) // 触发更新
return result
}
}
function reactive(obj) {
return new Proxy(obj, reactiveHandler)
}
| 特性 | ref | reactive |
|---|---|---|
| 数据类型 | 基本类型 | 对象 |
| 访问方式 | .value属性 | 直接访问 |
| 模板使用 | 自动解包 | 直接使用 |
| 重新赋值 | 保持响应式 | 失去响应式 |
javascript复制const count = ref(0) // 基本类型
const user = reactive({ name: 'Alice' }) // 对象
// 模板中:
// count可以直接使用,不需要.value
// user.name直接访问
javascript复制const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
// 带setter的计算属性
const editableFullName = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(newValue) {
const [first, last] = newValue.split(' ')
firstName.value = first
lastName.value = last
}
})
javascript复制// 监听单个ref
watch(count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`)
})
// 监听多个源
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
// 处理变化
})
// 深度监听对象
watch(
() => user,
(newUser) => {
console.log('user changed', newUser)
},
{ deep: true }
)
// 立即执行
watch(
count,
(newVal) => {
console.log('current count:', newVal)
},
{ immediate: true }
)
在实际项目中,我总结了几条经验:
javascript复制<script setup>
// 导入的组件可以直接使用
import MyComponent from './MyComponent.vue'
// 变量和函数可以直接在模板中使用
const count = ref(0)
function increment() {
count.value++
}
// 使用defineProps定义props
const props = defineProps({
title: String
})
// 使用defineEmits定义emit
const emit = defineEmits(['change'])
// 使用defineExpose暴露组件实例方法
defineExpose({
reset() {
count.value = 0
}
})
</script>
javascript复制// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
// 在组件中使用
<script setup>
import { useMouse } from './useMouse'
const { x, y } = useMouse()
</script>
<template>
Mouse position: {{ x }}, {{ y }}
</template>
在实际项目中,我通常会建立composables目录,按功能组织:
| 特性 | Vue2 | Vue3 |
|---|---|---|
| 响应式系统 | Object.defineProperty | Proxy |
| 打包体积 | 更大 | 更小(Tree-shaking) |
| 性能 | 较慢 | 更快(编译时优化) |
| TypeScript支持 | 有限 | 原生支持 |
| 组合API | 无 | 有(Options API仍可用) |
javascript复制// Vue2选项式API
export default {
data() {
return {
count: 0
}
},
computed: {
double() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
console.log('component mounted')
}
}
// Vue3组合式API
<script setup>
import { ref, computed, onMounted } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
onMounted(() => {
console.log('component mounted')
})
</script>
javascript复制// Vue2全局组件
Vue.component('MyComponent', {
// 选项
})
// Vue3全局组件
app.component('MyComponent', {
// 选项
})
在实际项目中,我常用的性能优化策略包括:
问题:解构reactive对象后失去响应式
javascript复制const state = reactive({ count: 0 })
const { count } = state // 失去响应式
解决方案:使用toRefs
javascript复制const state = reactive({ count: 0 })
const { count } = toRefs(state) // 保持响应式
问题:组件循环引用导致栈溢出
解决方案:使用异步组件或动态导入
javascript复制// 父组件
<script setup>
import { defineAsyncComponent } from 'vue'
const Child = defineAsyncComponent(() => import('./Child.vue'))
</script>
问题:scoped样式不生效
解决方案:使用深度选择器
css复制/* 使用 ::v-deep 或 :deep() */
::v-deep .child-component { color: red; }
:deep(.child-component) { color: red; }
code复制src/
├── assets/ # 静态资源
├── components/ # 公共组件
│ ├── ui/ # UI基础组件
│ └── features/ # 功能组件
├── composables/ # 组合式函数
├── stores/ # Pinia状态管理
├── router/ # 路由配置
├── utils/ # 工具函数
├── styles/ # 全局样式
├── views/ # 页面组件
├── App.vue # 根组件
└── main.js # 应用入口
在实际项目中,我通常会制定以下规范:
命名约定:
文件结构:
代码分割:
javascript复制import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
test('increments counter', async () => {
const wrapper = mount(Counter)
const button = wrapper.find('button')
await button.trigger('click')
expect(wrapper.text()).toContain('Count: 1')
})
在实际项目中,我通常会:
typescript复制<script setup lang="ts">
import { ref } from 'vue'
interface User {
id: number
name: string
email?: string
}
const user = ref<User>({
id: 1,
name: 'Alice'
})
</script>
typescript复制<script setup lang="ts">
interface Props {
title: string
count?: number
items?: string[]
}
const props = defineProps<Props>()
</script>
typescript复制<script setup lang="ts">
const emit = defineEmits<{
(e: 'update', value: number): void
(e: 'submit', payload: { name: string }): void
}>()
</script>
typescript复制import { defineStore } from 'pinia'
interface UserState {
name: string
age: number
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
name: '',
age: 0
}),
actions: {
updateName(name: string) {
this.name = name
}
}
})
案例:电商平台商品列表页加载缓慢
解决方案:
效果:页面加载时间从3.2秒降低到1.1秒
在多年的Vue项目开发中,我总结了以下几点经验:
一个特别有用的技巧是:在开发复杂组件时,我通常会先设计组件接口(props/emit),再实现内部逻辑。这种"由外而内"的开发方式能帮助我保持组件职责单一,接口清晰。
另一个实践是:为每个组件编写简单的使用示例和API文档,即使只是项目内部使用。这大大提高了组件的可复用性和团队协作效率。