《stateman》是波神的一个超级轻量的单页路由,拜读之后写写自己的小总结。
stateman的github地址
简单使用
以下文章全部以该Demo作为例子讲解。
Html:
复制代码
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) 表示终止路由跳转 ....} 复制代码