VueRouter 使用
先让我们使用 vueCli 搭建一个简单的项目,我们选择 vue2 进行学习,快速看一下 VueRouter 的用法。
npm install -g @vue/cli // 安装 vueCli
vue create hello-world // 创建项目,创建过程中记得选择 VueRouter
npm run serve // 启动项目
在创建出来的项目中,用到 VueRouter 的地方就以下几处:
- 在 router/index.js 中使用 Vue.use 注册 VueRouter 插件
- 在 router/index.js 中书写路由规则 routes,并传入构造函数创建 VueRouter 实例
- 在 main.js 中传入 vueRouter 实例到 Vue 构造函数
- 在 App.vue 中使用 router-link 和 router-view 组件生成页面
再看看最终的效果:
切换 router-link 时,router-view 对应的组件改变,且浏览器路由地址改变。同时,如果改变浏览器地址会后退,也会改变页面。
模拟 vueRouter
我们要按照上面的 history 模式模拟一个简易的 vueRouter 插件,满足简单的使用需求。
首先,我们根据 vueRouter 的使用,一步一步来分析:
1. 使用 Vue.use 注册 VueRouter 插件
首先。了解一下 Vue.use,它接收一个函数或者对象,如果是一个方法则直接调用,如果是一个对象则调用其 install 方法。install 方法接收 2 个参数,一个是 Vue 的构造函数,一个是可选的选项对象(这里不做设置)。
所以从这句话来看,我们可以将 VueRouter 看做一个有 install 静态方法的类。
class VueRouter {
static install(Vue) {
}
}
我们再想一想作为一个插件,它只需注册一次,不会重复注册的。那我们不妨在 install 方法上添加一个属性表明此组件是否被注册过。
class VueRouter {
static install(Vue) {
if (VueRouter.install.installed) return
VueRouter.install.installed = true
}
}
2. 书写路由规则 routes,并传入构造函数创建 VueRouter 实例
我们可以简单定义一下 VueRouter 的构造函数,参数是包含 routes 路由规则的参数 options。对于 VueRouter 实例,它应该要存储传入的 options ,且它需要一个表示当前路由地址的属性,且这个属性必须是响应式的,因为需要监听当前路由地址的变化,从而使对应的组件也自动更新。默认当前路由地址为 '/'。
我们可以使用 Vue 提供的 observable 方法将传入的对象转换为一个响应式对象。这里要用到 Vue 上提供的方法,所以在 install 中接收 Vue 的时候,应该存为全局变量便于使用。
let _Vue = null
class VueRouter {
static install(Vue) {
if (VueRouter.install.installed) return
VueRouter.install.installed = true
_Vue = Vue
}
constructor(options) {
this.options = options
this.data = _Vue.observable({ current: '/' })
}
}
3. 传入 vueRouter 实例到 Vue 构造函数
创建 Vue 根实例时传入 router 实例,会往所有 Vue 实例上添加 $router 属性。
我们仔细分析这句话,有三个重点:
- 在 VueRouter 类中怎么写会让代码等待 Vue 创建实例的时候再执行?
这要利用 Vue 给我们提供的钩子函数,在创建 Vue 实例的时候,会执行 beforeCreate 钩子函数,我们可以使用 Vue.mixin 进行全局混入,这个会影响注册之后所有创建的 Vue 实例。
- 怎么判断是否是 Vue 根实例的创建
只有在创建根实例的时候,才会传入 router 选项。
- 怎么往所有 Vue 实例上添加 $router 属性
往 Vue 的原型上添加,那以后创建的所有 Vue 实例上就会有这个 $router 属性。
class VueRouter {
static install(Vue) {
if (VueRouter.install.installed) return
VueRouter.install.installed = true
_Vue = Vue
_Vue.mixin({
beforeCreate(){
// 这段代码写在 beforeCreate 中,this 的值指向的就是 Vue 实例,那么 this.$options.router 就可以取到传入的 router 选项。
if(this.$options.router){
_Vue.prototype.$router = this.$options.router
}
}
})
}
constructor(options) {
this.options = options
this.data = _Vue.observable({ current: '/' })
}
}
4. 使用 router-link 和 router-view 组件生成页面
在 vueRouter 构造函数的最后,可以声明一个 initComponent 函数专门用于初始化 router-link 和 router-view 组件:
class VueRouter {
constructor(options) {
...
this.initComponent()
}
initComponent() {
_Vue.component("router-link", ...)
_Vue.component("router-view", ...)
}
}
router-link 的结构:<router-link to="/">Index</router-link>
- 最终要渲染成一个超链接 --
<a> - 接收一个字符串类型的参数 to 作为超链接的地址 --
props - 将标签之间的内容作为超链接的内容渲染出来 --
<slot>
_Vue.component("router-link", {
props: { to: String },
template: "<a :href='to'><slot></slot></a>"
})
router-view 则根据 当前路由地址 在 路由规则 中找到对应显示组件的:
const route = this.options.routes.find(i => this.data.current === i.path)
_Vue.component("router-view", route.component)
替换 vueRouter
目前看起来我们已经完成了 vueRouter 的模拟,现在让我们将我们刚创建的 vue 项目中的 vueRouter 替换为我们模拟的代码:
// hello-world\src\router\index.js 文件
// import VueRouter from 'vue-router'
import VueRouter from './my-router'
为了成功运行,我们还需要将在 vue.config.js 文件中添加配置:
module.exports = defineConfig({
runtimeCompiler: true
})
这是由于我们模拟的 vueRouter 中注册组件时使用了 template,而 runtime-only 版本的 vue 是不包含编译 template 的模块的,所以需要修改配置使用完整版本的 vue。
但是当我们替换之后,会发现,当我们点击 router-link 试图切换 router-view 时,只是改变了浏览器的路由地址,并触发了整个页面的刷新重新向服务器发送请求,而 router-view 并没有改变。这是由于 router-link 暂时只是一个链接,我们需要根据 history 和 hash 模式做出不同的响应。
history 模式
history 模式是基于 HTML5 的 History API。
当我们点击 router-link 超链接时,实际就调用了 pushState 这个 API,这个方法会改变浏览器地址栏中的地址,并将这个地址保存到浏览记录中,但不会发送请求,所以这一系列的操作都是在客户端完成的。
- 阻止 a 链接的默认行为
- 调用 pushState 改变浏览器地址栏中的地址,pushState 接收三个参数,分别是 data, title(网页的标题), url(要转跳的路径),暂时只设置 url 即可。
- 修改 current 的值,同步更新 router-view 组件。
initComponent() {
const self = this
_Vue.component("router-link", {
props: { to: String },
template: "<a href='' @click='handleClick'><slot></slot></a>",
methods: {
handleClick(e) {
e.preventDefault()
history.pushState({}, "", this.to)
self.data.current = this.to
}
}
})
}
但是但是哦,这样改了 router-link 之后,router-view 还是没有改变,我们再来看看 router-view 的代码:
const route = this.options.routes.find(i => this.data.current === i.path)
_Vue.component("router-view", route.component)
我们在 vueRouter 的构造函数中直接取到了当前的地址 '/',由此取到了 HomeView,当我们改变 current 时,并没有触发这个组件的改变。我想了一些办法除了使用 render 函数,也没有想到其他好的方法可以当 current 这个响应式数据变化时,同时引起页面的改变:
_Vue.component("router-view", {
// render渲染函数接收一个 createElement 方法作为第一个参数用来创建 VNode。
render(h) {
const route = self.options.routes.find(i => self.data.current === i.path)
return h(route.component)
}
})
createElement 接收一个组件作为参数时,会创建出组件的 vnode。在 render 中使用了 current 这个响应式数据,当其变化的时候,会重新执行 render 函数,从而达到切换 router-view 对应组件的作用。
当当当!history 下的路由组件已经处理好啦,切换 router-link 改变 router-view ,以下是全部代码:
initComponent() {
const self = this
_Vue.component("router-link", {
props: { to: String },
template: "<a href='' @click='handleClick'><slot></slot></a>",
methods: {
handleClick(e) {
e.preventDefault()
history.pushState({}, "", this.to)
self.data.current = this.to
}
}
})
_Vue.component("router-view", {
render(h) {
const route = self.options.routes.find(i => self.data.current === i.path)
return h(route.component)
}
})
}
但是,当我们点击浏览器的前进后退按钮的时候,浏览器地址变化了,但是 router-view 的内容却没有变化,这就需要我们在 vueRouter 的构造函数中添加一个 initEvent 方法用于监听 popState 事件。
initEvent() {
window.addEventListener("popstate", () => {
this.data.current = window.location.pathname
})
}
完整代码:
let _Vue = null
export default class VueRouter {
static install(Vue) {
if (VueRouter.install.installed) return
VueRouter.install.installed = true
_Vue = Vue
_Vue.mixin({
beforeCreate(){
if(this.$options.router){
_Vue.prototype.$router = this.$options.router
}
}
})
}
constructor(options) {
this.options = options
this.data = _Vue.observable({ current: '/' })
this.initComponent()
this.initEvent()
}
initComponent() {
const self = this
_Vue.component("router-link", {
props: { to: String },
template: "<a href='' @click='handleClick'><slot></slot></a>",
methods: {
handleClick(e) {
e.preventDefault()
history.pushState({}, "", this.to)
self.data.current = this.to
}
}
})
_Vue.component("router-view", {
render(h) {
const route = self.options.routes.find(i => self.data.current === i.path)
return h(route.component)
}
})
}
initEvent() {
window.addEventListener("popstate", () => {
this.data.current = window.location.pathname
})
}
}
history 模式: 通过 history.pushState 修改浏览器地址,触发更新; 通过监听 popstate 事件监听浏览器前进或后退,触发更新。
hash 模式
到目前,我们已经简单模拟了 vueRouter 在 history 模式下的代码,且可以正常进行使用。那接下来我们替换成 hash 模式,看下 hash 模式的实现原理:
hash 模式:通过 location.hash 修改 hash ,触发更新; 通过监听 hashchange 事件监听浏览器前进或后退,触发更新。
let _Vue = null
export default class VueRouter {
static install(Vue) {
if (VueRouter.install.installed) return
VueRouter.install.installed = true
_Vue = Vue
_Vue.mixin({
beforeCreate(){
if(this.$options.router){
_Vue.prototype.$router = this.$options.router
}
}
})
}
constructor(options) {
this.options = options
this.data = _Vue.observable({ current: '/' })
this.initComponent()
this.initEvent()
}
initComponent() {
const self = this
_Vue.component("router-link", {
props: { to: String },
template: "<a href='' @click='handleClick'><slot></slot></a>",
methods: {
handleClick(e) {
e.preventDefault()
if (self.options.mode === 'history') {
history.pushState({}, "", this.to)
} else {
window.location.hash = this.to
}
self.data.current = this.to
}
}
})
_Vue.component("router-view", {
render(h) {
const route = self.options.routes.find(i => self.data.current === i.path)
return h(route.component)
}
})
}
initEvent() {
if (this.options.mode === 'history') {
window.addEventListener('popstate', () => {
this.data.current = window.location.pathname
})
} else {
window.addEventListener('hashchange', () => {
this.data.current = window.location.hash.substring(1)
})
}
}
}