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之中;