Skip to content

Instantly share code, notes, and snippets.

@cicihu
Last active December 25, 2015 06:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cicihu/6930872 to your computer and use it in GitHub Desktop.
Save cicihu/6930872 to your computer and use it in GitHub Desktop.
漫步 Ember.js

概念

总体来说,Ember.js 基本遵循经典的 MVC 架构体系。和许多声称是 JavaScript MVC 的框架相比,Ember.js 更加正统。模型、视图(模板)、控制器,一一对应,职责明确且符合惯例。而 RESTful 数据适配和路由器等机制则无时无刻不让人想起 Ruby on Rails 框架以及许多追随者的身影。最新的组件(Components)机制又紧跟 Web/Browser 发展的潮流,毫不落伍。

模板(Template)

  • Handlebars;
  • 描绘应用的用户界面;
  • 依靠模型的支持,当模型改变时会自动更新自己;
  • 在 Plain HTML 的基础上,还可以包括:
    • 表达式(Expressions)
      比如 {{name}},可从对应的模型中获取 name 属性的值并填充到 HTML 中。
    • 插口(Outlets)
      当用户在应用中四处浏览的时候,路由器控制着不同的模板连接至插口,从而组合成视图。从入口视图开始,总会有至少一个 {{outlet}} 声明(插口的助手方法),这样其他模板就可以从这里进入变成新的视图。具体是哪个模板以及在什么情况下进入则由路由器来约束。
    • 组件(Components)
      自定义的 HTML 元素。通过创建可重用的控制元件(即组件)来消除模板的重复。

路由器(Router)

路由器把 URL 规则转化为层叠嵌套的模板,每一个模板依靠模型的支持。当模板及其支撑模型展示在用户的浏览器中时,Ember.js 会自动保持 URL 的同步。

这样一来,用户就可以通过 URL 来分享应用中的任何一个页面了,当某人点击了这个 URL,看到的页面一定会和分享者一样。

组件(Components)

组件即是你可以自定义的 HTML 标签,你可以用 Handlebars 来控制它在哪里出现,而它的行为则由你写的 JavaScript 代码来实现。如此一来你便可以创造可重用的控制元件从而简化你的模板。

模型(Models)

模型是保存持久化状态的对象。模板的职责是把这些状态转变为 HTML 呈现给用户。在多数应用中,尽管 Ember.js 不知道你使用的后端是什么,不过模型是通过 HTTP JSON API 来获取的。

路由(Route)

路由是一个对象,它告知模板应该显示哪个模型。

控制器(Controllers)

控制器是保存应用程序状态的对象。除了模型之外,模板还可以有选择的拥有一个控制器,并且可以通过它们(模型与控制器)得到属性。

命名约定

Ember.js 拥有一些命名约定以便把各种对象整合在一起,遵循这些命名约定是必要的。

顶级命名空间

首先,创建应用的顶级命名空间:

window.App = Ember.Application.create();

App 就是你的应用(充当命名空间是它的职责),因为它是 Ember.Application 的一个实例。你可以把 App 替换成任意一个名字,比如 Blog 什么的,只要它不会在 window 下存在命名冲突。这暗示着:任何时候我们使用 Ember.js,App 是你唯一需要暴露给 window 的对象,其他后续创建的一切都是从属于 App 这个顶级命名空间的,这样 Ember.js 才能很好地组织和管理它们,并且也杜绝了和其他 JS 类库冲突的情况发生。

在这之后,当应用程序启动时(在浏览器中加载)Ember.js 会开始查找以下部分:

App.ApplicationRoute
App.ApplicationController
application 模板

对 Ember.js 来说,application 模板是整个应用的默认入口,是主模板。如果显式定义了 App.ApplicationController,那么 Ember.js 会创建一个 App.ApplicationController 的实例作为 application 模板的控制器。这意味着 template 模板可以获得控制器中的属性。

