前言
作为
vue
生态圈中的重要一员:vue-router
已经成为了前端工程师要掌握的基本技能之一。本文抛开了vue-router
的日常使用,从源码入手,一起学习下源码并自己尝试实现一个自己的vue-router
。阅读过程中,如有不足之处,恳请斧正。
本文共2000余字,阅读本篇大概需要15分钟。如有不足之处,恳请斧正
此处推荐一篇之前实现一个自己的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) }
从入口文件中我们可以看出里面包含了以下几个主要几个步骤:
初始化路由模式
根据传入的
routes
参数生成路由状态表获取当前路由对象
初始化路由函数
注册
Hooks
等事件添加
install
装载函数
install注册函数
上述暴露出的
router
类中挂载了一个install
方法,这里我们对其做一个简要的分析(这也是我们下面实现一个自己路由的思维引导)。在我们引入vue-router
并且实例化它的时候,vue-router
内部帮助我们将router
实例装载入vue
的实例中,这样我们才可以在组件中可以直接使用router-link、router-view
等组件。以及直接访问this.$router、this.$route
等全局变量,这里主要归功于install.js
帮助实现这一个过程,主要分以下几个步骤:
- 首先引入
vue-router
后需要利用beforeCreate
生命周期进行装载它,用来初始化_routerRoot,_router,_rout
e
等数据, - 同时设置全局访问变量
$router
和$router
- 完成
router-link
和router-view
两个组件的注册
在源码install.js
中可以体现
import View from './components/view'import Link from './components/link'export let _Vueexport 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
值变化。
<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>老 yuantitle>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
需要实现以下几点功能
初始化
_routerRoot
,_router
,_route
等数据,设置全局访问变量
$router
和$router
完成
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
类中的功能。按照上文源码中的分析,我们需要实现以下几点功能:
- 生成根据传入的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}
- 定义当前路由变量,通过劫持进行实时渲染对应组件
- 定义一个函数,具体实现不同模式应对应使用的处理方法
具体代码如下
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 ( this.handleClick} href={mode === {this.$slots.default} ) } }) 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
原理的大门,后面的就要靠大家自己坚持继续深入学习了。
(转载)
作者:黄小虫
原文链接:https://juejin.im/post/5e435815f265da57340233ee
一套从0到1开发的前后端一体项目+Nginx部署node.js实战,请注意查收
END 点击