Emberjs——Ember Data介绍

英文原文地址:https://github.com/emberjs/data

Ember Data 是一个用于加载、更新模型的库,模型来自持久层(如从JSON API获取),更新完成后保存更改。它提供了许多专门为浏览器端的JavaScript环境设计的工具方法。

当前是测试版本,但能完成基本任务,对于特殊情况还不能处理。

创建一个Store(存储器)实例

每个应用都有一个或多个存储器。存储器将成为已加载模型的贮存器,并且检索还未被加载的模型。

创建store(存储器)实例的代码如下:

App.store = DS.Store.create({
  revision: 6
});

注:revision属性是在1.0版本之前,ember-data用于提醒你公共API的重大更改。对于新的应用,只需要将revision设置为这个数字即可。查看BREAKING_CHANGES.md(https://github.com/emberjs/data/blob/master/BREAKING_CHANGES.md)以获取更多信息。

你可以通过指定一个adapter(适配器)告诉你的store如何与后端交互。Ember Data带有一个RESTful JSON API适配器。你可以通过adapter属性指定该适配器:

App.store = DS.Store.create({
  revision: 6,
  adapter: DS.RESTAdapter.create({ bulkCommit: false })
});

注:REST在维基百科上的介绍

RESTAdapter支持下面的选项:

·bulkCommit (默认值:false):批量提交。如果你的REST API不知持批量操作,可以通过设置该值为false来关闭。

·namespace (默认值:undefined):命名空间。A leading URL component under which all REST URLs reside, without leading slash, e.g. api (for /api/authors/1-type URLs).[此句没看明白]。

·url (默认值:""):一个为位于不同url或子域的API自定义的url,使用包括http在内的有效域名。例如“http://api.emberjs.com”。注:仅在服务器与浏览器支持CORS(维基百科--http://zh.wikipedia.org/wiki/CORS)时才能工作。

RESTful还在开发中。

定义Models(模型)

通过创建DS.Model的子类来表示每一种数据类型:

App.Person = DS.Model.extend();

通过DS.attr指定模型拥有的任一属性的attributes(暂翻译为属性特征)。属性特征表示的值将存在于底层的JSON对象中,同时也可通过Ember对象显示出来。

你可以像使用其他属性一样使用属性特征,包括作为计算属性的一部分。这些属性特征确保属性值可以从底层JSON对象中被检索并作为后续需要持久保存。

示例代码:

App.Person = DS.Model.extend({
  firstName: DS.attr('string'),
  lastName: DS.attr('string'),
  birthday: DS.attr('date'),

  fullName: function() {
    return this.get('firstName') + ' ' + this.get('lastName');
  }.property('firstName', 'lastName')
});

有效的属性类型有string,number,boolean,date。你还可以注册自定义的属性类型。例如,这儿有一个属性类型boolString,用于转换布尔值为字符串“Y”或“N”:

DS.attr.transforms.boolString = {
    from: function(serialized) {
        if (serialized === 'Y') {
            return true;
        }

        return false;
    },

    to: function(deserialized) {
        if (deserialized) {
            return "Y";
        }
        return "N";
    }
}

默认情况下,store(首字母小写表示类的实例)使用model的id属性作为主键,这可以通过设置父类的primaryKey属性来指定一个不同的主键:

DS.Model.extend({
    primaryKey: 'guid'
});

关联模型

模型可以与其他模型关联起来。Ember Data包括了几个内置的方法,以帮助你确定你的模型如何相互关联的。

Belongs To 方法

belongsTo设置了一对一的关系,在下面的例子中,不仅能找到author的profile,还可以找到与profile相关联的author:

App.Profile = DS.Model.extend({
  about: DS.attr('string'),
  postCount: DS.attr('number'),
  author: DS.belongsTo('App.Author')
});

App.Author = DS.Model.extend({
  profile: DS.belongsTo('App.Profile'),
  name: DS.attr('string')
});

Has Many 方法

hasMany方法描述了一对多的关系。在博客的例子中,一篇文章可能有多个评论:

App.Comment = DS.Model.extend({
    content: DS.attr('string'),
    post: DS.belongsTo('App.Post')
});

App.Post = DS.Model.extend({
    content: DS.attr('string'),
    comments: DS.hasMany('App.Comment')
});

 

关系表示

一对一

默认的REST适配器支持几种不同的关系变化以最佳的适应你的应用需求。我们将依次检验每种不同的关系类型。

让我们先讨论典型的一对一关系。我们将使用上面提到的Profile和Author的例子。在那个例子中,Profile属于一个Author,并且Author拥有一个Profile。最简单的方式去描述这两个记录的JSON对象结构如下:

// Author
{
  "author": {
      "id": 1,
      "name": "Tom Dale"
    }
  }
}

