赞
踩
关注 前端开发博客,回复“加群”
加入我们一起学习,天天进步
作者:wangkaiwd
https://github.com/wangkaiwd/simple-vue-router
vue-router
是开发vue
项目中必不可少的依赖,为了能更好的理解其实现原理,而源码阅读起来又过于复杂和枯燥,笔者这里实现一个简易版本的vue-rouer
,帮助自己来更好的理解源码。
其功能如下:
通过Vue
插件形式使用
支持hash
模式
支持嵌套路由
router-view
组件
router-link
组件
路由守卫
基础
demo
单独新建了一个分支 ,方便学习和查看
在实现自己的router
之前,我们先使用官方的包来书写一个基础demo
,之后我们会以这个demo
为需求,一步步实现我们自己的vue-router
。
demo
的代码逻辑如下:
App
页面中拥有Home
和About
俩个链接
点击Home
会跳转到Home
页面
点击About
会跳转到About
页面
而About
又有to a
和to b
俩个链接,分别跳转到a
和b
页面
下面开始使用我们自己写的vue-router
来实现上边展示的功能。
intall
方法vue-router
使用方式如下:
- import Vue from 'vue';
- import VueRouter from '../my-router';
- import Home from '../views/Home.vue';
- import About from '@/views/About';
- Vue.use(VueRouter);
- const routes = [
- {
- path: '/home',
- name: 'Home',
- component: Home
- },
- {
- path: '/about',
- name: 'About',
- component: About
- }
- ];
- const router = new VueRouter({
- routes
- });
- export default router;
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
之后会在main.js
中将router
作为配置项传入:
- import Vue from 'vue';
- import App from './App.vue';
- import router from './router';
-
- Vue.config.productionTip = false;
-
- new Vue({
- router,
- render: h => h(App)
- }).$mount('#app');
由用法我们可以知道vue-router
是一个类,并且它有一个静态方法install
:
- import install from '@/my-router/install';
-
- class VueRouter {
- constructor (options) {
-
- }
- init(app) {
-
- }
- }
-
- VueRouter.install = install;
- export default VueRouter;
install
方法中会为所有组件添加$router
以及$route
属性,并且会全局注册router-view
以及router-link
组件。我们在install.js
中来单独书写install
的逻辑:
- import RouterView from '@/my-router/components/view';
- import RouterLink from '@/my-router/components/link';
-
- const install = (Vue) => {
- Vue.mixin({
- beforeCreate () {
- const { router } = this.$options;
- // mount $router property for all components
- if (router) {
- // 用_rootRouter来存储根实例
- this._rootRouter = this;
- // 为根实例添加$router属性
- this.$router = router;
- // 在实例上定义响应式属性,但是这个API可能会发生变化,所以Vue并没有在文档中提供
- Vue.util.defineReactive(this, '$route', this.$router.history.current);
- // 初始化路由
- router.init(this);
- } else {
- this._rootRouter = this.$parent && this.$parent._rootRouter;
- if (this._rootRouter) {
- this.$router = this._rootRouter.$router;
- // 在为$route赋值时,会重新指向新的地址,导致子组件的$route不再更新
- // this.$route = this._rootRouter.$route;
- Object.defineProperty(this, '$route', {
- get () {
- return this._rootRouter.$route;
- }
- });
- }
- }
- }
- });
- Vue.component('RouterView', RouterView);
- Vue.component('RouterLink', RouterLink);
- };
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
在install
方法中做了如下几件事:
为所有组件的实例添加_rootRouter
,值为根实例,方便获取根实例上的属性和方法
在根实例执行beforeCreate
钩子时执行VueRouter
实例的init
方法
为所有组件的实例添加$router
属性,值为VueRouter
实例
为所有组件添加$route
属性,值为当前的路由信息(之后会介绍它的由来)
hashchange
事件vue-router
在hash
模式下可以利用hash
值的切换来渲染对应的组件,原理其实是利用页面地址hash
值发生改变不会刷新页面,并且会触发hashchange
事件。
在history
目录下,新建hash.js
来存放hash
值变化,组件进行切换的逻辑:
- import { getHash } from '@/my-router/util';
- import { createRoute } from '@/my-router/create-matcher';
-
- const ensureSlash = () => {
- if (!location.hash) {
- location.hash = '/';
- }
- };
-
- class HashHistory {
- constructor (router) {
- this.router = router;
- // 绑定this指向
- this.onHashchange = this.onHashchange.bind(this);
- // 默认hash值为'/'
- ensureSlash();
- }
-
- listenEvent () {
- window.addEventListener('hashchange', this.onHashchange);
- }
-
- onHashchange () {
- }
- }
- export default HashHistory;
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
在VueRouter
实例执行init
方法时,监听hashchange
事件:
- class VueRouter {
- constructor (options) {
- this.history = new HashHistory(this);
- }
-
- init (app) {
- // 第一次渲染时也需要手动执行一次onHashchange方法
- this.history.onHashchange();
- this.history.listenEvent();
- }
- }
在onHashchange
方法中,需要根据当前页面地址的hash
值来找到其对应的路由信息:
- class HashHistory {
- // some code ...
- onHashchange () {
- const path = getHash();
- const route = this.router.match(path);
- }
- }
为了找到当前的路由信息,HashHistory
中调用了VueRouter
的match
方法。match
方法放到了create-matcher.js
中来实现:
- // create-matcher.js
- export const createRoute = (route, path) => {
- const matched = [];
- // 递归route的所有父路由,生成matched数组,并和path一起返回,作为当前的路由信息
- while (route) {
- matched.unshift(route);
- route = route.parent;
- }
- return {
- path,
- matched
- };
- };
-
- function createMatcher (routes) {
- const pathMap = createRouteMap(routes);
- // need to get all matched route, then find current routes by matched and router-view
- const match = (path) => {
- const route = pathMap[path];
- return createRoute(route, path);
- };
- return {
- match
- };
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
- // create-route-map.js
- function addRouteRecord (routes, pathMap, parent) {
- routes.forEach(route => {
- const { path, children, ...rest } = route;
- // 拼接子路由path
- const normalizedPath = parent ? parent.path + '/' + path : path;
- // 将parent也放入到属性中,方便之后生成matched数组
- pathMap[normalizedPath] = { ...rest, path: normalizedPath, parent };
- if (children) {
- // 继续遍历子路由
- addRouteRecord(children, pathMap, route);
- }
- });
- }
-
- const createRouteMap = (routes, pathMap = {}) => {
- addRouteRecord(routes, pathMap);
- return pathMap;
- };
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
createMatcher
会通过createRouteMap
生成hash
值和路由的映射关系:
- const pathMap = {
- '/about': {
- path: '/about',
- name: 'About',
- children: [
- // ...
- ],
- parent: undefined
- }
- // ...
- }
这样我们可以很方便的通过hash
值来获取路由信息。
最终我们调用match
方法得到的路由信息结构如下:
- {
- "path": "/about/a",
- "matched": [
- {
- "path": "/about",
- "name": "About",
- "component": About,
- "children": [
- {
- "path": "a",
- "name": "AboutA",
- "component": A
- },
- {
- "path": "b",
- "name": "AboutB",
- "component": B
- }
- ]
- },
- // ...
- ]
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
需要注意的是对象中的matched
属性,它里面存放的是当前hash
匹配的所有路由信息组成的数组。在实现嵌套路由时会用到matched
数组,因为嵌套路由本质上是router-view
组件的嵌套,所以可以根据router-view
在组件中的深度在matched
中找到对应的匹配项,然后进行展示。
现在我们回到hashHistory
的onHashchange
方法,它会调用VueRouter
实例的match
方法,代码如下:
- class VueRouter {
- constructor (options) {
- this.matcher = createMatcher(options.routes);
- this.history = new HashHistory(this);
- }
-
- init (app) {
- this.history.onHashchange();
- this.history.listenEvent();
- }
-
- match (path) {
- return this.matcher.match(path);
- }
- }
在hashHistory
中将其赋值给实例中的current
属性:
- class HashHistory {
- constructor (router) {
- // pass instance of VueRoute class, can call methods and properties of instance directly
- this.router = router;
- // 当前的路由信息,在current更新后,由于其不具有响应性,所以尽管值更新了,但是不会触发页面渲染
- // 需要将其定义为响应式的数据
- this.current = createRoute(null, '/');
- this.onHashchange = this.onHashchange.bind(this);
- ensureSlash();
- }
-
- listenEvent () {
- window.addEventListener('hashchange', this.onHashchange);
- }
-
- onHashchange () {
- const path = getHash();
- const route = this.router.match(path);
- this.current = route
- }
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
为了方便用户访问当前路由信息,并且让其具有响应性,会通过Vue.util.defineReactive
来为vue
的根实例提供响应性的$route
属性,并在每次页面初始化以及路径更新时更新$route
:
- class HashHistory {
- constructor (router) {
- // pass instance of VueRoute class, can call methods and properties of instance directly
- this.router = router;
- // 当前的路由信息,在current更新后,由于其不具有响应性,所以尽管值更新了,但是不会触发页面渲染
- // 需要将其定义为响应式的数据
- this.current = createRoute(null, '/');
- this.onHashchange = this.onHashchange.bind(this);
- }
- // some code ...
- onHashchange () {
- const path = getHash();
- const route = this.router.match(path);
- // 将当前路由赋值给根实例,app会在router.init方法中进行初始化
- this.router.app.$route = this.current = route
- }
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
在install
方法中为根实例定义$route
属性,并将所有子组件实例的$route
属性赋值为根实例的$route
属性:
- const install = (Vue) => {
- Vue.mixin({
- beforeCreate () {
- const { router } = this.$options;
- // mount $router property for all components
- if (router) {
- this._rootRouter = this;
- this.$router = router;
- // 定义响应性$route属性
- Vue.util.defineReactive(this, '$route', this.$router.history.current);
- router.init(this);
- } else {
- this._rootRouter = this.$parent && this.$parent._rootRouter;
- if (this._rootRouter) {
- this.$router = this._rootRouter.$router;
- // 这样直接赋值会导致引用刷新而无法改变$route
- // this.$route = this._rootRouter.$route;
- // 获取根组件实例的$route属性,其具有响应性
- Object.defineProperty(this, '$route', {
- get () {
- return this._rootRouter.$route;
- }
- });
- }
- }
- }
- });
- };
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
到这里,我们已经可以在地址切换时获取到对应的路由信息,接下来我们实现router-view
来展示对应的组件。
router-view
组件router-view
组件需要展示当前匹配的hash
所对应的component
,这里采用函数式组件来实现:
- export default {
- name: 'RouterView',
- render (h) {
- // 记录组件的深度,默认为0
- let depth = 0;
- const route = this.$parent.$route;
- let parent = this.$parent;
- // 递归查找父组件,如果父组件是RouterView组件,深度++
- // 最终的深度即为路由的嵌套层数
- while (parent) {
- if (parent.$options.name === 'RouterView') {
- depth++;
- }
- parent = parent.$parent;
- }
- // 根据深度从matched中找到对应的记录
- const record = route.matched[depth];
- if (record) { // /about会匹配About页面,会渲染About中的router-view,此时record为undefined
- return h(record.component);
- } else {
- return h();
- }
- }
- };
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
这里必须要为组件指定name
属性,方便进行递归查找,进行深度标识。
到这里,我们为路由信息中添加的matched
数组,终于派上了用场,其与router-view
组件的深度depth
进行巧妙结合,最终展示出了所有匹配到的路由组件。
router-link
组件router-link
主要支持以下几个功能:
进行路由跳转
通过传入的tag
渲染不同标签
为当前激活的router-link
添加router-link-active
类名
在页面中vue-router
也支持通过router-link
来进行路由跳转,其实现比较简单:
- export default {
- props: {
- to: {
- type: String,
- },
- tag: {
- type: String,
- default: () => 'a'
- }
- },
- computed: {
- active () {
- return this.$route.matched.map(item => item.path).includes(this.to);
- }
- },
- methods: {
- onClick () {
- this.$router.push(this.to);
- }
- },
- render () {
- return (
- <this.tag
- onClick={this.onClick}
- href="javascript:;"
- class={{ 'router-link-active': this.active }}
- >
- {this.$slots.default}
- </this.tag>
- );
- }
- };
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
router-link
可以接受tag
来渲染不同的标签,默认会渲染a
标签。当点击router-link
的时候,其内部会调用VueRouter
实例的push
方法:
- class VueRouter {
- // some code ...
- push (path) {
- location.hash = path;
- }
- // some code ...
- }
- // some code ...
push
方法会切换页面的hash
,当hash
发生变化后,就会触发hashchange
事件,执行事件处理函数onHashchange
,重新通过path
匹配对应的路由信息。
在代码中我们通过计算属性active
来计算当前的router-link
是否激活,需要注意的是当子路由激活时父路由也会激活。如果matched
的path
属性组成的数组中包含this.to
,说明该router-link
被激活。用户可以通过router-link-active
类来设置激活样式。
beforeEach
钩子在日常开发中,经常会用到beforeEach
全局前置守卫,让我们在进入页面之前执行一些逻辑:
- // some code ....
- const router = new VueRouter({
- routes
- });
- // 在每次进入页面之前,都会先执行所有的beforeEach中的回调函数
- router.beforeEach((to, from, next) => {
- console.log(1);
- setTimeout(() => {
- // 调用下一个回调函数
- next();
- }, 1000);
- });
- router.beforeEach((to, from, next) => {
- console.log(2);
- next();
- });
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
在每次进入页面之前,vue-router
会先执行beforeEach
中的回调函数,并且只有当用户调用回调函数中传入的next
函数后,才会执行之后的beforeEach
中的回调。
当所有beforeEach
中的回调执行完毕后,调用next
函数会更新路由信息,然后通过router-view
来显示对应的组件。其实现如下:
- // my-router/index.js
- class VueRouter {
- constructor (options) {
- // some code ...
- this.beforeEachs = [];
- }
-
- // cache in global, execute before get matched route record
- beforeEach (fn) {
- this.beforeEachs.push(fn);
- }
- }
-
- // my-router/history/hash.js
- class HashHistory {
- // some code ...
- onHashchange () {
- const path = getHash();
- const route = this.router.match(path);
- // 当用户手动调用next时,会执行下一个beforeEach钩子,在所有的钩子执行完毕后,会更新当前路由信息
- const next = (index) => {
- const { beforeEachs } = this.router;
- if (index === beforeEachs.length) {
- //update route after executed all beforeEach hook
- this.router.app.$route = this.current = route;
- return;
- }
- const hook = beforeEachs[index];
- hook(route, this.current, () => next(index + 1));
- };
- next(0);
- }
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
上述代码的执行流程如下:
将beforeEach
中传入的函数放到全局的数组beforeEachs
中
在根据路径匹配最新的路由信息时,先执行beforeEachs
中存储的函数
根据一个递增的index
来读取beforeEachs
中的函数,执行时传入新的路由信息route
、旧的路由信息this.current
,以及需要用户调用的回调函数
当用户调用回调后,index+1
继续执行next
函数,进而执行beforeEachs
中的下一个函数
当执行完beforeEachs
中的所有函数后,为$route
赋值最新的路由信息
庆祝一下????,这里我们已经完成了文章开头定下的所有目标!
希望在读完文章之后,能让读者对vue-router
的底层实现有更深入的了解,明白日常使用的API
是怎么来的,从而更加熟练的使用vue-router
。
最后,如果文章有帮到你的话,希望能点赞鼓励一下作者????。
公众号后台回复:vue router,获取源码地址。
“在看”吗?在看就点一下吧
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。