一、概述
1.什么是前端路由?
- 传统路由:用一些超链接来实现页面切换和跳转
- vue-router:组件之间的切换,无需刷新页面 其本质就是:建立并管理url和对应组件之间的映射关系.
2.如何实现前端路由?
实现前端路由,需要解决两个核心:
- 如何改变 URL 却不引起页面刷新?
- 如何检测 URL 变化了? 下面分别使用 hash 和 history 两种实现方式回答上面的两个核心问题。
hash 实现
hash 是 URL 中 hash (#) 及后面的那部分,常用作锚点在页面内进行导航,改变 URL 中的 hash 部分不会引起页面刷新
通过 hashchange 事件监听 URL 的变化,改变 URL 的方式只有这几种:
- 通过浏览器前进后退改变 URL
- 通过
<a>标签改变 URL - 通过window.location改变URL
<!DOCTYPE html>
<html lang="en">
<body>
<ul>
<ul>
<!-- 定义路由 -->
<li><a href="#/home">home</a></li>
<li><a href="#/about">about</a></li>
<!-- 渲染路由对应的 UI -->
<div id="routeView"></div>
</ul>
</ul>
</body>
<script>
let routerView = routeView;
// 监听hashchange事件
window.addEventListener('hashchange', () => {
let hash = location.hash;
console.info("监听hashchange事件:",hash);
routerView.innerHTML = hash;
})
// 监听了load事件
window.addEventListener('DOMContentLoaded', () => {
if (!location.hash) { //如果不存在hash值,那么重定向到#/
location.hash = "/";
console.info("监听了load事件:",hash);
} else { //如果存在hash值,那就渲染对应UI
let hash = location.hash;
console.info("监听了load事件:",hash);
routerView.innerHTML = hash;
}
</script>
</html>
/**
1. 我们通过a标签的href属性来改变URL的hash值(当然,你触发浏览器的前进后退按钮也可以,或者在控制台输入window.location赋值来改变hash)
2. 我们监听**hashchange**事件。一旦事件触发,就改变**routerView**的内容,若是在vue中,这改变的应当是**router-view**这个组件的内容
3. 为何又监听了load事件?这时应为页面第一次加载完不会触发 hashchange,因而用load事件来监听hash值,再将视图渲染成对应的内容。
*/
history 实现
history 提供了 pushState 和 replaceState 两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新
history 提供类似 hashchange 事件的 popstate 事件,但 popstate 事件有些不同:
- 通过浏览器前进后退改变 URL 时会触发 popstate 事件
- 通过pushState/replaceState或
<a>标签改变 URL 不会触发 popstate 事件。 - 好在我们可以拦截 pushState/replaceState的调用和
<a>标签的点击事件来检测 URL 变化 - 通过js 调用history的back,go,forward方法课触发该事件
<!DOCTYPE html>
<html lang="en">
<body>
<ul>
<ul>
<li><a href='/home'>home</a></li>
<li><a href='/about'>about</a></li>
<div id="routeView"></div>
</ul>
</ul>
</body>
<script>
let routerView = routeView;
// 监听了load事件
window.addEventListener('DOMContentLoaded', onLoad);
// 监听popState事件
window.addEventListener('popstate', ()=>{
routerView.innerHTML = location.pathname;
})
function onLoad () {
routerView.innerHTML = location.pathname;
var linkList = document.querySelectorAll('a[href]'); // 找A标签
linkList.forEach(el => el.addEventListener('click', function (e) {
e.preventDefault(); // 通知 Web 浏览器不要执行与事件关联的默认动作,也就是去掉A标签默认的行为事件
history.pushState(null, '', el.getAttribute('href'));
console.log(el.getAttribute('href'))
// 页面无刷新,但是可以改变URL和产生历史记录
// 第一个是历史记录(必须写) 第二个是title(可以不写,直接写""),第三个是接下来的url后面加的东西
// el.getAttribute('href'):获取A标签href的属性值
routerView.innerHTML = location.pathname;
}))
}
</script>
</html>
/**
1.我们通过a标签的href属性来改变URL的path值(当然,你触发浏览器的前进后退按钮也可以,或者在控制台输入history.go,back,forward赋值来触发popState事件)。这里需要注意的就是,当改变path值时,默认会触发页面的跳转,所以需要拦截` <a> ` 标签点击事件默认行为, 点击时使用 pushState 修改 URL并更新手动 UI,从而实现点击链接更新 URL 和 UI 的效果。
2.我们监听**popState**事件。一旦事件触发,就改变**routerView**的内容。
3.load事件则是一样的
*/
小结hash和history的区别
hash模式:(hash 本来是拿来做页面定位的)
- 地址栏会有#(描点:和CSS里面的#一样,用来做页面定位的);
- hash虽然出现url中,但不会被包含在HTTP请求中,对后端完全没有影响,因此改变hash不会重新加载页面;
- hash的而传参是基于url的,如果要传递复杂的数据,会有体积的限制;
history模式:
-
history模式不仅可以在url里放参数,还可以将数据存放在一个特定的对象中。
-
利用了HTML5 History Interface 中新增的pushState()和replaceState()方法。(需要特定浏览器的支持)history不能运用与IE8以下;
window.history.pushState(state,title,url); //state:需要保存的数据,这个数据在触发popstate事件时,可以在event.state里获取 //title:标题,基本没用,一般传null //url:设定新的历史纪录的url。新的url与当前url的origin必须是一样的,否则会抛出错误。url可以时绝对路径,也可以是相对路径。 //如 当前url是 https://www.baidu.com/a/,执行history.pushState(null, null, './qq/'),则变成 https://www.baidu.com/a/qq/, //执行history.pushState(null, null, '/qq/'),则变成 https://www.baidu.com/qq/ window.history.replaceState(state,title,url) //与pushState 基本相同,但她是修改当前历史纪录,而 pushState 是创建新的历史纪录 window.addEventListener("pospstate",function(){ //监听浏览器前进后退事件,pushState与replaceState方法不会触发 }) window.history.back()//后退 window.history.forward()//前进 window.history.go(1)//前进一部,-2回退两不,window.history.lengthk可以查看当前历史堆栈中页面的数量
二、基于Vue实现VueRouter
我们先利用vue-cli建一个项目
删除一些不必要的组建后项目目录暂时如下:
// app.vue
<template>
<div id="app">
<div id="nav">
<router-link to="/home">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
</div>
</template>
// router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import About from "../views/About.vue"
Vue.use(VueRouter)
const routes = [
{
path: '/home',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
]
const router = new VueRouter({
mode:"history",
routes
})
export default router
// Home.vue
<template>
<div class="home">
<h1>这是Home组件</h1>
</div>
</template>
// About.vue
<template>
<div class="about">
<h1>这是about组件</h1>
</div>
</template>
最终运行结果如下:
然后改用自己的路由,创建自己的VueRouter,于是创建myVueRouter.js文件。
再将VueRouter引入 改成我们的myVueRouter.js
//router/index.js
import Vue from 'vue'
import VueRouter from './myVueRouter' //修改代码
import Home from '../views/Home.vue'
import About from "../views/About.vue"
Vue.use(VueRouter)
const routes = [
{
path: '/home',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
];
const router = new VueRouter({
mode:"history",
routes
})
export default router
三、从最本质的角度去理解一下VueRouter
理解思路:看VUE中是如何引入VueRouter的,我们可以跟着人家的思路去创建自己的VueRouter
- 安装VueRouter,再通过
import VueRouter from 'vue-router'引入 - 先
const router = new VueRouter({...}),再把router作为参数的一个属性值,new Vue({router}) - 通过Vue.use(VueRouter) 使得每个组件都可以拥有store实例 就从这个引入过程我们可以理想到下列的内容:
我们是通过new VueRouter({...})获得一个router实例,也就是说,我们引入的VueRouter其实是一个类。
// 所以我们就可以完全类比理解成VueRouter就是这个样子的
class VueRouter{...}
我们还使用了Vue.use(),而Vue.use的一个原则就是执行对象的install这个方法
// 所以我们就可以完全类比理解成VueRouter又是这个样子的
class VueRouter{...}
VueRouter.install = function () {...}
最终,我们初始化自己的myVueRouter.js文件就是下面这样子的:
class VueRouter{...}
VueRouter.install = function () {...}
export default VueRouter
四、分析Vue.use
Vue.use(plugin);
参数plugin:
- { Object | Function } plugin
用法: - 安装Vue.js插件。如果插件是一个对象,必须提供install方法。
- 如果插件是一个函数,它会被作为install方法。调用install方法时,会将Vue作为参数传入。
- install方法被同一个插件多次调用时,插件也只会被安装一次。 如何开发VUE插件可以去这里了解《点击去了解》
作用:
注册插件,此时只需要调用install方法并将Vue作为参数传入即可。但在细节上有两部分逻辑要处理:
- 插件的类型,可以是install方法,也可以是一个包含install方法的对象。
- 插件只能被安装一次,保证插件列表中不能有重复的插件。
实现:
// 在Vue.js上新增了use方法,并接收一个参数plugin。
Vue.use = function(plugin){
// 判断插件是不是已经别注册过,如果被注册过,则直接终止方法执行
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
if(installedPlugins.indexOf(plugin)>-1){
return this;
}
// 其他参数
// toArray方法我们在就是将类数组转成真正的数组。使用toArray方法得到arguments。
// 除了第一个参数之外,剩余的所有参数将得到的列表赋值给args,然后将Vue添加到args列表的最前面。
// 这样做的目的是保证install方法被执行时第一个参数是Vue,其余参数是注册插件时传入的参数。
const args = toArray(arguments,1);
args.unshift(this);
// 由于plugin参数支持对象和函数类型,所以通过判断plugin.install和plugin哪个是函数,
// 即可知用户使用哪种方式祖册的插件,然后执行用户编写的插件并将args作为参数传入。
if(typeof plugin.install === 'function'){
plugin.install.apply(plugin,args);
}else if(typeof plugin === 'function'){
plugin.apply(null,plugin,args);
}
// 将插件添加到installedPlugins中,保证相同的插件不会反复被注册,这就是为什么插件不会呗重复加载
installedPlugins.push(plugin);
return this;
}
那么我们的VueRouter.js就是下面酱紫:
//myVueRouter.js
let Vue = null;
class VueRouter{
}
VueRouter.install = function (v) {
Vue = v;
console.log(v);
//新增代码
Vue.component('router-link',{
render(h){
return h('a',{},'首页')
}
})
Vue.component('router-view',{
render(h){
return h('h1',{},'首页视图')
}
})
};
export default VueRouter
运行后的结果是下面酱紫,就说明我们前面的所有假设没毛病
五、完善install方法
install 一般是给每个vue实例添加东西的,在这里就是给每个组件添加$route和$router($router是VueRouter的实例对象,$route是当前路由对象,也就是说$route是$router的一个属性)
首先,看下一下我们的入口文件main.js文件
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
router,
render: function (h) { return h(App) }
}).$mount('#app')
// 我们可以发现这里只是将router ,也就是./router导出的store实例,作为Vue 参数的一部分
// 但是这里就是有一个问题咯,这里的Vue 是根组件啊。也就是说目前只有根组件有这个router值,而其他组件是还没有的,所以我们需要让其他组件也拥有这个router。
所以,我们就可以这样子完善一下我们的install方法
//myVueRouter.js
let Vue = null;
class VueRouter{
}
VueRouter.install = function (v) {
Vue = v;
// 新增代码
Vue.mixin({
// 1. mixin的作用是将mixin的内容混合到Vue的初始参数options中。
beforeCreate(){
// 1. 为什么是beforeCreate而不是created呢?因为如果是在created操作的话,$options已经初始化好了。
// 1. 如果判断当前组件是根组件的话,就将我们传入的router和_root挂在到根组件实例上。
if (this.$options && this.$options.router){ // 如果是根组件
this._root = this; //把当前实例挂载到_root上
this._router = this.$options.router;
}else { //如果是子组件
// 如果判断当前组件是子组件的话,就将我们_root根组件挂载到子组件。注意是**引用的复制**,因此每个组件都拥有了同一个_root根组件挂载在它身上。
this._root= this.$parent && this.$parent._root
}
Object.defineProperty(this,'$router',{
get(){
return this._root._router
}
})
}
})
Vue.component('router-link',{
render(h){
return h('a',{},'首页')
}
})
Vue.component('router-view',{
render(h){
return h('h1',{},'首页视图')
}
})
};
export default VueRouter
这里就会have一个question:为什么判断当前组件是子组件,就可以直接从父组件拿到_root根组件呢? A:父子组件的执行顺序是酱紫的:父beforeCreate-> 父created -> 父beforeMounte -> 子beforeCreate ->子create ->子beforeMount ->子 mounted -> 父mounted,我们的答案显而易见,在执行子组件的beforeCreate的时候,父组件已经执行完beforeCreate了,那理所当然父组件已经有_root了。
然后我们通过以下代码将$router挂载到组件实例上。
Object.defineProperty(this,'$router',{
get(){
return this._root._router
}
})
这种思想也是一种代理的思想,我们获取组件的$router,其实返回的是根组件的_root._router
六、完善VueRouter类
我们先看看我们new VueRouter类时传进了什么玩楞
//router/index.js
import Vue from 'vue'
import VueRouter from './myVueRouter'
import Home from '../views/Home.vue'
import About from "../views/About.vue"
Vue.use(VueRouter)
const routes = [
{
path: '/home',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
];
const router = new VueRouter({
mode:"history",
routes
})
export default router
可见,传入了一个为数组的路由表routes,还有一个代表 当前是什么模式的mode。因此我们可以先这样实现VueRouter
此时,我们可以开始类比的写法了:
class VueRouter{
constructor(options) {
this.mode = options.mode || "hash"
this.routes = options.routes || [] //你传递的这个路由是一个数组表
}
}
但是我们直接处理routes是十分不方便的,所以我们先要转换成key:value的键值对形式
//myVueRouter.js
let Vue = null;
class VueRouter{
constructor(options) {
this.mode = options.mode || "hash"
this.routes = options.routes || [] //你传递的这个路由是一个数组表
this.routesMap = this.createMap(this.routes)
console.log(this.routesMap);
}
createMap(routes){
// reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,
// 最终计算为一个值。对空数组是不会执行回调函数的。
return routes.reduce((pre,current)=>{
pre[current.path] = current.component
return pre;
},{})
}
}
通过createMap我们可以将A转换成B
A:
const routes = [
{
path: '/home',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
B:
路由中需要存放当前的路径,来表示当前的路径状态 为了方便管理,可以用一个对象来表示
//myVueRouter.js
let Vue = null;
// 新增代码
class HistoryRoute {
constructor(){
this.current = null
}
}
class VueRouter{
constructor(options) {
this.mode = options.mode || "hash"
this.routes = options.routes || [] //你传递的这个路由是一个数组表
this.routesMap = this.createMap(this.routes)
// 新增代码
this.history = new HistoryRoute();
}
createMap(routes){
return routes.reduce((pre,current)=>{
pre[current.path] = current.component
return pre;
},{})
}
}
Q:我们现在发现这个current也就是 当前路径还是null,所以我们需要进行初始化。
初始化的时候判断是是hash模式还是 history模式。,然后将当前路径的值保存到current里
//myVueRouter.js
let Vue = null;
class HistoryRoute {
constructor(){
this.current = null
}
}
class VueRouter{
constructor(options) {
this.mode = options.mode || "hash"
this.routes = options.routes || [] //你传递的这个路由是一个数组表
this.routesMap = this.createMap(this.routes)
this.history = new HistoryRoute();
// 新增代码
this.init()
}
// 新增代码---初始化
init(){
if (this.mode === "hash"){
// 先判断用户打开时有没有hash值,没有的话跳转到#/
location.hash? '':location.hash = "/";
window.addEventListener("load",()=>{
this.history.current = location.hash.slice(1)
})
window.addEventListener("hashchange",()=>{
this.history.current = location.hash.slice(1)
})
} else{
location.pathname? '':location.pathname = "/";
window.addEventListener('load',()=>{
this.history.current = location.pathname
})
window.addEventListener("popstate",()=>{
this.history.current = location.pathname
})
}
}
createMap(routes){
return routes.reduce((pre,current)=>{
pre[current.path] = current.component
return pre;
},{})
}
}
七、完善$route
前面我们了解到,要先实现VueRouter的history.current的时候,才能获得当前的路径,而现在已经实现了,那么就可以着手实现$route了。
很简单,跟实现$router一样
let Vue = null;
class VueRouter{ }
VueRouter.install = function (v) {
Vue = v;
//mixin的作用是将mixin的内容混合到Vue的初始参数options中。相信使用vue的同学应该使用过mixin了。
Vue.mixin({
//为什么是beforeCreate而不是created呢?因为如果是在created操作的话,$options已经初始化好了。
beforeCreate(){
//如果判断当前组件是根组件的话,就将我们传入的router和_root挂在到根组件实例上。
if (this.$options && this.$options.router){ // 如果是根组件
this._root = this; //把当前实例挂载到_root上
this._router = this.$options.router;
//如果判断当前组件是子组件的话,就将我们_root根组件挂载到子组件。
//注意是**引用的复制**,因此每个组件都拥有了同一个_root根组件挂载在它身上。
}else { //如果是子组件
this._root= this.$parent && this.$parent._root
}
Object.defineProperty(this,'$router',{
get(){
return this._root._router
}
});
// 新增代码
Object.defineProperty(this,'$route',{
get(){
return this._root._router.history.current
}
})
}
})
Vue.component('router-link',{
render(h){
return h('a',{},'首页')
}
})
Vue.component('router-view',{
render(h){
return h('h1',{},'首页视图')
}
})
};
八、完善router-view组件
现在我们已经保存了当前路径,也就是说现在我们可以获得当前路径,然后再根据当前路径从路由表中获取对应的组件进行渲染
Vue.component('router-view',{
render(h){
let current = this._self._root._router.history.current
let routeMap = this._self._root._router.routesMap;
return h(routeMap[current])
}
})
代码解释:render函数里的this指向的是一个Proxy代理对象,代理Vue组件,而我们前面讲到每个组件都有一个_root属性指向根组件,根组件上有_router这个路由实例。 所以我们可以从router实例上获得路由表,也可以获得当前路径。 然后再把获得的组件放到h()里进行渲染。
Q:现在已经实现了router-view组件的渲染,但是有一个问题,就是你改变路径,视图是没有重新渲染的,所以需要将_router.history进行响应式化。
Vue.mixin({
beforeCreate(){
if (this.$options && this.$options.router){ // 如果是根组件
this._root = this; //把当前实例挂载到_root上
this._router = this.$options.router;
// 新增代码
Vue.util.defineReactive(this,"xxx",this._router.history)
//ue.util.defineReactive()API:它就是Vue监听`current`变量重要执行者,
// 在这里也就是用来使得this._router.history对象得到监听
}else { //如果是子组件
this._root= this.$parent && this.$parent._root
}
Object.defineProperty(this,'$router',{
get(){
return this._root._router
}
});
Object.defineProperty(this,'$route',{
get(){
return this._root._router.history.current
}
})
}
})
想要了解ue.util.defineReactive()API知识点的,请《点击这里》
因此当我们第一次渲染router-view这个组件的时候,会获取到this._router.history这个对象,从而就会被监听到获取this._router.history。就会把router-view组件的依赖wacther收集到this._router.history对应的收集器dep中,因此this._router.history每次改变的时候。this._router.history对应的收集器dep就会通知router-view的组件依赖的wacther执行update() ,从而使得router-view重新渲染(其实这似乎就是vue响应式的内部原理啊)
然后通过改变地址栏的数据home/about来测试router-view的重新渲染是否成功
九、完善router-link组件
我们先看下router-link是怎么使用的。
<router-link to="/home">Home</router-link>
<router-link to="/about">About</router-link>
也就是说父组件间to这个路径传进去,子组件接收就好 因此我们可以这样实现
Vue.component('router-link',{
props:{
to:String
},
render(h){
let mode = this._self._root._router.mode;
let to = mode === "hash"?"#"+this.to:this.to
return h('a',{attrs:{href:to}},this.$slots.default)
}
})
我们把router-link渲染成a标签,当然这时最简单的做法。 通过点击a标签就可以实现url上路径的切换。从而实现视图的重新渲染
十、最终结果
最后,我们的myVueRouter.js文件就是这样子的啊
let Vue = null;
class HistoryRoute {
constructor(){
this.current = null
}
}
class VueRouter{
constructor(options) {
this.mode = options.mode || "hash"
this.routes = options.routes || [] //你传递的这个路由是一个数组表
this.routesMap = this.createMap(this.routes)
this.history = new HistoryRoute();
this.init()
}
init(){
if (this.mode === "hash"){
// 先判断用户打开时有没有hash值,没有的话跳转到#/
location.hash? '':location.hash = "/";
window.addEventListener("load",()=>{
this.history.current = location.hash.slice(1)
})
window.addEventListener("hashchange",()=>{
this.history.current = location.hash.slice(1)
})
} else{
location.pathname? '':location.pathname = "/";
window.addEventListener('load',()=>{
this.history.current = location.pathname
})
window.addEventListener("popstate",()=>{
this.history.current = location.pathname
})
}
}
createMap(routes){
return routes.reduce((pre,current)=>{
pre[current.path] = current.component
return pre;
},{})
}
}
VueRouter.install = function (v) {
Vue = v;
Vue.mixin({
beforeCreate(){
if (this.$options && this.$options.router){ // 如果是根组件
this._root = this; //把当前实例挂载到_root上
this._router = this.$options.router;
Vue.util.defineReactive(this,"xxx",this._router.history)
}else { //如果是子组件
this._root= this.$parent && this.$parent._root
}
Object.defineProperty(this,'$router',{
get(){
return this._root._router
}
});
Object.defineProperty(this,'$route',{
get(){
return this._root._router.history.current
}
})
}
})
Vue.component('router-link',{
props:{
to:String
},
render(h){
let mode = this._self._root._router.mode;
let to = mode === "hash"?"#"+this.to:this.to
return h('a',{attrs:{href:to}},this.$slots.default)
}
})
Vue.component('router-view',{
render(h){
let current = this._self._root._router.history.current
let routeMap = this._self._root._router.routesMap;
return h(routeMap[current])
}
})
};
export default VueRouter