✅表明已经完成javascript
❎留到下期css
先了解了浏览器的
history
原理,才能更好的结合vue-router
源码一步步了解它的实现。若是这块已经有了解能够直接跳过。html
HTML5
开始提供了对history
栈中内容的操做。经过history.pushState/replaceState
实现添加地址到history
栈中。vue
pushState() 须要三个参数: 一个状态对象, 一个标题 (目前被忽略), 和 (可选的) 一个URL. 让咱们来解释下这三个参数详细内容:java
状态对象
— 状态对象state是一个JavaScript对象,经过pushState () 建立新的历史记录条目。不管何时用户导航到新的状态,popstate事件就会被触发,且该事件的state属性包含该历史记录条目状态对象的副本。node
状态对象能够是能被序列化的任何东西。缘由在于Firefox将状态对象保存在用户的磁盘上,以便在用户重启浏览器时使用,咱们规定了状态对象在序列化表示后有640k的大小限制。若是你给 pushState() 方法传了一个序列化后大于640k的状态对象,该方法会抛出异常。若是你须要更大的空间,建议使用 sessionStorage 以及 localStorage.webpack
标题
— Firefox 目前忽略这个参数,但将来可能会用到。在此处传一个空字符串应该能够安全的防范将来这个方法的更改。或者,你能够为跳转的state传递一个短标题。web
URL
— 该参数定义了新的历史URL记录。注意,调用 pushState() 后浏览器并不会当即加载这个URL,但可能会在稍后某些状况下加载这个URL,好比在用户从新打开浏览器时。新URL没必要须为绝对路径。若是新URL是相对路径,那么它将被做为相对于当前URL处理。新URL必须与当前URL同源,不然 pushState() 会抛出一个异常。该参数是可选的,缺省为当前URL。vue-router
改变历史记录条目segmentfault
@clickA
history.pushState({ page: 1 }, "", "a.html");
@clickB
history.pushState({ page: 2 }, "", "b.html");
复制代码
当历史记录条目更改时,将触发popstate
事件。若是被激活的历史记录条目是经过对history.pushState()
的调用建立的,或者受到对history.replaceState()
的调用的影响,popstate
事件的state
属性包含历史条目的状态对象的副本。
须要注意的是调用history.pushState()
或history.replaceState()
不会触发popstate
事件。只有在作出浏览器动做
时,才会触发该事件,如用户点击浏览器的回退按钮(或者在Javascript
代码中调用history.back()
)
触发浏览器回退按钮
window.addEventListener('popstate', ()=>{
console.log(location.href)
})
复制代码
整体来讲就是使用了
history
的方法来控制浏览器的路由,结合vue
实现数据与视图更新。上面咱们已经讲了history
的使用原理,接下来结合vue-router
具体来看一下
install.js
Object.defineProperty
将 _router
挂载在 Vue
原型的 $router
属性的 get
函数上。这样能够经过 this.$router
来调用 _router
。使用get
的好处是,保证了安全性,只能读不能修改 $router
。// 项目内能够经过 this.$router 来获取到
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
复制代码
而后,在 Vue.mixin
中注入 beforeCreate
钩子函数,每一个组件都会调用 registerInstance
, 经过 Vue.util.defineReactive
将 _route
进行监听,这样每次进入到新的页面就会设置当前的路由。
// 在 `beforeCreate` 中调用了 `registerInstance`
// 其实就是调用了 router-view 组件中的 registerRouteInstance 方法
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
// 初始化设置监听 popstate
// 并将 this._route = route
this._router.init(this)
// 亮点在这!!!
// 将 _route 添加监听,当修改 history.current 时就能够触发更新了
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
// 注册实例,调用 router-view 中的方法,修改 route 值,从而更新视图
registerInstance(this, this)
},
destroyed () {
// 销毁注册实例,由于注册的实例是 undefined
registerInstance(this)
}
})
复制代码
router-view
实现视图更新router-view
是一个函数式组件,页面中 beforeCreate
钩子调用registerRouteInstance
来修改当前 route
实例,因为 _route
已经被监听了,因此当 matched.instances[name]
发生变化的时候,会从新触发 render
更新视图。
omponents/view.js
data.registerRouteInstance = (vm, val) => {
const current = matched.instances[name]
// 注册路由实例,若是与当前路由与原来路由相等则不变,若是不相等则更新实例
if (
(val && current !== vm) ||
(!val && current === vm)
) {
// 修改当前路由实例
matched.instances[name] = val
}
}
复制代码
建立路由 createRoute
,经过解析location
等操做,返回一个route
对象
src/util/route.js
export function createRoute ( record: ?RouteRecord, location: Location, redirectedFrom?: ?Location, router?: VueRouter ): Route {
const stringifyQuery = router && router.options.stringifyQuery
let query: any = location.query || {}
try {
query = clone(query)
} catch (e) {}
const route: Route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),
matched: record ? formatMatch(record) : []
}
if (redirectedFrom) {
route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
}
return Object.freeze(route)
}
复制代码
这里主要讲了,
vue-router
的install
,router-view
实现视图渲染,create-route
建立路由实例,还有如何实现与vue的结合,实现数据绑定等。因为篇幅的问题,再多细节的东西就没有讲了,有兴趣你们能够翻翻源码。
讲完原理给你们捋一下 route 跟 router 的区别,经过源码很容易看出他们的不一样
$router
是router是VueRouter的一个对象,经过Vue.use(VueRouter)和VueRouter构造函数获得一个router的实例对象,这个对象中是一个全局的对象,他包含了全部的路由包含了许多关键的对象和属性。
$route 就是一个路由的对象,咱们经过 createRoute 建立出来的 route 对象,里面包括
$route.path
字符串,等于当前路由对象的路径,会被解析为绝对路径,如 "/home/news" 。
$route.params
对象,包含路由中的动态片断和全匹配片断的键值对
$route.query
对象,包含路由中查询参数的键值对。例如,对于 /home/news/detail/01?favorite=yes ,会获得$route.query.favorite == 'yes' 。
$route.router
路由规则所属的路由器(以及其所属的组件)。
$route.matched
数组,包含当前匹配的路径中所包含的全部片断所对应的配置参数对象。
$route.name
当前路径的名字,若是没有使用具名路径,则名字为空。
原理是在路由的 meta
里设置 auth
属性,进入路由以前判断 meta.auth
是否为 true
,若是为 true
再判断,是否已经登录,没有登录的话调 login
方法去登录,登录成功后 回调 code === 0
继续进入页面
const beforeEnter = (to, from, next) => {
if (to.meta && to.meta.auth) {
// 未登录走登录逻辑
if (!isLogin()) {
const nextPage = (res) => {
if (res.code === 0) {
next(true)
} else {
next(false)
}
};
let targetUrl = location.href.split('#')[0] + '#' + to.fullPath
// 这里是你的登录逻辑
login({
// 回调后进入页面
callback: nextPage,
// 目标页面,登录成功后进入目标页面
targetUrl: targetUrl
});
} else {
next(true)
}
} else {
next(true)
}
}
复制代码
在 Foo 组件设置登录
const routes = [
{
path: '/Foo',
name: 'Foo',
meta: {
auth: true,
},
component: () => ('Foo.vue'),
},
{
path: '/Bar',
name: 'Bar',
component: () => ('Bar.vue'),
},
]
复制代码
设置滚动行为,并添加路由,若是有 savedPosition
说明是第二次进入并已经触发过滚动,因此会滚动到以前打开的位置,若是是第一次进入没有savedPosition
则滚动到最顶层。
const router = new Router({
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { x: 0, y: 0 }
}
},
routes
})
复制代码
<keep-alive>
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 <transition>
类似,<keep-alive>
是一个抽象组件:它自身不会渲染一个 DOM
元素,也不会出如今父组件链中。
当组件在 <keep-alive>
内被切换,它的 activated
和 deactivated
这两个生命周期钩子函数将会被对应执行。
<!-- 须要缓存的视图组件 -->
<router-view v-if="$route.meta.keepAlive">
</router-view>
</keep-alive>
<!-- 不须要缓存的视图组件 -->
<router-view v-if="!$route.meta.keepAlive">
</router-view>
复制代码
路由配置
const routers = [
{
path: '/list',
name: 'list',
component: () => import('./views/keep-alive/list.vue'),
meta: {
keepAlive: true
}
}
]
复制代码
由于在咱们项目里面常常会有列表跳详情,而后又详情返回列表的状况,因此咱们能够根据项目需求来判断是否须要被缓存,若是被缓存了就会出现下面的状况须要注意
有时咱们须要经过给页面传参来判断页面展现什么内容,好比详情页 #/detail?infoId=123456
,咱们须要根据 infoId
来展现不一样的内容
咱们通常习惯会这样写
async created() {
const res = await this.pullData()
}
async pullData () {
return this.$http.get('http://test.m.com/detail', { infoId })
}
复制代码
当咱们经过列表再次进入详情页时,虽然infoId
已经变了infoId=234567
,可是页面并无改变,是由于该页面被keep-alive
了,created
不会再次触发,created
只在建立的时候执行一次。
为了解决这个问题,咱们就须要对 $route
进行监听,只要 route
发生变化咱们就更新页面
watch: {
'$route': {
// 页面初始化时当即触发一次
immediate: true,
handler(to, from) {
// 只有进入当前页面的时候,拉取数据
if(to.path === '/detail') {
this.pullData();
}
}
}
}
复制代码
这样还会带来下面的问题,就是物理键返回的时候也会刷新页面,下面是对物理键返回的处理
为何要检测物理返回键?好比你有这样列表页,点击进去是一个是一个详情页,而后返回的时候列表刷新了,找不到原来的位置,这种时候对用户的体验很是很差。咱们看一下例子。
那么咱们如何去优化它?
思路就是在用户返回到列表页的时候不刷新数据,只有在用户主动进入列表的时候才会刷新数据,咱们看一下效果
下面是实现的代码,原理就是监听 popstate
,当浏览器返回的时候会触发 popstate
,这时咱们标记 isBack
为 ture
。在 setTimeout 0
以后判断 isBack
(是否为浏览器返回),若是不是浏览器返回的再刷新数据。
@Component
export default {
data() {
return {
// 用来判断是不是经过返回键返回的
isBack: false
}
},
created () {
// 若是是物理键返回的就设置 isBack = true
this.$_onBack(()=>{
this.isBack = true;
});
},
watch: {
'$route': {
immediate: true,
handler(to, from) {
// 每次进入路由重置 isBack = false
this.isBack = false;
if(to.path === '/list') {
// 等待路由的 popstate 监听结束
setTimeout(()=>{
!this.isBack && this.pullData();
})
}
}
}
}
}
复制代码
_onBack 实现
,就是监听了 popstate
,由于vue-router
是操做了history
的状态,而浏览器返回的时候就会触发 popstate ,利用这个特性来判断是否为浏览器返回键返回
_onBack(cb) {
window.addEventListener(
"popstate",
(e) => {
if(typeof cb === 'function') {
if(e.state) {
cb(true)
}
}
},
false
);
};
复制代码
利用的是 vue
的 transition
组件,结合 vue-router
,在路由上作一些过渡效果。先看图说话
<template>
<div class="wrap">
<transition :name="transitionName">
<router-view class="child-view"></router-view>
</transition>
</div>
</template>
<script> export default { data() { return { transitionName: 'turning-down' } }, watch: { '$route' (to, from) { if(to.path > from.path) { // 进入下一页 this.transitionName = 'turning-up'; }else{ // 返回上一页 this.transitionName = 'turning-down'; } } } } </script>
<style scoped lang="scss"> .child-view { position: absolute; left: 0; top: 0; width: 100%; height: 100%; transition: all 4s ease; transform-origin: 0% center; } .turning-down-enter{ opacity: 1; transform-origin: left; transform-style: preserve-3d; -webkit-transform: perspective(1000px) rotateY(-180deg); transform: perspective(1000px) rotateY(-180deg); } .turning-up-leave-active { transform-style: preserve-3d; transform: perspective(1000px) rotateY(-180deg); z-index: 100; } </style>
复制代码
配置路由
export default [
{
path: '/Home',
name: 'home',
component: () =>
import('../views/vue/vue-router/Home.vue'),
children: [
{
path: '/Home/First',
name: 'Home-First',
component: () =>
import('../views/vue/vue-router/First.vue'),
},
{
path: '/Home/Second',
name: 'Home-Second',
component: () =>
import('../views/vue/vue-router/Second.vue'),
}
]
}
]
复制代码
经过监听 Home
页面的路由变化,来改变 transitionName
,路由切换时切换 transition
组件的 enter/leave-active
样式,所以能够在路由切换时作到翻书效果。
随着项目的增大,项目中的页面可能达到好几十个,甚至更多,那么如何将这些页面进行管理呢?咱们的作法就是,将路由按照功能进行区分。
好比咱们分了5个区间,每一个区间有个数不一样的路由
-- a.js
-- b.js
-- c.js
-- d.js
-- e.js
复制代码
咱们须要将这五个路由分别引进来,并进行结合
import a from 'routers/a'
import b from 'routers/b'
import c from 'routers/c'
import d from 'routers/d'
import e from 'routers/e'
const routers = [].concat(a, b, c, d, e)
复制代码
之后咱们每次建一个新的分区,都要手动加上相应的逻辑,这样看起来很不方便,那么咱们有没有好的解决办法呢?
下面是我作的路由分区,利用 webpack
的 require.context
方法,将全部须要的路径导出来,require.context
有三个参数
第一个参数
,匹配的路径目录,(从当前目录开始算起)第二个参数
,是否须要深层遍历第三个参数
,正则匹配,匹配出你须要的路径须要注意的点,require
不能直接导出变量名
例如,下面的例子会报错
const a = './route/a.js'
// 会报错,a 不是一个模块
require(a)
复制代码
因此 require
中只能加字符串或者使用字符串拼接
const a = 'a.js'
require('./route/' + a)
复制代码
这样webpack会把 ./route/
下全部文件打包成模块,你才可使用 require
去引用
下面是一个完成的例子
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
const routes = []
const context = require.context('./router', true, /\/[\w]+\.(js|ts)$/)
context.keys().forEach(_ => {
const path = _.replace('./', '')
routes.push(...require('./router/' + path).routes)
})
export default new Router({
routes: [
{ path: '/', redirect: '/Home' },
...routes
]
})
复制代码
参考文章