// Profile
{
  "profile": {
    "id": 1,
    "about": "Tom Dale is a software engineer that drinks too much beer.",
    "postCount": 1984,
    "author_id": 1
  }
}

注意上面的例子,Author的JSON中并未包含任何与Profile相关的信息。如果我们这样去请求对应的profile:

author.get('profile');

REST适配器会发送一个请求到一个URL:/profiles?author_id=1。(查询Profile的Author不会产生额外的请求,因为与Author对应的ID是内部的响应。)

作为性能优化,REST API会在返回的Author JSON中附带Profile的ID。

// Author with included Profile id
{
  "author": {
    "id": 1,
    "name": "Tom Dale",
    "profile_id": 1
  }
}

现在,如果你请求author的profile,两件事中的一件会发生。如果带有ID的Profile已经在应用运行过程载入,它会立刻返回,不产生任何额外请求。否则,REST 适配器会发送请求到 /profile/1 去载入指定的profile。

在某些情况下,如果你知道你将总会使用到两个相关联的记录,你可以通过将记录包含在同一个JSON中来最小化HTTP请求。

一种方式是直接嵌入到父记录。比如,我们可以改写上面的例子:

{
  "authors": [{
    "id": 1,
    "name": "Tom Dale",
    "profile": {
      "id": 1,
      "about": "Tom Dale is a software engineer that drinks too much beer.",
      "postCount": 1984,
      "author_id": 1
    }
  }]
}

如果你这样做,注意到Profile.find(1)仍然会触发一个Ajax请求,直到你第一次访问嵌入的profile记录(Author.find(1).get('profile'))。

另一种方式是使用上面的格式(ID内嵌),然后侧载(sideloading)记录。例如:

{
  "authors": [{
    "id": 1,
    "name": "Tom Dale",
    "profile_id": 1
  }],

  "profiles": [{
    "id": 1,
    "about": "Tom Dale is a software engineer that drinks too much beer.",
    "postCount": 1984,
    "author_id": 1
  }]
}

然而,想象服务器返回一个Person的JSON对象:

{
  "person": {
    "id": 1,
    "name": "Tom Dale",
    "tags": [{
      "id": 1,
      "name": "good-looking"
    },

    {
      "id": 2,
      "name": "not-too-bright"
    }]
  }
}

这种情况下,它是一个嵌入对象的数组,而不是关联ID的数组。要让store正确的理解,需要设置embedded选项为true:

App.Person = DS.Model.extend({
    tags: DS.hasMany('App.Tag', { embedded: true })
});

另外,也可以改变被映射到关联的数据属性。假设一个Person的JSON如下:

{
  "person": {
    "id": 2,
    "name": "Carsten Nielsen",
    "tag_ids": [1, 2]
  }
}

这种情况下你可以指定关联中的key属性:

App.Person = DS.Model.extend({
    tags: DS.hasMany('App.Tag', { key: 'tag_ids' })
});

查找特定的记录实例

你可以通过唯一ID检索记录,使用find方法:

var model = App.store.find(App.Person, 1);

如果该记录已经加载,它会立刻返回。否则,一个空对象会返回。你可以设置绑定或观察属性到你感兴趣的属性上;一旦数据从持久层返回,所有指定的属性将自动更新。

除了方法find(),所有下面提到的方法都以类似的方式操作。

查询记录实例

你可以将一个对象作为第二个参数做为服务器查询。在这种情况下,你会得到一个ModelArray对象。

App.people = App.store.find(App.Person, { page: 1 });

起初,这个people数组不含有元素。过后,我们将看到适配器如何填充people数组。因为people是Ember数组,你可以立即把它插入到DOM里。当它被填充之后,Ember的绑定会自动更新DOM。