如果显式定义了 App.ApplicationRoute,那么 Ember.js 在渲染 template 模板之前会先执行位于 App.ApplicationRoute 内的路由器钩子方法(Router Hooks)。

写一个简单的例子来说明以上细节:

App.ApplicationRoute = Ember.Route.extend({                  // 1.1
  setupController: function(controller) {                    // 1.2
    controller.set('title', 'Nice to meet you, Ember.js!');  // 1.3
  }
});

App.ApplicationController = Ember.Controller.extend({        // 2.1
  appName: 'An Example Ember.js Application'                 // 2.2
});
<!-- 3 -->
<h1>{{appName}}</h1>
<h2>{{title}}</h2>

再详细的过一遍,看看都发生了什么:

  1. App.ApplicationRoute
  2. 这是 application 的路由定义,它引导 Ember.js 渲染 application 模板,这一点即使没有显式定义它也是一样的。此处显式定义是为了编写路由器钩子方法。
  3. setupController 就是一个路由器钩子方法(不是唯一的一个),它将对应的控制器实例 controller 传进方法内以便进一步的设置。此时对应的模板是 application,这就体现了 Ember.js 的命名约定:模板是 application,对应的路由就是 ApplicationRoute,下面的 ApplicationController 也是一样的道理。
  4. set 方法为 controller 这个对象设置属性,之后可以在 application 模板里获取这个属性的值。
  5. App.ApplicationController
  6. 这是 application 的控制器定义,它的实例对象会成为 application 模板的控制器。
  7. 这是为控制器实例对象定义的另外一个属性,稍后也会在模板里调用。
  8. 这是 application 模板了,{{appName}}{{title}} 的值分别来自上面路由里的钩子方法和控制器里的属性定义。

在 Ember.js 应用里,控制器总是以类定义的形式编写,你无须去实例化它们,Ember.js 会负责去实例化并提供给对应的模板。这使得测试变得更容易,并且能保证每一个控制器在整个应用里只有一个对应的实例。

简单路由(Simple Route)

应用中的每一个路由都会有一个控制器和一个模板,它们的名称都和路由的一样。

因此像这样的一个路由定义:

App.Router.map(function() {
  this.route('favorites');
});

当用户导航至 /favorites 时,Ember.js 会寻找这些对象:

App.FavoritesRoute
App.FavoritesController
favorites 模板

模板的渲染是这样的:Ember.js 会把 favorites 模板的内容插入到 application 模板内的 {{outlet}},同时实例化 App.FavoritesController 作为这个模板的控制器。

再重复一遍,如果显示声明了 App.FavoritesRoute,那么 Ember.js 会在渲染模板前先调用它。

除了 setupController 这个钩子方法之外,还有一个很常用的钩子方法 model,该方法可以让你为模板指定一个模型。例如:

App.PostsRoute = Ember.Route.extend({
  model: function() {
    return this.store.find('posts');
  }
});

this.store 会找到应用的数据仓库,之后我们会详细介绍它。现在就把它看作是我们向数据源获取数据的途径便可以了。

在这个例子中,我们没有定义 PostsController,这是因为模型是一个数组,Ember.js 会自动为我们提供一个 Ember.ArrayController 的实例对象来持有这个数组。因此你可以把 ArrayController 看做是模型本身,这有两大好处:

  • 控制器可以随时替换模型而无须通知视图;

  • 控制器可以提供模型里没有的数据,比如说计算后属性视图专用状态等。这样可以清楚的分离视图、控制器和模型三者之间的相关性(解耦)。

模板遍历控制器中的项了:

