Vue3入门到项目实战
前阵子有个朋友拉我进群,说“你们搞前端的,是不是天天在背API,像在背新华字典”。我笑回一句:字典背不背不重要,重要的是别在项目上线前夜才发现字典丢了。Vue3刚出来的时候,我也踩过一堆坑。模板里莫名其妙报警告,控制台冒红字像过年放鞭炮。最离谱的一次,一个数据改了,页面愣是没反应,最后发现是因为用了错误的方式声明响应式,白忙活一下午。从那时起我就在想:Vue3到底怎么学才不“虚”?光看文档像读说明书,干项目又像直接上高速。其实两者之间,差的就是一套能从入门直通实战的路线。
第一次用Vue3重构老项目时,我把选项式API一股脑换成组合式API,结果代码像被猫抓过的毛线团。ref、reactive、computed、watch全混在一起,哪里都生效,哪里都可能崩。后来我给自己定了规矩:简单状态用ref,对象结构优先用reactive;模板里别写太多逻辑,逻辑都收进函数里;副作用明确来源和清理时机。慢慢地,页面不再“抽风”,同事问我是不是偷偷换了框架。其实只是把理解对齐了:Vue3不是在逼你用新语法,而是在逼你理清数据流向。一旦想清楚“谁改、怎么改、改了影响谁”,坑就少了一大半。