<ul>
{{#each App.people}}
  <li>{{fullName}}</li>
{{/each}}
</ul>

这允许你请求store的一个数组信息,并保持你的视图代码与数组如何填充完全无关。

注:如果从ModelArray里人工检索记录,你需要使用objectAt(index)方法。由于对象不是一个JavaScript数组,使用[]符号将无法正常工作。

查找模型类型的所有记录

要找到某种类型的所有记录,使用store的findAll()方法:

var people = App.store.findAll(App.Person);

所有当前已经加载的与查找类型相关的记录会被包含在ModelArray并立刻返回。如果有必要,你的适配器也有机会加载该类型的额外的记录。

每当该类型的新的记录被加载进store时,由findAll方法返回的ModelArray将更新以反映新的数据。这意味着你可以将它传递给Ember模板中的#each助手,它会保持新数据的加载。

过滤已加载记录

你可以用store的filter()方法过滤任一模型类型的所有记录,使用一个函数来决定记录的去留。为了避免无谓的物化记录对象,只有从持久层返回的原始数据哈希被传递。

要将记录保留,让函数返回true。否则,返回false或undefined:

var oldPeople = App.store.filter(App.Person, function(data) {
    if (data.age > 80) { return true; }
});

创建新记录

你可以用createRecord()方法,基于特定模型来创建新记录:

var wycats = App.store.createRecord(App.Person, { name: "Brohuda" });

新记录不会保存到持久层,直到调用了store的commit()方法。

更新记录

更新记录只需简单的改变他们的属性。更新的记录不会保存直到调用了store的commit()方法,这样你可以批量改变记录并一次性提交。

删除记录

调用deleteRecord()方法来删除记录:

var person = App.store.find(App.Person, 1);
person.deleteRecord();

记录不会在持久层删除,直到调用了store的commit()方法。然后,删除的记录会立刻从它的ModelArray和关联中移除。

记录的生命周期

当记录的生命周期中某一事件发生时,可以调用函数。

didCreate - 当记录在持久层成功创建时调用;

didUpdate - 当记录在持久层成功更新时调用;

didLoad - 当数据从持久层完成加载时调用;

didDelete - 当记录从持久层中成功删除时调用。

例子:

App.Person = DS.Model.extend({
    didLoad: function() {
        alert(this.get('firstName') + " finished loading.");
    }
});

你也可以检查记录的状态属性,确定它的状态。

isLoaded - 当记录完成加载时为true,在本地创建的模型总是true;

isDirty - 当创建、更新、删除记录后但未保存时为true;

isSaving - 当记录处于保存进程时为true;

isDeleted - 记录已被删除为true,不论本地还是服务器;

isError - 当记录处于一个错误状态时为true;

加载数据

你可以预加载数据到store,当用户需要时即可使用。

加载单独一个记录,使用load()方法:

App.store.load(App.Person, {
    id: 1,
    firstName: "Peter",
    lastName: "Wagenet"
});

加载多个记录,使用loadMany()方法:

App.store.loadMany(App.Person, [{
    id: 2,
    firstName: "Erik",
    lastName: "Brynjolsofosonsosnson"
},

{
    id: 3,
    firstName: "Yehuda",
    lastName: "Katz"
}]);

Adapter API

适配器是一个对象,它接收来自store的请求,并将其转换成对持久层对应的行为。持久层通常是一个HTTP API,但可以是任何东西,如浏览器的本地存储。

创建适配器

首先,创建一个DS.Adapter的实例:

App.adapter = DS.Adapter.create();

设置adapter属性以告诉store使用哪个适配器:

App.store = DS.Store.create({
  revision: 3,
  adapter: App.adapter
});

然后,调用适配器需要的方法,方法描述于下文。

find()方法

调用find()方法,传入指定ID,来获取并填充一个记录。一旦记录被找到,调用store的load()方法:

App.Person = DS.Model.extend();
App.Person.reopenClass({
    url: '/people/%@'
});

DS.Adapter.create({
    find: function(store, type, id) {
        var url = type.url;
        url = url.fmt(id);

        jQuery.getJSON(url, function(data) {
            // data is a Hash of key/value pairs. If your server returns a
            // root, simply do something like:
            // store.load(type, id, data.person)
            store.load(type, id, data);
        });
    }
});

当你调用store.find(type,id)时,store会调用你的适配器的find()方法。

注:本文档其余部分中,我们将使用我们适配器中的url属性。这不是写一个适配器的唯一方法。举例来说,你可以简单地把一个case语句放到每个方法中,每个类型做些不同的事情。或者你可以暴露适配器中使用的类型上不同的信息。我们简单地使用url来说明如何写一个适配器。

findMany()方法


调用findMany()方法,传入一组指定ID,来获取并填充所有记录。默认情况下,findMany()会重复的调用find(),但这会很低效。如果可以,你的服务器应该支持一种通过一组ID找到多个项目的方法。

一旦请求ID对应的数据准备好填充到store中时,调用store的loadMany()方法:

App.Person = DS.Model.extend();
App.Person.reopenClass({
    url: '/people?ids=%@'
});

DS.Adapter.create({
    findMany: function(store, type, ids) {
        var url = type.url;
        url = url.fmt(ids.join(','));

        jQuery.getJSON(url, function(data) {
            // data is an Array of Hashes in the same order as the original
            // Array of IDs. If your server returns a root, simply do something
            // like:
            // store.loadMany(type, ids, data.people)
            store.loadMany(type, ids, data);
        });
    }
});

在Rails中执行findMany[此部分暂不翻译]

It is extremely easy to implement an endpoint that will find many items in Ruby on Rails. Simply define the index action in a standard resourceful controller to understand an :ids parameter.

class PostsController < ApplicationController
  def index
    if ids = params[:ids]
      @posts = Post.where(:id => ids)
    else
      @posts = Post.scoped
    end

    respond_with @posts
  end
end

findQuery()方法

当store的find()方法被一个查询调用时调用。你的适配器的findQuery()方法会传递一个ModelArray,你应当将其填充入从服务器返回的结果。

App.Person = DS.Model.extend();
App.Person.reopenClass({
    collectionUrl: '/people'
});

DS.Adapter.create({
    findQuery: function(store, type, query, modelArray) {
        var url = type.collectionUrl;

        jQuery.getJSON(url, query, function(data) {
            // data is expected to be an Array of Hashes, in an order
            // determined by the server. This order may be specified in
            // the query, and will be reflected in the view.
            //
            // If your server returns a root, simply do something like:
            // modelArray.load(data.people)
            modelArray.load(data);
        });
    }
});

在你的适配器中,你可以做任意查询,但最常见的是将Ajax请求的数据部分发送到服务器。

你的服务器将负责返回一个JSON数据数组。当你将数据加载到modelArray,这个数组的元素将同时被加载到store里。

findAll()方法

当store的findAll()方法被调用时调用,如果你不做任何事,只有已加载的模型会被包含在结果中。否则,这是加载任何未加载的该类型的记录的机会。这个方法的实行与findMany()类似;查看上面的例子。

createRecord()方法

当store的commit()方法被调用,且有记录需要在服务器上创建时,store会调用适配器的creatRecord()方法。

一旦store调用适配器的creatRecord()方法,它将进入saving状态,此时试图编辑模型会导致错误。

调用creatRecord()方法很直接:

App.Person = DS.Model.extend();
App.Person.reopenClass({
    url: '/people/%@'
});

DS.Adapter.create({
    createRecord: function(store, type, model) {
        var url = type.url;

        jQuery.ajax({
            url: url.fmt(model.get('id')),
            data: model.get('data'),
            dataType: 'json',
            type: 'POST',

            success: function(data) {
                // data is a hash of key/value pairs representing the record.
                // In general, this hash will contain a new id, which the
                // store will now use to index the record. Future calls to
                // store.find(type, id) will find this record.
                store.didCreateRecord(model, data);
            }
        });
    })
});

createRecords()方法

你可以在适配器上调用createRecords()方法以获得更好的效率,它会一次性发送所有新模型到服务器。

App.Person = DS.Model.extend();
App.Person.reopenClass({
    collectionUrl: '/people'
});

DS.Adapter.create({
    createRecords: function(store, type, array) {
        jQuery.ajax({
            url: type.collectionUrl,
            data: array.mapProperty('data'),
            dataType: 'json',
            type: 'POST',

            success: function(data) {
                // data is an array of hashes in the same order as
                // the original records that were sent.
                store.didCreateRecords(type, array, data);
            }
        });
    })
});