<ul>
  {{#each controller}}
  <li>{{title}}</li>
  {{/each}}
</ul>

动态片段(Dynamic Segments)

若路由使用了动态片段,那么其模型是什么取决于用户为该片段提供的值。

以这段代码为例:

App.Router.map(function() {
  this.resource('post', { path: '/posts/:post_id' });
});

在本例中,首先 Ember.js 寻找以下对象:

App.PostRoute
App.PostController
post 模板

接着路由处理 model 钩子方法把动态片段 :post_id 转换为模型;serialize 是另一个钩子方法,用于把模型对象反转,为路由提供动态片段部分对应的 URL 参数:

App.PostRoute = Ember.Route.extend({
  model: function(params) {
    return this.store.find('post', params.post_id);
  },

  serialize: function(post) {
    return { post_id: post.get('id') };
  }
});

因为这个模式太通用了,所以实际上它是路由的默认行为,也就是说你无须显式声明上述代码路由就会自动做这些处理:

若动态片段是以 _id 结尾,那么缺省的 model 钩子方法将会把前面的部分转换为模型的类名,并且这个类名是位于应用程序的命名空间之下的。如 post -> App.Post。接着就调用 find 方法,以模型方法名和动态片段值为参数查找模型实例。

缺省的 serialize 钩子方法会把模型实例的 id 属性值赋给动态片段。

路由、控制器、以及模板的缺省值

如果你不给 post 指定路由(即 App.PostRoute),Ember.js 仍然会渲染 post 模板并且给它一个 App.PostController 的控制器实例。

如果你不给 post 指定控制器(即 App.PostController),Ember.js 会自动为你创造一个实例。这个实例的类型基于路由里 model 钩子方法的返回值,如果是数组就创建一个 ArrayController;如果不是就创建一个 ObjectController

如果你连 post 模板都不写,那么 Ember.js 什么都不会渲染。

嵌套

你可以在 resource 下嵌套简单路由或 resource

App.Router.map(function() {
  this.resource('posts', function() { // the `posts` route
    this.route('favorites');          // the `posts.favorites` route
    this.resource('post');            // the `post` route
  });
});

resource 是路由、控制器、以及模板命名的开头。即使像 post 这样的资源(resource)是嵌套的,它的路由依旧是 App.PostRoute,控制器依旧是 App.PostController,模板依旧是 post(因为它是一个资源而不是简单路由)。

当你在资源内部嵌套了单个路由的时候,路由名称附加在资源的后面,以 . 分隔。

路由名称 控制器 路由 模板
posts PostsController PostsRoute posts
posts.favorites PostsFavoritesController PostsFavoritesRoute posts/favorites
post PostController PostRoute post

首要原则是:名词(posts, articles...)用资源,形容词或动词(favorites, edit)用路由。这样保证了嵌套不会制造出夸张的长域名,也避免了常用形容词和动词的相互冲突。

索引路由(The Index Route)

在每一级嵌套中(包括顶级),Ember.js 为 / 路径自动提供了一个 index 路由。

比如说你写了一个简单路由:

App.Router.map(function() {
  this.route('favorites');
});

它实际等价于:

App.Router.map(function() {
  this.route('index', { path: '/' });
  this.route('favorites');
});

如果用户导航至 /,Ember.js 会寻找以下对象:

App.IndexRoute
App.IndexController
index 模板

Ember.js 会把 index 模板的内容插入到 application 模板内的 {{outlet}}。若用户导航至 /favorites,那么 favorites 模板将替换 index 模板。

嵌套路由也是一样的,比如:

App.Router.map(function() {
  this.resource('posts', function() {
    this.route('favorites');
  });
});

其实等价于:

App.Router.map(function() {
  this.route('index', { path: '/' });
  this.resource('posts', function() {
    this.route('index', { path: '/' });
    this.route('favorites');
  });
});

所以,当用户导航至 /posts 时,当前的路由其实是 posts.index,Ember.js 会寻找下列对象:

App.PostsIndexRoute
App.PostsIndexController
posts/index 模板

模板的渲染过程如下:

  1. posts 模板渲染至 application 模板的 {{outlet}}
  2. posts/index 模板渲染至 posts 模板的 {{outlet}}

最后,假设用户导航至了 /posts/favorites,那么 posts/favorites 模板将替换 posts/index 模板。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment