Vue Router 是 Vue.js 官方的路由管理器,它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。本文主要通过模拟vue-router案例,来深入探讨下vue-router原理的实现过程
路由是什么?
在前后端分离之前,一般提到的路由都是后端路由,路由通过一个请求,然后分发到指定的路径,匹配对应的处理程序,它的作用就是分发请求,把对应的请求分发到对应的位置
前端路由与后端路由
后端路由
后端路由可以理解为服务器将浏览器请求的url解析之后映射成对应的函数,这个函数会根据资源类型的不同进行不同的操作,如果是静态资源,那么就进行文件读取,如果是动态数据,那么就会通过数据库进行一些增删查改的操作
优点:利于SEO且安全性较高;
缺点:代码耦合度高,加大了服务器压力,且http请求受限于网络环境,影响用户体验
前端路由
随着前端单页应用(SPA)的兴起,前端页面完全变成了组件化,不同的页面就是不同的组件,页面的切换就是组件的切换;页面切换的时候不需要再通过http请求,直接通过JS解析url地址,然后找到对应的组件进行渲染,如果需要服务端内容,可以通过发送ajax请求获取
前端路由与后端路由最大的不同就是不需要再经过服务器,直接在浏览器下通过JS解析页面之后就可以拿到相应的页面
优点:组件切换不需要发送http请求,切换跳转快,用户体验好
缺点:前进后退会重新发送请求,没有合理的利用缓存且不利于SEO
路由模式
vue-router 提供了三种运行模式:
- hash: 使用 URL hash 值来作路由。默认模式。
- history: 依赖 HTML5 History API 和服务器配置。查看 HTML5 History 模式。
- abstract: 支持所有 JavaScript 运行环境,如 Node.js 服务器端。
hash 模式
hash模式是vue-router的默认路由模式,它是基于锚点,以及onhashchange事件,通过锚点的值作为路由地址,当地址发生变化后,触发onhashchange事件,即根据路径决定页面中呈现的内容。
- hash模式的标志是在域名之后带有一个#,#后面内容作为路由地址,可以通过?携带参数,如果只改变#后面的内容,浏览器不会向服务器请求地址,会将地址记录到访问历史中
1 | http://localhost:8080/#/home?id=1 |
- 使用window.location.hash获取到当前url的hash,通过hashchange方法可以监听url中hash的变化
1 | window.addEventListener("hashchange", function(){}, false) |
- hash模式同样支持浏览器的前进后退操作,因为当hash模式的url改变后,这个url会存入浏览器的历史记录,所以可以通过浏览器完成前进后退
history模式
- history模式是另一种前端路由模式,它基于HTML5的History API,通过window.location.pathname获取到当前url的路由地址
- 当调用 history.push() 时,路径会发生变化,要向服务器发生请求
- history模式下,通过
history.pushState()
和history.replaceState()
方法可以修改url地址(只更改浏览器地址栏地址,并将地址记录到历史记录中,不向服务器发送请求),可以结合popstate
方法监听url中路由的变化,也就是说,可以使用pushState()
实现客户端路由,但是需要在IE10
以后使用 - history模式的特点是实现更加方便,可读性更强,同时因为没有了#,url也更加美观
它的劣势也比较明显,当用户刷新或直接输入地址时会向服务器发送一个请求,所以history模式需要服务端进行支持,将路由都重定向到根路由
History 模式演示
vue-cli自带web服务器配置好对history支持,为了演示之前的问题,通过node或者Nginx服务器。
使用vue-cli创建的vue项目默认是hash模式,可以通过mode字段更改模式
1 | const router =new vueRouter({ |
- History 需要服务器的支持
- 单页应用中,服务端不存在
http://www.testurl.com/login
这样的地址,会返回找不到该页面 - 在服务端应该除了静态资源外都返回单页应用的
index.html
node
项目目录结构
- 使用history模式没有服务器支持的情况
1 | // 02-backend/app.js |
运行node app.js
启动服务,在浏览器运行http://localhost:3000
就可看到具体项目,如下图所示
F5整体刷新后,浏览器发送请求,但是node服务器中没有出来/about地址,node服务器输出一个默认404页面
- 在node中配置history模式
1 | npm install connect-history-api-fallback |
1 | // 02-backend/app.js |
当开启服务器支持history模式的时候,F5刷新后浏览器发送http://localhost:3000/about
请求,服务器接收到请求后,判断请求的路径是否有对应的页面,没有会把当前单页应用默认的index.html
(根目录下面(打包文件中的index.html))返回给浏览器,浏览器接收到页面后会判断当前路由地址,在客户端解析路由地址对应的组件,加载对应内容并渲染到浏览器中。
nginx
- 从官网下载 nginx 的压缩包
- 把压缩包解压到 c 盘根目录,c:\nginx-1.18.0 文件夹,注意:目录不能有中文
打开命令行,切换到目录 c:\nginx-1.18.0
启动 nginx,默认端口是80,可以在配置文件nginx.conf中修改
1 | $ start nginx.exe |
- 重启
1 | $ nginx.exe -s reload |
- 停止
1 | $ nginx.exe -s stop |
启动服务后在浏览器中可以看到项目,nginx没有处理vue-router的history模式,当刷新浏览器时候请求/video时服务器不存在对应的页面从而返回404页面,如下图所示
解决:在nginx.conf配置try_files
Try_files
:试着访问当前浏览器请求路径所对应的文件$uri
:当前请求的路径,会找这个路径对应的文件,找到直接返回,没有找到继续从$uri/
找index.html
,没有找到,返回单页应用的(根路径下)的index.html
abstract模式
abstract模式是使用一个不依赖于浏览器的浏览历史虚拟管理后端。
根据平台差异可以看出,在 Weex 环境中只支持使用 abstract 模式。 不过,vue-router 自身会对环境做校验,如果发现没有浏览器的 API,vue-router 会自动强制进入 abstract 模式,所以 在使用 vue-router 时只要不写 mode 配置即可,默认会在浏览器环境中使用 hash 模式,在移动端原生环境中使用 abstract 模式。 (当然,你也可以明确指定在所有情况下都使用 abstract 模式)
VueRouter 实现原理
vue-router是前端路由,当路径切换的时候在浏览器端判断当前路径,并加载当前路径对应的组件
vue的前置知识
在模拟 Vue-Router 的过程中,我们需要简单了解一些相关的知识
插件 Vue.use()
Vue.use()方法用于插件安装,通过它可以将一些功能或API入侵到Vue内部,它接收一个参数,如果这个参数是对象,那么Vue.use()会执行这个install方法,如果接收到的参数是一个函数,那么这个函数会作为install方法被执行
install方法在执行的时候也会接收到一个参数,这个参数就是当前Vue的实例
通过接收到的Vue实例,可以定义一些全局方法或属性,也可以通过prototype对Vue的实例方法进行扩展
1 | class vueRouter { |
Vue.use() 会自动阻止多次注册相同插件,届时即使多次调用也只会注册一次该插件。
混入 Vue.mixin()
混入(mixin),用来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
1 | // 定义一个混入对象 |
全局混入
混入也可以进行全局注册。使用时格外小心!一旦使用全局混入,它将影响每一个之后创建的Vue实例。使用恰当时,这可以用来为自定义选项注入处理逻辑。
1 | // 为自定义的选项 'myOption' 注入一个处理器。 |
vue.observable(object)
创建响应式对象,创建出的对象可以直接用在渲染函数或者计算属性上面,并且会在发生变更时触发相应的更新。也可以作为最小化的跨组件状态存储器,用于简单的场景:
1 | const state = Vue.observable({ count: 0 }) |
在 Vue 2.x 中,被传入的对象会直接被 Vue.observable 变更,所以如这里展示的,它和被返回的对象是同一个对象。
在 Vue 3.x 中,则会返回一个可响应的代理,而对源对象直接进行变更仍然是不可响应的。
因此,为了向前兼容,我们推荐始终操作使用 Vue.observable 返回的对象,而不是传入源对象。
插槽 slot
元素作为承载分发内容的出口。插槽内可以包含任何模板代码,包括 HTML、其他组件等
1 | Vue.component('alert-box', { |
render函数
字符串模板的代替方案,允许你发挥 JavaScript 最大的编程能力。该渲染函数接收一个createElement方法(h)作为第一个参数用来创建虚拟DOM。
如果组件是一个函数组件,渲染函数还会接收一个额外的context参数,为没有实例的函数组件提供上下文信息。
Vue 选项中的 render 函数若存在,则 Vue 构造函数不会从 template 选项或通过 el 选项指定的挂载元素中提取出的 HTML 模板编译渲染函数。
运行时和完整版的vue
- 运行版 Vue
vue-cli创建项目默认是运行时版本,不支持 template 模板,需要打包的时候提前编译,把template编译成render函数,使用render函数创建虚拟DOM,并把它渲染到视图
使用render函数,替代template模板,代码如下
1 | export default class VueRouter { |
在运行时版本单文件组件template为何可以正常工作?
在打包时候将单文件组件template编译成render函数,即预编译
- 完整版 Vue
包含运行时和编译器,体积比运行时版大10K左右,编译器作用就是程序运行的时候把模板转换成render函数。
在vue.config.js中,开启使用包含运行时编译器的Vue核心版本,代码如下:
1 | module.exports = { |
vue-router使用步骤
- 在router/index.js中注册路由插件,创建router对象
1 | import Vue from 'vue'; |
- 在main.js中注册router对象
1 | import Vue from 'vue' |
- 在App.vue中创建链接和路由组件占位
1 | <template> |
vueRouter模拟实现
history模式为例
实现思路
install()
install() 方法是 VueRouter 类中的静态方法,当使用 Vue.use(fun | obj) 注册插件时,会调用 install() 方法。
1 | /** |
constructor()
VueRouter 类的构造函数,接收一个 Options 选项,它的的返回值是一个 VueRouter 对象。
1 | /** |
createRouteMap()
初始化routeMap属性,把构造函数中选项的 routes(路由规则),转换成键值对的形式,存储到 routeMap对象中。
1 | createRouteMap(){ |
initComponents()
用来创建router-link和router-view组件
1 | initComponents(vue){ |
initEvent()
initEvent(),注册 popstate 事件,当历史发生变化时,进行触发。即点击浏览器的前进后退按钮时,触发 popstate 事件。
1 | initEvent(){ |
init()
用来初始化调用其他函数,在install()调用
1 | init(){ |
hash 模式下,使用window.location.hash获取到当前url的hash,通过hashchange方法可以监听url中hash的变化,其余方面与 history 模式类似,代码如下:
1 | let _vue = null |
案例代码详见:vueRoter-custom