updateRecord()方法

updateRecord()方法与createRecord()一样,除了在记录被保存后,需要调用store的didUpdateRecord()方法。

App.Person = DS.Model.extend();
App.Person.reopenClass({
    url: '/people/%@'
});

DS.Adapter.create({
    updateRecord: function(store, type, model) {
        var url = type.url;

        jQuery.ajax({
            url: url.fmt(model.get('id')),
            data: model.get('data'),
            dataType: 'json',
            type: 'PUT',

            success: function(data) {
                // data is a hash of key/value pairs representing the record
                // in its current state on the server.
                store.didUpdateRecord(model, data);
            }
        });
    })
});

updateRecords()方法

再一次,updateRecords()与createRecords()类似

App.Person = DS.Model.extend();
App.Person.reopenClass({
    collectionUrl: '/people'
});

DS.Adapter.create({
    updateRecords: function(store, type, array) {
        jQuery.ajax({
            url: type.collectionUrl,
            data: array.mapProperty('data'),
            dataType: 'json',
            type: 'PUT',

            success: function(data) {
                // data is an array of hashes in the same order as
                // the original records that were sent.
                store.didUpdateRecords(array);
            }
        });
    })
});

deleteRecord()方法

要删除记录,调用deleteRecord()方法,完成后调用store的didDeleteRecord()方法。