很多人问,Vue3的组件到底怎么拆才算合理。我见过两个极端:一个是所有东西塞进一个文件,像把厨房、卧室和厕所放在同一个房间;另一个是拆到每个按钮都有一个文件,导入路径长得像绕口令。实战里,我喜欢按“功能边界”拆。列表页有它自己的请求、状态和交互,就归到一起;弹窗能独立跑,就单独拎出去;公共的表格、表单、校验规则,打成可复用的模块。项目结构也顺势清晰:views放页面,components放UI,composables放逻辑,utils放工具,router和pinia各管一路。拆得合理,改需求才不会像拆炸弹——剪哪根线都会炸。
上线前的联调往往是“照妖镜”。我曾在一个订单项目里,因为没处理好异步竞态,用户连续点两次,金额被算了两遍。Vue3本身不负责防手抖,也不替你管请求顺序,这些都得你自己补。于是我把请求封装成队列,关键操作加锁,后台返回前把按钮置灰。另一个教训是环境配置:开发时跑得飞快,一到预发就各种跨域和路径不对。后来我把环境变量、路由守卫、错误边界统一收口,项目像穿了件防风外套,冷不丁的怪风没那么容易灌进来。工具链稳了,人才能稳。
写到这里,算是把开头那句“字典丢了”的担忧摊开来说:Vue3的入门不是背完概念就结束,而是用项目把概念盘活。你会在真实场景里理解ref和reactive的取舍,明白什么时候用watch而不是computed,体会到组合式函数怎么把重复代码变成复利。项目也不一定要多庞大,一个带登录、列表、详情、权限校验的中后台,足够把Vue3的核心跑通。从配置到路由,从状态到组件通信,从请求封装到错误处理,一步步搭起来,信心是一行行代码长出来的。
接下来的内容,我会沿着这条路线展开:先搭环境与项目骨架,再把常用语法和响应式系统落到具体例子,然后演示一个完整的中后台项目怎么从零写到上线,最后聊工程化、性能与维护。希望你不只“会用”,还能“用稳”。
一、环境与项目骨架
学Vue3,先把工具链理顺。现在主流是Vite,开发启动快,构建也干脆。安装完Node.js后,用命令行初始化项目:
npm create vue@latest
它会问你是否用TypeScript、Router、Pinia、单元测试等。新手建议Router和Pinia都选上,TypeScript也打开,早期多写几行类型,后面少问几小时“为什么是undefined”。项目生成后,目录大致长这样:
src/
assets/
components/
views/
router/
stores/
composables/
utils/
App.vue
main.ts
入口文件main.ts里,会用createApp挂载根组件,并注册Router和Pinia。这时候先别急着写业务,跑起来再说:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'
const app = createApp(App)
app.use(router)
app.use(createPinia())
app.mount('#app')
运行npm run dev,浏览器弹出欢迎页,第一步就算站稳了。
二、语法与响应式系统
Vue3的核心变化之一是响应式系统的重写。Object.defineProperty换成了Proxy,对象和数组的变化都能捕获。但对我们来说,更直观的改变是API风格。
ref与reactive
ref适合基本类型和简单对象,模板里会自动解包,不用写.value:
<script setup>
import { ref } from 'vue'
const count = ref(0)
function add() {
count.value++
}
</script>
<template>
<div>{{ count }}</div>
<button @click="add">加一</button>
</template>
reactive更适合对象结构,尤其表单、表格数据:
import { reactive } from 'vue'
const form = reactive({
name: '',
age: null as number | null
})
两者可以混用,但别在同一个对象里来回切换,否则容易混乱。
computed与watch
computed是做“衍生状态”的,比如一个总价:
import { computed } from 'vue'
const total = computed(() => {
return list.value.reduce((sum, item) => sum + item.price * item.count, 0)
})
watch则用于“副作用”,比如路由变化拉数据,或者本地缓存回填:
import { watch } from 'vue'
watch(() => route.params.id, (newId) => {
if (newId) fetchDetail(newId)
}, { immediate: true })
注意清理副作用:定时器、事件监听、异步请求,在组件卸载或重新触发前要停掉,避免内存泄漏。
组合式函数(composables)
把重复逻辑抽离成函数,是Vue3最顺手的一招。比如封装一个表格列表:
// composables/useTable.ts
import { ref, onMounted } from 'vue'
export function useTable(fetchFn) {
const list = ref([])
const loading = ref(false)
const load = async (params) => {
loading.value = true
try {
list.value = await fetchFn(params)
} finally {
loading.value = false
}
}
onMounted(() => load())
return { list, loading, load }
}
在组件里:
import { useTable } from '@/composables/useTable'
const { list, loading, load } = useTable(api.getList)
逻辑被“拎”出来,组件只剩编排,协作起来清晰很多。
三、项目实战:中后台示例
纸上谈兵容易“翻车”,我们直接动手做一个精简但完整的中后台:登录、权限、列表、详情、搜索与分页。
路由与权限控制
路由配置放在router/index.ts,用meta字段标记权限:
{
path: '/order',
name: 'Order',
component: () => import('@/views/OrderList.vue'),
meta: { requiresAuth: true, roles: ['admin', 'operator'] }
}
全局路由守卫做拦截:
router.beforeEach((to, from, next) => {
const user = store.user
if (to.meta.requiresAuth && !user.token) {
next('/login')
} else if (to.meta.roles && !to.meta.roles.includes(user.role)) {
next('/403')
} else {
next()
}
})
权限粒度可以更细,比如按钮级用指令或函数控制显隐,这里先保主干。
状态管理
用Pinia替代Vuex,写法更“函数风”。一个用户store:
// stores/user.ts
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
role: '' as string,
profile: null as any
}),
actions: {
login(data) {
// 调用接口,存token
},
logout() {
this.token = ''
this.role = ''
}
}
})
在组件里直接解构使用:
const user = useUserStore()
const handleLogout = () => user.logout()
状态集中,调试也方便,Vue DevTools里能看到变化轨迹。
列表页:请求、搜索、分页
列表页最琐碎,也最能检验代码质量。把请求、参数、表格状态封装在一起:
// composables/useOrderList.ts
import { ref, watch } from 'vue'
export function useOrderList() {
const list = ref([])
const loading = ref(false)
const pagination = ref({ page: 1, size: 20, total: 0 })
const filters = reactive({ status: '', keyword: '' })
const fetch = async () => {
loading.value = true
try {
const res = await api.getOrders({ ...filters, ...pagination.value })
list.value = res.data
pagination.value.total = res.total
} finally {
loading.value = false
}
}
watch(() => [filters.status, filters.keyword], () => {
pagination.value.page = 1
fetch()
})
return { list, loading, pagination, filters, fetch }
}
页面只负责模板和事件绑定:
<template>
<div>
<input v-model="filters.keyword" placeholder="搜索" />
<select v-model="filters.status">
<option value="">全部</option>
<option value="pending">待处理</option>
</select>
<el-table :data="list" v-loading="loading">
<!-- 列定义 -->
</el-table>
<el-pagination
@current-change="(p) => { pagination.page = p; fetch() }"
:current-page="pagination.page"
:page-size="pagination.size"
:total="pagination.total"
/>
</div>
</template>
逻辑和视图分离后,改搜索、加字段、调接口都不容易“牵一发动全身”。
错误与边界处理
中后台少不了兜底。全局异常捕获可以用app.config.errorHandler,接口层统一处理错误码,重要操作加二次确认与日志。页面级别可以用<KeepAlive>缓存列表状态,减少重复请求。
四、工程化与上线
项目写得顺,还得发得稳。Vite配置里区分环境,.env.development和.env.production放不同接口前缀和调试开关。
构建时注意分包:路由懒加载、第三方库抽离,减少首屏体积。比如:
{
path: '/report',
component: () => import('@/views/Report.vue')
}
配置build.rollupOptions做更细粒度拆分。对性能敏感的地方,用v-once、v-memo减少不必要的重渲染。大列表用虚拟滚动,避免卡顿。
上线前检查:路由权限跑通、必填校验不漏、加载态友好、错误提示明确。用工具跑一遍Lighthouse,解决明显的性能告警。日志和监控接入,能帮你更快定位“只在用户手机上出现”的问题。
五、结语
从Vue3入门到项目实战,最怕的不是语法多,而是“学了却用不起来”。把知识点落到一个能跑通的项目里,环境、路由、状态、请求、组件拆分、错误处理、性能优化,一环扣一环,理解才会扎实。你不需要一次性记住所有API,只需要在真实场景中反复使用,把决策变成习惯。
项目不在大,在于闭环;技术不在新,在于可控。Vue3给了我们更细粒度的控制力和更清晰的组合方式,剩下的,就是用一次次的实践把它“盘”熟。当你能从容地加需求、修Bug、调性能,而不再担心“会不会又踩坑”,入门才算真正完成,实战才算刚刚开始。