Vue3+Vue-router4从0到1完整搭建怎么做?新手老手都该避开的坑有哪些?
最近好多朋友问我这个问题——要么是刚从原生H5、小程序转过来想入门前端全栈,把Vue3+TS+路由作为第一套技术栈;要么是还在用Vue2+Vue-router3,团队要升级但自己没摸透版本差,怕改崩项目;还有几个做了半年Vue3的,居然还在用keep-alive老写法,或者路由传参总莫名其妙失效,其实这俩核心工具的搭配,核心逻辑没变,但细节调整真不少,从路由实例创建、嵌套路由写法、传参方式、导航守卫,到和Composition API的结合,每一步都有新东西,也每一步都有容易踩的雷,今天我就从0开始,用纯日常的思路聊怎么搭,顺便把收集到的10个高频坑一次性说清楚,最后再补个适合日常开发的进阶小技巧,比如权限控制的简化写法、动态路由的懒加载优化,应该能覆盖大部分人的需求。
先搭Vue3项目框架
别着急装Vue-router,第一步得先有个干净的Vue3项目环境,不管你用Vue CLI还是Vite,现在主流肯定是Vite了,毕竟启动速度快到飞起,热更新延迟几乎为0,写代码爽很多,我用Vite举例子,命令很简单,打开终端(Windows用PowerShell或者CMD管理员模式,Mac/Linux直接Terminal就行):
先全局安装create-vite@latest,不过其实现在npx更方便,不用全局占空间:
npm create vite@latest my-vue3-router-app -- --template vue-ts
cd my-vue3-router-app
npm install
这三条命令敲完,一个带TypeScript的基础Vue3项目就跑起来了,等下看效果可以敲npm run dev,但先别急,先把路由装了。
正确安装并引入Vue-router4
这里第一个小细节新手经常错:Vue-router3只兼容Vue2,Vue-router4只兼容Vue3,千万不要装错版本!而且Vite创建的项目默认package.json里的依赖没有路由,要手动装,终端里接着敲:
npm install vue-router@4
装完之后,怎么引入?这和Vue2完全不一样了,不是直接new Vue({ router })就行,而是要先创建路由实例,再在main.ts里用app.use()挂载,具体步骤:
在src目录下创建router文件夹
新建router的文件夹,里面放index.ts——这是约定俗成的路由入口文件,方便项目管理,同事接手一看就懂。
配置路由入口文件index.ts
这是搭建的核心环节,得一步一步来,首先引入Vue-router的核心API,然后定义路由规则数组,最后创建路由实例并导出。 先看最基础的代码结构,里面注释我写得很细,方便理解:
// 引入createRouter和createWebHistory这两个核心API,Hash模式用createWebHashHistory
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
// 引入路由组件,这里可以先随便建两个测试组件,比如Home.vue和About.vue
// 测试组件怎么建?后面讲,先继续写路由
// 引入方式有两种:同步引入(适合常用的首页等组件,减少首屏加载后切换的延迟)和懒加载(适合用户可能不常用的页面,比如设置、个人中心,减少首屏加载体积)
// 同步引入:
// import Home from '../views/Home.vue'
// 懒引入(推荐,用箭头函数,配合Webpack/Vite的代码分割):
const Home = () => import('../views/Home.vue')
const About = () => import('../views/About.vue')
const User = () => import('../views/User.vue') // 后面讲嵌套路由和传参会用到
// 定义路由规则数组,每个对象就是一条路由记录,用RouteRecordRaw类型约束,避免写错属性
const routes: Array<RouteRecordRaw> = [
{
path: '/', // 路由路径,必须以/开头
name: 'home', // 路由名称,可选但强烈建议加,用name跳转更安全,改path不会影响跳转代码
component: Home, // 对应的组件
meta: { // meta是路由元信息,超级好用,后面讲权限控制、keep-alive会用到
title: '首页',
keepAlive: true, // 标记是否需要缓存组件
requiresAuth: false // 标记是否需要登录才能访问
}
},
{
path: '/about',
name: 'about',
component: About,
meta: {
title: '关于我们',
keepAlive: false,
requiresAuth: false
}
}
]
// 创建路由实例
const router = createRouter({
// history模式:URL没有#,更美观,但部署到服务器时需要配置Nginx/Apache,否则刷新页面会404
// hash模式:URL有#,兼容性好,不用配置服务器,但看起来有点旧,适合不想折腾部署的小项目
history: createWebHistory(import.meta.env.BASE_URL), // import.meta.env.BASE_URL是Vite的环境变量,指向项目根目录
routes, // 把刚才定义的路由规则传进去
scrollBehavior(to, from, savedPosition) { // 路由跳转时的滚动行为,这个也是Vue-router3没有但很实用的配置
// savedPosition是浏览器记录的上次滚动位置,只有当用户点击浏览器前进/后退按钮时才有值
if (savedPosition) {
return savedPosition
} else {
// 没有savedPosition时,默认滚动到顶部
return { top: 0 }
}
}
})
// 导出路由实例
export default router
创建对应的测试组件
刚才路由里引用了Home.vue、About.vue、User.vue,得在src目录下新建views文件夹,放这些页面级组件,组件级的放在components文件夹里,这也是前端通用的规范,别搞混。 随便写两个简单的测试组件就行: 比如Home.vue:
<template>
<div class="home">
<h1>欢迎来到Vue3+Vue-router4的测试首页</h1>
<p>这里用的是keep-alive缓存哦,你可以输入一些内容再跳转,回来应该还在</p>
<input type="text" v-model="inputValue" placeholder="随便输入点什么试试" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const inputValue = ref('')
</script>
<style scoped>
.home {
padding: 2rem;
}
input {
margin-top: 1rem;
padding: 0.5rem 1rem;
font-size: 1rem;
border-radius: 4px;
border: 1px solid #ddd;
}
</style>
About.vue更简单:
<template>
<div class="about">
<h1>关于我们</h1>
<p>这是一个测试页面,不需要缓存,所以首页的跳转逻辑不会保留这个页面的状态</p>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
.about {
padding: 2rem;
}
</style>
在main.ts里挂载路由
现在路由实例和组件都有了,最后一步是在Vue应用入口文件main.ts里挂载路由,这样整个应用才能识别路由规则,进行页面跳转。 打开main.ts,把默认的代码改一下:
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router' // 引入刚才导出的路由实例
// 创建Vue应用实例
const app = createApp(App)
// 挂载路由
app.use(router)
// 挂载到DOM节点
app.mount('#app')
在App.vue里配置路由出口和导航链接
这是最后一步基础搭建!路由出口是
<template>
<!-- 导航栏 -->
<nav class="nav">
<router-link to="/" class="nav-item">首页</router-link>
<router-link to="/about" class="nav-item">关于我们</router-link>
</nav>
<!-- 路由出口,要缓存的话,外面包个<keep-alive>,结合meta里的keepAlive属性 -->
<router-view v-slot="{ Component }">
<keep-alive>
<!-- 只有当路由的meta.keepAlive为true时,才会被缓存 -->
<component :is="Component" v-if="$route.meta.keepAlive" />
</keep-alive>
<!-- 不需要缓存的组件直接渲染 -->
<component :is="Component" v-if="!$route.meta.keepAlive" />
</router-view>
</template>
<script setup lang="ts">
</script>
<style scoped>
.nav {
display: flex;
gap: 2rem;
padding: 1rem 2rem;
background-color: #f5f5f5;
border-bottom: 1px solid #ddd;
}
.nav-item {
text-decoration: none;
color: #333;
font-size: 1.1rem;
}
/* 激活状态的样式,exact-active是完全匹配路径才激活,不会激活/about */
.router-link-exact-active {
color: #42b983;
font-weight: bold;
}
</style>
现在敲npm run dev,打开浏览器访问给的地址(通常是http://localhost:5173),点击导航栏,页面会切换,首页输入的内容跳转到关于我们再回来还在,完美!基础搭建搞定了。
新手老手都该避开的10个高频坑
基础搭建看起来简单,但实际开发中很多人会在这里栽跟头,我整理了10个踩过或者见过别人踩最多的坑,按频率排序:
坑1:history模式部署后刷新页面404
这绝对是Top1的坑!刚才也提过,history模式的URL没有#,服务器不知道这是前端路由,会直接去服务器找对应的文件,找不到就404,解决办法很简单,分两种情况:
本地开发没问题,部署到Nginx
在Nginx的配置文件里,找到对应的server块,加一条try_files指令:
location / {
try_files $uri $uri/ /index.html;
}
意思是:先找URL对应的文件,找不到找对应的文件夹,再找不到就返回根目录的index.html,这样前端路由就能接管了。
部署到GitHub Pages/Gitee Pages/Vercel这些静态托管平台
GitHub Pages比较麻烦,要么用hash模式,要么加一个404.html文件,内容和index.html一模一样;Gitee Pages同理;Vercel就简单多了,不用任何配置,自动处理。
坑2:路由传参用params刷新后数据丢失
这个坑不管新老都踩!Vue-router3里用params传参时,就算刷新页面数据也不会丢(只要路由配置里带了动态参数),但Vue-router4里不一样了! 等下,这里得先分清楚两种params传参:带动态路由参数的params和不带动态路由参数的state params(哦不对,Vue-router4里不带动态路由参数的params已经被废弃了!对,废弃了!很多人还在用,这就是坑的根源)。
正确的params传参(带动态路由参数)
路由规则里必须定义动态参数,比如刚才的User.vue,修改路由规则:
{
path: '/user/:id', // 这里的:id就是动态参数,必须以:开头
name: 'user',
component: User,
meta: { '用户详情',
keepAlive: false,
requiresAuth: false
}
}
然后跳转的时候,有两种方式:
- 用
的to属性: <router-link :to="{ name: 'user', params: { id: 123 } }">用户123</router-link> <!-- 注意:如果用path跳转的话,params会被忽略!必须用name!Vue-router4更严格了! --> - 用Composition API里的useRouter跳转:
<script setup lang="ts"> import { useRouter } from 'vue-router' const router = useRouter()
const goToUser = () => { router.push({ name: 'user', params: { id: 123 } }) }
``` 获取参数的时候,用useRoute: ```vue ```错误的params传参(不带动态路由参数,已废弃)
比如路由规则里是path: '/user',然后跳转时用router.push({ name: 'user', params: { id: 123 } }),Vue-router4会直接给你警告,而且刷新页面后params.id会变成undefined,完全没用。
如果要传不在URL里的隐私数据,应该用query传参吗?不对,query也在URL里,也不安全。正确的替代方案是用state属性:
跳转时:
router.push({ name: 'user', state: { id: 123, secret: 'abc123' } })
获取时:
console.log(route.state)
state属性的数据不在URL里,刷新页面后(只要浏览器没关,sessionStorage还在)数据不会丢,但浏览器关闭后就没了,适合临时的隐私数据。
坑3:keep-alive缓存失效
刚才基础搭建里已经写了keep-alive的正确用法,但很多人还是会失效,常见原因有三个:
原因1:没有用v-slot结合component
Vue-router3里直接在
原因2:meta.keepAlive属性写错了
比如写成了keepalive(小写a),或者属性值是字符串'true'而不是布尔值true,这些都会导致失效。
原因3:组件有多个根节点
Vue3支持组件有多个根节点,但如果被keep-alive缓存的组件有多个根节点,缓存就会失效!解决办法是给组件加一个根节点,比如或者(但有些场景下可能不行,最好用
Vue-router3里导航守卫必须调用next()才能继续跳转,但Vue-router4里不一样了!导航守卫的返回值替代了next()的作用: 刚才在params传参里提过,但太重要了,单独列出来!Vue-router4里用path跳转的话,params属性会被完全忽略,不管路由规则里有没有动态参数。 Composition API的规则就是这样,use开头的组合式函数必须在setup的顶层或者组件的生命周期钩子函数里调用,不能在普通函数、回调函数(比如setTimeout、Promise.then)或者事件处理函数的嵌套函数里调用,否则会报错: 这个坑Vue-router3也有,但很多人没注意,到了Vue-router4还是踩,比如有一个用户详情页 加一个唯一的key,让Vue-router每次匹配到新的路由时都创建新的组件实例,最简单的key就是 但这个办法有个缺点:如果组件有缓存,加key会导致缓存失效,每次跳转都会重新创建组件,缓存就白加了。 用watch监听 这个办法更灵活,推荐使用。 刚才基础搭建里写的懒加载写法是对的: 错误写法2: 刚才基础搭建里用了RouteRecordRaw类型约束路由规则数组,但meta属性的类型默认是any,没有约束,容易写错属性名或者属性值类型,解决办法是扩展RouteMeta接口,加类型约束:
在router/index.ts的顶部,或者单独建一个types/vue-router.d.ts文件(推荐单独建,方便全局使用): 这样在写路由规则的meta属性时,编辑器会有智能提示,写错属性名或者属性值类型会直接报错,开发体验好很多。 比如权限控制里,登录页的meta.requiresAuth是false,没问题,但如果不小心把登录页的requiresAuth写成了true,就会导致无限重定向:没登录→重定向到登录页→登录页需要登录→没登录→重定向到登录页→循环,浏览器会直接报错。
解决办法很简单,在全局前置守卫里加一个判断:如果要重定向的地址是登录页本身,就直接返回true,不要再重定向: 基础搭建和避坑都讲完了,最后补两个我日常开发中经常用的进阶小技巧,能提高开发效率: 刚才在meta里加了title属性,现在可以用全局后置守卫自动设置页面标题,不用每个页面都手动写 Vite有一个功能叫 webpackPreload和webpackPrefetch的区别:坑4:导航守卫里next()的用法变了
// 在router/index.ts里,导出路由实例之前加
router.beforeEach((to, from) => {
// 这里模拟登录状态,实际开发中应该从Pinia/Vuex或者localStorage里取
const isLoggedIn = true
// 如果要访问的页面需要登录但没登录,重定向到登录页
if (to.meta.requiresAuth && !isLoggedIn) {
// 返回登录页的路由地址
return { name: 'login' }
}
// 其他情况继续跳转
return true
})
坑5:用path跳转时params被忽略
// 错误写法!
router.push({ path: '/user/123', params: { name: '张三' } })
// 正确写法1(带动态参数用name):
router.push({ name: 'user', params: { id: 123, name: '张三' } }) // 这里的name不在URL里,刷新会丢,但id不会
// 正确写法2(不带动态参数用state):
router.push({ path: '/user/123', state: { name: '张三' } })
// 正确写法3(如果要把name放在URL里,改成query或者动态参数):
router.push({ path: '/user/123', query: { name: '张三' } }) // URL变成/user/123?name=张三
坑6:useRouter和useRoute必须在setup或生命周期钩子函数里调用
Cannot read properties of undefined (reading 'push')或者Cannot read properties of undefined (reading 'params')。
举个错误和正确的例子:<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter() // 正确,setup顶层
const handleClick = () => {
// 错误!不能在事件处理函数里调用useRouter!(哦不对,刚才说的是嵌套函数,这里事件处理函数本身是setup里的顶层函数,其实可以?不对等下,严格来说是不能在组件初始化完成之后的异步操作里调用useRouter/useRoute?不对我测试过,setTimeout里调用会报错,事件处理函数的同步代码里调用不会,但为了保险起见,最好统一在setup顶层调用)
// 哦对,正确的做法是统一在setup顶层调用,然后赋值给变量,后面随便用
setTimeout(() => {
// const router = useRouter() // 错误!这里是异步回调,组件已经初始化完成了,上下文丢失
router.push('/') // 正确!用setup顶层的变量
}, 1000)
}
</script>
坑7:动态路由参数改变时组件不重新渲染
/user/:id,从/user/123跳转到/user/456,URL变了,但组件的内容没变,因为Vue-router默认会复用已有的组件实例,不会重新创建,所以setup函数不会重新执行,生命周期钩子函数也不会重新触发。
解决办法有两个:办法1:给
$route.fullPath,因为fullPath是整个URL(包括query和hash),每次路由跳转只要fullPath变了,key就变了,组件就会重新渲染:<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" :key="$route.fullPath" v-if="$route.meta.keepAlive" />
</keep-alive>
<component :is="Component" :key="$route.fullPath" v-if="!$route.meta.keepAlive" />
</router-view>
办法2:监听$route的变化
$route.params或者$route.fullPath的变化,然后在回调函数里重新请求数据或者更新组件状态,这样既不会复用组件实例的旧数据,也不会破坏keep-alive的缓存:<script setup lang="ts">
import { useRoute, watch } from 'vue'
import { getUserInfo } from '../api/user'
const route = useRoute()
const userInfo = ref({})
// 初始化时请求一次数据
const fetchUserInfo = async () => {
userInfo.value = await getUserInfo(route.params.id as string)
}
fetchUserInfo()
// 监听params.id的变化,变化时重新请求数据
watch(() => route.params.id, (newId) => {
if (newId) {
fetchUserInfo()
}
})
</script>
坑8:路由懒加载的写法不对导致打包失败
const Home = () => import('../views/Home.vue'),但很多人会写成同步引入的写法加个括号,或者写成动态变量,导致打包工具(Vite/Webpack)无法识别,无法进行代码分割,打包失败或者懒加载失效。
比如错误写法1:const Home = () => import(`../views/${'Home'}.vue`) // 动态变量必须是静态字符串拼接,不能有纯变量
// 哦不对,Vite支持简单的动态变量,但必须是相对于当前文件的路径,而且不能有嵌套太深的纯变量,
const fileName = 'Home'
const Home = () => import(`../views/${fileName}.vue`) // 这个Vite其实也支持,但Webpack可能需要配置,为了兼容性最好用纯静态字符串
// 这个不是懒加载,是同步引入!
const Home = import('../views/Home.vue')
坑9:路由元信息meta没有类型约束
// types/vue-router.d.ts
import 'vue-router'
declare module 'vue-router' {
interface RouteMeta {: string // 页面标题,可选
keepAlive?: boolean // 是否需要缓存,可选
requiresAuth?: boolean // 是否需要登录,可选
// 还可以加其他自己需要的属性,比如权限等级、页面图标等
}
}
坑10:全局前置守卫里没有处理无限重定向
router.beforeEach((to, from) => {
const isLoggedIn = true
const loginPath = { name: 'login' }
// 如果已经登录但要访问登录页,重定向到首页
if (isLoggedIn && to.name === 'login') {
return { name: 'home' }
}
// 如果要访问的页面需要登录但没登录,且不是登录页本身,重定向到登录页
if (to.meta.requiresAuth && !isLoggedIn && to.name !== 'login') {
return loginPath
}
return true
})
适合日常开发的进阶小技巧
技巧1:自动设置页面标题
document.title = 'xxx':// 在router/index.ts里,全局前置守卫之后加
router.afterEach((to) => {
// 从meta里取title,如果没有就用默认标题
const title = to.meta.title || '我的Vue3应用'
document.title = title
})
技巧2:动态路由的懒加载优化(Vite专属)
dynamic import with eager,可以把一些常用的懒加载组件预加载到浏览器缓存里,用户点击导航栏时切换更流畅,不会有加载延迟(虽然懒加载组件的体积很小,但预加载能进一步提升体验),比如首页、关于我们、登录页这些高频访问的页面,可以预加载:// 首页预加载
const Home = () => import(/* @vite-ignore */ '../views/Home.vue')
// 哦不对,Vite的预加载是用/* webpackPrefetch: true */或者/* webpackPreload: true */,但Vite也支持,而且更简单,用/* @vite-plugin-pwa:skip */不是,哦对,Vite 2.6+支持/* @vite-import-glob eager */,但单独预加载某个组件的话,用/* webpackPreload: true */就行,Vite会自动转换:
const Home = () => import(/* webpackPreload: true */ '../views/Home.vue')
const About = () => import(/* webpackPreload: true */ '../views/About.vue')
const Login = () => import(/* webpackPreload: true */ '../views/Login.vue')
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网


