前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Vue路由实现原理

Vue路由实现原理

原创
作者头像
愤怒的小鸟
修改2021-01-14 18:09:06
修改2021-01-14 18:09:06
1.3K00
代码可运行
举报
文章被收录于专栏:web shareweb share
运行总次数:0
代码可运行

一、Location对象 History对象

Location对象的属性

属性

描述

hash

设置或返回从井号 (#) 开始的 URL(锚)。

host

设置或返回主机名和当前 URL 的端口号。

hostname

设置或返回当前 URL 的主机名。

protocol

设置或返回当前 URL 的协议。

href

设置或返回完整的 URL。

pathname

设置或返回当前 URL 的路径部分。

port

设置或返回当前 URL 的端口号。

search

设置或返回从问号 (?) 开始的 URL(查询部分)。

Location对象的方法

方法

描述

assign()

加载新的文档。

reload()

重新加载当前文档。

replace()

用新的文档替换当前文档。

hashchange()

监听hashchange变化。

上面的属性和方法中除了hash,其他都会重新加载页面。

H5中的History对象的属性(部分)

属性

描述

length

历史记录数组的长度

state

表示当前的处在哪个记录上

H5中的History对象的方法(部分)

方法

描述

back()

等效于用户点击回退按钮

forward()

等效于用户点击前进按钮

go(num)

通过指定一个相对于当前页面位置的数值加载页面

pushState(json, "", url)

添加一条记录

replaceState(json, "", url)

替换掉一条记录

其中pushState方法和replaceState方法可以分别增加和替换掉一条记录(必须同源),而不会重新加载页面。

window.location.hash和window.history.pushState(或replaceState)唯一的不同是通过hash改变url带入#,而后者不会。而Vue 路由的两种模式就是基于location和history这2个对象的。

二、路由模式对比

1. 两种模式对比

观赏性

兼容性

实用性

命名空间

Hash

>ie8

直接使用

同一document

History

>ie10

需后端配合

同源

2. history模式404

当我们使用history模式时,如果没有进行配置,刷新页面会出现404。

1. 原因: 因为history模式的url是真实的url,服务器会对url的文件路径进行资源查找,找不到资源就会返回404。

2. 解决方案: 使用webpack-dev-server的里的historyApiFallback属性来支持HTML5 History Mode。

三、路由实现

1. Hash路由

1. HashHistory.push()

代码语言:javascript
代码运行次数:0
运行
复制
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  this.transitionTo(location, route => {
    pushHash(route.fullPath)
    onComplete && onComplete(route)
  }, onAbort)
}

function pushHash (path) {
  window.location.hash = path
}

transitionTo()方法是父类中定义的是用来处理路由变化中的基础逻辑的,push()方法最主要的是对windowhash进行了直接赋值:

代码语言:javascript
代码运行次数:0
运行
复制
window.location.hash=route.fullPath

hash的改变会自动添加到浏览器的访问历史记录中。 那么视图的更新是怎么实现的呢,我们来看看父类History中的transitionTo()方法:

代码语言:javascript
代码运行次数:0
运行
复制
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  const route = this.router.match(location, this.current)
  this.confirmTransition(route, () => {
    this.updateRoute(route)
    ...
  })
}

updateRoute (route: Route) {
  
  this.cb && this.cb(route)
  
}

listen (cb: Function) {
  this.cb = cb
}

可以看到,当路由变化时,调用了Hitory中的this.cb方法,而this.cb方法是通过History.listen(cb)进行设置的,回到VueRouter类定义中,找到了在init()中对其进行了设置:

代码语言:javascript
代码运行次数:0
运行
复制
init (app: any /* Vue component instance */) {
    
  this.apps.push(app)

  history.listen(route => {
    this.apps.forEach((app) => {
      app._route = route
    })
  })
}

appVue组件实例,但是Vue作为渐进式的前端框架,本身的组件定义中应该是没有有关路由内置属性_route,如果组件中要有这个属性,应该是在插件加载的地方,即VueRouterinstall()方法中混入Vue对象的,install.js的源码:

代码语言:javascript
代码运行次数:0
运行
复制
export function install (Vue) {
  
  Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) {
        this._router = this.$options.router
        this._router.init(this)
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      }
      registerInstance(this, this)
    },
  })
}

通过Vue.mixin()方法,全局注册一个混合,影响注册之后所有创建的每个Vue实例,该混合在beforeCreate钩子中通过Vue.util.defineReactive()定义了响应式的_route属性。所谓响应式属性,即当_route值改变时,会自动调用Vue实例的render()方法,更新视图。

代码语言:javascript
代码运行次数:0
运行
复制
$router.push()-->HashHistory.push()-->History.transitionTo()-->History.updateRoute()-->{app._route=route}-->vm.render()