App.Person = DS.Model.extend();
App.Person.reopenClass({
    url: '/people/%@'
});

DS.Adapter.create({
    deleteRecord: function(store, type, model) {
        var url = type.url;

        jQuery.ajax({
            url: url.fmt(model.get('id')),
            dataType: 'json',
            type: 'DELETE',

            success: function() {
                store.didDeleteRecord(model);
            }
        });
    })
});

deleteRecords()方法

同样的,你应该懂了吧?

App.Person = DS.Model.extend();
App.Person.reopenClass({
    collectionUrl: '/people'
});

DS.Adapter.create({
    deleteRecords: function(store, type, array) {
        jQuery.ajax({
            url: type.collectionUrl,
            data: array.mapProperty('data'),
            dataType: 'json',
            type: 'DELETE',

            success: function(data) {
                store.didDeleteRecords(array);
            }
        });
    })
});

commit()方法

为了获得最大的效率,你可以将所有挂起的更改(创建,更新,删除)打包成一个迷你的数据包。要做到这一点,执行执行commit()方法,这将发送一切需要的信息到持久层。

这是默认的适配器的commit()方法:

commit: function(store, commitDetails) {
  commitDetails.updated.eachType(function(type, array) {
    this.updateRecords(store, type, array.slice());
  }, this);

  commitDetails.created.eachType(function(type, array) {
    this.createRecords(store, type, array.slice());
  }, this);

  commitDetails.deleted.eachType(function(type, array) {
    this.deleteRecords(store, type, array.slice());
  }, this);
}

连接到视图

Ember Data 总会立刻返回某一类型的记录或记录数组,即使底层的JSON对象还未从服务器返回。

一般情况下,这意味着,你可以用Ember的Handlebars模板引擎将它们插入到DOM中,当适配器填充完成后,它们会自动更新。

比如,你请求一个ModelArray:

App.people = App.store.find(App.Person, { firstName: "Tom" });

你会获得一个空的ModelArray。Ember Data会接着请求适配器用记录填充ModelArray,通常这会发起一个Ajax请求。然而,你可以立即在你的模板中引用它。

<ul>
{{#each App.people}}
    <li>{{fullName}}</li>
{{/each}}
</ul>

一旦适配器调用了modelArray.load(array),DOM会自动用新的信息填充。

对记录本身也一样。例如,你可以请求单条记录:

App.person = App.store.find(App.Person, 1);

你会立刻获得一个未填充的Person对象。你可以立即在视图中引用它:

{{App.person.fullName}}

最初,这将是空的,但适配器调用store.load(hash)时,它会用所提供的信息自动更新。

如果你想在记录加载进程时显示不同的内容,可以使用record的isLoaded属性:

{{#with App.person}}
    {{#if isLoaded}}
        Hello, {{fullName}}!
    {{else}}
        Loading...
    {{/if}}
{{/with}}

注:对于ModelArray来说也是一样的,同样使用isLoaded属性。

当记录保存时,你还可以向用户表面这一状态,比如:

{{#with App.person}}
    <h1 {{bindAttr class="isSaving"}}>{{fullName}}</h1>
{{/with}}

这种情况下,你会得到名为is-saving的class,你可以在CSS里将它的内容改为灰色或增加一个不同的样式。

posted @ 2012-11-14 21:23  Kiinlam  阅读(3357)  评论(0编辑  收藏  举报