手写Vue-router源码:(hash模式简易版)
- 用户使用Vue.use()时,实际执行了模块中的install方法,而install方法可以接收到Vue的实例,而此时既可以在install方法中为Vue的实例上使用mixin去扩展相应的内容
let Vue;
// 创建Vue-Router的类
class MyRouter{
static install(_Vue){
Vue = _Vue;
Vue.mixin({
beforeCreate(){
Vue.prototype.$myRouter = "来啦,小老弟!"
}
})
}
}
export default MyRouter
- 以需求带动开发,以vue-router的简单配置开始进行开发:
new MyRouter({
routes: [
{
path: "/",
component: Home,
beforEnter(from, to, next){
console.log(`beforeEnter from ${from} to ${to}`);
next();
}
},
{
path: "/about",
component: () => import(/* webpackChunkName: "about" */ "./views/about.vue")
}
]
})
- 框架单页应用的路由模式: (源码以hash模式为例)
- hash模式: 使用url#后面的锚点来区分组件,hash改变的时候,页面不会被重新加载,但是会触发onhashchange事件
- history模式: hash模式的url书写起来较丑,而使用mode:hitory模式的路由,这种路由充分利用了html5 history interface中新增的pushState()和replaceState()方法,这两个方法应用于浏览器的记录栈,在当前已有的back,forward,go基础之上,提供了对历史记录的修改功能。只是在当它们执行修改的时候,虽然会改变当前的url,但是浏览器并不会立即向服务端发送请求
(1)初始化Router默认属性配置,构建基本的contructor()
// 接收new Router传递的配置参数
constructor(options){
this.$options = options; // 缓存配置
this.routeMap = {}; // 创建路由和页面映射的map
// 使用Vue的响应式机制,来在路由切换的时候做一些相应
this.app = new Vue({
data: {
current: '/' // 当前的路由,默认为根路由
}
});
}
- 初始化启动路由,由组件的use进行启动,在启动路由时做至少三件事: 监听onhashchange, 处理映射路由表,初始化组件
// 启动整个路由,由组件use负责启动
init(){
// 1. 监听hashchange事件
this.bindEvents();
// 2. 处理路由表
this.createRouteMap();
// 3. 初始化组件: router-view, router-link
this.initComponent();
// 其他: 生命周期, 路由守卫 ... 后处理
}
同时更新之前的install方法:
static install(_Vue){
Vue = _Vue;
Vue.mixin({
beforeCreate(){
// 启动路由:
if(this.$options.router){
// 入口
this.$options.router.init()
}
}
})
}
- 简单处理init中的方法:
// 绑定事件
bindEvents(){
// 监听hashchang改变
window.addEventListener("hashchange", this.onHashChange.bind(this), false)
// 监听初始化页面
window.addEventListener("load", this.onHashChange.bind(this), false)
}
onHashChange(e){
console.log("hash改变", e)
}
// 处理路由表:
createRouteMap(){
// 根据路由做路由和配置之间的映射
this.$options.routes.forEach(item => {
this.routeMap[item.path] = item;
})
}
// 注册组件, 这里先仅仅注册router-view组件:
initComponent(){
// 注册router-view组件
Vue.component('router-view', {
// 通过调用vue的render方法去渲染一个虚拟DOM组件
render: h => {
const component = this.routeMap[this.app.current].component;
return h(component)
}
})
}
- 处理hashchange监听,例用Vue的响应式机制完成基本的hash路由切换,此时基本完成最小执行版本
// 获取当前的hash值
getHash(){
return window.location.hash.slice(1) || '/'
}
onHashChange(){
// 1. 获取当前的hash值
let hash = this.getHash();
// 2. 修改this.app.current, 利用了Vue的响应式机制
this.app.current = hash;
}
- 实现router-link组件: 这里不采用template模板的形式,而是调用render函数,写为虚拟DOM形式构建组件:
Vue.component('router-link', {
props: {
to: String
},
render(h) {
return h('a', {
attrs: {
href: '#' + this.to // 转为hash路由
},
// 默认的组件插槽内容,取出作为动态虚拟DOM组件的子组件, 这里的this.$slots为当前组件内部的插槽内容
}, [this.$slots.default]
)
}
})
- 实现vue-router的路由的生命周期,实现beforeEnter生命周期事件,其他的生命周期不在实现,内容基本相似,只是执行时机的不同
// 获取当前跳转的hash路由和跳转到的hash路由
getFromAndTo(e){
let from, to;
if(e.newURL){
// 这是一个hashChange的事件
from = e.oldURL.split('#')[1];
to = e.newURL.split("#")[1];
}else{
// 这是第一次加载页面load触发的事件
from = "";
to = this.getHash();
}
return {from, to}
}
// 简单修改hashChange事件
onHashChange(e){
// 1. 获取当前的hash值
let hash = this.getHash();
// 路由跳转开始,执行第一个生命周期
let router = this.routeMap[hash];
let {from, to} = this.getFromAndTo(e);
if(router.beforeEnter){
// 存在beforeEnter生命周期,则执行第一个声明周期
router.beforeEnter(from, to, () => {
this.app.current = hash;
})
}else{
// 2. 修改this.app.current, 利用了Vue的响应式机制
this.app.current = hash;
}
}
- 完整代码: (这个是一个极简版本,关于完整的核心代码大家可以去github查看vue-router源码)
let Vue;
class MyRouter{
static install(_Vue){
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.app = new Vue({
data: {
current: '/'
}
});
}
init(){
this.bindEvents();
this.createRouteMap();
this.initComponent();
}
bindEvents(){
window.addEventListener("hashchange", this.onHashChange.bind(this), false)
window.addEventListener("load", this.onHashChange.bind(this), false)
}
getHash(){
return window.location.hash.slice(1) || '/'
}
getFromAndTo(e){
let from, to;
if(e.newURL){
from = e.oldURL.split('#')[1];
to = e.newURL.split("#")[1];
}else{
from = "";
to = this.getHash();
}
return {from, to}
}
onHashChange(e){
let hash = this.getHash();
let router = this.routeMap[hash];
let {from, to} = this.getFromAndTo(e);
if(router.beforeEnter){
router.beforeEnter(from, to, () => {
this.app.current = hash;
})
}else{
this.app.current = hash;
}
}
createRouteMap(){
this.$options.routes.forEach(item => {
this.routeMap[item.path] = item;
})
}
initComponent(){
Vue.component('router-view', {
render: h => {
const component = this.routeMap[this.app.current].component;
return h(component)
}
})
Vue.component('router-link', {
props: {
to: String
},
render(h) {
return h('a', {
attrs: {
href: '#' + this.to
},
}, [this.$slots.default]
)
}
})
}
push(url){
// hash模式,直接赋值,如果时history模式,使用pushState
window.location.hash = url;
}
}
export default MyRouter