2. HashHistory.replace()

replace()方法与push()方法不同之处在于,它并不是将新路由添加到浏览器访问历史栈顶,而是替换掉当前的路由:

代码语言:javascript
代码运行次数:0
运行
复制
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  this.transitionTo(location, route => {
    replaceHash(route.fullPath)
    onComplete && onComplete(route)
  }, onAbort)
}
  
function replaceHash (path) {
  const i = window.location.href.indexOf('#')
  window.location.replace(
    window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
  )
}

可以看出,它与push()的实现结构基本相似,不同点它不是直接对window.location.hash进行赋值,而是调用window.location.replace方法将路由进行替换。

3. 监听地址栏

上面的VueRouter.push()VueRouter.replace()是可以在vue组件的逻辑代码中直接调用的,除此之外在浏览器中,用户还可以直接在浏览器地址栏中输入改变路由,因此还需要监听浏览器地址栏中路由的变化 ,并具有与通过代码调用相同的响应行为,在HashHistory中这一功能通过setupListeners监听hashchange实现:

代码语言:javascript
代码运行次数:0
运行
复制
setupListeners () {
  window.addEventListener('hashchange', () => {
    if (!ensureSlash()) {
      return
    }
    this.transitionTo(getHash(), route => {
      replaceHash(route.fullPath)
    })
  })
}

该方法设置监听了浏览器事件hashchange,调用的函数为replaceHash,即在浏览器地址栏中直接输入路由相当于代码调用了replace()方法。

2. History路由

1. HTML5History.push()

代码语言:javascript
代码运行次数:0
运行
复制
window.history.pushState(stateObject,title,url)
  • stateObject:当浏览器跳转到新的状态时,将触发popState事件,该事件将携带这个stateObject参数的副本
  • title:所添加记录的标题
  • url:所添加记录的url

2. HTML5History.replace()

代码语言:javascript
代码运行次数:0
运行
复制
window.history,replaceState(stateObject,title,url)
  • stateObject:当浏览器跳转到新的状态时,将触发popState事件,该事件将携带这个stateObject参数的副本
  • title:所添加记录的标题
  • url:所添加记录的url
代码语言:javascript
代码运行次数:0
运行
复制
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  const { current: fromRoute } = this
  this.transitionTo(location, route => {
    pushState(cleanPath(this.base + route.fullPath))
    handleScroll(this.router, route, fromRoute, false)
    onComplete && onComplete(route)
  }, onAbort)
}

replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  const { current: fromRoute } = this
  this.transitionTo(location, route => {
    replaceState(cleanPath(this.base + route.fullPath))
    handleScroll(this.router, route, fromRoute, false)
    onComplete && onComplete(route)
  }, onAbort)
}

// src/util/push-state.js
export function pushState (url?: string, replace?: boolean) {
  saveScrollPosition()
  // try...catch the pushState call to get around Safari
  // DOM Exception 18 where it limits to 100 pushState calls
  const history = window.history
  try {
    if (replace) {
      history.replaceState({ key: _key }, '', url)
    } else {
      _key = genKey()
      history.pushState({ key: _key }, '', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
}

export function replaceState (url?: string) {
  pushState(url, true)
}

3. 监听地址栏

HTML5History中添加对修改浏览器地址栏URL的监听popstate是直接在构造函数中执行的:

代码语言:javascript
代码运行次数:0
运行
复制
constructor (router: Router, base: ?string) {
  
  window.addEventListener('popstate', e => {
    const current = this.current
    this.transitionTo(getLocation(this.base), route => {
      if (expectScroll) {
        handleScroll(router, route, current, true)
      }
    })
  })
}

四、实现一个前端路由demo

1. 环境搭建

代码语言:javascript
代码运行次数:0
运行
复制
/*
* package.json
*/
{
  "name": "web_router",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "webpack-dev-server --config ./webpack.config.js"
  },
  "author": "",
  "license": "",
  "devDependencies": {
    "html-webpack-plugin": "^3.2.0",
    "webpack": "^4.28.1",
    "webpack-cli": "^3.2.1",
    "webpack-dev-server": "^3.1.14"
  }
}
代码语言:javascript
代码运行次数:0
运行
复制
/*
* webpack.config.js
*/
'use strict';

const path = require('path');

const webpack = require('webpack');

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: '[name].js'
  },
  devServer: {
    clientLogLevel: 'warning',
    hot: true,
    inline: true,
    open: true,
    //在开发单页应用时非常有用,它依赖于HTML5 history API,如果设置为true,所有的跳转将指向index.html (解决histroy mode 404)
    historyApiFallback: true,
    host: 'localhost',
    port: '8089',
    compress: true
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true
    })
  ]
};

