实现简单的vue-router
1.使用vue-cli创建一个配置Router,且是history模式的项目,可以看到使用vue-router的相关代码:
// route-demo/src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
const router = new VueRouter({
mode: 'history',
routes
})
export default router
// route-demo/src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
router,
render: h => h(App)
}).$mount('#app')
2.Vue.use(VueRouter)
在Vue源码中可以看到,它的的use方法会调用传入对象的install方法,并把Vue的构造函数传递给install方法,plugin.install(app, ...options)
调用的方式说明install是个静态方法
// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
_instance: null,
version,
get config() {
return context.config
},
set config(v) {
if (__DEV__) {
warn(
`app.config cannot be replaced. Modify individual options instead.`
)
}
},
use(plugin: Plugin, ...options: any[]) {
if (plugin && isFunction(plugin.install)) {
installedPlugins.add(plugin)
plugin.install(app, ...options)
} else if (isFunction(plugin)) {
installedPlugins.add(plugin)
plugin(app, ...options)
}
return app
},
}
在src下新建个vuerouter文件夹,然后新建index.js文件, 先实现install方法:
// route-demo/src/vuerouter/index.js
let _Vue = null
export default class VueRouter {
static install(Vue) {
// 判断插件是否已经安装,因为方法或函数本身也是一个对象,所以我们可以在VueRouter.install定义一个属性installed,表示是否已经安装
if (VueRouter.install.installed) return
VueRouter.install.installed = true
// 全局变量保存Vue构造函数,后面要用
_Vue = Vue
}
}
在实际代码中,我们会经常使用$router访问router对象,可以在install方法中把创建Vue实例时传入的router对象注入到Vue实例,获取Vue实例的可以使用混入beforeCreate方法,在beforeCreate方法中可以拿到传入的router对象,混入后组件和实例都会执行该赋值,我们只需要赋值一次,所以判断一下是否存在options上是否存在router对象,存在说明是Vue实例
// route-demo/src/vuerouter/index.js
let _Vue = null
export default class VueRouter {
static install(Vue) {
// 判断插件是否已经安装,因为方法或函数本身也是一个对象,所以我们可以在VueRouter.install定义一个属性installed,表示是否已经安装
if (VueRouter.install.installed) return
VueRouter.install.installed = true
// 全局变量保存Vue构造函数,后面要用
_Vue = Vue
// 将创建Vue时传入的router对象,注入到Vue实例
_Vue.mixin({
beforeCreate() {
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router
}
}
})
}
}
3.接着看到以下代码:
// route-demo/src/router/index.js
const router = new VueRouter({
mode: 'history',
routes
})
查看VueRouter的实现,可以看到
export declare class VueRouter {
constructor(options?: RouterOptions)
app: Vue
/**
* Original options object passed to create the Router
*/
options: RouterOptions
/**
* Configured mode when creating the Router instance.
*/
mode: RouterMode
/**
* Current {@link Route}
*/
currentRoute: Route
// ... 一些操作路由的方法以及路由守卫,先不关注
}
实现VueRouter的构造方法:
// route-demo/src/vuerouter/index.js
let _Vue = null
export default class VueRouter {
static install(Vue) {
// 判断插件是否已经安装,因为方法或函数本身也是一个对象,所以我们可以在VueRouter.install定义一个属性installed,表示是否已经安装
if (VueRouter.install.installed) return
VueRouter.install.installed = true
// 全局变量保存Vue构造函数,后面要用
_Vue = Vue
// 将创建Vue时传入的router对象,注入到Vue实例
_Vue.mixin({
beforeCreate() {
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router
}
}
})
}
constructor (options) {
// 记录参数对象
this.options = options
// 记录路径和组件的对应关系
this.routeMap = {}
// 定义为响应式变量
this.data = _Vue.observable({
// 当前访问路径, 默认是'/'
current: '/'
})
}
}
4.定义一个createRouteMap方法,解析传入的路由规则routes,并赋值给routeMap:
// route-demo/src/vuerouter/index.js
let _Vue = null
export default class VueRouter {
static install(Vue) {
// 判断插件是否已经安装,因为方法或函数本身也是一个对象,所以我们可以在VueRouter.install定义一个属性installed,表示是否已经安装
if (VueRouter.install.installed) return
VueRouter.install.installed = true
// 全局变量保存Vue构造函数,后面要用
_Vue = Vue
// 将创建Vue时传入的router对象,注入到Vue实例
_Vue.mixin({
beforeCreate() {
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router
}
}
})
}
constructor (options) {
// 记录参数对象
this.options = options
// 记录路径和组件的对应关系
this.routeMap = {}
// 定义为响应式对象
this.data = _Vue.observable({
// 当前访问路径, 默认是'/'
current: '/'
})
}
createRouteMap () {
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
})
}
}
5.初始化组件方法initComponents方法,这里只实现router-view和router-link, 完了以后在beforeCreate方法中调用两个初始化方法:
// route-demo/src/vuerouter/index.js
let _Vue = null
export default class VueRouter {
static install(Vue) {
// 判断插件是否已经安装,因为方法或函数本身也是一个对象,所以我们可以在VueRouter.install定义一个属性installed,表示是否已经安装
if (VueRouter.install.installed) return
VueRouter.install.installed = true
// 全局变量保存Vue构造函数,后面要用
_Vue = Vue
// 将创建Vue时传入的router对象,注入到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)
}
createRouteMap () {
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
})
}
initComponents (Vue) {
Vue.component('router-link', {
props: {
to: String
},
render(h) {
// h函数生成虚拟DOM,第一个参数是元素选择器,这里使用标签,
// 第二个参数是属性设置,第三个参数是子元素,这里直接取标签中间的值,使用默认插槽的方式获取
return h('a', {
attrs: {
href: this.to
}
},[this.$slots.default]
)
}
})
// 这里需要定义一个self变量保存this,指向VueRouter实例
let self = this
Vue.component('router-view', {
render(h) {
// 根据当前路径找到对应的组件,并使用h函数直接调用后返回
return h(self.routeMap[self.data.current])
}
})
}
}
更换route-demo/src/router/index.js中引用VueRouter的路径,改为自己定义的vuerouter文件
6.可以看到切换页面时,只有地址栏发生了变化,内容没有响应,现在在router-link定义时添加点击事件
let _Vue = null
export default class VueRouter {
static install(Vue) {
// 判断插件是否已经安装,因为方法或函数本身也是一个对象,所以我们可以在VueRouter.install定义一个属性installed,表示是否已经安装
if (VueRouter.install.installed) return
VueRouter.install.installed = true
// 全局变量保存Vue构造函数,后面要用
_Vue = Vue
// 将创建Vue时传入的router对象,注入到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)
}
createRouteMap () {
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
})
}
initComponents (Vue) {
Vue.component('router-link', {
props: {
to: String
},
render(h) {
// h函数生成虚拟DOM,第一个参数是元素选择器,这里使用标签,
// 第二个参数是属性设置,第三个参数是子元素,这里直接取标签中间的值,使用默认插槽的方式获取
return h('a', {
attrs: {
href: this.to
},
// 事件也属于属性
on: {
click: this.clickhandle
}
},[this.$slots.default]
)
},
methods: {
clickhandle(e) {
// 使用pushState仅改变路径栏的地址
history.pushState({}, "", this.to)
// 将当前路径变更
this.$router.data.current = this.to
// 阻止默认响应
e.preventDefault()
}
},
})
// 这里需要定义一个self变量保存this,指向VueRouter实例
let self = this
Vue.component('router-view', {
render(h) {
// 根据当前路径找到对应的组件,并使用h函数直接调用后返回
return h(self.routeMap[self.data.current])
}
})
}
}
访问页面,可以看到点击链接可以正常切换
但是浏览器前进/后退键还是不生效,这时需要监听popstate,改变current的值
// route-demo/src/vuerouter/index.js
let _Vue = null
export default class VueRouter {
static install(Vue) {
// 判断插件是否已经安装,因为方法或函数本身也是一个对象,所以我们可以在VueRouter.install定义一个属性installed,表示是否已经安装
if (VueRouter.install.installed) return
VueRouter.install.installed = true
// 全局变量保存Vue构造函数,后面要用
_Vue = Vue
// 将创建Vue时传入的router对象,注入到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) {
// h函数生成虚拟DOM,第一个参数是元素选择器,这里使用标签,
// 第二个参数是属性设置,第三个参数是子元素,这里直接取标签中间的值,使用默认插槽的方式获取
return h('a', {
attrs: {
href: this.to
},
// 事件也属于属性
on: {
click: this.clickhandle
}
},[this.$slots.default]
)
},
methods: {
clickhandle(e) {
// 使用pushState仅改变路径栏的地址
history.pushState({}, "", this.to)
// 将当前路径变更
this.$router.data.current = this.to
// 阻止默认响应
e.preventDefault()
}
},
})
// 这里需要定义一个self变量保存this,指向VueRouter实例
let self = this
Vue.component('router-view', {
render(h) {
// 根据当前路径找到对应的组件,并使用h函数直接调用后返回
return h(self.routeMap[self.data.current])
}
})
}
initEvent() {
window.addEventListener('popstate', () => {
this.data.current = window.location.pathname
})
}
}
这时,点击浏览器的前进/后退按键,内容也跟着变化。
源码见:[https://gitee.com/caicai521/vue-study/tree/master/route-demo](