Vue-Router 原理实现
vue-router 的基础使用
1. Vue.use 注册路由插件:
router.js 文件
import VueRouter from 'vue-router'
Vue.use(VueRouter)
Vue.use 用于安装插件。
Vue.use(plugin) 用法:
- 如果 plugin 是一个对象,必须提供
install方法; - 如果 plugin 是一个函数,它会被作为
install方法。
install 方法调用时,会将 Vue 作为参数传入。
Vue.use(plugin) 作用:
- 检查插件是否安装,如果安装了就不再安装;
- 如果没有安装,调用 plugin 的
install方法,并传入Vue实例;
2. 创建 router 对象:
router.js 文件
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: "about" */ '../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: "about" */ '../views/Photo.vue')
}
]
const router = new VueRouter({
routes
})
routes:路由规则,每个对象都是路由规则。
3. 注册 router 对象:
main.js 文件
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
// 3. 注册 router 对象, 会注入 $route(路由规则,提供路由的数据:路径、参数),$router(路由对象,VueRouter 对象,提供路由相关的方法:push、replace 等,也有一个 currentRoute 属性用于获取当前路由信息 path、params、query 等)
router,
render: h => h(App)
}).$mount('#app')
注册 router 对象, 会注入 $route(路由规则,提供路由的数据:路径、参数),$router(路由对象,VueRouter 对象,提供路由相关的方法:push、replace 等,也有一个 currentRoute 属性用于获取当前路由信息 path、params、query 等)
4. 创建路由组件 <router-view>:
App.vue 文件
<template>
<div id="app">
<div>
<img src="@/assets/logo.png" alt="" />
</div>
<div id="nav">
<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>
5. 创建路由组件 <router-link>:
App.vue 文件
<template>
<div id="app">
<div>
<img src="@/assets/logo.png" alt="" />
</div>
<div id="nav">
<!-- 5. 创建路由组件 <router-link> -->
<router-link to="/">Index</router-link> |
<router-link to="/blog">Blog</router-link> |
<router-link to="/photo">Photo</router-link>
</div>
<router-view />
</div>
</template>
动态路由
const routes = [
{
// id 在此处为一个占位符,真正使用时,会动态传入
path: '/detail/:id',
name: 'Detail',
// 开启 props,会把 URL 中的参数传递给组件,在组件中通过 props 来接受 URL 参数
props: true,
// 当页面不是必要的时候,可以使用页面懒加载的方式,当访问到此地址的时候,页面才会被加载
component: ()=>import('../views/Detail.vue')
}
]
1. 通过当前路由规则,获取数据:
<template>
<div>
通过当前路由规则获取:{{ $route.params.id }}
</div>
</template>
2. 路由规则中开启 props 传参:
<template>
<div>
通过开启 props 获取:{{ id }}
</div>
</template>
export default {
name: 'Detail',
props: ['id']
}
嵌套路由
- 提取相同部分到新的组件:
<template>
<div>
<img width="25%" src="@/assets/logo.png"></img>
</div>
<div>
// 对应的页面的组件会替换掉 router-view 这个组件的位置
<router-view></router-view>
</div>
<div>Footer</div>
</template>
对应的页面的组件会替换掉 router-view 这个组件的位置。
- 嵌套路由的路由规则配置:
import Layout from '@/component/Layout.vue'
import Index from '@/views/Index.vue'
import Login from '@/views/Login.vue'
const routes = [
{
name: 'login',
path: '/login',
component: Login,
},
{
// 父页面路径
path: '/',
component: Layout,
children: [
{
name: 'index',
// 子页面路径
path: '',
component: Index,
},
{
name: 'detail',
// 子页面路径
path: 'detail/:id',
props: true,
component: () => import('@/views/Detail.vue')
},
],
}
]
在使用的时候,嵌套路由会将父子页面路径合并,然后先加载父组件,再加载子组件,然后合并到一起渲染。
子组件中的 path 可以是相对路径,也可以是绝对路径。
编程式导航
- push 方法:
- 指定跳转的路由路径进行跳转;
- 指定跳转的路由名称进行跳转;
// 指定跳转的路由路径进行跳转
this.$router.push('/')
// 指定跳转的路由名称进行跳转
this.$router.push({ name: 'index' })
- replace 方法:
- 与 push 方法类似,均可跳转到指定的路径,但是 replace 方法不会记录新增本次历史,而是把最新的一条历史替换为此次跳转的历史;
this.$router.replace('/login')
- push 方法指定路由参数:
- params 指定的路由参数,页面刷新时无法再次获取;
- query 指定的路由参数,页面刷新时也可以再次获取;
this.$router.push({ name: 'Detail', params: { id: 1 } })
this.$router.push({ name: 'Detail', query: { id: 1} })
- go 方法:
- go 方法是跳转到历史中的某一次访问的页面;
- 若为负数,表示返回,值为-1,效果与
$router.back()方法一致; - 若为正数,表示前进;
this.$router.go(-1)
Hash 和 History 模式区别
都是客户端路由的使用方式,当路由地址发生变化的时候,不会向服务器发送请求。使用 js 监听路由地址的变化,然后根据不同的地址,渲染不同的页面组件。如果需要服务器端内容,则需要发送 ajax 请求进行获取。
1. 表现形式的区别:
-
Hash 模式:携带了其他与数据无关的符号(#、?、=);
-
History 模式:一个正常 url 的格式,但是需要服务端配置支持;
2. 原理的区别:
- 原理:
- Hash 模式:基于 URL 中的锚点和 onhashchange 事件实现的;
- History 模式:基于 H5 中的 History API 实现的;
- 区别:
-
hash 模式兼容性更好,IE8 以后兼容;history 模式(使用到了 h5 的 API:history.pushState() history.replaceState()) IE10 以后才支持;
-
history 模式和 hash 模式都是客户端修改 URL,所以性能相差不大;
-
history 模式,如果没有对应的路由的规则,可能会发起一个真正原请求,需要后端来处理,如果不处理,就会产生 404;
-
History 模式的使用
- History 需要服务器的支持;
- 单页应用中,如果服务端不存在 www.testurl.com/login 这样的地址会返回找不到改页面;
- 在服务端应该除了静态资源外都返回单页应用的 index.html;
- history.push 和 history.pushState 方法的区别是 push 方法会在路径发生变化的同时向服务端发送请求,而 pushState 方法不会向服务器发送请求,只会改变浏览器地址栏中的地址,然后记录到历史记录中。
History 模式 - Node.js 服务器配置
const path = require('path');
// 导入 express
const express = require('express');
const app = express()
// 导入处理 history 模式的模块
const history = require('connect-history-api-fallback');
// 注册处理 history 模式的中间件
app.use(history())
// 处理静态资源的中间件,网站根目录 ../web
app.use(express.static(path.join(__dirname, '../web')));
app.listen(3000, () => {
console.log('服务器开启,端口:3000');
});
History 模式 - nginx 服务器配置
- 从官网上下载 nginx 的压缩包;
- 把压缩包解压(目录中不要包含中文及特殊字符);
- 打开命令行,切换到 nginx 所在目录;
nginx 相关命令:
# 启动
start nginx
# 重启
nginx -s reload
# 停止
nginx -s stop
nginx 配置 vue history 模式:
- 打包 vue 项目,并将打包好的文件复制到 nginx 映射的目录中(nginx 默认目录是 html 目录);
- 启动 nginx(启动的默认端口是 80 端口,若被占用,需要修改 nginx.conf 配置文件);
- 配置 nginx.conf 文件(localtion 中增加 try_files uri/ /index.html );
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 10086;
server_name localhost;
location / {
root html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
}
}
Vue Router 实现原理
Vue 前置知识
- 插件;
- 混入;
- Vue.observable();
- 插槽;
- render 函数;
- 运行时和完整版的 Vue;
Hash 模式
URL中 # 后面的内容只作为路径地址;(可以直接通过location.url切换路由地址,如果只改变了路径中 # 后面的内容,浏览器不会向服务器请求这个地址,只会把这个地址记录到浏览器的访问历史当中)- 监听
hashchange事件;(hash 改变以后会触发hashchange事件) - 根据当前路由地址找到对应组件重新渲染;
History 模式
- 通过
history.pushState()方法改变地址栏;(该方法只改变地址栏,并把地址记录到浏览器访问历史当中,并不会真正跳转到该地址,即不会向服务器发送请求) - 监听
popstate事件;(可以监听到浏览器地址的变化 ,在popstate事件处理函数中,可以记录改变后的地址;【注】当调用 pushState() 和 replaceState() 方法的时候,并不会触发 popstate 事件,当点击浏览器前进和后退按钮的时候(即:当历史状态被激活的时候),或者调用 history.back() 方法时才会触发该事件) - 根据当前路由地址找到对应组件重新渲染;
Vue Router 模拟实现
核心代码
// router/index.js
// 注册插件
Vue.use(VueRouter)
// 创建路由对象
const router = new VueRouter({
routers: [
{ name: 'home', path: '/', component: homeComponent }
]
})
// main.js
// 创建 Vue 实例,注册 router 对象
new Vue({
router,
render: h=> h(App)
}).$mount('#app')
Vue Router 类图
classDiagram
class VueRouter {
+ options
+ data
+ routeMap
+ Constructor(Options): VueRouter
_ install(Vue): void
+ init(): void
+ initEvent(): void
+ initRouteMap(): void
+ initComponents(Vue): void
}
install
let _Vue = null;
export default class VueRouter {
static install(Vue) {
console.log("install: ", Vue);
// 1. 判断当前插件是否已经被安装;
if (VueRouter.install.installed) {
return;
}
VueRouter.install.installed = true;
// 2. 把 Vue 构造函数记录到全局变量;
_Vue = Vue;
// 3. 把创建 Vue 实例时候传入的 router 对象注入到每一个 Vue 实例上;(即挂载到构造函数的原型上)
// _Vue.prototype.$router = this.$options.router;
// 混入
_Vue.mixin({
beforeCreate() {
console.log("beforeCreate: ", this.$options.router);
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router;
this.$options.router.init();
}
},
});
}
}
constructor
let _Vue = null;
export default class VueRouter {
...
constructor(options) {
console.log("constructor");
this.options = options;
this.routeMap = {};
this.data = _Vue.observable({
current: "/",
});
}
}
initRouteMap
let _Vue = null;
export default class VueRouter {
...
/**
* 解析 optins 中的路由规则
*/
initRouteMap() {
console.log("initRouteMap");
// 遍历所有路由规则,把路由规则解析成键值对的形式,并存储到 routeMap 中;
this.options.routes.forEach((route) => {
this.routeMap[route.path] = route.component;
});
}
}
initComponents(router-link)
let _Vue = null;
export default class VueRouter {
...
/**
* 创建 router-view 和 router-link 组件
*
* @param { Object } Vue Vue 的构造函数
*/
initComponents(Vue) {
console.log("initComponents");
Vue.component("router-link", {
props: {
to: String,
},
template: '<a :href="to"><slot></slot></a>',
});
}
}
init
let _Vue = null;
export default class VueRouter {
...
init() {
this.initRouteMap();
this.initComponents(_Vue);
}
}
Vue 的构建版本
此时使用我们编写的 vuerouter 会出现如下问题
错误提示:你使用的是 Vue 的纯运行时构建,模板编译器不可用。要么预先将模板编译成渲染函数,要么使用包含编译器的构建。
Vue 的构建版本:
- 运行时版:不支持 template 模板,需要打包的时候提前编译,把 template 模板编译成 render 函数,运行时版再通过 render 函数创建虚拟 dom 然后再渲染到视图;
- 完整版:包含运行时和编译器,体积比运行时版大 10K 左右,编译器版作用是在程序运行的时候把模板转换成 render 函数,因此性能不及运行时版;
vue-cli 创建的项目,默认使用运行时版的 Vue,因此会产生此错误;
完整版的 Vue
-
Type:
boolean -
Default:
false是否使用包含运行时编译器的 Vue 构建版本。设置为
true后你就可以在 Vue 组件中使用template选项了,但是这会让你的应用额外增加 10kb 左右。更多细节可查阅:Runtime + Compiler vs. Runtime only。
vue.config.js
const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({
runtimeCompiler: true,
});
运行时版本的 Vue
不带编译器的 Vue 不支持组件中的 template 选项。因为编译器的作用是把 template 编译成 render 函数,所以在运行时版本的 Vue 中,我们的组件可以直接写 render 函数,可见使用运行时版本的 Vue 解决此问题的重点落在 render 函数的写法;
/vuerouter(自定义 vue-router)/index.js
let _Vue = null;
export default class VueRouter {
...
/**
* 创建 router-view 和 router-link 组件
*
* @param { Object } Vue Vue 的构造函数
*/
initComponents(Vue) {
console.log("initComponents");
Vue.component("router-link", {
props: {
to: String,
},
// template: '<a :href="to"><slot></slot></a>',
render(h) {
// h 是一个函数,其作用是创建虚拟 dom,h 函数由 Vue 传入
// 第一个参数:选择器;
// 第二个参数:设置属性,通过 attrs;
// 第三个参数:设置标签间的内容,此处通过 this.$slots.default 获取默认插槽的方式获取;
return h(
"a",
{
attrs: {
href: this.to,
},
},
[this.$slots.default]
);
},
});
}
...
}
initComponents(router-view)
let _Vue = null;
export default class VueRouter {
...
/**
* 创建 router-view 和 router-link 组件
*
* @param { Object } Vue Vue 的构造函数
*/
initComponents(Vue) {
console.log("initComponents");
Vue.component("router-link", {
props: {
to: String,
},
// template: '<a :href="to"><slot></slot></a>',
render(h) {
// h 是一个函数,其作用是创建虚拟 dom,h 函数由 Vue 传入
// 第一个参数:选择器;
// 第二个参数:设置属性,通过 attrs;
// 第三个参数:设置标签间的内容,此处通过 this.$slots.default 获取默认插槽的方式获取;
return h(
"a",
{
attrs: {
href: this.to,
},
on: {
click: this.clickHandler,
},
},
[this.$slots.default]
);
},
methods: {
clickHandler(e) {
history.pushState({}, "", this.to);
this.$router.data.current = this.to;
e.preventDefault();
},
},
});
const self = this;
Vue.component("router-view", {
render(h) {
const component = self.routeMap[self.data.current];
// h 函数也可以直接将路由组件转化为虚拟 dom;
return h(component);
},
});
}
...
}
initEvent
问题背景:
按照我们现有的实现,当我们点击浏览器前进和后退按钮的时候,会发现页面没有重新加载当前地址栏中地址对应的组件。
解决思路:
把当前地址栏地址(路由路径部分)取出,并把改地址作为当前路由地址,存储到 this.data.current 中。
let _Vue = null;
export default class VueRouter {
...
/**
* 用于包装 initRouteMap 方法和 initComponents 方法,便于调用
*/
init() {
this.initRouteMap();
this.initComponents(_Vue);
this.initEvent();
}
initEvent() {
window.addEventListener("popstate", () => {
// 把当前地址栏地址(路由路径部分)取出,并把改地址作为当前路由地址,存储到 this.data.current 中
this.data.current = window.location.pathname;
});
}
}