Ember.js项目开发之 Ember Data

Ember.js项目开发之 Ember Data

Ember.js 是一个基于MVVM模型的开源框架,该框架主要用于创建复杂的多页面应用程序。它最大的特点是:持续推出最新的特性,并不会丢弃任何旧功能。

与大多数前端开发框架不同,使用Ember.js,必须遵循一套严格的JS体系结构,也就是说,Ember.js 并不具备高度的灵活性。不过,正是得益于这套JS体系结构,Ember.js 明显更加完善、稳定,可以使用其任意版本推出的工具与最新版本集成,却不必过分担忧兼容性问题。本文开始入门Ember.js项目开发之 Ember Data

Ember Data 是一套功能强大的工具,用于格式化请求,规范化响应以及有效地管理本地数据缓存。

Ember.js 本身可与任何类型的后端一起使用:RESTJSON:APIGraphQL 或其他后端类型。

什么是Ember Data模型?

Ember Data 中,模型是代表应用程序提供给用户的基础数据的对象。请注意,尽管 Ember Data 模型具有 model 相同的名称,但它与 Routes 方法的概念不同 。

不同的应用程序可能具有非常不同的模型,具体取决于它们要解决的问题。例如,照片共享应用程序可能具有一个 Photo 代表特定照片的模型,以及一个 PhotoAlbum 代表一组照片的模型。相比之下,在线购物应用程序将可能有不同的型号,像ShoppingCartInvoiceLineItem

模型往往是持久化,这意味着用户在关闭浏览器窗口时不会期望模型数据丢失。为确保没有数据丢失,如果用户对模型进行了更改,则需要将模型数据存储在不会丢失的位置。

通常,大多数模型是从使用数据库存储数据的服务器加载并保存到服务器的。通常,会将模型的JSON表示来回发送到已编写的HTTP服务器。但是,Ember 使用其他持久性存储变得容易,例如使用IndexedDB保存到用户的硬盘上,或者使避免编写和托管自己的服务器的托管存储解决方案。

从存储中加载模型后,组件便知道如何将模型数据转换为用户可以与之交互的UI。有关组件如何获取模型数据的更多信息,请参见“ 指定路线的模型” 指南。

首先,使用Ember Data可能会感觉与JavaScript应用程序的方式有所不同。许多开发人员都熟悉使用Ajax从端点获取原始JSON数据,这乍看起来似乎很容易。但是,随着时间的流逝,复杂性会渗入应用程序代码中,从而使其难以维护。

与Ember数据,管理模型为应用程序的增长变得既简单和容易。

一旦了解了Ember Data,将拥有更好的方法来管理应用程序中数据加载的复杂性。这将使代码得以发展和壮大,并具有更好的可维护性。

Ember Data的灵活性

由于使用了适配器模式,因此可以将 Ember Data 配置为与许多不同种类的后端一起使用。有一个完整的适配器生态系统和几个内置适配器 ,可让Ember应用程序与不同类型的服务器通信。

默认情况下,Ember Data旨在与JSON:API一起使用。JSON:API是用于构建常规,健壮且高性能的API的正式规范,该API允许客户端和服务器通信模型数据。

JSON:API 标准化了JavaScript应用程序与服务器的通信方式,因此可以减少前端和后端之间的耦合,并拥有更大的自由来更改堆栈。

如果需要将Ember.js应用程序与没有可用适配器的服务器集成在一起(例如,手动滚动了不遵循任何JSON规范的API服务器),则Ember Data可以进行配置以使其工作服务器返回的任何数据。

Ember Data还旨在与流服务器一起使用,例如由WebSockets驱动的服务器。可以打开服务器的套接字,并在发生任何更改时将更改推送到Ember Data中,从而为应用程序提供始终保持最新状态的实时用户界面。

Store 和单一的真相来源

构建Web应用程序的一种常用方法是将用户界面元素与数据提取紧密耦合。例如,假设正在编写博客应用程序的admin部分,该部分具有列出当前登录用户的草稿功能。

在项目开发过程中可能很想让该组件负责获取和存储数据:

import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import fetch from "fetch";

export default class ListOfDraftsComponent extends Component {
    @tracked drafts;

    constructor() {
        super(...arguments);

        fetch("/drafts").then((data) => {
            this.drafts = data;
        });
    }
}

然后,可以在组件模板中显示草稿列表,如下所示:

<ul>
  {{#each this.drafts key="id" as |draft|}}
    <li>{{draft.title}}</li>
  {{/each}}
</ul>

这对于list-of-drafts 组件非常有用。但是,应用程序可能由许多不同的组件组成。在另一页上,可能希望组件显示草稿数。可能会想将现有willRender 代码复制并粘贴到新组件中。

import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import fetch from "fetch";

export default class DraftsButtonComponent extends Component {
    @tracked drafts;

    constructor() {
        super(...arguments);

        fetch("/drafts").then((data) => {
            this.drafts = data;
        });
    }
}
<LinkTo @route="drafts" @tagName="button">
  Drafts ({{this.drafts.length}})
</LinkTo>

不幸的是,该应用程序现在将针对相同的信息发出两个单独的请求。冗余数据的获取不仅浪费带宽,而且影响应用程序的感知速度,而且代价不菲,而且这两个值很容易不同步。自己可能已经使用了Web应用程序,其中项目列表与工具栏中的计数器不同步,从而导致令人沮丧和不一致的体验。

应用程序的UI和网络代码之间也存在紧密的联系。如果URL或JSON有效负载的格式发生更改,则很可能会导致难以跟踪的方式破坏所有UI组件。

良好设计的SOLID原则告诉我们,对象应该承担单一责任。组件的责任应该是向用户呈现模型数据,而不是获取模型。

好的Ember应用程序采用了不同的方法。Ember Data提供了一个 存储库,该存储库是应用程序中模型的中央存储库。路线及其相应的控制器可以向store询问模型,而store负责知道如何获取它们。

这也意味着store可以检测到两个不同的组件在请求相同的模型,从而使应用仅能从服务器获取一次数据。可以将store视为应用程序模型的通读缓存。路由及其相应的控制器都可以访问此共享存储;当他们需要显示或修改模型时,他们首先要向store询问。

Ember Data在每个路由和控制器中都注入了存储服务,因此可以通过 this.store 来访问它!

Models

在Ember Data中,每个Model都由一个子类表示,该子类Model定义了提供给用户的数据的属性,关系和行为。

Model定义了服务器将提供的数据类型。例如,一个Person Model可能具有一个firstName字符串birthday属性和一个日期属性:

import Model, { attr } from "@ember-data/model";

export default class PersonModel extends Model {
    @attr("string") firstName;
    @attr("date") birthday;
}

Model还描述了它与其他对象的关系。例如,一个order可能有多个line-items,而一个 line-item可能属于一个特定的order。

import Model, { hasMany } from "@ember-data/model";

export default class OrderModel extends Model {
    @hasMany("line-item") lineItems;
}
import Model, { belongsTo } from "@ember-data/model";

export default class LineItemModel extends Model {
    @belongsTo("order") order;
}

Model本身没有任何数据,它们定义了特定实例的属性,关系和行为,称为 records。

Records

一个record是一个包含从服务器加载数据模型的实例。应用程序还可以创建新记录并将其保存回服务器。

record由其模型类型和ID唯一标识。

例如,如果正在编写联系人管理应用程序,则可能有一个Person模型。应用中的单个记录可能具有的类型person和ID 1或steve-buscemi。

this.store.findRecord("person", 1); // => { id: 1, name: 'steve-buscemi' }

第一次保存记录时,服务器通常会将ID分配给记录,但是也可以在客户端生成ID。

Adapter

一个适配器是转换来自Ember请求(例如“找到为1的ID的用户”)转换请求到服务器的对象。

例如,如果应用程序要求PersonID为的 1,则Ember应该如何加载它?通过HTTP还是WebSocket?如果是HTTP,则是URL /person/1还是/resources/people/1

适配器负责回答所有这些问题。每当应用程序向store询问尚未缓存的记录时,它都会向适配器询问。如果更改记录并保存,则存储会将记录移交给适配器,以将适当的数据发送到服务器,并确认保存成功。

适配器使完全更改API的实现方式,而不会影响Ember应用程序代码。

Caching

store将自动缓存记录。如果已经加载了一条记录,那么第二次请求将始终返回相同的对象实例。这样可以最大程度地减少往返服务器的次数,并允许应用程序尽快将其UI呈现给用户。

例如,应用程序第一次要求store提供person ID 为 1 的 记录时,它将从服务器中获取该信息。

但是,下次应用程序请求person ID 为1时,store将注意到它已经从服务器检索并缓存了该信息。它不会向其他应用发送相同信息的请求,而是为应用提供与第一次提供的记录相同的记录。此功能(无论查找多少次,总是返回相同的记录对象)有时被称为身份映射。

使用身份映射非常重要,因为它可以确保在UI的一部分中所做的更改会传播到UI的其他部分。这也意味着不必手动保持记录同步-可以按ID要求记录,而不必担心应用程序的其他部分是否已经请求并加载了它。

返回缓存记录的一个缺点是,可能会发现自将数据首次加载到store的身份映射以来,数据的状态已更改。为了防止这种过时的数据长期存在问题,每次从存储中返回缓存的记录时,Ember Data都会在后台自动发出请求。当输入新数据时,将更新记录,并且如果自初始渲染以来记录发生了更改,则将使用新信息重新渲染模板。

架构概述

当应用程序第一次向 store 请求一条记录时,store 看到它没有本地副本,就从适配器请求它,适配器将从持久层检索记录,通常,这将是来自HTTP服务器的记录并以 JSON 格式表示。

ember data 架构概述

如上图所示,适配器不能总是立即返回请求的记录。在这种情况下,适配器必须向服务器发出异步请求,并且只有在该请求完成加载后,才能使用其备份数据创建记录。

由于这种异步性,存储立即从该方法返回一个 Promise findRecord()。同样,存储对适配器的任何请求也将返回promise。

一旦对服务器的请求返回了请求记录的JSON负载,适配器将使用JSON解析返回给store的promise。

然后,store使用该JSON,使用JSON数据初始化记录,并使用新加载的记录解析返回给应用程序的Promise。

ember data json

如果请求存储已经在其缓存中的记录,该怎么办。

ember data cache

在这种情况下,由于store已经知道该记录,因此它将返回一个promise,该promise将立即与该记录一起解决。由于它已经在本地保存了副本,因此无需向适配器(以及服务器)索要副本。