10分钟教你 手写 Vue-Router
Posted X可乐
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了10分钟教你 手写 Vue-Router相关的知识,希望对你有一定的参考价值。
Vue-Router 原理
Hash 模式
- URL 中 # 后面的内容作为路径地址
- 监听 hashchange 事件
- 根据当前路由地址找到对应组件重新渲染
History 模式
- 通过 history.pushState() 方法改变地址栏
- 监听 popstate 事件
- 根据当前路由地址找到对应组件重新渲染
手写 Router
下列方法皆为拆解方法,最终会重组在一起
分析
回顾核心代码
Vue.use(VueRouter) // Vue.use 内部传入对象时,会调用对象的 install 方法
// 创建路由对象
const router = new VueRouter(
routes: [
name: 'home', path: '/', component: homeComponent
]
)
// 创建 Vue 实例,注册 router 对象
new Vue(
router,
render: h => h(App)
).$mount('#app')
- Vue.use 内部传入对象时,会调用对象的 install 方法,所以我们就来先处理一下 install 方法
install 方法
- install 接收两个参数
- vue 的构造函数
- 可选的选项对象(我们这里没用到,所以不传递)
- 方法内部分为 3 步
- 如果插件已经安装,直接返回
- 把 vue 的构造函数记录到全局变量中,因为将来我们要在 VueRouter 的实例方法中使用 vue 的构造函数
- 把创建 Vue 实例时候传入的 router 对象注入到 Vue 实例上
let _Vue = null
export default class VueRouter
// 传入两个参数,一个是 vue 的构造函数,第二个是 可选的 选项对象
static install (Vue)
// 1. 如果插件已经安装直接返回
if (VueRouter.install.installed && _vue === true) return
VueRouter.install.installed = true // 表示插件已安装
// 2. 把 vue 的构造函数记录到全局变量中,因为将来我们要在 VueRouter 的实例方法中使用 vue 的构造函数
_Vue = Vue
// 3. 把创建 Vue 实例时候传入的 router 对象注入到 Vue 实例上
Vue.mixin(
beforeCreate ()
if (this.$options.router)
_Vue.prototype.$router = this.$options.router
)
constructor
再次我们不需要过多的操作,只需要声明 3 个属性
- this.options = options
记录构造函数中传入的选项 options
- this.routeMap =
把 options 中传入的 routes 也就是路由规则解析出来,键值对的形式,键---路由地址,值---路由组件
- this.data = _Vue.observable( current: ‘/’ )
observable 创建响应式对象,对象中存储当前路由地址,默认是 /
constructor (options)
// 记录构造函数中传入的选项 options
this.options = options
// 把 options 中传入的 routes 也就是路由规则解析出来,键值对的形式,键---路由地址,值---路由组件
this.routeMap =
// data 是一个响应式对象,因为 data 中存储的是当前路由地址,路由变化时要自动加载组件
this.data = _Vue.observable( // observable 创建响应式对象
current: '/' // 存储当前路由地址,默认是 /
)
createRouteMap
把构造函数中传过来的 选项中的 routes 也就是路由规则转换为键值对的形式转换到routeMap对象中 键---路由地址,值---路由组件
createRouteMap ()
// 遍历所有的路由规则,以键值对的形式存储在 routeMap 对象中
this.options.routes.forEach(route =>
this.routeMap[route.path] = route.component
)
initComponents
该方法用来创建 router-link 与 router-view
router-link
initComponents (Vue)
Vue.component('router-link',
props:
to: String
,
template: '<a :href="to"><slot></slot></a>'
)
注意点
-
如果我们使用 vue_cli 创建项目并且运行时,使用上述创建方法会出现一些报错,原因在于创建标签时使用了 template
-
而 vue_cli 使用的 运行时版的 vue,不支持 template 模板,需要打包的时候提前编译
解决方法有两种
-
修改 vue.config.js 配置使用完整版的 vue
完整包含运行时和编译器,体积比运行时版大 10k 左右,程序运行的时候把模板转换成 render 函数
module.exports = // 渲染完整版 vue runtimeCompiler: true
-
使用 render 函数
运行时版本的 vue 不带编译器,所以不支持组件中的 template 选项,编译器的作用就是把 template 编译成 render 函数,运行时的组件可以直接写 render 函数
单文件组件时一直使用 template 没写 render 是因为在打包的过程中把单文件的 template 编译成 render 函数了,这叫做预编译
render
-
render 函数接受一个参数,通常叫做 h 作用是帮我们创建虚拟 DOM,h 由 vue 传递
-
h 函数接受 3 个参数
- 创建这个元素的选择器。
- 给标签设置一些属性,如果是 DOM 对象的属性,需要添加到 attrs 中。
- 生成的元素的子元素,所以是数组形式
initComponents (Vue) Vue.component('router-link', props: to: String , /* 运行时版本的 vue 不带编译器,所以不支持组件中的 template 选项,编译器的作用就是把 template 编译成 render 函数,运行时的组件可以直接写 render 函数 单文件组件时一直使用 template 没写 render 是因为在打包的过程中把单文件的 template 编译成 render 函数了,这叫做预编译 */ render (h) // 该函数接收一个参数,通常叫做 h 作用是帮我们创建虚拟 DOM,h 由 vue 传递 return h('a', // h 函数接受 3 个参数,1. 创建这个元素的选择器。2. 给标签设置一些属性,如果是 DOM 对象的属性,需要添加到 attrs 中。3. 生成的元素的子元素,所以是数组形式 attrs: href: this.to , [this.$slots.default]) // template: '<a :href="to"><slot></slot></a>' )
router-view
const self = this
Vue.component('router-view',
// 获取到当前路由地址对应的路由组件
render (h)
const component = self.routeMap[self.data.current]
return h(component)
)
集合上述方法 检查是否存在问题
- 因为我们默认只会调用 install 方法,但是我们还有一些其他方法需要调用,还需要一个 init 方法,在 install 方法 被调用后,调用
createRouteMap() 与 initComponents()
init ()
this.createRouteMap()
this.initComponents(_Vue)
-
将上述所有方法整合(为了便于阅读,去掉注释)
// ../vuerouter/index.js let _Vue = null export default class VueRouter static install (Vue) if (VueRouter.install.installed && _Vue === true) return VueRouter.install.installed = true _Vue = Vue Vue.mixin( beforeCreate () if (this.$options.router) _Vue.prototype.$router = this.$options.router this.$options.router.init() ) constructor (options) this.options = options this.routeMap = this.data = _Vue.observable( current: '/' ) init () this.createRouteMap() this.initComponents(_Vue) this.initEvent() createRouteMap () this.options.routes.forEach(route => this.routeMap[route.path] = route.component ) initComponents (Vue) Vue.component('router-link', props: to: String , render (h) return h('a', attrs: href: this.to , [this.$slots.default]) ) const self = this Vue.component('router-view', render (h) const component = self.routeMap[self.data.current] return h(component) ) // 记得替换 router/index.js 中引入的 VueRouter import Vue from 'vue' // import VueRouter from 'vue-router' import VueRouter from '../vuerouter' import Home from '../views/Home.vue' import Home from '../views/About.vue' Vue.use(VueRouter) const routes = [ path: '/', name: 'Home', component: Home , path: '/about', name: 'About', component: About ] const router = new VueRouter( routes ) export default router
- 实际测试后,我们会发现,上述仍然存在一些小问题,也就是我们在点击 router-link 时,会改变路径刷新页面,但是在单页面组件中,我们是不需要刷新页面的,所以我们需要对 router-link 在做一点小调整
完善 router-link
-
我们需要给 a 标签添加 1 个 方法,这个方法有三个作用
- 通过 pushState 方法 改变浏览器地址栏,但不给服务器发请求
- 将当前路由路径 同步给默认值
- 阻止标签默认事件
pushState 方法接受 3 个参数
- data。
- title 网页的标题。
- url 地址。
Vue.component('router-link', props: to: String , render (h) return h('a', attrs: href: this.to , on: click: this.clickHandler , [this.$slots.default]) , methods: clickHandler (e) // 1. 通过 pushState 方法 改变浏览器地址栏,但不给服务器发请求 history.pushState(, '', this.to) // 该方法接受 3 个参数,1. data。2. title 网页的标题。3. url 地址 this.$router.data.current = this.to e.preventDefault() )
最终的完善
将上边的 router-link 完善后,我们的 Vue-Router 就实现了,但是还差一个小功能,也就是浏览器左上角的前进后退就失效了,所以我们需要在添加一个小方法来实现最终的完善
- 其实就是通过 监听 popstate 将当前的路由路径赋值给 current 来达到左上角的前进后退功能**(记得将其添加到 init 方法中)**
initEvent ()
window.addEventListener('popstate', () =>
this.data.current = window.location.pathname
)
- 最终完整版代码(分为两版,有注释的在最后)
let _Vue = null
export default class VueRouter
static install (Vue)
if (VueRouter.install.installed && _Vue === true) return
VueRouter.install.installed = true
_Vue = Vue
Vue.mixin(
beforeCreate ()
if (this.$options.router)
_Vue.prototype.$router = this.$options.router
this.$options.router.init()
)
constructor (options)
this.options = options
this.routeMap =
this.data = _Vue.observable(
current: '/'
)
init ()
this.createRouteMap()
this.initComponents(_Vue)
this.initEvent()
createRouteMap ()
this.options.routes.forEach(route =>
this.routeMap[route.path] = route.component
)
initComponents (Vue)
Vue.component('router-link',
props:
to: String
,
render (h)
return h('a',
attrs:
href: this.to
,
on:
click: this.clickHandler
, [this.$slots.default])
,
methods:
clickHandler (e)
history.pushState(, '', this.to)
this.$router.data.current = this.to
e.preventDefault()
)
const self = this
Vue.component('router-view',
render (h)
const component = self.routeMap[self.data.current]
return h(component)
)
initEvent ()
window.addEventListener('popstate', () =>
this.data.current = window.location.pathname
)
let _Vue = null
export default class VueRouter
// 传入两个参数,一个是 vue 的构造函数,第二个是 可选的 选项对象
static install (Vue)
// 1. 如果插件已经安装直接返回
if (VueRouter.install.installed && _Vue === true) return
VueRouter.install.installed = true // 表示插件已安装
// 2. 把 vue 的构造函数记录到全局变量中,因为将来我们要在 VueRouter 的实例方法中使用 vue 的构造函数
_Vue = Vue
// 3. 把创建 Vue 实例时候传入的 router 对象注入到 Vue 实例上
Vue.mixin(
beforeCreate ()
if (this.$options.router)
_Vue.prototype.$router = this.$options.router
this.$options.router.init()
)
constructor (options)
// 记录构造函数中传入的选项 options
this.options = options
// 把 options 中传入的 routes 也就是路由规则解析出来,键值对的形式,键---路由地址,值---路由组件
this.routeMap =
// data 是一个响应式对象,因为 data 中存储的是当前路由地址,路由变化时要自动加载组件
this.data = _Vue.observable( // observable 创建响应式对象
current: '/' // 存储当前路由地址,默认是 /
)
init ()
this.createRouteMap()
this.initComponents(_Vue)
this.initEvent()
// 把构造函数中传过来的 选项中的 routes 也就是路由规则转换为键值对的形式转换到routeMap对象中 键---路由地址,值---路由组件
createRouteMap ()
// 遍历所有的路由规则,以键值对的形式存储在 routeMap 对象中
this.options.routes.forEach(route =>
this.routeMap[route.path] = route.component
)
// 创建两个组件 router-link 与 router-view
initComponents (Vue) // 接收参数,减少与外界联系
Vue.component('router-link',
props:
to: String
,
/*
运行时版本的 vue 不带编译器,所以不支持组件中的 template 选项,编译器的作用就是把 template 编译成 render 函数,运行时的组件可以直接写 render 函数
单文件组件时一直使用 template 没写 render 是因为在打包的过程中把单文件的 template 编译成 render 函数了,这叫做预编译
*/
render (h) // 该函数接收一个参数,通常叫做 h 作用是帮我们创建虚拟 DOM,h 由 vue 传递
return h('a', // h 函数接受 3 个参数,1. 创建这个元素的选择器。2. 给标签设置一些属性,如果是 DOM 对象的属性,需要添加到 attrs 中。3. 生成的元素的子元素,所以是数组形式
attrs:
href: this.to
,
on:
click: this.clickHandler
, [this.$slots.default])
,
methods:
clickHandler (e)
// 1. 通过 pushState 方法 改变浏览器地址栏,但不给服务器发请求
history.pushState(, '', this.to) // 该方法接受 3 个参数,1. data。2. title 网页的标题。3. url 地址
this.$router.data.current = this.to
e.preventDefault()
// template: '<a :href="to"><slot></slot></a>'
)
const self = this
Vue.component('router-view',
// 获取到当前路由地址对应的路由组件
render (h)
const component = self.routeMap[self.data.current]
return h(component)
)
initEvent ()
window.addEventListener('popstate', () =>
this.data.current = window.location.pathname
)
以上是关于10分钟教你 手写 Vue-Router的主要内容,如果未能解决你的问题,请参考以下文章