0
点赞
收藏
分享

微信扫一扫

浅析vue-router源码并尝试实现一个简单的前端路由

浅析vue-router源码并尝试实现一个简单的前端路由_封装

前言

作为​​vue​​​生态圈中的重要一员:​​vue-router​​​已经成为了前端工程师要掌握的基本技能之一。本文抛开了​​vue-router​​​的日常使用,从源码入手,一起学习下源码并自己尝试实现一个自己的​​vue-router​​。阅读过程中,如有不足之处,恳请斧正。

本文共2000余字,阅读本篇大概需要15分钟。如有不足之处,恳请斧正



浅析vue-router源码并尝试实现一个简单的前端路由_初始化_02

此处推荐一篇之前实现一个自己的vuex的文章,可与本篇搭配观看



从0到1手写一个vuex

源码浅析

首先来看下源码目录结构:

// 路径:node_modules/vue-router
├── dist ---------------------------------- 构建后文件的输出目录
├── package.json -------------------------- 不解释
├── src ----------------------------------- 包含主要源码
│ ├── components --------------------------- 路由组件
│ │ ├── link.js ---------------- router-link组件
│ │ ├── view.js -- router-view组件
│ ├── history -------------------------- 路由模式
│ │ ├── abstract.js ------------------------ abstract路由模式
│ │ ├── base.js ----------------------- history路由模式
│ │ ├── errors.js ------------------ 错误类处理
│ │ ├── hash.js ---------------------- hash路由模式
│ │ ├── html5.js -------------------------- HTML5History模式封装
│ ├── util ---------------------------- 工具类功能封装
│ ├── create-matcher.js ------------------------- 路由映射表
│ ├── create-route-map.js ------------------------------- 创建路由映射状态树
│ ├── index.js ---------------------------- 主入口文件
| ├── install.js ---------------------------- 路由装载文件

入口开始分析

vue-router实例


从入口文件​index.js​​中我们可以看到暴露出了一个​VueRouter​​类,这个就是我们在 ​vue​​ 项目中引入 ​vue-router​​ 的时候所用到的​new Router()​ 其中具体内部代码如下(为了方便阅读,省略部分代码)

