Vue-Router原理实现
1、内容提要
- Vue Router基础回顾(通过Vue Router的基本使用方式来分析他的实现)
- Hash 模式和 History 模式(History模式要结合服务器一起使用)
- 模拟实现自己的 Vue Router
2、Vue Router 基础回顾
router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Index from '../views/Index.vue'
// 1. 注册路由插件
// Vue.use 用来注册插件,他里面需要接收一个参数,
// 如果这个参数是函数的话,Vue.use内部会直接调用这个函数来注册组件,
// 如果传入对象的话,Vue.use会调用传入对象的install方法来注册插件
Vue.use(VueRouter)
// 路由规则
const routes = [
{
path: '/',
name: 'Index',
component: Index
},
{
path: '/blog',
name: 'Blog',
// 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: "blog" */ '../views/Blog.vue')
},
{
path: '/photo',
name: 'Photo',
// 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: "photo" */ '../views/Photo.vue')
}
]
// 2. 创建 router 对象
const router = new VueRouter({
routes
})
export default router
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
// 3. 注册 router 对象,在创建vue实例的时候,如果配置了router选项,
// 他会给vue实例注入两个属性,一个是$route,路由规则以及$router路由对象,
// 我们通过路由对象可以调用一些相应的方法,比如push,back,go,等
router,
render: h => h(App)
}).$mount('#app')
App.vue
<template>
<div id="app">
<div>
<img src="@/assets/logo.png" alt="">
</div>
<div id="nav">
<!-- 5. 创建链接 -->
<router-link to="/">Index</router-link> |
<router-link to="/blog">Blog</router-link> |
<router-link to="/photo">Photo</router-link>
</div>
<!-- 4. 创建路由组件的占位,当路径匹配到一个组件后,
会把这个组件加载进来,并最终会替换掉 router-view 所处位置 -->
<router-view/>
</div>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
}
#nav a {
font-weight: bold;
color: #2c3e50;
}
#nav a.router-link-exact-active {
color: #42b983;
}
</style>
总结:
- 首先要去创建一些和路由相关的组件,也就是我们的视图。
- 注册路由插件,调用Vue.use去注册vue-router。
- 创建一个router对象,在创建路由对象的时候,我们要去配置一些路由规则。
- 注册router对象,也就是我们在创建vue实例的时候要在选项里配置我们创建好的router对象。
- 通过router-view来设置占位,当我们路径匹配成功之后,会把匹配到的组件替换掉router-view的位置。
- 最后,我们通过router-link来创建一些链接。
3、动态路由
动态路由传参的两种方式:
router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Index from '../views/Index.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Index',
component: Index
},
{
// 路径中携带参数,:id相当于占位符,将来真正在使用时会传入对应id
path: '/detail/:id',
name: 'Detail',
// 开启 props,会把 URL 中的参数传递给组件
// 在组件中通过 props 来接收 URL 参数
props: true,
// 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: "detail" */ '../views/Detail.vue')
}
]
const router = new VueRouter({
routes
})
export default router
views/Detail.vue
<template>
<div>
<!-- 动态路由传参方式1:通过当前路由规则,获取数据,但这种方式不太好,
会让当前组件强依赖与路由,即在使用该组件时必须有路由传递相应参数。 -->
通过当前路由规则获取:{{ $route.params.id }}
<br>
<!-- 动态路由传参方式2:路由规则中开启 props 传参,与父子组件传值的方式一样,
不依赖与路由规则,所以该组件可以使用在任何地方,只需要传递一个id参数即可。推荐使用 -->
通过开启 props 获取:{{ id }}
</div>
</template>
<script>
export default {
name: 'Detail',
props: ['id']
}
</script>
<style>
</style>
4、嵌套路由
当多个路由组件都有相同内容时,我们可以把这些相同的内容提取到一个公共的组件中。
在上图中,首页和详情页都有相同的头和尾,所以我们可以提取一个新组件layout,然后把头和尾放到layout中。而对于变化的位置,我们放一个router-view去占位,将来若我们去访问首页的地址的时候,他会把我们layout组件和首页的组件合并输出。
首页代码:views/Index.vue
<template>
<div>
这里是首页 <br>
<router-link to="login">登录</router-link> |
<router-link to="detail/5">详情</router-link>
</div>
</template>
<script>
export default {
name: 'Index'
}
</script>
<style>
</style>
详情页代码:views/Detail.vue
<template>
<div>
<!-- 方式1: 通过当前路由规则,获取数据 -->
通过当前路由规则获取:{{ $route.params.id }}
<br>
<!-- 方式2:路由规则中开启 props 传参 -->
通过开启 props 获取:{{ id }}
</div>
</template>
<script>
export default {
name: 'Detail',
props: ['id']
}
</script>
<style>
</style>
登录页代码:Login.vue
<template>
<div>
这是登录页面
<br>
<router-link to="/">首页</router-link>
</div>
</template>
<script>
export default {
name: 'Login'
}
</script>
<style scoped>
</style>
Layout代码:components/Layout.vue
<template>
<div>
<div>
<img width="25%" src="@/assets/logo.png">
</div>
<div>
<router-view></router-view>
</div>
<div>
Footer
</div>
</div>
</template>
<script>
export default {
name: 'layout'
}
</script>
<style scoped>
</style>
上面的首页和详情页与之前的代码是一样的,但现在还有一个要求就是首页和详情页都需要有相同的头和尾。所以我们把相同的头和尾提取到了components/Layout.vue中,在Layout.vue中用一张图片作为头,用内容为Footer的div标签来作为尾。除了这些相同的部分之外,我们的首页和详情页也有自己独特的内容,这些都用router-view去占位,将来运行的时候,首先会去加载Layout组件然后再去加载首页或详情页,把我们首页或详情页对应的组件替换掉router-view的位置。而Login.vue组件是不需要有相同的头和尾,所以不需要使用嵌套路由。
接下来再看看路由是如何配置的:
router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
// 加载组件
import Layout from '@/components/Layout.vue'
import Index from '@/views/Index.vue'
import Login from '@/views/Login.vue'
Vue.use(VueRouter)
const routes = [
{
name: 'login',
path: '/login',
component: Login
},
// 嵌套路由
{
path: '/',
component: Layout,
// 嵌套路由在使用的时候,会把我们外部路径和我们children里配置的路由规则中的路径进行合并,
// 然后再分别去加载我们的Layout组件,然后再加载我们对应的组件。
children: [
{
name: 'index',
// children里面的path在配置的时候可以是相对路径也可以是绝对路径,像这里的空字符串就是相对路径
// 这里用绝对路径可以是'/'
path: '',
component: Index
},
{
name: 'detail',
// 这里用绝对路径可以是'/detail/:id'
path: 'detail/:id',
props: true,
component: () => import('@/views/Detail.vue')
}
]
}
]
const router = new VueRouter({
routes
})
export default router
在router/index.js中,代码和之前都基本一样,我们主要来看一下路由规则是如何配置的。
在路由规则里我们首先配置的是登录页面,当访问登录页面的时候,直接去加载我们对应的Login.vue组件。
接下来是加载首页和详情页,因为我们的首页和详情页都有相同的头和尾,而我们的头和尾都提取到了Layout.vue组件中来,即我们使用了嵌套路由。
而嵌套路由在使用的时候,会把我们外部路径和我们children里配置的路由规则中的路径进行合并,然后再分别去加载我们的Layout组件,然后再加载我们对应的组件,把他们合并到一起。当我们外层的path是根路径的话,children里面首页的path可以写成空字符串,另外,children里面的path在配置的时候可以是相对路径也可以是绝对路径,像首页里的空字符串就是相对路径。使用相对路径时,当我们访问根的时候,会先加载layout,再去加载index,然后合并到一起,渲染出来。接下来再看详情页的配置,我们外面的path是斜杆,detail里配置的是相对路径,我们详情页的路径其实就是外部path拼接里面的path。
5、编程式导航
我们在页面跳转的时候,使用的是router-link超链接,但我们做登录页面的时候,需要点击按钮跳转到首页,这时候就需要使用编程式导航,调用$router.push方法。下面我们一起回顾一下编程式导航常用的一些方法。
views/Login.vue
<template>
<div>
用户名:<input type="text" /><br />
密 码:<input type="password" /><br />
<button @click="push"> push </button>
</div>
</template>
<script>
export default {
name: 'Login',
methods: {
push () {
// 调用$router.push的时候,参数可以接收两种方式,
// 第一种方式是字符串,即我们需要跳转的路由地址
this.$router.push('/')
// 第二种方式是传对象,需要设置路由的name,
// name是由router/index.js中的name定义的,称为命名式导航
// this.$router.push({ name: 'Home' })
}
}
}
</script>
<style>
</style>
router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Index from '../views/Index.vue'
Vue.use(VueRouter)
const routes = [
{
// 在router/index.js的路由选项中,除了设置path以外,还起了一个name,称为命名式导航
path: '/',
name: 'Index',
component: Index
},
{
path: '/login',
name: 'Login',
// 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/Login.vue')
},
{
path: '/detail/:id',
name: 'Detail',
props: true,
component: () => import(/* webpackChunkName: "detail" */ '../views/Detail.vue')
}
]
const router = new VueRouter({
routes
})
export default router
views/Index.vue
<template>
<div class="home">
<div id="nav">
<router-link to="/">Index</router-link>
</div>
<button @click="replace"> replace </button>
<button @click="goDetail"> Detail </button>
</div>
</template>
<script>
export default {
name: 'Index',
methods: {
replace () {
// replace方法和push方法有些类似,都可以跳转到我们指定的路径,参数形式也是一样的
// 不过replace方法不会记录本次历史,而会把我们当前的历史改变成/login
this.$router.replace('/login')
},
goDetail () {
// 在push中通过params来指定路由参数
this.$router.push({ name: 'Detail', params: { id: 1 } })
}
}
}
</script>
views/Detail.vue
<template>
<div>
路由参数:{{ id }}
<button @click="go"> go(-2) </button>
</div>
</template>
<script>
export default {
name: 'Detail',
props: ['id'],
methods: {
go () {
// go是跳转到历史中的某一次,可以是负数,即后退,如果是-1的话,和$router.back的效果是一样的
this.$router.go(-2)
}
}
}
</script>
<style>
</style>
6、Hash 模式和 History 模式的区别
首先,我们要强调不管是Hash模式还是History模式,都是客户端路由的实现方式,也即当路由发生变化时,不会向服务器发送请求,而是用JS接收路径的变化,然后根据不同的地址,渲染不同的内容。如果需要服务器端内容的话,会发送AJAX请求来获取。
表现形式的区别
- Hash模式:https://music.163.com/#/playlist?id=3102961863
- History模式:https://music.163.com/playlist/3102961863
注意用History模式还需服务端配置。
原理的区别
- Hash模式是基于锚点,以及onhashchange事件,通过锚点的值作为路由地址,当地址变化后触发onhashchange事件,然后根据路径决定页面呈现的内容。
- History模式是基于HTML5的History API
- history.pushState() IE 10以后才支持(与history.push的区别是push会改变路径并向服务器发送请求,而pushState只改变路径并记录到历史记录中,并不会发送请求「所有事情都在客户端完成」)
- history.replaceState
开启History模式
const router = new VueRouter({
// mode: 'hash',
mode: 'history',
routes
})
7、History模式
- History模式需要服务器的支持
- 在单页应用中,服务端不存在 http://www.testurl.com/login 这样的地址,会返回找不到该页面。
- 在服务器端应该除了静态资源外都返回单页应用的 index.html
先来看以下代码:
views/404.vue,当用户访问不存在的路由地址的时候,就会输出该组件的内容。
<template>
<div>
您要查看的页面不存在
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>
App.vue,在App.vue中放了三个链接,前两个路由地址是存在的,当访问/video地址时,因没有配置该路由地址,所以会输出views/404.vue组件中的内容。
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link> |
<router-link to="/video">Video</router-link>
</div>
<router-view/>
</div>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
}
#nav a {
font-weight: bold;
color: #2c3e50;
}
#nav a.router-link-exact-active {
color: #42b983;
}
</style>
router/index.js,路由的配置和之前大致是一样的,首先配置了首页的路由,然后又配置了/about页面的路由,但没有/video的路由地址,如果我们请求的路径不匹配这两个路由地址的话,那此时就会输出404页面。
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')
},
{
path: '*',
name: '404',
component: () => import(/* webpackChunkName: "404" */ '../views/404.vue')
}
]
const router = new VueRouter({
// 当创建VueRouter的时候,配置了mode选项,把他的值设置为history。
// 因为默认情况下,他是hash模式,我们想要演示history模式就需要将mode改为'history'
mode: 'history',
routes
})
export default router
注意Vue-cli自带的web服务器已经配置好了对history模式的支持,所以我们使用vue-cli自带的web服务器时不会出现因服务器没配置history模式兼容而导致找不到文件的问题。所以我们还需要对项目进行打包到Node服务器或Nginx服务器上来。
8、History 模式 - Node.js
node代码
const path = require('path')
// 导入处理 history 模式的模块
const history = require('connect-history-api-fallback')
// 导入 express
const express = require('express')
const app = express()
// 注册处理 history 模式的中间件
app.use(history())
// 处理静态资源的中间件,网站根目录 ../web
app.use(express.static(path.join(__dirname, '../web')))
// 开启服务器,端口是 3000
app.listen(3000, () => {
console.log('服务器开启,端口:3000')
})
将网站源代码打包后放在node代码文件所在文件夹上一层的web文件夹下,然后运行node文件。
在上面的代码中app.use(history())
这行代码就是注册处理 history 模式的中间件,让服务端开启history模式。当我们去掉这一行并重新开启node服务器时,每次刷新页面,都会向服务端发起对应的请求,若服务端中没有相应的页面则会出错。而当我们开启服务端history模式支持的时候,我们每次刷新浏览器向服务器发送请求时,因我们开启了history模式的支持,服务器便会判断当前请求的页面服务器上有没有,如果没有,则会将我们单页应用默认的首页index.html返回给浏览器。浏览器接收到页面后会再去判断路由地址,加载对应组件内容并渲染到页面中。
9、History 模式 - nginx
Nginx服务器配置history模式步骤:
从官网下载 Nginx 的压缩包
把压缩包解压到 c 盘根目录,c:\nginx-1.23.1 文件夹
修改nginx文件夹下的 conf\nginx.conf 文件:
location / {
#指定当前网站所在根目录
root html;
#指定当前网站默认的首页
index index.html index.htm;
#新添加内容
#尝试读取$uri(当前请求的路径),如果读取不到读取$uri/这个文件夹下的首页
#如果都获取不到返回根目录中的 index.html
try_files $uri $uri/ /index.html;
}
- 将打包好的前端项目拷贝到nginx文件夹下的html文件夹中(直接替换index.html)
- 打开命令行,切换到 c:\nginx-1.23.1 文件夹。输入
start nginx
启动Nginx。注意Nginx会在后台启动,不会阻塞命令行运行。 - 补充命令:
nginx -s reload
:重启Nginx(当修改了配置文件,则需要重启Nginx。)。nginx -s stop
:停止Nginx
10、VueRouter 实现原理
复习完 Vue-Router 的基本使用后,接下来我们自己模拟一个 Vue Router,通过模拟 Vue Router 了解它的实现原理。(模拟中使用history模式)
Vue 前置知识:
- 插件
- 混入
- Vue.observable()
- 插槽
- render 函数
- 运行时和完整版的 Vue
回顾Vue-router的实现原理:Vue-router是前端路由,当路径切换的时候,在浏览器端判断当前路径,并加载当前路径对应的组件。
回顾 Hash 模式:
- 把URL中 # 后面的内容作为路径地址,可以直接通过location.url来切换浏览器中的地址,如果只改变了#后的内容,浏览器不会向服务器请求这个地址,但会记录到浏览器的访问历史中。
- 监听 hashchange 事件,当hash发生改变后,会触发hashchange事件,在hashchange事件中,我们记录当前路由地址,并找到该路径对应的组件重新渲染。
回顾 History 模式:
- History模式的路径就是一个普通的URL,我们通过history.pushState()方法改变地址栏,并把当前地址记录到浏览器的访问历史中,不会真正跳转到指定路径(浏览器不会向服务器发送请求)。
- 通过监听 popstate 事件,可以监听到浏览器历史操作的变化,在popstate的事件处理函数中可以记录改变后的地址。需要注意的是,当调用pushState或replaceState的时候,并不会触发popstate事件。当点击浏览器的前进,后退按钮或调用history的back或forward方法的时候该事件才会被触发。
- 当地址改变后,要根据当前的地址,找到对应的组件进行重新渲染。
11、VueRouter 模拟实现-分析
首先,我们来分析一下如何实现一个自己的VueRouter。我们先来回顾一下VueRouter是如何使用的,通过VueRouter的使用我们可以快速分析出如何去实现。
下面是VueRouter在使用时的核心代码:
// router/index.js
// 调用Vue.use()去注册插件,Vue功能的强大主要就在于他的插件机制,VueRouter、VueX等组件都是通过插件去实现的
// Vue.use方法可以传入函数或对象,如果传入函数的话,Vue.use内部会直接调用这个函数,
// 如果传入对象的话,Vue.use内部会调用传入对象的 install 方法,
// 在这里传入的是一个对象,所以后续我们要去实现一个install方法。
Vue.use(VueRouter)
// 创建路由对象,可以看出VueRouter是一个构造函数或一个类,我们可以用类去实现,
// 并且这个类也应该有一个静态的install方法,因为上面我们直接把VueRouter传给了Vue.use,类也是对象
const router = new VueRouter({
// 创建路由对象
// VueRouter的构造函数需要接收一个参数,里面是对象的形式,有路由规则,用来记录路由名称,地址和组件等信息
routes: [
{ name: 'home', path: '/', component: homeComponent }
]
})
// main.js
// 创建 Vue 实例,注册 router 对象
new Vue({
router,
render: h => h(App)
}).$mount('#app')
从上面的分析我们可以抽象出VueRouter的类图:
类图是用来描述这个类中所有成员的,有了类图,我们剩下的工作就比较简单了,就是实现这个类中的属性和方法。
这个类图中有三部分,最上面是类的名字,中间部分是类的属性,最后是类中的方法。
其中options属性的作用是记录构造函数中传入的对象。
routeMap是一个对象,用来记录我们路由地址和组件的对应关系,将来我们会把路由规则解析到routeMap中来。
data是一个对象,里面有一个属性current用来记录当前路由地址,此处设置data对象的目的是,我们需要一个响应式的对象,即data要是一个响应式的对象,因为路由地址发生变化后,对应的组件需要自动更新。我们可以调用Vue.observable方法把data对象设置成响应式对象。
再往下是我们VueRouter中的方法,“+”代表这是对外公开的方法,“_”代表这是静态的方法。可以看到install就是一个静态的方法,它用来实现Vue的插件机制,我们会首先来实现这个方法。
之后是构造函数Constructor,构造函数中帮我们初始化刚刚我们说过的属性,然后是几个初始化的方法。
init方法是用来调用下面的三个方法,这里是把不同的代码分割到不同的方法中去实现。
initEvent方法是用来注册popState事件,用来监听浏览器历史的变化。
CreateRouteMap方法是用来初始化routeMap属性的,它把构造函数中传入的路由规则转换成键值对的形式存储到routeMap中来。routeMap对象的键是路由的地址,值就是对应的组件,我们在router-view组件中会使用到routeMap。
最后是initComponents方法,他用来创建router-link和route-view这两个组件。
12、VueRouter-install
下面就让我们开始自己来实现一个简单的VueRouter吧!(注意我们实现的VueRouter只需要接收routes路由地址。)
首先我们创建VueRouter中的静态方法install,当使用Vue.use注册插件的时候会调用install。
// 先声明一个全局变量_Vue,将来保存Vue构造函数
let _Vue = null
export default class VueRouter {
// Vue.use中调用install方法的时候会传递两个参数,一个是Vue的构造函数,第二个是可选的选项对象
static install (Vue) {
// 1、如果插件已经安装直接返回
if (VueRouter.install.installed && _Vue === Vue) return
VueRouter.install.installed = true
// 2、把Vue构造函数记录到全局变量中来,因为将来我们在VueRouter实例方法中还要使用该构造函数,
// 如我们在创建router-view,router-link这两个组件的时候就要调用Vue.component去创建
_Vue = Vue
// 全局注册混入对象,并设置一个beforeCreate。
// 在beforeCreate这个钩子函数中就可以获取到Vue实例,并给其原型上注入$router属性
_Vue.mixin({
beforeCreate () {
// 注意当前混入的对象在所有Vue实例中都会有,即组件中也会存在beforeCreate,
// 将来执行时beforeCreate就会执行很多次,而我们给Vue原型上挂载$router只希望执行一次
// 所以我们还需要判断如果是组件的话就不执行,如果是Vue实例才执行。
// 只需要判断当前实例的$options是否有router即可,因为只有Vue的$options选项中才有router属性,组件中是没有的。
if (this.$options.router) {
// 3、把 router 对象注入到所有 Vue 实例上(并且所有组件也都是Vue实例)
_Vue.prototype.$router = this.$options.router
}
}
})
}
}
13、VueRouter-构造函数
在完成install后,我们再来实现VueRouter构造函数。通过之前的类图分析我们可以知道构造函数需要接收一个参数,options选项,并返回VueRouter对象。在构造函数中我们需要初始化3个属性:options、data以及routeMap,其中data是一个响应式的对象,因为data中需要存储当前的路由地址,当路由变化的时候要自动加载组件,所以data需要设置成响应式的对象。
// 构造函数需要接收一个options参数
constructor (options) {
// 初始化options,记录传入的选项
this.options = options
// routeMap对象用来存储options中的routes路由规则,其中键是路由地址,值为路由组件。
// 将来在router-view组件里会根据当前的路由地址在routeMap里找到对应组件并渲染到浏览器中来
this.routeMap = {}
// data应该是一个响应式的对象,其中有一个current属性用来记录当前我们的路由地址。
// 可以使用Vue中提供的observable方法去创建一个响应式的对象,并可以直接用在渲染函数或计算属性里。
this.data = _Vue.observable({
// 当前的默认路径
current:"/"
})
}
14、VueRouter-createRouteMap
接下来,让我们再去实现一下createRouteMap方法。实现这个方法之前先回顾一下类图中提到的内容,createRouteMap方法的作用是把我们构造函数中传过来的选项中的routes路由规则转换为键值对的形式存储到routeMap里。routeMap中的键存储的是路由地址,值就是该路由地址对应的组件,将来在路由地址发生变化的时候,我们可以很容易地根据这个地址来routeMap这个对象里找到对应组件并渲染到视图中来。
createRouteMap(){
//遍历所有的路由规则,并把路由规则解析成键值对的形式存储到routeMap中
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
});
}
15、VueRouter-router-link
本节,我们将会去实现initComponent方法,这个方法中,我们要创建两个组件,分别是router-link和router-view,我们先来实现router-link组件、
// initComponent方法接收一个参数,Vue的构造函数,通过参数传递而非直接使用之前保存的_Vue是为了减少这个方法和外部的依赖
initComponent(Vue){
// router-link组件需要接收一个字符串类型的参数to,即超链接的地址。
// 而最终渲染的内容在router-link标签之间。即将来我们要把router-link这个标签的内容渲染到a标签中去。
// 使用Vue.component去创建组件,第一个参数是组件的名称,第二个参数是组件的选项
Vue.component("router-link",{
props:{
to:String
},
template: '<a :href="to"><slot></slot></a>'
})
}
在创建完router-link组件后,我们可以测试一下,在测试时我们需要先执行createRouteMap以及initComponents这两个初始化方法。但直接调用这两个方法会有些不方便,所以我们再创建一个init方法,来帮我们把这两个方法包装起来,让外部使用更方便些。
init(){
this.createRouteMap()
// 传入Vue构造函数,即全局变量_Vue
this.initComponent(_Vue)
}
之后我们可以回到install方法中,调用init方法:
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
// 调用init方法
this.$options.router.init()
}
}
})
}
之后在初始化vue项目的./src/router/index.js
中导入我们自己的vuerouter
import Vue from 'vue'
// 导入我们自己的vuerouter
import VueRouter from '../vuerouter'
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
之后打开终端,输入npm run serve
启动服务器并打开相关地址,之后我们发现会报一个“模板编译器不可用”的错误。这是因为Vue的版本包括运行时版和完整版,运行时版本不支持template模板,需要打包的时候提前编译,把template编译成render函数。完整版包含运行时和编译器,但体积会比运行时版大10K左右,程序运行时会把模板转换成render函数,性能不如运行时版本。vue-cli创建的项目默认使用的是运行时版本的Vue,所以会出现之前的错误。下面我们会尝试使用两种方法去解决这个问题。
16、VueRouter-完整版的Vue
本节中通过使用完整版本的Vue去解决之前的问题。解决方法很简单,参考官方网站:配置参考 | Vue CLI (vuejs.org),即我们需要在项目根目录创建 vue.config.js 文件,并添加 runtimeCompiler属性:
// 项目根目录/vue.config.js
module.exports = {
runtimeCompiler: true
}
之后关闭服务器并重新输入npm run serve
启动服务器后就能正常显示内容了,并且我们也可以看到router-link组件经过渲染后也都变成了a标签了。
17、VueRouter-render
在使用完整版的Vue解决问题之后,让我们再来用另一种方式,解决之前的问题。我们知道运行时版本的Vue不带编译器,也即不支持组件中的template选项,但我们可以直接手写render函数来在运行时版本的Vue中运行。另外注意我们在使用单文件组件时,可以直接写template模板而正常工作的原因是在打包的过程中,会先经过预编译将单文件组件的template编译成render函数。
下面我们来使用render函数去解决之前的问题(记得删除之前的 vue.config.js 并重启服务器)
initComponent(Vue){
Vue.component("router-link",{
props:{
to:String
},
// render函数是渲染函数,这个函数有一个参数,一般命名为h,这个h函数的作用是帮我们创建虚拟DOM,
// render函数中调用这个h函数并把它的结果返回,这个h函数是Vue传给我们的
render(h){
// h函数的使用方法有很多,这里我们只演示其中一种,h函数可以接收三个参数,
// 第一个参数是我们要创建的元素对应的选择器,可以直接使用标签选择器。
// 第二个参数可以给标签设置一些属性,设置属性的时候,DOM对象的属性用attrs
// 第三个参数可以设置生成的元素中的子元素,所以是数组的形式
return h("a",{
attrs:{
// 这里因为我们实现的是history模式,所以不需要拼‘#’号
href: this.to
},
// 这里通过代码方式获取默认slot内容并放进数组中来(这里是a标签中的内容)
},[this.$slots.default])
},
})
}
之后我们打开浏览器,可以发现结果和之前是一样的。
18、VueRouter-router-view
接下来,我们再创建router-view组件。router-view组件相当于一个占位符,在router-view组件内部我们要根据当前路由地址获取到对应的路由组件并渲染到router-view中的位置中来。
initComponent(Vue){
// ......router-link......
// 用self去保存this来供后面获取当前路由地址使用
const self = this
// 我们还是调用Vue.component去创建组件
// 在router-view组件中,我们要获取到当前路由地址对应的组件,我们可以直接在render函数中完成
Vue.component("router-view",{
render(h){
// 在render函数内部,我们先要找到当前路由的地址,
// 再根据当前路由的地址去routeMap对象中找我们路由地址中对应的组件,
// 之后再调用h函数,帮我们把找到的组件转换成虚拟DOM直接返回。
// 这里h函数还可以直接把一个组件转成虚拟DOM。
// 注意这里的this是在render函数中,所以之前需要先用self保存能获取当前路由地址的this
const cm = self.routeMap[self.data.current]
return h(cm)
}
})
}
我们打开浏览器测试一下效果,会发现每次点击超链接时浏览器都会直接向服务器发送请求,而在现在的单页应用中我们是不希望浏览器向服务器发请求的,我们想在客户端去处理路径变化的操作。router-view虽然写完了,但想要的效果还没达到,这里明显是缺一件事情,我们可以先分析一下。
因我们每次点击超链接的时候,浏览器都会向服务器发送请求,而在单页应用中,我们不希望浏览器发送请求,所以我们需要给超链接注册一个点击事件,取消超链接后续内容的执行,不让其跳转,并且同时我们还要将地址栏中的内容改成href中的值。
我们要改地址栏中的地址,又不让浏览器去请求服务器,这时我们应当能想到一个方法,history的pushstate方法。history的pushstate方法会改变地址栏中的路径,但不会向服务器发送请求,还会把这次路径记录到历史中来,另外pushState仅仅只是帮我们把浏览器中的地址改变了,对于显示的内容还需要我们去处理。即我们还需要把当前地址记录到我们的this.data.current中来,因current属性的作用就是记录当前地址的,且this.data是响应式的数据,也即当其发生变化时,会去自动更新视图。我们通过observable创建的响应式对象,可以用在我们组件的渲染函数,也就是render函数中。
接下来,根据之前的分析,我们要去处理一下超链接,给它注册一个点击事件,阻止它默认行为的执行。
initComponent(Vue){
Vue.component("router-link",{
props:{
to:String
},
render(h){
// 之前我们提到,h函数的第二个参数是给对应的DOM对象注册属性的,
// 其实,我们也可以给DOM对象来注册事件
return h("a",{
attrs:{
href: this.to
},
// 用on属性给对应DOM对象注册事件,click表示点击事件,clickhander在methods中定义
on:{
click: this.clickhander
}
},[this.$slots.default])
},
methods:{
clickhander(e){
// 先调用pushState方法来改变浏览器的地址栏(不会向服务器发送请求)。
// pushState方法有三个参数,第一个参数data对象是将来去触发popState时传给popState事件对象的参数,
// 第二个title表示网页标题,第三个url表示当前超链接要跳转的地址
history.pushState({},"",this.to)
// 把当前路径记录到data.current中,注意我们之前注册插件的时候把$router属性挂载到了原型属性上,
// 所以所有的Vue实例都可以访问到$router对象。
// 又因为data是响应式的对象,所以当其改变后会重新加载对应组件。
this.$router.data.current = this.to
// 用preventDefault方法来阻止事件的默认行为
e.preventDefault()
}
}
})
// ......router-view......
}
到此,我们router-view组件就实现完毕了,看上去我们的功能都已经实现,但其实这里还有部分问题,就是当我们点击浏览器中的后退与前进按钮时,浏览器中的内容并没有发生改变,而对于该问题的解决方法我们将在下一节中填坑。
19、VueRouter-initEvent
最后,我们再来填上上一节中埋下的坑,即如何处理当点击浏览器中的后退与前进按钮时,浏览器中的内容并没有发生改变的问题。也即实现类图中的最后一个方法initEvent,在这个方法中我们要来注册一个事件popState,当地址栏改变时,改变data.current使浏览器加载对应的组件并渲染出来。popState事件是当历史发生变化的时候会被触发的,我们调用pushState或replaceState方法时是不会触发该事件的。
initEvent(){
// 在事件处理函数中,我们要做的是将当前地址栏中的路径部分取出来并存储到this.data.current中,
// 注意这里的this就是VueRouter对象,因为这里写的是箭头函数,不绑定this,this还是上一层的this
window.addEventListener("popstate",() => {
this.data.current = window.location.pathname
})
}
到此,initEvent方法我们就已经完成了,但别忘记了还要调用该方法。这也是需要初始化的方法,所以我们在init里调用:
init(){
this.createRouteMap()
this.initComponent(_Vue)
// 在init中调用initEvent方法
this.initEvent()
}
之后我们打开浏览器进行测试,此时点击浏览器的前进后退就会显示正常的内容了。到此,我们才算是真正完成了一个简易版本的VueRouter。
附上全部代码:
let _Vue = null
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.routeMap = {}
this.data = _Vue.observable({
current:"/"
})
// 这里我们在构造函数中就调用了init方法
this.init()
}
init(){
this.createRouteMap()
this.initComponent(_Vue)
this.initEvent()
}
createRouteMap(){
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
});
}
initComponent(Vue){
Vue.component("router-link",{
props:{
to:String
},
render(h){
return h("a",{
attrs:{
href:this.to
},
on:{
click:this.clickhander
}
},[this.$slots.default])
},
methods:{
clickhander(e){
history.pushState({},"",this.to)
this.$router.data.current=this.to
e.preventDefault()
}
}
})
const self = this
Vue.component("router-view",{
render(h){
const cm=self.routeMap[self.data.current]
return h(cm)
}
})
}
initEvent(){
window.addEventListener("popstate",()=>{
this.data.current = window.location.pathname
})
}
}