2. 实现代码

  • 初始化配置参数
代码语言:javascript
代码运行次数:0
运行
复制
/*
* src/index.js
*/
import { HashRouter } from './hash';
import { HistoryRouter } from './history';
import { ROUTELIST } from './routeList';

//路由模式
const MODE = 'hash';  

class WebRouter {
  constructor({ mode = 'hash', routeList }) {
    this.router = mode === 'hash' ? new HashRouter(routeList) : new HistoryRouter(routeList);
  }
  push(path) {
    this.router.push(path);
  }
  replace(path) {
    this.router.replace(path);
  }
  go(num) {
    this.router.go(num);
  }
}

const webRouter = new WebRouter({
  mode: MODE,
  routeList: ROUTELIST
});
  • 定义路由列表
代码语言:javascript
代码运行次数:0
运行
复制
/*
* src/routeList.js
*/
export const ROUTELIST = [
  {
    path: '/',
    name: 'index',
    component: 'This is index page'
  },
  {
    path: '/hash',
    name: 'hash',
    component: 'This is hash page'
  },
  {
    path: '/history',
    name: 'history',
    component: 'This is history page'
  },
  {
    path: '*',
    name: 'notFound',
    component: '404 NOT FOUND'
  }
];
  • html文件
代码语言:javascript
代码运行次数:0
运行
复制
/*
* src/index.html
*/
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>前端路由</title>
  </head>
  <body>
    <div id="page"></div>
  </body>
</html>
  • base路由
代码语言:javascript
代码运行次数:0
运行
复制
/*
* src/base.js
*/
const ELEMENT = document.querySelector('#page');

export class BaseRouter {
 //list = 路由列表
  constructor(list) {
    this.list = list;
  }
  render(state) {
   //匹配当前的路由,匹配不到则使用404配置内容 并渲染~
    let ele = this.list.find(ele => ele.path === state);
    ele = ele ? ele : this.list.find(ele => ele.path === '*');
    ELEMENT.innerText = ele.component;
  }
}
  • Hash模式
代码语言:javascript
代码运行次数:0
运行
复制
/*
* src/hash.js
*/
import { BaseRouter } from './base.js'; 

export class HashRouter extends BaseRouter {
  constructor(list) {
    super(list);
    this.handler();
    //监听hash变化事件,hash变化重新渲染  
    window.addEventListener('hashchange', e => {
      this.handler();
    });
  }
  //渲染
  handler() {
    this.render(this.getState());
  }
  //获取当前hash
  getState() {
    const hash = window.location.hash;
    return hash ? hash.slice(1) : '/';
  }
  //获取完整url
  getUrl(path) {
    const href = window.location.href;
    const i = href.indexOf('#');
    const base = i >= 0 ? href.slice(0, i) : href;
    return `${base}#${path}`;
  }
  //改变hash值 实现压入 功能
  push(path) {
    window.location.hash = path;
  }
  //使用location.replace实现替换 功能 
  replace(path) {
    window.location.replace(this.getUrl(path));
  }
  //这里使用history模式的go方法进行模拟 前进/后退 功能
  go(n) {
    window.history.go(n);
  }
}
  • History模式
代码语言:javascript
代码运行次数:0
运行
复制
/*
* src/history.js
*/
import { BaseRouter } from './base.js';

export class HistoryRouter extends BaseRouter {
  constructor(list) {
    super(list);
    this.handler();
    //监听历史栈信息变化,变化时重新渲染
    window.addEventListener('popstate', e => {
      this.handler();
    });
  }
  //渲染
  handler() {
    this.render(this.getState());
  }
  //获取路由路径
  getState() {
    const path = window.location.pathname;
    return path ? path : '/';
  }
  //使用pushState方法实现压入功能
  //PushState不会触发popstate事件,所以需要手动调用渲染函数
  push(path) {
    history.pushState(null, null, path);
    this.handler();
  }
  //使用replaceState实现替换功能  
  //replaceState不会触发popstate事件,所以需要手动调用渲染函数
  replace(path) {
    history.replaceState(null, null, path);
    this.handler();
  }
  go(n) {
    window.history.go(n);
  }
}

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
作者已关闭评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、Location对象 和 History对象
  • Location对象的属性
  • Location对象的方法
  • H5中的History对象的属性(部分)
  • H5中的History对象的方法(部分)
  • 二、路由模式对比
    • 1. 两种模式对比
    • 2. history模式404
  • 三、路由实现
    • 1. Hash路由
      • 3. 监听地址栏
    • 2. History路由
  • 四、实现一个前端路由demo
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档