开发中vuerouter选用哪种模式(详解Vue3中router带来了哪些变化)

开发中vuerouter选用哪种模式(详解Vue3中router带来了哪些变化)(1)

作者: Leiy

转发链接:https://segmentfault.com/a/1190000022582928

前言

Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。

本文基于的源码版本是 vue-next-router alpha.10,为了与 Vue 2.0 中的 Vue Router 区分,下文将 vue-router v3.1.6 称为 vue2-router。

本文旨在帮助更多人对新版本 Router 有一个初步的了解,如果文中有误导大家的地方,欢迎留言指正。

重大改进

此次 Vue 的重大改进随之而来带来了 Vue Router 的一系列改进,现阶段(alpha.10)相比 vue2-router 的主要变化,总结如下:

1. 构建选项 mode

由原来的 mode: "history" 更改为 history: createWebHistory()。(设置其他 mode 也是同样的方式)。

// vue2-router const router = new VueRouter({ mode: 'history', ... }) // vue-next-router import { createRouter, createWebHistory } from 'vue-next-router' const router = createRouter({ history: createWebHistory(), ... })

2. 构建选项 base

传给 createWebHistory()(和其他模式) 的第一个参数作为 base。

//vue2-router const Router = new VueRouter({ base: __dirname, }) // vue-next-router import { createRouter, createWebHistory } from 'vue-next-router' const router = createRouter({ history: createWebHistory('/'), ... })