export default class VueRouter {
constructor (options: RouterOptions = {}) {
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
this.matcher = createMatcher(options.routes || [], this)
let mode = options.mode || 'hash'
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
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}`)
}
}
}
init () {}
function registerHook (){}
go(){}
push(){}
VueRouter.install = install
VueRouter.version = '__VERSION__'
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter)
}

从入口文件中我们可以看出里面包含了以下几个主要几个步骤:


  1. 初始化路由模式
  2. 根据传入的​routes​参数生成路由状态表
  3. 获取当前路由对象
  4. 初始化路由函数
  5. 注册​Hooks​等事件
  6. 添加​install​装载函数

install注册函数

上述暴露出的​router​​类中挂载了一个​install​​方法,这里我们对其做一个简要的分析(这也是我们下面实现一个自己路由的思维引导)。在我们引入​vue-router​​并且实例化它的时候,​vue-router​​内部帮助我们将​router​​实例装载入​vue​​的实例中,这样我们才可以在组件中可以直接使用​router-link、router-view​​等组件。以及直接访问​this.$router、this.$route​​等全局变量,这里主要归功于​install.js​帮助实现这一个过程,主要分以下几个步骤:

  1. 首先引入​vue-router​​后需要利用​beforeCreate​​生命周期进行装载它,用来初始化​_routerRoot,_router,_rout​​​e​​等数据,
  2. 同时设置全局访问变量​$router​​和​$router
  3. 完成​router-link​​和​router-view​ 两个组件的注册


在源码​install.js​中可以体现

import View from './components/view'
import Link from './components/link'


export let _Vue


export function install (Vue) {
if (install.installed && _Vue === Vue) return
install.installed = true
_Vue = Vue
// 混入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
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
// 设置全局访问变量$router
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})


Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// 注册组件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
}

上述我们对​vue-router​​的源码做了一个粗略的脉络梳理,下面我们将实现一个简化版的​vue-router​。在此之前我们需要简单的了解一些知识点


前置知识


我们都知道​vue-router​​提供一个​mode​​参数配置,我们可以设置​history​​ 或者是​hash​ 两种参数背后的实现原理也各不相同


hash​的实现原理

​​http://localhost:8080/#login​​

#​​符号本身以及它后面的字符称之为​hash​​,可通过​window.location.hash​​属性读取。H5新增了一个​hashchange​​来帮助我们监听浏览器链接的​hash​值变化。

<!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>老 yuan</title>
</head>
<body>
<h1 id="app"></h1>
<a href="#/jin">掘金</a>
<a href="#/sn">黄小虫</a>
<script>
window.addEventListener('hashchange',()=>{
app.innerHTML = location.hash.slice(2)
})
</script>
</body>
</html>

history​的实现原理

​​http://localhost:8080/login​​​​同样​​H5​​​也新增了​pushState​​和​popstate​​来帮助我们无感知刷新浏览器​url

<body>
<h1 id="app"></h1>
<a onclick="to('jin')">掘金</a>
<a onclick="to('sn')">黄小虫</a>
<script >
function to(pathname) {
history.pushState({},null,pathname)
app.innerHTML = pathname
}
window.addEventListener('popstate',()=>{
to(location.pathname)
})
</script>
</body>

在我们理清楚无感知刷新url的原理后,我们要基于这些原理封装出一个​​v​​​ue-router


开始实现自己的vue-router

实现install装载方法

首先我们需要初始化一下我们项目结构,新建​simple-vue-router.js​​,根据上面分析,这里我们需要暴露出一个​router​​类,其中需要包含一个​install​方法

let Vue // 用于保存vue实例
class VueRouter(){ // router类
}
function install(_vue){ // 装载函数
}
export default {
VueRouter,
install
}


其中的​install​需要实现以下几点功能

  1. 初始化​_routerRoot_router_route​等数据,
  2. 设置全局访问变量​$router​和​$router
  3. 完成​router-link​和 ​router-view​两个组件的注册

代码如下:

let Vue // 用于保存vue实例
class VueRouter {
// router类
}
VueRouter.install = function(_vue) {
// 装载函数
//每个组件都有 this.$router / this.$route 所以要mixin一下
Vue = _vue
// 在每个组件中都可以获取到 this.$router与this.$route,这里进行混入vue实例中
Vue.mixin({
beforeCreate() {
// 如果是根组件则
if (this.$options && this.$options.router) {
this._root = this //把当前vue实例保存到_root上
this._router = this.$options.router // 把router的实例挂载在_router上
} else {
// 如果是子组件则去继承父组件的实例(这样所有组件共享一个router实例)
this._root = this.$parent._root
}
// 定义router实例 当访问this.$router时即返回router实例
Object.defineProperty(this, '$router', {
get() {
return this._root._router
}
})
// 定义route 当访问this.$route时即返回当前页面路由信息
Object.defineProperty(this, '$route', {
get() {
return {}
}
})
}
})
// 全局注册 router的两个组件
Vue.component('router-link', {
render(h) {}
})
Vue.component('router-view', {
render(h) {}
})
}
export default VueRouter


实现​router​类


上述实现了​install​​方法帮助我们将​router​​挂载在​vue​​实例中,接下来我们需要完善一下​router​类中的功能。按照上文源码中的分析,我们需要实现以下几点功能:

  1. 生成根据传入的rotues参数生成,路由状态表。即如若传入参数为
routes:[
{
path: '/',
name: 'index',
component: index
},
{
path: '/login',
name: 'login',
component: login
},
{
path: '/learn',
name: 'learn',
component: learn
},
]

将其用path为key,component为value的规律格式化为

{
'/':index,
'/login':login,
'/learn':learn
}
  1. 定义当前路由变量,通过劫持进行实时渲染对应组件
  2. 定义一个函数,具体实现不同模式应对应使用的处理方法


具体代码如下

let Vue // 用于保存vue实例
class VueRouter {
// router类
constructor(options) {
// 默认为hash模式
this.mode = options.mode || 'hash'
this.routes = options.routes || []
// 路由映射表
this.routeMaps = this.generateMap(this.routes)
// 当前路由
this.currentHistory = new historyRoute()
// 初始化路由函数
this.initRoute()
}
generateMap(routes) {
return routes.reduce((prev, current) => {
prev[current.path] = current.component
return prev
}, {})
}
initRoute() {
// 这里仅处理hash模式与history模式
if (this.mode === 'hash') {
// 先判断用户打开时url中有没有hash,没有重定向到#/
location.hash ? '' : (location.hash = '/')
// 监控浏览器load事件,改变当前存储的路由变量
window.addEventListener('load', () => {
this.currentHistory.current = location.hash.slice(1)
})
window.addEventListener('hashchange', () => {
this.currentHistory.current = location.hash.slice(1)
})
} else {
location.pathname ? '' : (location.pathname = '/')
window.addEventListener('load', () => {
this.currentHistory.current = location.pathname
})
window.addEventListener('popstate', () => {
this.currentHistory.current = location.pathname
})
}
}
}
class historyRoute {
constructor() {
this.current = null
}
}
VueRouter.install = function(_vue) {
// 省略部分代码
}
export default VueRouter


完善代码,实现实时刷新页面视图

在构建完​r​​outer​​类之后,我们发现还存在一个问题,那就是当前路由状态​currentHistory.current​​还是静态的,当我们改变当前路由的时候页面并不会显示对应模板。这里我们可以利用​vue​自身的双向绑定机制实现

具体代码如下

let Vue // 用于保存vue实例
class VueRouter {
// router类
constructor(options) {
// 默认为hash模式
this.mode = options.mode || 'hash'
this.routes = options.routes || []
// 路由映射表
this.routeMaps = this.generateMap(this.routes)
// 当前路由
this.currentHistory = new historyRoute()
// 初始化路由函数
this.initRoute()
}
generateMap(routes) {
return routes.reduce((prev, current) => {
prev[current.path] = current.component
return prev
}, {})
}
initRoute() {
// 这里仅处理hash模式与history模式
if (this.mode === 'hash') {
// 先判断用户打开时url中有没有hash,没有重定向到#/
location.hash ? '' : (location.hash = '/')
// 监控浏览器load事件,改变当前存储的路由变量
window.addEventListener('load', () => {
this.currentHistory.current = location.hash.slice(1)
})
window.addEventListener('hashchange', () => {
this.currentHistory.current = location.hash.slice(1)
})
} else {
location.pathname ? '' : (location.pathname = '/')
window.addEventListener('load', () => {
this.currentHistory.current = location.pathname
})
window.addEventListener('popstate', () => {
this.currentHistory.current = location.pathname
})
}
}
}
class historyRoute {
constructor() {
this.current = null
}
}
VueRouter.install = function(_vue) {
// 装载函数
//每个组件都有 this.$router / this.$route 所以要mixin一下
Vue = _vue
// 在每个组件中都可以获取到 this.$router与this.$route,这里进行混入vue实例中
Vue.mixin({
beforeCreate() {
// 如果是根组件则
if (this.$options && this.$options.router) {
this._root = this //把当前vue实例保存到_root上
this._router = this.$options.router // 把router的实例挂载在_router上
//利用vue工具库对当前路由进行劫持
Vue.util.defineReactive(this,'route',this._router.currentHistory)
} else {
// 如果是子组件则去继承父组件的实例(这样所有组件共享一个router实例)
this._root = this.$parent._root
}
// 定义router实例 当访问this.$router时即返回router实例
Object.defineProperty(this, '$router', {
get() {
return this._root._router
}
})
// 定义route 当访问this.$route时即返回当前页面路由信息
Object.defineProperty(this, '$route', {
get() {
return {
// 当前路由
current: this._root._router.history.current
}
}
})
}
})
// 全局注册 router的两个组件
Vue.component('router-link', {
props: {
to: String,
tag: String
},
methods: {
handleClick(event) {
// 阻止a标签默认跳转
event && event.preventDefault && event.preventDefault()
let mode = this._self._root._router.mode
let path = this.to
this._self._root._router.currentHistory.current = path
if (mode === 'hash') {
window.history.pushState({}, '', '#/' + path.slice(1))
} else {
window.history.pushState({}, '', path.slice(1))
}
}
},
render(h) {
let mode = this._self._root._router.mode
let tag = this.tag || 'a'
return (
<tag on-click={this.handleClick} href={mode === 'hash' ? `#${this.to}` : this.to}>
{this.$slots.default}
</tag>
)
}
})
Vue.component('router-view', {
render(h) {
// 这里的current通过上面的劫持已经是动态了
let current = this._self._root._router.currentHistory.current
let routeMap = this._self._root._router.routeMaps
return h(routeMap[current]) // 动态渲染对应组件
}
})
}
export default VueRouter

到此为止,一个简单的路由管理器已经完成。实际上相对​vue-router​​来说还是缺少了很多诸如导航守卫、动态路由等功能。千里之行始于足下,本篇文章旨在通过简要分析​vue-router​​的原理以及实践一个简单的路由器帮助大家走进​vue-router​原理的大门,后面的就要靠大家自己坚持继续深入学习了。


(转载)

作者:黄小虫


​​一套从0到1开发的前后端一体项目+Nginx部署node.js实战,请注意查收​​


浅析vue-router源码并尝试实现一个简单的前端路由_封装_03END




举报

相关推荐

0 条评论