10分钟彻底搞懂微信小程序单页面应用路由
单页面应用特征
「假设:」在一个web页面中,有1个按钮,点击可跳转到站内其他页面。
「多页面应用:」点击按钮,会从新加载一个html资源,刷新整个页面;
「单页面应用:」点击按钮,没有新的html请求,只发生局部刷新,能营造出一种接近原生的体验,如丝般顺滑。
SPA单页面应用为什么可以几乎无刷新呢?因为它的SP——single-page。在第一次进入应用时,即返回了唯一的html页面和它的公共静态资源,后续的所谓“跳转”,都不再从服务端拿html文件,只是DOM的替换操作,是模(jia)拟(zhuang)的。
那么js又是怎么捕捉到组件切换的时机,并且无刷新变更浏览器url呢?靠hash和HTML5History。
hash路由
特征
- 类似www.xiaoming.html#bar就是哈希路由,当#后面的哈希值发生变化时,不会向服务器请求数据,可以通过hashchange事件来监听到URL的变化,从而进行DOM操作来模拟页面跳转
- 不需要服务端配合
- 对SEO不友好
原理
HTML5History路由
特征
- History模式是HTML5新推出的功能,比之hash路由的方式直观,长成类似这个样子www.xiaoming.html/bar,模拟页面跳转是通过history.pushState(state,title,url)来更新浏览器路由,路由变化时监听popstate事件来操作DOM
- 需要后端配合,进行重定向
- 对SEO相对友好
原理
HTML5History
vue-router源码解读
以Vue的路由vue-router为例,我们一起来撸一把它的源码。
Tips:因为,本篇的重点在于讲解单页面路由的两种模式,所以,下面只列举了一些关键代码,主要讲解:
- 注册插件
- VueRouter的构造函数,区分路由模式
- 全局注册组件
- hash/HTML5History模式的push和监听方法
- transitionTo方法
注册插件
首先,作为一个插件,要有暴露一个install方法的自觉,给Vue爸爸去use。
源码的install.js文件中,定义了注册安装插件的方法install,给每个组件的钩子函数混入方法,并在beforeCreate钩子执行时初始化路由:
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)
}else{
this._routerRoot=(this.$parent&&this.$parent._routerRoot)||this
}
registerInstance(this,this)
},
//全文中以...来表示省略的方法
...
});
区分mode
然后,我们从index.js找到整个插件的基类VueRouter,不难看出,它是在constructor中,根据不同mode采用不同路由实例的。
...
import{install}from'./install';
import{HashHistory}from'./history/hash';
import{HTML5History}from'./history/html5';
...
exportdefaultclassVueRouter{
staticinstall:()=>void;
constructor(options:RouterOptions={}){
if(this.fallback){
mode='hash'
}
if(!inBrowser){
mode='abstract'
}
this.mode=mode
switch(mode){
case'history':
this.history=newHTML5History(this,options.base)
break
case'hash':
this.history=newHashHistory(this,options.base,this.fallback)
break
case'abstract':
this.history=newAbstractHistory(this,options.base)
break
default:
if(process.env.NODE_ENV!=='production'){
assert(false,`invalidmode:${mode}`)
}
}
}
}
全局注册router-link组件
这个时候,我们也许会问:使用vue-router时,常见的
回到install.js文件,它引入并全局注册了router-view、router-link组件:
importViewfrom'./components/view';
importLinkfrom'./components/link';
...
Vue.component('RouterView',View);
Vue.component('RouterLink',Link);
在./components/link.js中,
consthandler=e=>{
if(guardEvent(e)){
if(this.replace){
router.replace(location,noop)
}else{
router.push(location,noop)
}
}
};
就像最开始提到的,VueRouter构造函数中对不同mode初始化了不同模式的History实例,因而router.replace、router.push的方式也不尽相同。接下来,我们分别扒拉下这两个模式的源码。
hash模式
history/hash.js文件中,定义了HashHistory类,这货继承自history/base.js的History基类。
它的prototype上定义了push方法:在支持HTML5History模式的浏览器环境中(supportsPushState为true),调用history.pushState来改变浏览器地址;其他浏览器环境中,则会直接用location.hash=path来替换成新的hash地址。
其实,最开始读到这里是有些疑问的,既然已经是hash模式为何还要判断supportsPushState?原来,是为了支持scrollBehavior,history.pushState可以传参key过去,这样每个url历史都有一个key,用key保存了每个路由的位置信息。
同时,原型上绑定的setupListeners方法,负责监听hash变更的时机:在支持HTML5History模式的浏览器环境中,监听popstate事件;而其他浏览器中,则监听hashchange。监听到变化后,触发handleRoutingEvent方法,调用父类的transitionTo跳转逻辑,进行DOM的替换操作。
import{pushState,replaceState,supportsPushState}from'../util/push-state'
...
exportclassHashHistoryextendsHistory{
setupListeners(){
...
consthandleRoutingEvent=()=>{
constcurrent=this.current
if(!ensureSlash()){
return
}
//transitionTo调用的父类History下的跳转方法,跳转后路径会进行hash化
this.transitionTo(getHash(),route=>{
if(supportsScroll){
handleScroll(this.router,route,current,true)
}
if(!supportsPushState){
replaceHash(route.fullPath)
}
})
}
consteventType=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)
handleScroll(this.router,route,fromRoute,false)
onComplete&&onComplete(route)
},
onAbort
)
}
}
...
//处理传入path成hash形式的URL
functiongetUrl(path){
consthref=window.location.href
consti=href.indexOf('#')
constbase=i>=0?href.slice(0,i):href
return`${base}#${path}`
}
...
//替换hash
functionpushHash(path){
if(supportsPushState){
pushState(getUrl(path))
}else{
window.location.hash=path
}
}
//util/push-state.js文件中的方法
exportconstsupportsPushState=
inBrowser&&
(function(){
constua=window.navigator.userAgent
if(
(ua.indexOf('Android2.')!==-1||ua.indexOf('Android4.0')!==-1)&&
ua.indexOf('MobileSafari')!==-1&&
ua.indexOf('Chrome')===-1&&
ua.indexOf('WindowsPhone')===-1
){
returnfalse
}
returnwindow.history&&typeofwindow.history.pushState==='function'
})()
HTML5History模式
类似的,HTML5History类定义在history/html5.js中。
定义push原型方法,调用history.pusheState修改浏览器的路径。
与此同时,原型setupListeners方法对popstate进行了事件监听,适时做DOM替换。
import{pushState,replaceState,supportsPushState}from'../util/push-state';
...
exportclassHTML5HistoryextendsHistory{
setupListeners(){
consthandleRoutingEvent=()=>{
constcurrent=this.current;
constlocation=getLocation(this.base);
if(this.current===START&&location===this._startLocation){
return
}
this.transitionTo(location,route=>{
if(supportsScroll){
handleScroll(router,route,current,true)
}
})
}
window.addEventListener('popstate',handleRoutingEvent)
this.listeners.push(()=>{
window.removeEventListener('popstate',handleRoutingEvent)
})
}
push(location:RawLocation,onComplete?:Function,onAbort?:Function){
const{current:fromRoute}=this
this.transitionTo(location,route=>{
pushState(cleanPath(this.base+route.fullPath))
handleScroll(this.router,route,fromRoute,false)
onComplete&&onComplete(route)
},onAbort)
}
}
...
//util/push-state.js文件中的方法
exportfunctionpushState(url?:string,replace?:boolean){
saveScrollPosition()
consthistory=window.history
try{
if(replace){
conststateCopy=extend({},history.state)
stateCopy.key=getStateKey()
history.replaceState(stateCopy,'',url)
}else{
history.pushState({key:setStateKey(genStateKey())},'',url)
}
}catch(e){
window.location[replace?'replace':'assign'](url)
}
}
transitionTo处理路由变更逻辑
上面提到的两种路由模式,都在监听时触发了this.transitionTo,这到底是个啥呢?它其实是定义在history/base.js基类上的原型方法,用来处理路由的变更逻辑。
先通过constroute=this.router.match(location,this.current)对传入的值与当前值进行对比,返回相应的路由对象;接着判断新路由是否与当前路由相同,相同的话直接返回;不相同,则在this.confirmTransition中执行回调更新路由对象,并对视图相关DOM进行替换操作。
exportclassHistory{
...
transitionTo(
location:RawLocation,
onComplete?:Function,
onAbort?:Function
){
constroute=this.router.match(location,this.current)
this.confirmTransition(
route,
()=>{
constprev=this.current
this.updateRoute(route)
onComplete&&onComplete(route)
this.ensureURL()
this.router.afterHooks.forEach(hook=>{
hook&&hook(route,prev)
})
if(!this.ready){
this.ready=true
this.readyCbs.forEach(cb=>{
cb(route)
})
}
},
err=>{
if(onAbort){
onAbort(err)
}
if(err&&!this.ready){
this.ready=true
//https://github.com/vuejs/vue-router/issues/3225
if(!isRouterError(err,NavigationFailureType.redirected)){
this.readyErrorCbs.forEach(cb=>{
cb(err)
})
}else{
this.readyCbs.forEach(cb=>{
cb(route)
})
}
}
}
)
}
...
}
最后
好啦,以上就是单页面路由的一些小知识,希望我们能一起从入门到永不放弃~~
到此这篇关于10分钟彻底搞懂微信小程序单页面应用路由的文章就介绍到这了,更多相关小程序单页面应用路由内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。