4. 捕获所有路由 ( /* ) 时,现在必须使用带有自定义正则表达式的参数进行定义:/:catchAll(.*)。

// vue2-router const router = new VueRouter({ mode: 'history', routes: [ { path: '/user/:a*' }, ], }) // vue-next-router const router = createRouter({ history: createWebHistory(), routes: [ { path: '/user/:a:catchAll(.*)', component: component }, ], })

当路由为 /user/a/b 时,捕获到的 params 为 {"a": "a", "catchAll": "/b"}。

5. router.match 与 router.resolve 合并在一起为router.resolve,但签名略有不同。

// vue2-router ... resolve ( to: RawLocation, current?: Route, append?: boolean) { ... return { location, route, href, normalizedTo: location, resolved: route } } // vue-next-router function resolve( rawLocation: Readonly<RouteLocationRaw>, currentLocation?: Readonly<RouteLocationNormalizedLoaded> ): RouteLocation & { href: string } { ... let matchedRoute = matcher.resolve(matcherLocation, currentLocation) ... return { fullPath, hash, query: normalizeQuery(rawLocation.query), ...matchedRoute, redirectedFrom: undefined, href: routerHistory.base fullPath, } }

6. 删除 router.getMatchedComponents,可以从router.currentRoute.value.matched 中获取。

router.getMatchedComponents 返回目标位置或是当前路由匹配的组件数组 (是数组的定义/构造类,不是实例)。通常在服务端渲染的数据预加载时使用。

[{ aliasOf: undefined beforeEnter: undefined children: [] components: {default: {…}, other: {…}} instances: {default: null, other: Proxy} leaveGuards: [] meta: {} name: undefined path: "/" props: ƒ (to) updateGuards: [] }]

7. 如果使用 <transition>,则可能需要等待 router 准备就绪才能挂载应用程序。

app.use(router) // Note: on Server Side, you need to manually push the initial location router.isReady().then(() => app.mount('#app'))

一般情况下,正常挂载也是可以使用 <transition> 的,但是现在导航都是异步的,如果在路由初始化时有路由守卫,则在 resolve 之前会出现一个初始渲染的过渡,就像给 <transiton> 提供一个 appear 一样。

8. 在服务端渲染 (SSR) 中,需要使用一个三目运算符手动传递合适的 mode。

let history = isServer ? createMemoryHistory() : createWebHistory() let router = createRouter({ routes, history }) // on server only router.push(req.url) // request url router.isReady().then(() => { // resolve the request })

9. push 或者 resolve 一个不存在的命名路由时,将会引发错误,而不是导航到根路由 "/" 并且不显示任何内容。

在 vue2-router 中,当 push 一个不存在的命名路由时,路由会导航到根路由"/" 下,并且不会渲染任何内容。

const router = new VueRouter({ mode: 'history', routes: [{ path: '/', name: 'foo', component: Foo }] } this.$router.push({name: 'baz'})

浏览器控制台只会提示如下警告,并且 url 会跳转到根路由 / 下。

在 vue-next-router 中,同样做法会引发错误。

const router = createRouter({ history: routerHistory(), routes: [{ path: '/', name: 'foo', component: Foo }] }) ... import { useRouter } from 'vue-next-router' ... const router = userRouter() router.push({name: 'baz'})) // 这段代码会报错

Active-RFCS

以下内容的改进来自 active-rfcs(active 就是已经讨论通过并且正在实施的特性)。

  • 0021-router-link-scoped-slot
  • 0022-router-merge-meta-routelocation
  • 0028-router-active-link
  • 0029-router-dynamic-routing
  • 0033-router-navigation-failures - 本文略
router-link-scoped-slot

这个 rfc 主要提议及改进如下:

  • 删除 tag prop - 使用作用域插槽代替
  • 删除 event prop - 使用作用域插槽代替
  • 增加 scoped-slot API
  • 停止自动将 click 事件分配给内部锚点
  • 添加 custom prop 以完全支持自定义的 router-link 渲染

在 vue2-router 中,想要将 <roter-link> 渲染成某种标签,例如 <button>,需要这么做:

<router-link to="/" tag="button">按钮</router-link> !-- 渲染结果 --> <button>按钮</button>

根据此次 rfc,以后可能需要这样做:

<router-link to="/" custom v-slot="{ navigate, isActive, isExactActive }"> <button role="link" @click="navigate" :class="{ active: isActive, 'exact-active': isExactActive }"> 按钮 </button> <router-link> !-- 渲染结果 --> <button role="link">按钮</button>

更多详细的介绍请看这个 rfc 。

router-active-link

这个 rfc 改进的缘由是 gayhub 上名为 zamakkat 的大哥提出来的,他的 issues 主要内容是,有一个嵌套组件,像这样:

Foo (links to /pages/foo) |-- Bar (links to /pages/foo/bar)

需求:需要突出显示当前选中的页面(并且只能突出显示一项)。

  • 如果用户打开 /pages/foo,则仅 Foo 高亮显示。
  • 如果用户打开 /pages/foo/bar,则仅 Bar 应高亮显示。

但是,Bar 页面也有分页,选择第二页时,会导航到 /pages/foo/bar?page=2。vue2-router 默认情况下,路由匹配规则是「包含匹配」。也就是说,当前的路径是/pages 开头的,那么 <router-link to="/pages/*"> 都会被设置 CSS 类名。

在这个示例中,如果使用「精确匹配模式」(exact: true),则精确匹配将匹配 /pages/foo/bar,不会匹配 /pages/foo/bar?page=2 因为它在比较中包括查询参数 ?page=2,所以当选择第二页面时,Bar 就不高亮显示了。

所以无论是「精确匹配」还是「包含匹配」都不能满足此需求。

为了解决上述问题和其他边界情况,此次改进使得 router-link-active 应用方式更严谨,处理此问题的核心:

// 确认路由 isActive 的行为 function includesParams( outer: RouteLocation['params'], inner: RouteLocation['params'] ): boolean { for (let key in inner) { let innerValue = inner[key] let outerValue = outer[key] if (typeof innerValue === 'string') { if (innerValue !== outerValue) return false } else { if ( !Array.isArray(outerValue) || outerValue.length !== innerValue.length || innerValue.some((value, i) => value !== outerValue[i]) ) return false } } return true }

详情请参见这个 rfc。

router-merge-meta-routelocation

在 vue2-router中,在处理嵌套路由时,meta 仅包含匹配位置的 route meta信息。 看个栗子:

{ path: '/parent', meta: { nested: true }, children: [ { path: 'foo', meta: { nested: true } }, { path: 'bar' } ] }

在导航到 /parent/bar 时,只会显示当前路由对应的 meta 信息为 {},不会显示父级的 meta 信息。

meta: {}

所以在这种情况下,需要通过 to.matched.some() 检查 meta 字段是否存在,而进行下一步逻辑。

router.beforeEach((to, from, next) => { if (to.matched.some(record => record.meta.nested)) next('/login') else next() })

因此为了避免使用额外的 to.matched.some, 这个 rfc 提议,将父子路由中的 meta 进行第一层合并(同 Object.assing())。如果再遇到上述嵌套路由时,将可以直接通过 to.meta 获取信息。

router.beforeEach((to, from, next) => { if (to.meta.nested) next('/login') else next() })

更多详细介绍请看这个 rfc。

router-dynamic-routing

这个 rfc 的主要内容是,允许给 Router 添加和删除(单个)路由规则。

  • router.addRoute(route: RouteRecord) - 添加路由规则
  • router.removeRoute(name: string | symbol) - 删除路由规则
  • router.hasRoute(name: string | symbol): boolean - 检查路由是否存在
  • router.getRoutes(): RouteRecord[] - 获取当前路由规则的列表

相比 vue2-router 删除了动态添加多个路由规则的 router.addRoutes API。

在 Vue 2.0 中,给路由动态添加多个路由规则时,需要这么做:

router.addRoutes( [ { path: '/d', component: Home }, { path: '/b', component: Home } ] )

而在 Vue 3.0 中,需要使用 router.addRoute() 单个添加记录,并且还可以使用更丰富的 API:

router.addRoute({ path: '/new-route', name: 'NewRoute', component: NewRoute }) // 给现有路由添加子路由 router.addRoute('ParentRoute', { path: 'new-route', name: 'NewRoute', component: NewRoute }) // 根据路由名称删除路由 router.removeRoute('NewRoute') // 获得路由的所有记录 const routeRecords \= router.getRoutes()

关于 RfCS 上提出的改进,这里就介绍这么多,想了解更多的话,请移步到 active-rfcs。

走进源码

相比 vue2-router 的 ES6-class 的写法 vue-next-router 的 function-to-function 的编写更易读也更容易维护。

Router 的 install

暴露的 Vue 组件解析入口相对来说更清晰,开发插件时定义的 install 也简化了许多。

我们先看下 vue2-router 源码中 install 方法的定义:

import View from './components/view' import Link from './components/link' export let _Vue export function install (Vue) { // 当 install 方法被同一个插件多次调用,插件将只会被安装一次。 if (install.installed && _Vue === Vue) return install.installed = true _Vue = Vue const isDef = v => v !== undefined const registerInstance = (vm, callVal) => { let i = vm.$options._parentVnode if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) { i(vm, callVal) } } // 将 router 全局注册混入,影响注册之后所有创建的每个 Vue 实例 Vue.mixin({ beforeCreate () { if (isDef(this.$options.router)) { this._routerRoot = this this._router = this.$options.router this._router.init(this) Vue.util.defineReactive(this, '_route', this._router.history.current) } else { this._routerRoot = (this.$parent && this.$parent._routerRoot) || this } // 注册实例,将 this 传入 registerInstance(this, this) }, destroyed () { registerInstance(this) } }) // 将 $router 绑定的 vue 原型对象上 Object.defineProperty(Vue.prototype, '$router', { get () { return this._routerRoot._router } }) // 将 $route 手动绑定到 vue 原型对象上 Object.defineProperty(Vue.prototype, '$route', { get () { return this._routerRoot._route } }) // 注册全局组件 RouterView、RouterLink Vue.component('RouterView', View) Vue.component('RouterLink', Link) const strats = Vue.config.optionMergeStrategies // use the same hook merging strategy for route hooks strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created }

我们可以看到,在 2.0 中,Router 提供的 install() 方法中更触碰底层,需要用到选项的私有方法 _parentVnode(),还会用的 Vue.mixin() 进行全局混入,之后会手动将 $router、$route 绑定到 Vue 的原型对象上。

VueRouter.install = install VueRouter.version = '__VERSION__' // 以 src 方法导入 if (inBrowser && window.Vue) { window.Vue.use(VueRouter) }

做了这么多事情之后,然后会在定义 VueRouter 类的文件中,将 install() 方法绑定到 VueRouter 的静态属性 install 上,以符合插件的标准。

安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。

我们可以看到,在 2.0 中开发一个插件需要做的事情很多,install 要处理很多事情,这对不了解 Vue 的童鞋,会变得很困难。

说了这么多,那么 vue-next-router 中暴露的 install 是什么样的呢?applyRouterPlugin() 方法就是处理 install() 全部逻辑的地方,请看源码:

import { App, ComputedRef, reactive, computed } from 'vue' import { Router } from './router' import { RouterLink } from './RouterLink' import { RouterView } from './RouterView' export function applyRouterPlugin(app: App, router: Router) { // 全局注册组件 RouterLink、RouterView app.component('RouterLink', RouterLink) app.component('RouterView', RouterView) //省略部分代码 // 注入 Router 实例,源码其他地方会用到 app.provide(routerKey, router) app.provide(routeLocationKey, reactive(reactiveRoute)) }

基于 3.0 使用 composition API 时,没有 this 也没有混入,插件将充分利用 provide 和 inject 对外暴露一个组合函数即可,当然,没了 this 之后也有不好的地方,看这里。

provide 和 inject 这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。

再来看下 vue-next-router 中 install() 是什么样的:

export function createRouter(options: RouterOptions): Router { // 省略大部分代码 const router: Router = { currentRoute, addRoute, removeRoute, hasRoute, history: routerHistory, ... // install install(app: App) { applyRouterPlugin(app, this) }, } return router }

很简单,在 vue-next-router 提供的 install() 方法中调用applyRouterPlugin 将 Vue 和 Router 作为参数传入。

最后在应用程序中使用 Router 时,只需要导入 createRouter 然后显示调用 use() 方法,传入 Vue,就可以在程序中正常使用了。

import { createRouter, createWebHistory } from 'vue-next-router' const router = createRouter({ history: createWebHistory(), strict: true, routes: [ { path: '/home', redirect: '/' } }) const app = createApp(App) app.use(router)

没有全局 $router、$route

我们知道在 vue2-router 中,通过在 Vue 根实例的 router 配置传入 router 实例,下面这些属性成员会被注入到每个子组件。

  • this.$router - router 实例。
  • this.$route - 当前激活的路由信息对象。

但是 3.0 中,没有 this,也就不存在在 this.$router | $route 这样的属性,那么在 3.0 中应该如何使用这些属性呢?

我们首先看下源码暴露的 api 的地方:

// useApi.ts import { inject } from 'vue' import { routerKey, routeLocationKey } from './injectionSymbols' import { Router } from './router' import { RouteLocationNormalizedLoaded } from './types' // 导出 useRouter export function useRouter(): Router { // 注入 router Router (key 与 上文的 provide 对应) return inject(routerKey)! } // 导入 useRoute export function useRoute(): RouteLocationNormalizedLoaded { // 注入 路由对象信息 (key 与 上文的 provide 对应) return inject(routeLocationKey)! }

源码中,useRouter 、 useRoute 通过 inject 注入对象实例,并以单个函数的方式暴露出去。

在应用程序中只需要通过命名导入的方式导入即可使用。

import { useRoute, useRouter } from 'vue-next-router' ... setup() { const route = useRoute() const router = useRouter() ... // router -> this.$router // route > this.$route router.push('/foo') console.log(route) // 路由对象信息 }

除了可以命名导入 useRouter 、 useRoute 之外,还可暴露出很多函数,以更好的支持 tree-shaking(期待新版本的发布吧)。

NavigationFailureType RouterLink RouterView createMemoryHistory createRouter createWebHashHistory createWebHistory onBeforeRouteLeave onBeforeRouteUpdate parseQuery stringifyQuery useLink useRoute useRouter ...

最后

我想,就介绍这么多吧,上文介绍到的只是改进的一部分,感觉还有很多很多东西需要我们去了解和掌握,新版本给我们带来了更灵活的编程,让我们共同期待 vue 3.0 到到来吧。

推荐Vue学习资料文章:

《Vue项目部署及性能优化指导篇「实践」》

《Vue高性能渲染大数据Tree组件「实践」》

《尤大大细品VuePress搭建技术网站与个人博客「实践」》

《10个Vue开发技巧「实践」》

《是什么导致尤大大选择放弃Webpack?【vite 原理解析】》

《带你了解 vue-next(Vue 3.0)之 小试牛刀【实践】》

《带你了解 vue-next(Vue 3.0)之 初入茅庐【实践】》

《实践Vue 3.0做JSX(TSX)风格的组件开发》

《一篇文章教你并列比较React.js和Vue.js的语法【实践】》

《手拉手带你开启Vue3世界的鬼斧神工【实践】》

《深入浅出通过vue-cli3构建一个SSR应用程序【实践】》

《怎样为你的 Vue.js 单页应用提速》

《聊聊昨晚尤雨溪现场针对Vue3.0 Beta版本新特性知识点汇总》

《【新消息】Vue 3.0 Beta 版本发布,你还学的动么?》

《Vue真是太好了 壹万多字的Vue知识点 超详细!》

《Vue Koa从零打造一个H5页面可视化编辑器——Quark-h5》

《深入浅出Vue3 跟着尤雨溪学 TypeScript 之 Ref 【实践】》

《手把手教你深入浅出vue-cli3升级vue-cli4的方法》

《Vue 3.0 Beta 和React 开发者分别杠上了》

《手把手教你用vue drag chart 实现一个可以拖动 / 缩放的图表组件》

《Vue3 尝鲜》

《总结Vue组件的通信》

《手把手让你成为更好的Vue.js开发人员的12个技巧和窍门【实践】》

《Vue 开源项目 TOP45》

《2020 年,Vue 受欢迎程度是否会超过 React?》

《尤雨溪:Vue 3.0的设计原则》

《使用vue实现HTML页面生成图片》

《实现全栈收银系统(Node Vue)(上)》

《实现全栈收银系统(Node Vue)(下)》

《vue引入原生高德地图》

《Vue合理配置WebSocket并实现群聊》

《多年vue项目实战经验汇总》

《vue之将echart封装为组件》

《基于 Vue 的两层吸顶踩坑总结》

《Vue插件总结【前端开发必备】》

《Vue 开发必须知道的 36 个技巧【近1W字】》

《构建大型 Vue.js 项目的10条建议》

《深入理解vue中的slot与slot-scope》

《手把手教你Vue解析pdf(base64)转图片【实践】》

《使用vue node搭建前端异常监控系统》

《推荐 8 个漂亮的 vue.js 进度条组件》

《基于Vue实现拖拽升级(九宫格拖拽)》

《手摸手,带你用vue撸后台 系列二(登录权限篇)》

《手摸手,带你用vue撸后台 系列三(实战篇)》

《前端框架用vue还是react?清晰对比两者差异》

《Vue组件间通信几种方式,你用哪种?【实践】》

《浅析 React / Vue 跨端渲染原理与实现》

《10个Vue开发技巧助力成为更好的工程师》

《手把手教你Vue之父子组件间通信实践讲解【props、$ref 、$emit】》

《1W字长文 多图,带你了解vue的双向数据绑定源码实现》

《深入浅出Vue3 的响应式和以前的区别到底在哪里?【实践】》

《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》

《基于Vue/VueRouter/Vuex/Axios登录路由和接口级拦截原理与实现》

《手把手教你D3.js 实现数据可视化极速上手到Vue应用》

《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【上】》

《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【中】》

《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【下】》

《Vue3.0权限管理实现流程【实践】》

《后台管理系统,前端Vue根据角色动态设置菜单栏和路由》

作者: Leiy

转发链接:https://segmentfault.com/a/1190000022582928

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页