使用回顾
通常我们使用vue-router会单独创建一个js来写vue-router的逻辑,如 src/router/index.js,并在main.js中引入,例如。
// src/router/index.js
Vue.use(Router) // 启动路由
// 路由实例
let router=new Router({
routes:[
{
//路由信息
}
]
})
// src/main.js
new Vue({
router,
render: h => h(App)
}).$mount('#app')
源码位置
两个地方可以找到:
1、Github源码
2、打开自己使用的项目,进入node_modules目录,找到vue-router下的src目录,index.js就是vue-router的源码入口。
index.js: 主代码逻辑
install.js: vue插件注册
history目录:提供不同路由模式下的方法
util目录:提供各种方法,路径处理、query、route、scroll、push-state、location处理等等
components目录:提供router-view和router-link组件
插件的加载
首先,install.js中定义一个install方法,可以供Vue加载插件。index.js中定义了一个叫做VueRouter的类(后面再讲),并将install方法赋值给VueRouter.install。
当我们import Router from 'vue-router',并使用Vue.use(Router)的时候,Vue就调用install方法进行插件注册。install方法中的内容如下(已省略可以不关心的内容):
export function install (Vue) {
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this) //初始化路由
Vue.util.defineReactive(this, '_route', this._router.history.current) //定义了响应式的_route属性
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
}
})
Object.defineProperty(Vue.prototype, '$router', { //在vue原型上定义$router
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', { //在vue原型上定义$route
get () { return this._routerRoot._route }
})
Vue.component('RouterView', View) //注册router-view 组件
Vue.component('RouterLink', Link) //注册router-link 组件
}
install方法做了如下事情:
- 通过vue.mixin混入的方式在组件生命周期钩子beforeCreate函数中注入初始化方法,并执行this._router.init(this)。
- 通过Vue.util.defineReactive(this, '_route', this._router.history.current),定义了响应式的_route属性。
- 这里在全局注册的好处是,影响之后创建的每一个Vue实例。
- 通过Object.defineProperty在全局定义$router方法和$route属性,并且$route其实就是响应式的_route,因此我们也可以在watch中监听$route的变化。
- 这里也全局注册了router-view和router-link组件,在任何地方都可以直接使用。
- 关于install函数与vue的关系,或者想自己开发插件,可参考 官方插件文档
index.js中VueRouter做了什么?
我们还是先来看看简化后的源码:
export default class VueRouter {
constructor (options: RouterOptions = {}) {
let mode = options.mode || 'hash'
this.fallback =
mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
}
init (app: any /* Vue component instance */) {
this.apps.push(app)
// 不用重新创建 history 的监听
if (this.app) {
return
}
this.app = app
const history = this.history
if (history instanceof HTML5History || history instanceof HashHistory) {
const setupListeners = routeOrError => {
history.setupListeners()
handleInitialScroll(routeOrError) //处理滚动
}
history.transitionTo(
history.getCurrentLocation(),
setupListeners,
setupListeners
)
}
history.listen(route => {
this.apps.forEach(app => {
app._route = route
})
})
}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
this.history.push(location, resolve, reject)
})
} else {
this.history.push(location, onComplete, onAbort)
}
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// $flow-disable-line
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
this.history.replace(location, resolve, reject)
})
} else {
this.history.replace(location, onComplete, onAbort)
}
}
//各种钩子函数的定义,这里省略了 beforeEach go back forward 等
}
这里代码可以分为四个部分:一是:constructor构造函数;二是:init函数;三是:push和replace方法;四是:其他方法,如生命周期钩子、go、back等。
- constructor构造函数:这里面的操作就是看我们的页面应当采用什么mode。
- 根据传入的options对象判断是否传入了mode参数(默认是‘hash’),mode支持 hash/history/abstract 三种模式,
- 三种模式并分别对应:HashHistory / HTML5Histroy / AbstractHistory 三种类,这三种类都继承了base类,代码在history目录,这里面定义了各自的push、replace、监听器、transitionTo等方法。
- 对mode参数对应的类该进行实例化,this.history = new 三个类中的一个(this, options.base)
- init函数:
- 根据this.history进行监听器的初始化,在组件beforeCreate的时候将会调用。
- history.listen中 app._route = route,又遇到了这个响应式的_route,修改_route后会触发render函数切换组件的渲染,从而更新页面内容。
- push/replace函数:也是调用this.history封装的方法。
- 其他方法:这里忽略不计了。
前端路由
前端路由,是把不同路由对应不同的内容或页面的任务交给前端来做,以前是通过服务端根据 url 不同返回不同的页面来实现。
前端路由方案 :
hash:可能是大多数人了解的模式,主要是基于锚点的原理实现,简单易用。
history:即使用 html5 标准中的 history api 通过监听 popstate 事件来对 dom 进行操作。每次路由变化都会引起重定向
Hash路由原理
hash(#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页,也就是说 #是用来指导浏览器动作的,对服务器端完全无用,HTTP请求中也不会不包括#;同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置。
通过以下原理实现更新视图而不用重新请求页面
- hash虽然出现在URL中,但不会被包括在HTTP请求中。改变hash不会重新加载页面。
- 可通过window.history.hash获取hash值。
- 可以监听hash值的变化: window.addEventListener("hashchange", funcRef, false)。
- 每次hash改变就会在浏览器中增加一个访问记录。
我们来看看代码:
export class HashHistory extends History {
constructor (router: Router, base: ?string, fallback: boolean) {
super(router, base)
// 如果是回退hash的情况,并且判断当前路径是否有/#/。如果没有将会添加'/#/'
if (fallback && checkFallback(this.base)) {
return
}
// 获取hash值,判断其最前面是否有/,没有则添加(通过history.replace)
ensureSlash()
}
// 延迟到app mounts后才设置的监听器
setupListeners () {
const handleRoutingEvent = () => {
this.transitionTo(getHash(), route => {
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
}
const eventType = supportsPushState ? 'popstate' : 'hashchange'
window.addEventListener(
eventType,
handleRoutingEvent
)
this.listeners.push(() => {
window.removeEventListener(eventType, handleRoutingEvent)
})
}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
route => {
pushHash(route.fullPath)
onComplete && onComplete(route)
},
onAbort
)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
route => {
replaceHash(route.fullPath)
onComplete && onComplete(route)
},
onAbort
)
}
}
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
window.location.hash = path
}
}
function replaceHash (path) {
if (supportsPushState) {
replaceState(getUrl(path))
} else {
window.location.replace(getUrl(path))
}
}
HashHistory类定义了监听器、push、replace方法等。
- 监听方法:我们的监听器会在init函数中进行设置,在hash模式下调用 history.transitionTo(url,setupListeners,setupListeners)。不管transitionTo成功或失败都会执行setupListeners,然后通过 window.addEventListener 监听hashChange事件,在支持pushState的环境中会监听popstate事件。
- 手动触发:当我们调用this.$router.push的时候,执行this.transitonTo函数,transitionTo执行成功后的回调中执行pushHash(),并执行window.location.hash=pash方法,实现链接地址的修改。同理this.$router.replace执行window.location.replace(path)。当然,这里会根据当前环境是否支持pushState,如果支持则会分别调用window.history.pushState()和window.history.replaceState()。
- transitionTo是base类中定义的方法,主要是实现了导航守卫的功能和更新触发。transitionTo中执行了一个confirmTransition的方法,其中会执行一系列的事件队列,其中包括beforeHooks,然后在confirmTransition的onComplete回调中依次执行updateRoute和 afterHooks。 updateRoute中执行this.cb && this.cb(route) ,也就是更新响应式的_route,触发render函数更新组件。
总结: hash路由改变到试图更新流程如下:
$router.push() --> HashHistory.push() --> History.transitionTo() --> History.updateRoute() --> {app._route = route} --> vm.render()
History路由原理
History interface是浏览器历史记录栈提供的接口,通过back(), forward(), go()等方法,我们可以读取浏览器历史记录栈的信息,进行各种跳转操作。
从HTML5开始,History interface中pushState(), replaceState() 不仅可以读取,还可以修改浏览器历史记录栈,例如:
window.history.pushState(stateObject, title,URL)
window.history.replaceState(stateObject, title, URL)
//stateObject参数的副本
//title: 所添加记录的标题
//URL: 所添加记录的UR
- push:与hash模式类似,只是将window.hash改为history.pushState
- replace:与hash模式类似,只是将window.replace改为history.replaceState
- 监听地址:在HTML5History的构造函数中监听popState(window.onpopstate)
- 这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前URL改变了,但浏览器不会立即发送请求该URL(the browser won't attempt to load this URL after a call to pushState()),这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。
HTML5 History API提供了一种功能,能让开发人员在不刷新整个页面的情况下修改站点的URL,就是利用 history.pushState API 来完成 URL 跳转而无须重新加载页面;通常情况下,我们会选择使用History模式,原因就是Hash模式下URL带着‘#’会显得不美观;但实际上,这样选择一不小心也会出问题,例如:链接中带路由参数,如果后端没有对应的路由处理,就会返回404错误;
为解决这一问题,vue-router提供的方法是:在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。
源码:几乎与hash模式的一致,唯独只能使用HTML5的几个apid
history路由改变到试图更新流程如下:
$router.push() --> HTML5History.push() --> History.transitionTo() --> History.updateRoute() --> {app._route = route} --> vm.render()
Abstrack路由原理
支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式。其原理为用一个数组stack模拟出浏览器历史记录栈的功能。
如带所示,用stack作为路由栈,push的时候推入一个路由,replace的时候替换最后一个路由。它不会向前面两种路由调用location.hash、location.history、pushState等等浏览器的api。
constructor (router: Router, base: ?string) {
super(router, base)
this.stack = []
this.index = -1
}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.transitionTo(
location,
route => {
this.stack = this.stack.slice(0, this.index + 1).concat(route)
this.index++
onComplete && onComplete(route)
},
onAbort
)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.transitionTo(
location,
route => {
this.stack = this.stack.slice(0, this.index).concat(route)
onComplete && onComplete(route)
},
onAbort
)
}
源码中还有很多细节待完善!
例如:
router-view的实现及组件更新
transitionTo中导航守卫的逻辑、路由切换中做了些什么处理等