write by yinmingjun, 引用请注明。
序言
ember.js是本人看到过的最有野心的javascript的SPA框架之一,就其技术架构的设计来看,非常适合做大型SPA应用开发。不过另外一个方面,就是ember.js相关的技术文档是出奇的少,即使看ember官网提供的DEMO,也很难直观的体现出ember所具有的优势,估计很多人看过之后对ember.js还是懵懵懂懂。最近一直在读ember.js的代码,对ember.js的架构心仪不已。为了能够让更多的人了解ember,本人将基于代码分析,写一些介绍ember的技术文章,希望能对ember的推广做一点点事情。
ember.js在javascript框架中比较另类,其实更像是一个服务器端的框架。它在基础的继承体系和观察者模式上做了很多的基础工作,这个在我前面写的文章中能够看到。
接下来我们聚焦于ember.js的应用体系,了解ember.js应用框架的工作的过程。我们将从Ember.Application开始,分析Ember的应用模型的生命周期,解读ember的应用从route到页面render的过程,了解ember.js的容器概念,掌握ember.js的名称映射的处理规则。
ember应用的生命周期
1、Ember.Application的初始化过程
先看一下Ember.Application的初始化过程。
Ember.Application的创建代码:
App = Ember.Application.create();
时序图:
在调用Ember.Application的create之后,会触发一系列代码的执行,我们用时序图将执行的代码描述如下:
Application.create()
---->this.init() //create的初始化代码的入口
---->this.__container__ = this.buildContainer(); //初始化container
----> this.Router = this.Router || this.defaultRouter(); //设置this.Router
----> this.scheduleInitialize() //设置this._initialize在$.ready的时候执行
App.Router.map(function() { //填充路由表
//...
});
$.ready()
---->this._initialize()
----> this.register('router:main', this.Router);
---->Ember.runLoadHooks('application', this);
----> this.advanceReadiness();
----> Ember.run.once(this, this.didBecomeReady)
Ember.Run()
---->didBecomeReady
----> this.setupEventDispatcher()
----> this.ready();
----> this.startRouting()
----> this.resolve(this);
描述:
Application的创建过程比较特殊,先是通过Ember.Object的create方法创建对象的实例,然后运行其提供的init方法开始其对象实例的初始化过程,在init方法中,会通过buildContainer方法为Application创建容器,容器的概念后面我们会介绍。
注:
看到container这个名字,我会想起来java的spring框架,两者的概念接近,都是对象的容器和依赖注入的工具。
在init方法中,因用户提供的设置Route的代码还没执行,所以又提供了延迟初始化的代码,包装在其_initialize方法中,在jquery的ready方法中执行。到_initialize方法中的时候,用户的Route信息已经填充好了,接下来Application会向其container中注册'router:main'的名称,value设置为this.Router,作为router的根节点的总入口;然后运行名称为'application'的load队列,最后会调度this.didBecomeReady的运行。
在didBecomeReady方法中,会做DOM级别的初始化,然后通过this.startRouting开始做启动默认的路由,开始页面的render。
2、Application的startRouting过程
时序图:
Application.startRouting()
----> var router = this.__container__.lookup('router:main');
----> router.startRouting(); //router是Ember.Router类型
----> this.handleURL(location.getURL()); //是#后面的部分,作为route的相对url
----> this.router.handleURL(url) //Ember.Router的router成员的类型是Router,在router module中定义
----> var results = this.recognizer.recognize(url); //Router类的recognizer成员是RouteRecognizer类型,
//定义在route-recognizer module之中
---->collectObjects(this, results, 0, []);
---->setupContexts(router, objects); //设置router的运行上下文环境
----> eachHandler(partition.entered, function(handler, context, handlerInfo) {... //对每个handler执行函数
----> handler.setup(context) //每个handler都是Ember.Route的实例
----> var controller = this.controllerFor(this.routeName, context); //如果需要,会创建controller
----> this.controller= controller; //设置route的controller
----> this.setupController(controller, context); //初始化controller
----> this.renderTemplate(controller, context);
----> this.render();
----> view =setupView(view, container, options);
----> appendView(this, view, options);
----> router.didTransition(handlerInfos); //执行route的后事件
描述:
在Application的startRouting方法中,会查找其container中的名字是"router:main"来做最初的route,route的处理过程分两个阶段,第一个阶段是url的识别;第二个阶段是请求的派发。
url的识别过程是通过RouteRecognizer来完成的,Router其将获取的url规范化之后,通过RouteRecognizer将规范化后的url转换成解析后的结果,这个结果中的主要成分是route的name和route的handler。接下来,在请求派发的过程中,对每个handler调用其setup方法, 而在route的handler的setup的过程中,会创建和设置每个route的controller,并最终通过route的render方法,将关联的view输出到DOM树。
ember.js中的核心问题领域,就是根据其名称映射的规则+用户提供的route-map信息,构造出各个route的handler,并在根据规则创建controller的实例。
OK,我们接下来看ember.js提供的逻辑扩展点。ember.js提供了对Route的扩展;对Controller的扩展;对Template的名称映射;对View的映射规则。这些内容才是ember.js框架的核心问题领域。
解读ember的名称映射规则
1、了解ember的container概念
在前面Application的初始化的过程中,提到Application中会使用buildContainer获取一个container,这个container与spring中的container的概念是一脉相承的(与一般意义上的容器的概念大相径庭),其本意是对象的容器。在ember.js体系中,container的影响可谓巨大,渗透于ember.js对象创建的各个环节,是ember.js实现其名称映射规则的核心组件。
container定义于'container'模块,其包含几个方法,其中最主要的就是register、lookup和injection几个方法,分别对应与对象实例的注册和对象实例的查找和依赖的注入,涵盖contrainer的大部分的功能。我们以解析这几个方法为主线,研究container的工作过程。
我们先看看container中注册的name的结构,一般在container中注册的名字有下面的结构:
typeName:partialName
前面的部分是typeName,是用于区分名称的不同来源,如'route','controller','template'等,后面是类型下的名称,如'Index','post'等,大多来自url的解析过程。
在Ember.Application类的buildContainer方法里面,就注册了大量的名称,我们看看:
buildContainer:function(namespace) {
var container = new Ember.Container();
Ember.Container.defaultContainer = new DeprecatedContainer(container);
container.set=Ember.set;
container.normalize=normalize;
container.resolver=resolverFor(namespace);
container.optionsForType('view', { singleton: false });
container.optionsForType('template', { instantiate: false });
container.register('application:main', namespace, { instantiate: false });
container.register('controller:basic',Ember.Controller, { instantiate: false });
container.register('controller:object',Ember.ObjectController, { instantiate: false });
container.register('controller:array',Ember.ArrayController, { instantiate: false });
container.register('route:basic',Ember.Route, { instantiate: false });
container.register('event_dispatcher:main', Ember.EventDispatcher);
container.injection('router:main','namespace','application:main');
container.injection('controller','target','router:main');
container.injection('controller','namespace','application:main');
container.injection('route','router','router:main');
return container;
}
注释:
optionsForType是为特定的类型设置的构造的选项的API。injection用于提供对象实例的依赖注入,将指定的名称(类型)的构造的实例中,将指定的对象注入该实例的指定的属性之中。register可以指定自己的options,该options的使用的优先级明显是高于optionsForType指定的options。
接下来,我们看看Container的一些关键的实现。
Container的register方法:
register:function(type, name, factory, options) {
var fullName;
if (type.indexOf(':') !== -1){
options = factory;
factory = name;
fullName = type;
} else {
Ember.deprecate('register("'+type +'", "'+ name+'") is now deprecated in-favour of register("'+type+':'+name+'");', false);
fullName =type + ":" + name;
}
var normalizedName = this.normalize(fullName);
this.registry.set(normalizedName, factory);
this._options.set(normalizedName, options || {});
},
说明:
实例的注册方法,通过type和name来注册对象的factory。如果type中包含':',说明type包含的是fullname,去掉name参数。
Container的lookup方法:
lookup:function(fullName,options) {
fullName = this.normalize(fullName);
options = options || {};
if (this.cache.has(fullName) && options.singleton!== false) {
returnthis.cache.get(fullName);
}
var value =instantiate(this, fullName);
if (!value) { return; }
if (isSingleton(this, fullName) &&options.singleton!== false) {
this.cache.set(fullName, value);
}
return value;
},
Container的instantiate方法:
functioninstantiate(container, fullName) {
var factory =factoryFor(container, fullName);
var splitName = fullName.split(":"),
type = splitName[0],
value;
if (option(container, fullName,'instantiate') ===false) {
return factory;
}
if (factory) {
var injections = [];
injections =injections.concat(container.typeInjections.get(type) || []);
injections =injections.concat(container.injections[fullName] || []);
var hash =buildInjections(container, injections);
hash.container = container;
hash._debugContainerKey = fullName;
value =factory.create(hash);
return value;
}
}
说明:
通过factoryFor找到对象的factory,根据options的'instantiate'属性决定是否实例化,如果不实例化,将factory返回;如果实例化会将定义的依赖注入到对象之中。
Container的injection方法:
injection:function(factoryName,property,injectionName) {
if (this.parent) { illegalChildOperation('injection'); }
if (factoryName.indexOf(':') === -1) {
return this.typeInjection(factoryName, property, injectionName);
}
var injections = this.injections[factoryName] = this.injections[factoryName] || [];
injections.push({ property: property, fullName:injectionName});
},
说明:
如果factoryName参数中没有':',说明依赖注入的是整个大的类型;否则只是针对指定的factoryName的依赖注入。
Container的factoryFor和resolve方法:
function factoryFor(container, fullName) {
var name = container.normalize(fullName);
return container.resolve(name);
}
resolve:function(fullName) {
returnthis.resolver(fullName) || this.registry.get(fullName);
},
说明:
定位对象的factory,通过其resolver和registry来查找的,resolver的优先级更高。
注释:
顺便说一下,作为组织代码的主要的场所,Application通过其register和inject两个API发布了container的部分功能,对于注册对象工厂和依赖注入提供支持。代码如下:
register: function() {
var container = this.__container__;
container.register.apply(container, arguments);
},
inject: function(){
var container = this.__container__;
container.injection.apply(container, arguments);
},
2、Container的resolver的来源
上面看到,在container实例化一个对象之前,需要先找到其factory,这个过程需要container的resolver的帮助。那container的resolver是怎么来的呢?
在Application的buildContainer方法中,有一行代码:
container.resolver=resolverFor(namespace);
请记住namespace就是Application。
接下来看resolverFor方法:
functionresolverFor(namespace) {
varresolverClass=namespace.get('resolver')||Ember.DefaultResolver;
var resolver = resolverClass.create({
namespace: namespace
});
return function(fullName) {
return resolver.resolve(fullName);
};
}
也就是说,我们可以在Application的'resolver'属性中,指定我们提供的Resolver类;要么就使用Ember.DefaultResolver类。这个策略很灵活,我很喜欢。
3、Ember.DefaultResolver类分析
这个类我们先看其核心的resolve方法:
resolve:function(fullName) {
var parsedName = this.parseName(fullName),
typeSpecificResolveMethod= this[parsedName.resolveMethodName];
if (typeSpecificResolveMethod) {
var resolved =typeSpecificResolveMethod.call(this, parsedName);
if (resolved) { return resolved; }
}
return this.resolveOther(parsedName);
},
说明:
这个比较简单,先将fullName交给parseName方法解析,然后在自己的方法表中找parsedName.resolveMethodName方法 ,如果存在,调用该方法获取factory,否则通过resolveOther来获取factory。
parseName方法:
parseName:function(fullName) {
var nameParts = fullName.split(":"),
type = nameParts[0], fullNameWithoutType = nameParts[1],
name = fullNameWithoutType,
namespace = get(this, 'namespace'),
root=namespace;
if (type !== 'template' && name.indexOf('/') !== -1) {
var parts = name.split('/');
name = parts[parts.length - 1];
var namespaceName = capitalize(parts.slice(0, -1).join('.'));
root = Ember.Namespace.byName(namespaceName);
Ember.assert('You are looking for a ' + name + ' ' + type + ' in the ' + namespaceName + ' namespace, but the namespace could not be found', root);
}
return {
fullName: fullName,
type: type,
fullNameWithoutType: fullNameWithoutType,
name: name,
root: root,
resolveMethodName: "resolve" + classify(type)
};
},
说明:
解析的root是resolver的namespace属性,其实就是Application。这里有一个特例,如果type不是'template',如果名称中包含'/',会将'/'转换成'.',并将最后的一个部分作为name,前面的作为namespace,并从Ember.Namespace中查找对应名字的namespace作为root,不过这个部分的内容不是我们关注的重点了。resolveMethodName的结果是"resolve" + classify(type),也就是说template、route对应的方法是'resolveTemplate'、'resolveRoute'。
各个'resolve'方法:
resolveTemplate: function(parsedName) {
var templateName = parsedName.fullNameWithoutType.replace(/\./g, '/');
if (Ember.TEMPLATES[templateName]) {
return Ember.TEMPLATES[templateName];
}
templateName = decamelize(templateName);
if (Ember.TEMPLATES[templateName]) {
return Ember.TEMPLATES[templateName];
}
},
useRouterNaming: function(parsedName) {
parsedName.name = parsedName.name.replace(/\./g, '_');
if (parsedName.name === 'basic') {
parsedName.name = '';
}
},
resolveController: function(parsedName) {
this.useRouterNaming(parsedName);
return this.resolveOther(parsedName);
},
resolveRoute: function(parsedName) {
this.useRouterNaming(parsedName);
return this.resolveOther(parsedName);
},
resolveView: function(parsedName) {
this.useRouterNaming(parsedName);
return this.resolveOther(parsedName);
},
resolveOther: function(parsedName) {
var className = classify(parsedName.name) + classify(parsedName.type),
factory = get(parsedName.root, className);
if (factory) { return factory; }
}
说明:
作为template的处理,与其他类型的不同。首先,名称都是假定以'.'分割,在后续的处理上,template将'.'替换成'/',然后在template集合中查找,如果找不到,将template名称去小骆驼化(将大小写分割之处添加'_',并将首字母大写改成小写)之后再次查找,也就是说,对于'posts.index'的routeName,对应的是'posts/index'的template name。
而对于其他类型的resolve,会将名称中的'.'替换成'_',并做classify处理(会将'_'去掉,并将分割的单词大骆驼化处理),并将type部分的名称classify之后添加到后面(也就是说,对于'posts.index'的controller,经处理之后会映射成'PostsIndexController',route和view类似)。最后在提供的root(看上面的分析,一般是Application)中查找对应的属性,并将其返回值作为factory。有一点需要主要,name如果是basic会被特殊处理,名称会替换成空串。
值得注意的是resolve仅仅是一种映射的机制,任何一种类型都可以拿来resolve。
例:
'template:post' //=> Ember.TEMPLATES['post']
'template:posts/byline' //=> Ember.TEMPLATES['posts/byline']
'template:posts.byline' //=> Ember.TEMPLATES['posts/byline']
'template:blogPost' //=> Ember.TEMPLATES['blogPost']
// OR
// Ember.TEMPLATES['blog_post']
'controller:post' //=> App.PostController
'controller:posts.index' //=> App.PostsIndexController
'controller:blog/post' //=> Blog.PostController
'controller:basic' //=> Ember.Controller
'route:post' //=> App.PostRoute
'route:posts.index' //=> App.PostsIndexRoute
'route:blog/post' //=> Blog.PostRoute
'route:basic' //=> Ember.Route
'view:post' //=> App.PostView
'view:posts.index' //=> App.PostsIndexView
'view:blog/post' //=> Blog.PostView
'view:basic' //=> Ember.View
'foo:post' //=> App.PostFoo
4、route table的构造
一般,我们在创建应用之后,最关键的任务就是维护route table。在ember.js中,route table的维护的方式如下:
App.Router.map(function() {
// put your routes here
this.resource( 'index', { path: '/' } );
this.resource('posts', function() {
this.route('new');
});
});
ember.js对route table的维护是借助于DSL类完成的,上面的map的callback中的this,以及resource的callback中的this,都是DSL类的实例。
DSL类的部分代码如下:
DSL.prototype = {
resource: function(name, options, callback) {
if (arguments.length === 2 && typeof options === 'function') {
callback = options;
options = {};
}
if (arguments.length === 1) {
options = {};
}
if (typeof options.path !== 'string') {
options.path = "/" + name;
}
if (callback) {
var dsl = new DSL(name);
callback.call(dsl);
this.push(options.path, name, dsl.generate());
} else {
this.push(options.path, name);
}
},
push: function(url, name, callback) {
var parts = name.split('.');
if (url === "" || url === "/" || parts[parts.length-1] === "index") { this.explicitIndex = true; }
this.matches.push([url, name, callback]);
},
route: function(name, options) {
Ember.assert("You must use `this.resource` to nest", typeof options !== 'function');
options = options || {};
if (typeof options.path !== 'string') {
options.path = "/" + name;
}
if (this.parent && this.parent !== 'application') {
name = this.parent + "." + name;
}
this.push(options.path, name);
},
说明:
DSL的resource方法默认是有3个参数,name、options和callback,其中,name是用于映射的名称,options.path对应实际的url路径,callback用于书写resource内的route map。options是可以省略的参数,只提供name和callback参数,而callback参数也可以省略,只提供name参数。如果options中的path没提供,会通过'/'+name的方式构造path,这对name是'blog/post'的route,显然不是希望的结果。
DSL的route方法有2个参数,name和options,options参数同样可以省略。如果options中的path没提供,会用'/'+name来构造path。需要注意的是,如果存在parent,并且parent不是'application',那么会用parent+'.'+name作为route的最终名称。
5、route的handler的获取过程
这部分我们关注ember.js是如何找到一个url对应的handler的。看下面的代码:
function getHandlerFunction(router) {
var seen = {}, container = router.container,
DefaultRoute = container.resolve('route:basic');
return function(name) {
var routeName ='route:' + name,
handler =container.lookup(routeName);
if (seen[name]) { return handler; }
seen[name] = true;
if (!handler) {
if (name==='loading') { return {}; }
if (name==='failure') { return router.constructor.defaultFailureHandler; }
container.register(routeName,DefaultRoute.extend());
handler = container.lookup(routeName);
if (get(router, 'namespace.LOG_ACTIVE_GENERATION')) {
Ember.Logger.info("generated -> " + routeName, { fullName: routeName });
}
}
if (name === 'application') {
// Inject default `routeTo` handler.
handler.events = handler.events || {};
handler.events.routeTo = handler.events.routeTo || Ember.TransitionEvent.defaultHandler;
}
handler.routeName = name;
return handler;
};
}
说明:
这部分代码要结合前面的代码来看才会理解。这段代码用来生成Ember.Router的getHandler方法,其过程很清晰。首先,默认的Route来自Container的'route:basic',就是Ember.Route;然后,会将使用'route:'+name作为名称,尝试从container中查找route的handler,从我们上面的分析知道,这会是从Application中定位factory的过程。对于同一个名称只会查找route一次。如果没找到,会将DefaultRoute作为factory注册到container之中,会在后续的lookup实例化route,并返回。
6、route的url的解析
下面,看看一个实际route的url与route mapping中的url的匹配过程,下面的代码来自"route-recognizer"模块的parse方法:
function parse(route, names, types) {
// normalize route as not starting with a "/". Recognition will
// also normalize.
if (route.charAt(0) === "/") { route = route.substr(1); }
var segments = route.split("/"), results = [];
for (var i=0, l=segments.length; i<l; i++) {
var segment = segments[i], match;
if (match = segment.match(/^:([^\/]+)$/)) {
results.push(new DynamicSegment(match[1]));
names.push(match[1]);
types.dynamics++;
} else if (match = segment.match(/^\*([^\/]+)$/)) {
results.push(new StarSegment(match[1]));
names.push(match[1]);
types.stars++;
} else if(segment === "") {
results.push(new EpsilonSegment());
} else {
results.push(new StaticSegment(segment));
types.statics++;
}
}
return results;
}
parse方法用于解析一段url,并将解析的结果返回。从类型上来看,url是通过'/'分割的url的片段,每个片段可能的形式有:
-
':xxxx' ==> DynamicSegment
-
'*xxxx' ==> StarSegment
-
'' ==> EpsilonSegment
-
其他 ==> StaticSegment
这几个segment类型的定义如下:
function StaticSegment(string) { this.string = string; }
StaticSegment.prototype = {
eachChar: function(callback) {
var string = this.string, char;
for (var i=0, l=string.length; i<l; i++) {
char = string.charAt(i);
callback({ validChars: char });
}
},
regex: function() {
return this.string.replace(escapeRegex, '\\$1');
},
generate: function() {
return this.string;
}
};
function DynamicSegment(name) { this.name = name; }
DynamicSegment.prototype = {
eachChar: function(callback) {
callback({ invalidChars: "/", repeat: true });
},
regex: function() {
return "([^/]+)";
},
generate: function(params) {
return params[this.name];
}
};
function StarSegment(name) { this.name = name; }
StarSegment.prototype = {
eachChar: function(callback) {
callback({ invalidChars: "", repeat: true });
},
regex: function() {
return "(.+)";
},
generate: function(params) {
return params[this.name];
}
};
function EpsilonSegment() {}
EpsilonSegment.prototype = {
eachChar: function() {},
regex: function() { return ""; },
generate: function() { return ""; }
};
这几个Segment类型包含几个方法,eachChar、regex和generate,其中regex用于构造url匹配的正则表达式,generate用于从数据产生对应的url。
对正则表达式的匹配过程是在"route-recognizer"模块的State类的findHandler方法中:
function findHandler(state, path) {
var handlers = state.handlers, regex = state.regex;
var captures = path.match(regex), currentCapture = 1;
var result = [];
for (var i=0, l=handlers.length; i<l; i++) {
var handler = handlers[i], names = handler.names, params = {};
for (var j=0, m=names.length; j<m; j++) {
params[names[j]] = captures[currentCapture++];
}
result.push({ handler: handler.handler, params: params, isDynamic: !!names.length });
}
return result;
}
也就是说,会根据各个segment的regex的匹配的参数和提供的名称,来填充params。
这基本上明确了url进入参数列表的几种方式:
(1) 通过':xxxx'的形式;
(2) 通过'*xxxx'的形式;
两种方式是等价的,xxxx对应参数的名称,对应url的值是会被填充到params中作为value。
EpsilonSegment在处理中会被忽略;StaticSegment会被确保按字面的值来匹配(会去正则化处理)。
小结
本文从ember.js的应用模型入手,重点解读了ember.js的运行时序和其route mapping的规则,ember.js支持的这种形式化的映射规则很诱人,ember.js将很多繁琐的工作都接管过去,使开发者可以专注于业务逻辑的处理。这正是我喜欢的工作方式,希望有机会将ember.js引入到自己的作品之中。
下面将ember的name mapping的规则做一个小的汇总:
-
ember.js通过container支持对象工厂和依赖注入,Application通过register和inject API发布container的这部分功能;
-
container中管理的名称的格式是'type:name'的模式;
-
ember.js通过Ember.DefaultResolver来支持对象工厂的默认查找规则,默认的resolver可以通过Application的'resolver'属性替换;
-
DefaultResolver对template的处理,会将templateName中的'.'替换成'/',然后在template集合中查找,如果找不到,会将template名称去小骆驼化(将大小写分割之处添加'_',并将首字母大写改成小写)之后再次查找;
-
DefaultResolver对其他类型的resolve,会将名称中的'.'替换成'_',并做classify处理(会将'_'去掉,并将分割的单词大骆驼化处理),并将type部分的名称classify之后添加到后面(也就是说,对于'posts.index'的controller,经处理之后会映射成'PostsIndexController',route和view类似)。
-
DefaultResolver提供的root一般是Application,如果name是'blog/post'的形式,会从Blog类作为root,并从其中查找对应的名字。
-
resolve仅仅是一种映射的机制,任何一种类型都可以拿来resolve。
-
map的options中,如果对应的url没有提供,默认会以'/'+name的方式构造url;
-
route map中,如果是使用resource方法注册route,会设置其下的parent是resource的名字;
-
route map中,如果是使用route方法注册name-url的mapping,会用parent+'.'+name作为route的最终名称;
-
可以通过':xxxx'或'*xxxx'定义参数,从url中获取对应的值,填充到params之中;