《stateman》是波神的一个超级轻量的单页路由,拜读之后写写自己的小总结。
stateman的github地址 github.com/leeluolee/s…
简单使用
以下文章全部以该Demo作为例子讲解。
Html:
<ul>
<li><a href="#/home">/home"</a></li>
<li><a href="#/contact">/contact"</a></li>
<li><a href="#/contact/list">/contact/list</a></li>
<li><a href="#/contact/2">/contact/2</a></li>
<li><a href="#/contact/2/option">/contact/2/option</a></li>
<li><a href="#/contact/2/message">/contact/2/message</a></li>
</ul>
复制代码
Javascript:
const StateMan = require('../stateman');
let config = {
enter() {
console.log('enter: ' + this.name);
},
leave() {
console.log('leave: ' + this.name);
},
canLeave() {
console.log('canLeave: ' + this.name);
return true;
},
canEnter() {
console.log('canEnter: ' + this.name);
return true;
},
update() {
console.log('update: ' + this.name);
}
}
function create(o = {}){
o.enter= config.enter;
o.leave = config.leave;
o.canLeave = config.canLeave;
o.canEnter = config.canEnter;
o.update = config.update;
return o;
}
let stateman = new StateMan();
stateman
.state("home", config)
.state("contact", config)
.state("contact.list", config )
.state("contact.detail", create({url: ":id(\\d+)"}))
.state("contact.detail.option", config)
.state("contact.detail.message", config)
.start({});
复制代码
以上代码很简单,首先实例化StateMan,然后通过state函数来创建一个路由状态,同时传入路由的配置,最后通过start来启动,这时路由就开始工作了,以下讲解顺序会按照以上demo的代码执行顺序来讲解,一步一步解析stateman工作原理。
实例化路由:new StateMan()
function StateMan(options){
if(this instanceof StateMan === false){ return new StateMan(options)}
options = options || {};
this._states = {};
this._stashCallback = [];
this.strict = options.strict;
this.current = this.active = this;
this.title = options.title;
this.on("end", function(){
var cur = this.current,title;
while( cur ){
title = cur.title;
if(title) break;
cur = cur.parent;
}
document.title = typeof title === "function"? cur.title(): String( title || baseTitle ) ;
})
}
复制代码
这里的end事件会在state跳转完成后触发,这个后面会讲到,当跳转完成后会从当前state节点一层一层往上找到title设置赋给document.title
state树
stateman根据stateName的"."确定父子关系,整个路由的模块最终是上图右边的树状结构。
构建state树代码分析
StateMan.prototype.state
var State = require('./state.js');
var stateFn = State.prototype.state;
...
state: function(stateName, config){
var active = this.active;
if(typeof stateName === "string" && active){
stateName = stateName.replace("~", active.name)
if(active.parent) stateName = stateName.replace("^", active.parent.name || "");
}
// ^ represent current.parent
// ~ represent current
// only
return stateFn.apply(this, arguments);
}
复制代码
代码做了两件事:
- stateName的替换
- "~": 代表当前所处的active状态;
- "^": 代表active状态的父状态; 例如:
stateman.state({
"app.user": function() {
stateman.go("~.detail") // will navigate to app.user.detail
},
"app.contact.detail": function() {
stateman.go("^.message") // will navigate to app.contact.message
}
})
复制代码
- 使用State.prototype.state函数来找到或者创建state
stateFn.apply(this, arguments);
复制代码
State.prototype.state
state: function(stateName, config){
if(_.typeOf(stateName) === "object"){
for(var i in stateName){
this.state(i, stateName[i]); //注意,这里的this指向stateman
}
return this;
}
var current, next, nextName, states = this._states, i = 0;
if( typeof stateName === "string" ) stateName = stateName.split(".");
var slen = stateName.length, current = this;
var stack = [];
do{
nextName = stateName[i];
next = states[nextName];
stack.push(nextName);
if(!next){
if(!config) return;
next = states[nextName] = new State();
_.extend(next, {
parent: current,
manager: current.manager || current,
name: stack.join("."),
currentName: nextName
})
current.hasNext = true;
next.configUrl();
}
current = next;
states = next._states;
}while((++i) < slen )
if(config){
next.config(config);
return this;
} else {
return current;
}
}
复制代码
这个函数就是生成state树的核心,每一个state可以看作是一个节点,它的子节点由自己的_states来储存。在创建一个节点的时候,这个函数会将stateName以'.'分割,然后通过一个循环来从父节点向下检查,如果发现某一个节点不存在,就创建出来,同时配置它的url
state生成url:State.prototype.configUrl
configUrl: function(){
var url = "" , base = this, currentUrl;
var _watchedParam = [];
while( base ){
url = (typeof base.url === "string" ? base.url: (base.currentName || "")) + "/" + url;
// means absolute;
if(url.indexOf("^/") === 0) {
url = url.slice(1);
break;
}
base = base.parent;
}
this.pattern = _.cleanPath("/" + url);
var pathAndQuery = this.pattern.split("?");
this.pattern = pathAndQuery[0];
// some Query we need watched
_.extend(this, _.normalize(this.pattern), true);
}
复制代码
代码中以自己(当前state)为起点,向上连接父节点的url,如果url中带有^说明这是个绝对路径,这时候不会向上连接url
if(url.indexOf("^/") === 0) {
url = url.slice(1);
break;
}
复制代码
_.cleanPath(url): 把所有url的形式变成:'/some//some/' -> '/some/some'
_.normalize(path): 解析path
_.normalize('/contact/(detail)/:id/(name)');
=>
{
keys: [0, "id", 1],
matches: "/contact/(0)/(id)/(1)",
regexp: /^\/contact\/(detail)\/([\w-]+)\/(name)\/?$/
}
复制代码
启动路由:StateMan.prototype.start
start: function(options){
if( !this.history ) this.history = new Histery(options);
if( !this.history.isStart ){
this.history.on("change", _.bind(this._afterPathChange, this));
this.history.start();
}
return this;
},
复制代码
在启动路由的时候,同时做了3件事:
- 实例化history
- 监听history的change事件
- 启动history
这里监听了history的change事件这个动作,是连接stateman和history的桥梁。
history工作流程
history这边的代码逻辑比较清晰,所以不讲解太多代码,主要讲解流程。
主要的工作原理分为了3个路线:
- onhashchange:利用onhashchange事件来检测路由变化
- onpopstate:这个是html5新API,在我们点击浏览器前进后退时触发,也就是说hash改变的时候并不会出发这个事件,所有点击a标签的时候需要进行检测,点击a标签,阻止默认跳转,调用pushState来增加一条历史,然后路由触发跳转。
- iframe hack:在旧版本IE,IE8以下并不支持以上两个事件,这里设置了一个定时器,定时去查看路径是不是发生了变化,如果发生了变化,就触发路由跳转
生命周期:单页不同state之间的跳转
当路由跳转时,state树会按照以下顺序进行一系列的生命周期:
- 找到两个state节点的共同父节点
permission阶段:
- 从当前state节点往上到共同父节点进行canLeave
- 从共同父节点往下到目标节点进行canEnter
navigation阶段:
- 从当前state节点往上到共同父节点进行leave
- 从共同父节点往上到根节点进行update
- 从共同父节点往下到目标节点进行enter
流程分析
在stateman的start函数中有这么一句话:
this.history.on("change", _.bind(this._afterPathChange, this));
复制代码
上面说了,在history模块路由变化最终会触发change事件,所以这里会执行this._afterPatchChange函数
核心关键在于walk-transit-loop之间的循环和回调的执行。
第一次walk函数时为permission阶段,第二次为navigation阶段
每次walk函数执行2次transit函数,所以transit函数共执行4次
2次为从当前节点到共同父节点的遍历(canLeave、leave)
2次为从共同父节点到目标节点的遍历(canEnter、enter)
每次的遍历都是通过loop函数来执行,
节点之间的移动通过moveOn函数来执行
每一个函数我就不拿出来细讲了,没错,着一定是一篇假的源码解析。
这里提一下permission阶段的canLeave、canEnter是支持异步的。
permission阶段返回Promise
在_moveOn里面有这么一段代码:
function done( notRejected ){
if( isDone ) return;
isPending = false;
isDone = true;
callback( notRejected );
}
...
var retValue = applied[method]? applied[method]( option ): true;
...
if( _.isPromise(retValue) ){
return this._wrapPromise(retValue, done);
}
复制代码
另外,_wrapPromise函数为:
_wrapPromise: function( promise, next ){
return promise.then( next, function(){next(false)}) ;
}
复制代码
代码很少,理解起来也容易,就是在moveOn的时候如果canLeave、canEnter函数执行返回值是一个Promise,那么moveOn函数会终止,同时通过done传入这个Promise,在Fulfilled的时候触发,done函数会执行callback,也就是loop函数,从而继续生命周期的循环。
在不支持Promise的环境的异步
moveOn里面提供了option.sync函数来让我们手动停止moveOn的循环。
option.async = function(){
isPending = true;
return done;
}
...
if( !isPending ) done( retValue ) //代码的最后是这样的
复制代码
从最后一句来看,我们如果需要异步的话,举个例子,在canLeave函数中:
canLeave: function(option) {
var done = option.sync(); // return the done function
....
省略你的业务代码,在你业务代码结束后使用:
done(true) 表示继续执行
done(false) 表示终止路由跳转
....
}
复制代码