💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
路由的作用:根据不同的路径映射到不同的视图。 Vue-Router的能力十分强大,它支持hash、history、abstract3中路由方式,提供了<router-link>和<router-view>2种组件,还提供了简单路由配置和一系列好用的API。 ~~~ <!-- 使用 router-link 组件来导航. --> <!-- 通过传入 `to` 属性指定链接. --> <!-- <router-link> 默认会被渲染成一个 `<a>` 标签 --> <router-link to="/foo">Go to Foo</router-link> <router-link to="/bar">Go to Bar</router-link> ~~~ ~~~ <!-- 路由出口 --> <!-- 路由匹配到的组件将渲染在这里 --> <router-view></router-view> ~~~ ~~~ import Vue from 'vue' import VueRouter from 'vue-router' import App from './App' Vue.use(VueRouter) ~~~ ~~~ // 1. 定义(路由)组件。 // 可以从其他文件 import 进来 const Foo = { template: '<div>foo</div>' } const Bar = { template: '<div>bar</div>' } ~~~ ~~~ // 2. 定义路由 // 每个路由应该映射一个组件。 其中"component" 可以是 // 通过 Vue.extend() 创建的组件构造器, // 或者,只是一个组件配置对象。 // 我们晚点再讨论嵌套路由。 const routes = [ { path: '/foo', component: Foo }, { path: '/bar', component: Bar } ] ~~~ ~~~ // 3. 创建 router 实例,然后传 `routes` 配置 // 你还可以传别的配置参数, 不过先这么简单着吧。 const router = new VueRouter({ routes // (缩写)相当于 routes: routes }) ~~~ ~~~ // 4. 创建和挂载根实例。 // 记得要通过 router 配置参数注入路由, // 从而让整个应用都有路由功能 const app = new Vue({ el: '#app', render(h) { return h(App) }, router }) ~~~ ## 路由注册 Vue从它的设计上就是一个渐进式Javascript框架,它本身的核心是解决视图渲染的问题,其他能力就通过插件的方式来解决。 Vue-Router是官方维护的路由插件。 ### Vue.use Vue提供了Vue.use的全局API来注册这些插件,接受了一个plugin参数。每一个插件都需要实现一个静态的install方法,当我们只需Vue.use注册插件的时候,就会执行这个install方法,第一个参数是Vue对象。 ### 路由安装 当用户执行Vue.use(VueRouter)的时候,实际上就是执行install函数,且确保install逻辑只执行一次。 Vue-Router安装最重要的一步就是利用**Vue.mixin把beforeCreate和destroyed钩子函数注入到每一个组件中**。 ~~~ export function initMixin (Vue: GlobalAPI) { Vue.mixin = function (mixin: Object) { this.options = mergeOptions(this.options, mixin) return this } } ~~~ 实现实际非常简单,**就是把药混入的对象通过mergeOptions合并到Vue的options中**。**由于每个组件的构造函数都会在extend阶段合并Vue.options到自身的options中,所以也就相当于每个组件都定义了mixin定义的选项**。 Vue-Router的install方法: * 先看混入的beforeCreate构造函数,**this._router表示VueRouter实例本身**,在newVue时传入的; * 执行了this._router.init()方法**初始化router**; * 然后用defineReactive方法吧**this._route变成响应式对象**。 * 接着给Vue原型上定义了$router和$route2个属性的get方法,这就是为什么在组件实例上**可以访问this.$router以及this.$route**。 * 接着又通过Vue.component方法定义了全局的<router-link>和<router-view>2个组件,因此可以直接使用。 * 最后定义了路由中的钩子函数的合并策略,和普通的钩子函数一样。 ### 总结 Vue编写插件的时候通常要提供静态的install方法,我们通过`Vue.use(plugin)`时候,就是执行install方法。 Vue-Router的install方法会给每个组件注册beforeCreate和destoryed钩子函数。在`beforeCreate`做一些私有属性定义和路由初始化工作。 ## VueRouter对象 VueRouter的实现是一个类,我们先对它做一个简单分析。 构造函数定义了一些属性,其中this.app表示根Vue实例, this.apps保存持有$options.router属性的Vue实例 this.options保存传入的路由配置 this.beforeHooks,this.resolveHooks,this.afterHooks表示一些钩子函数 **this.matcher表示路由匹配器。** this.fallback表示浏览器不支持history.pushStart,根据传入的fallback配置参数,决定是否回退到hash模式 **this.mode**表示路由创建模式(history,hash,abstract) **this.history**表示路由历史的具体的实现实例。 ~~~ this.mode = mode switch (mode) { case 'history': this.history = new HTML5History(this, options.base) break case 'hash': this.history = new HashHistory(this, options.base, this.fallback) break case 'abstract': this.history = new AbstractHistory(this, options.base) break default: if (process.env.NODE_ENV !== 'production') { assert(false, `invalid mode: ${mode}`) } } ~~~ 实例化VueRouter后会返回它的实例router,**我们在new Vue的时候会把router作为配置的属性传入**。 ~~~ new Vue({ el: '#root', router, template: '<App/>', components: { App } }) ~~~ 上一节讲beforeCreate混入的时候代码 ~~~ beforeCreate() { if (isDef(this.$options.router)) { // ... this._router = this.$options.router this._router.init(this) // ... } } ~~~ 所以组件在执行beforeCreate钩子函数的时候,**如果传入了router实例,都会执行router.int方法**。 init很简单:传入参数是Vue实例,然后存储到this.apps中;只有根Vue实例会保存到this.app中,并且**会拿到this.history,根据它的不同类型来执行不同逻辑**。平时使用的hash路由多一些。 this.matcher.match方法去做匹配。 ### 总结 组件的初始化阶段,**执行到beforeCreate钩子函数的时候,会执行router.init方法**,然后又会执行history.transitionTo方法做**路由过渡**,进而引出了**matcher**的概念。 `this.$router.push({ path: `/GoodsDetails/${goodsId}`});` ~~~ flag:this.$route.params.flag==undefined?0:this.$route.params.flag, orderId:this.$route.params.orderId==undefined?null:this.$route.params.orderId, ~~~ ## matcher ~~~ xport type Matcher = { match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route; addRoutes: (routes: Array<RouteConfig>) => void; }; ~~~ Matcher返回2个方法,**match和addRoutes**。 路由中重要的2个概念:**Location和Route**。 * Location ~~~ declare type Location = { _normalized?: boolean; name?: string; path?: string; hash?: string; query?: Dictionary<string>; params?: Dictionary<string>; append?: boolean; replace?: boolean; } ~~~ Vue-Router中定义的Location数据结构和浏览器提供的window.location部分结构有点类似,他们都是对url的结构化描述 `/abc?foo=bar&baz=qux#hello`,它的`path`是`/abc`,`query`是`{foo:'bar',baz:'qux'}`。 * Route ~~~ declare type Route = { path: string; name: ?string; hash: string; query: Dictionary<string>; params: Dictionary<string>; fullPath: string; matched: Array<RouteRecord>; redirectedFrom?: string; meta?: any; } ~~~ Route表示的是路由中的一条线路,**除了描述类似Location的path、query、hash等,还有matched表示匹配到所有的RouteRecord**。 ### createMatcher 2个参数,一个是router(new VueRouter返回的实例),**一个是routes,是用户定义的路由配置**。 ~~~ const routes = [ { path: '/foo', component: Foo }, { path: '/bar', component: Bar } ] ~~~ createMatcher:首先要**创建一个路由映射表**。把用户的路由配置转换成一张路由映射表,包含3个部分, * pathList存储的所有path,是**为了记录路由配置中所有的path**。 patch表示一个patch到RouteRecord的映射关系,**通过path能快速查到对应的RouteRecord**。 nameMap表示那么到RouteRecord的映射关系。**通过name能快速查到对应的RouteRecord**。 RouteRecord:遍历routes为每一个route生成一条记录(我们配置的时候会配置子路由),所以整个**RouteRecord也就是一个树形结构**。通过深度遍历,可以拿到要给route下的完整记录。 **matcher是一个对象,它对外暴露了match和addRoutes方法**。 ### addRoutes addRoutes的作用是**动态添加路由配置**,因为时间开发中有些场景时不能把路由写死的,**需要根据一些条件动态添加路由**。 addRoutes方法十分简单,再次调用creatRouteMap即可,传入新的routes配置,由于patchList、pathMap,namPath都是引用类型,执行addRoutes后会修改他们的值。 ### match match方法接收3个参数,raw是RawLocation类型,它可以是一个url字符串,也可以是一个Location对象,currentRoute是Route类型,它表示当前路径, **match方法返回的是一个路径,它的作用是根据传入的raw和当前的路径currentRoute计算出一个新的路径并返回**。 ~~~ function match ( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route ~~~ 根据raw,current(currentRoute)计算出新的location,它主要处理了raw的两种情况,一种是params且没有path,一种是有path, 对于第一种情况,如果current有name,则计算出location也有name 计算出新的location后,对location的那么和path有两种情况 * name 有name的情况,根据nameMap匹配到recored,它就是一个RouterRecord对象,如果record不存在,则匹配失败。 然后拿到record对应的paramNames,再对比currentRoute中的params,把交集部分添加到location中,然后通过fillParams方法根据record.path和location.path计算出location.path。最后生成一套新路径。 * path 通过name我们可以很快的找到record,但是通过path并不能,因为计算后的location.path是一个真实路径,而record的path可能会有param,因此需要对所有的pathList做顺序遍历,然后通过matchRoute(根据record.regex,location.path,location.params)方法匹配,去生成一条心路径。 因为是顺序遍历,所以书写路由配置要注意路径的顺序,因为前面的会优先尝试匹配。 createRoute可以根据record和location创建出来,最终返回的是一套Route路径,在Vue-Router中,所有的Route最终都会通过createRoute函数创建,并且它最后是不可以被外部修改的。 ### 总结 我们了解了Location、Route、RouteRecord等概念,并通过**matcher的mach方法**,我们会**找到匹配的路径Route,这个对Route的切换**,组件的渲染都非常有意义。 ## 路径切换 history.transitionTo是Vue-Router中非常重要的方法,**当我们切换路由线路是,就会执行该方法**。 matcher,如何找到匹配的新线路。匹配到新线路后,又做了什么? transitionTo根据**目标location和当前路径this.current**执行this.router.match方法去**匹配到目标的路径**。this.current是history维护的当前路径。 拿到新的路径后,那么接下来就会执行confirmTransition方法去做真正的切换,由于这个过程可能有一些**异步操作(异步组件)**,会有成**功回调函数和失败回调函数**。 ### 导航守卫 实际就是发生在**路由路径切换的时候,执行的一系列钩子函数**。 顺序 1. 在失活的组件里调用离开守卫 2. 调用全局的beforeEach守卫 3. 在重用的组件里调用beforeRouteUpdate守卫 4. 在激活的路由配置里调用beforeEneter 5. 解析异步路由组件。 路由切换除了执行这些钩子函数,从表象上有2个地方发生改变,**一个是url发生变化,一个是组件发生变化**。 ### url 当我们点击router-link的时候,实际上最终会执行`router.push` ~~~ push (location: RawLocation, onComplete?: Function, onAbort?: Function) { this.history.push(location, onComplete, onAbort) } ~~~ `push`函数会先执行`this.transitionTo`做路径切换,在切换完成的回调函数中,执行`pushHash`函数: ~~~ function pushHash (path) { if (supportsPushState) { pushState(getUrl(path)) } else { window.location.hash = path } } ~~~ `pushState`会调用浏览器原生的`history`的`pushState`接口或者`replaceState`接口,**更新浏览器的 url 地址,并把当前 url 压入历史栈中**。 然后在`history`的初始化中,会设置一个**监听器,监听历史栈的变化**: 当点击浏览器**返回按钮**的时候,如果已经有 url 被压入历史栈,则会触发`popstate`事件,然后拿到当前要跳转的`hash`,执行`transtionTo`方法做一次路径转换。 在使用Vue-Router开发项目的时候,打开调试页面http://localhost:8080后**自动把url修改为http://localhost:8080/#/**? 在实例化HashHistory的时候,这是path为空,所以执行**replaceHash('/' + path)**,然后内部会执行一次getUrl,计算出来新的url为http://localhost:8080/#/,这就是url改变的原因。 ### 组件 路由最终的渲染离不开组件,Vue-Router内置了<router-view>组件,`<router-view>`是一个`functional`组件,**它的渲染也是依赖`render`函数** 渲染什么组件呢? 首先获取当前的路径 ~~~ const route = parent.$route ~~~ 我们给Vue的原型上定义了$route ~~~ Object.defineProperty(Vue.prototype, '$route', { get () { return this._routerRoot._route } }) ~~~ **$route定义在Vue.prototype上,每个组件实例访问$route属性,就是访问根实例的_route,也就是当前的路由线路**。 根据当前线路匹配的路径和depth找到对应的RouteRecord,进而找到该渲染的组件。 render函数的最后根据component渲染出对弈的组件vnode。 由于我们把跟**Vue实例的$route属性定义成响应式的**,我们在每个<route-router>执行render函数的时候,都会**访问parent.$route,触发了getter**,相当于**<route-router>对它有依赖**,然后在执行完transitionTo后,修改app._route的时候,**又触发了setter**,因此会通知**<router-view>的渲染watcher**更新,**重新渲染组件**。 Vue-Router 还内置了另一个组件`<router-link>`, 它支持**用户在具有路由功能的应用中(点击)导航**。 通过`to`属性指定**目标地址**,默认渲染成带有正确链接的`<a>`标签,可以通**过配置`tag`属性生成别的标签**。另外,当目标路由成功激活时,链接元素自动设置一个**表示激活的 CSS 类名**。 `<router-link>`比起写死的`<a href="...">`会好一些,理由如下: 1. **无论是HTML5 history模式还是hash模式,它的表现行为一致**,所以当你要切换路由模式,或者IE9降级使用hash模式,无需做任何变动。 2. 在HTML5 history模式下,**router-link会守卫点击事件**,让浏览器不在重新加载页面。 3. 当你在HTML5 history模式下使用base选项之后,所有的**to属性都不需要写(基路径)了**。 `<router-link>`标签的渲染也是基于`render`函数,它首先做了路由解析: ### 总结 路径变化时路由中最重要的功能,我们要记住以下内容: **路由始终会维护当前的路线,路由切换的时候把当前路线切换到目标路线,切换过程中会执行一系列的导航守卫钩子,会修改url,同样也会渲染对应的组件,切换完毕后会把目标线路更新替换当前线路,这样就会作为下一次的路径切换的依据**。