Ember-js-秘籍-全-

Ember.js 秘籍(全)

原文:zh.annas-archive.org/md5/c32a08b867c5f1547c510a2e64633fe0

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

单页、客户端 JavaScript 框架可能是 Web 的未来。在过去几年中,客户端框架已经发展得非常成熟。它们使得创建 Web 应用程序变得更加容易和响应。Ember.js 就是推动这一运动的主要框架之一。

本书深入解释了如何使用 Ember.js 框架,帮助您从初学者成长为专家。您将从基础知识开始,到本书结束时,您将在 Ember 中创建实时 Web 应用程序方面拥有坚实的基础。

我们将首先解释如何使用 Ember.js 框架及其相关工具的关键要点。您将学习如何有效地使用 Ember CLI 以及如何创建和部署您的应用程序。我们将通过观察绑定和观察者来仔细研究 Ember 对象模型和模板。接下来,我们将探讨 Ember 组件和模型以及 Ember Data。然后,我们将查看使用 QUnit 进行集成和验收测试。之后,我们将探讨身份验证和服务以及与 Ember 扩展件一起工作。我们将探索高级主题,例如服务和初始化器以及如何将它们一起使用来构建实时应用程序。

本书涵盖的内容

第一章, Ember CLI 基础,向您展示如何使用 Ember CLI 命令行工具以及如何处理项目的升级和部署。

第二章, Ember.Object 模型,展示了如何创建 Ember 对象和实例以及使用绑定、混合和可枚举。

第三章, Ember 模板,告诉您如何使用模板和模板助手。

第四章, Ember 路由器,展示了如何设置模型以及如何处理加载状态和重定向。

第五章, Ember 控制器,解释了如何使用属性并管理控制器之间的依赖关系。

第六章, Ember 组件,涵盖了传递属性、事件和动作。

第七章, Ember 模型和 Ember Data,解释了如何操作记录和自定义适配器。

第八章, 日志记录、调试和测试,展示了如何创建验收和单元测试以及 Ember 检查器。

第九章, 使用 Ember.js 的实际任务,讨论了如何使用服务、身份验证、Bootstrap 和 Liquid Fire。

第十章, 使用 Ember 的出色任务,解释了如何使用 Ember 验证、Firebase、WebSockets 和服务器端渲染。

第十一章, 实时网络应用,讨论了如何使用依赖注入、应用程序初始化器、运行循环和创建插件。

你需要这本书什么

  • Windows、Mac OS X 或 Linux

  • NPM 2.x 或更高版本

  • Node 4.0 或更高版本,可从 www.nodejs.org 免费获取

  • Bower 1.4 或更高版本

这本书面向谁

任何想要探索 Ember.js 并希望通过更少的编码来制作复杂网络应用的人会发现这本书很有用。建议有编码经验并熟悉 JavaScript。如果你听说过 Ember.js 或只是对单页应用框架的工作原理感到好奇,那么这本书适合你。

部分

在这本书中,你会发现一些经常出现的标题(准备就绪如何操作...它是如何工作的...还有更多...也见)。

为了清楚地说明如何完成食谱,我们使用以下这些部分:

准备就绪

这个部分告诉你在食谱中可以期待什么,并描述了如何设置任何软件或任何为食谱所需的初步设置。

如何操作...

这个部分包含遵循食谱所需的步骤。

它是如何工作的...

这个部分通常包含对前一个部分发生事件的详细解释。

还有更多…

这个部分包含有关食谱的附加信息,以便让读者对食谱有更多的了解。

也见

这个部分提供了对其他有用信息的有用链接。

习惯用法

在这本书中,你会发现许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 处理方式如下所示:"我们可以通过使用 include 指令来包含其他上下文。"

代码块设置如下:

{
  "usePods": true
}

当我们希望将你的注意力引到代码块的一个特定部分时,相关的行或项目将以粗体显示:

const Light = Ember.Object.extend({
  isOn: false,
  color: 'yellow',
  age: null,
  description: Ember.computed('isOn','color',function() {
    return 'The ' + this.get('color') + ' light is set to ' + this.get('isOn');
  }),
  fullDescription: Ember.computed('description','age',function() {
    return this.get('description') + ' and the age is ' + this.get('age')
  }),
 aliasDescription: Ember.computed.alias('fullDescription')
});

const bulb = Light.create({age: 22});
bulb.get('aliasDescription');//The yellow light is set to false and the age is 22.

任何命令行输入或输出都如下所示:

$ ember server --port 1234

新术语重要词汇将以粗体显示。屏幕上显示的单词,例如在菜单或对话框中,将以这种方式显示:“屏幕上的消息将显示Hello World! My name is John SmithHello World! My name is Erik Hanchett。”

注意

警告或重要提示将以这样的框显示。

小贴士

技巧和窍门将以这种方式显示。

读者反馈

我们始终欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。

要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书籍的标题。

如果您在某方面有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在您是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从您在www.packtpub.com的账户中下载示例代码文件,适用于您购买的所有 Packt Publishing 书籍。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support,并注册以将文件直接通过电子邮件发送给您。

勘误

尽管我们已经尽最大努力确保内容的准确性,错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者感到沮丧,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。

要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将显示在勘误部分下。

盗版

互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的非法复制我们的作品,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过 <copyright@packtpub.com> 联系我们,并提供涉嫌盗版材料的链接。

我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。

问题和建议

如果您对本书的任何方面有问题,您可以联系我们的 <questions@packtpub.com>,我们将尽力解决问题。

第一章:Ember CLI 基础

在本章中,我们将介绍以下食谱:

  • 安装 Ember CLI

  • 创建你的第一个项目

  • 探索 pods 和文件夹布局

  • 资产编译

  • 依赖管理

  • 升级你的项目

  • 部署

简介

Ember CLI 是一个基于Node.js的命令行界面工具,专为Ember.js中的应用程序编程设计。自其创建以来,这个工具已成为创建 Ember 应用程序的首选方法。

简而言之,Ember CLI 使得启动新的 Ember 应用程序变得简单。在其他框架中,你可能需要学习gulpgrunt作为你的任务运行器。在 Ember CLI 中,这一切都为你内置了。在 Ember.js 生态系统中拥有 Ember CLI 是一个变革,使这个框架在众多框架中脱颖而出。

Ember CLI 处理测试、编译和升级,甚至内置了网络服务器。Ember CLI 不仅为你生成样板代码,而且与许多测试框架很好地集成。它有一个强大的插件系统,可以扩展其当前功能之外的功能。

安装 Ember CLI

Ember CLI 的安装对于学习 Ember 至关重要,并将贯穿本书的始终。

准备工作

在安装 Ember CLI 之前,我们必须安装Node 包管理器npm)。npm是 JavaScript 的包管理器,默认情况下与 Node.js 一起安装。

你必须安装 Node.js 的 0.12 或更高版本,以便 Ember CLI 可以运行。如果可能,请尝试安装 4.0.0 或更高版本。这是首选版本。

Node.js 在包括 Windows、Mac 和 Linux 在内的几个主要平台上可用。安装 Node.js 有多种方法:

  • 一键安装程序:许多平台,如 Windows 和 Mac,都提供这种功能

  • HomebrewMacPorts:这对于 Mac OS 用户很有用

  • 下载 TAR 文件:下载 Node.js 的 TAR 文件并解压

  • 通过 Linux 包管理系统安装:Yum、apt-get 或 pacman 可用于在 Linux 环境中安装

Windows 或 Mac 的一键安装程序

这种方法到目前为止是最简单的。要安装 node,你需要打开nodejs.org/download的 node 网站。点击 Windows 或 Mac 的pkgmsiexe安装程序。下载后运行它。

Mac 的 Homebrew 或 MacPorts

如果你已经安装了 Homebrew,只需运行以下命令:

$ brew install node

另一方面,如果你正在使用 MacPorts,你可以使用 port install 命令:

$ sudo port install nodejs

小贴士

MacPorts 可以从www.macports.org安装。Homebrew 可以从brew.sh安装。两者都为 OS X 系统提供简单的包管理。

一个 TAR 文件

TAR 文件是一种存档文件类型。要通过 TAR 安装 node,你需要从 Node.js 网站下载 TAR 文件并解压和安装。一种方法是使用curl

我只会推荐这种方法,如果您正在使用 Linux 发行版。如果您在 Linux 上运行,您需要安装正确的工具来从源代码编译。在 Ubuntu 上,您需要安装 build-essential 和 curl 软件包:

$ curl http://nodejs.org/dist/node-latest.tar.gz | tar xz --strip-components=1
$ ./configure
$ sudo make install

Linux 软件包管理器

所有主要的 Linux 发行版都提供 Node.js 软件包。在 Ubuntu 上,您可以直接使用 apt-get

$ sudo apt-get install nodejs

在 Fedora 上,您可以使用 yum

$ yum install nodejs npm

检查您的 Linux 发行版以获取有关如何安装 Node.js 等软件包的更多详细信息。请注意,某些发行版可能提供过时的 Node.js 版本。在这种情况下,我建议您使用稍后将要讨论的 Node Version Manager (NVM) 安装方法。

测试安装

要测试您的安装,请运行 –v 命令:

$ node –v
$ npm –v

这将显示当前安装的版本。请注意,您必须运行 v0.12 或更高版本才能运行 Ember CLI。如果可能,请尝试运行 v4.0.0 或更高版本。

小贴士

NVM 是一个 bash 脚本,它可以帮助管理多个活动的 Node.js 版本。NVM 提供了一个非常简单的命令行界面,可以安装任何版本的 Node.js,而无需访问 Node.js 网站。它将每个安装分开,使得在版本之间切换变得非常容易。我建议大多数 Mac 和 Linux 的初学者运行此脚本。您可以在 github.com/creationix/nvm 下载 NVM。

如何操作...

我们需要使用 npm 来安装 Ember CLI。我们将使用 -g 选项全局安装它,这样就可以从命令行在任何地方运行它。

  1. 打开命令提示符并输入以下命令:

    $ sudo npm install –g ember-cli
    
    

    如果已安装 NVM,则命令开头不需要 sudo

  2. 在 Ember CLI 安装完成后,我们需要下载 Bower。Bower 是客户端编程的包管理器,也是 Ember.js 的另一个重要组件。在开始安装 Bower 之前,必须安装 Node.js 和 npm。我们将使用 Bower 来安装所有客户端库:

    $ sudo npm install –g bower
    
    

    与上一个命令类似,如果 Node.js 是通过 NVM 安装的,则命令开头不需要 sudo

  3. 最后一步是安装 PhantomJS。PhantomJS 是一个脚本化的无头浏览器,用于自动化和测试网页。它被 Ember CLI 所青睐,需要安装:

    $ npm install –g phantomjs
    
    
  4. 如果您在 Windows 上,请安装 Ember CLI Windows 工具:

    $ npm install ember-cli-windows –g
    
    
  5. 安装完成后,此工具可以在任何项目目录中运行:

    $ ember-cli-windows
    
    
  6. 确保下载并安装 Git for Windows: git-scm.com/downloads

    注意

    在 Windows 上工作

    Windows 上的构建时间可能比 Mac 或 Linux 更长。Ember CLI Windows 工具可以帮助加快和优化构建性能。只需在项目目录中运行它。您也可以将其作为插件下载。

    另一种帮助提高性能的方法是始终以提升权限运行 PowerShell/CMD。否则,可能会出现性能问题和错误。最后,尽量使用 npm 版本 3 或更高版本。在 Windows 中,较旧版本可能会遇到长文件路径问题。

    另一个实用的技巧如下:

    小贴士

    可选:安装 Watchman

    Watchman 是适用于 OS X 和类 UNIX 操作系统的文件监视服务。它由 Facebook 开发,是 Ember CLI 监视项目更改的更有效方式。如果没有安装,Ember CLI 将回退到使用 NodeWatcher。NodeWatcher 更容易出错,并且难以观察大型树。如果您的平台支持,请安装 Watchman。要下载和配置 Watchman,请访问 facebook.github.io/watchman/

它是如何工作的...

Ember CLI 是用 Node.js 编写的,可以通过 npm 安装。该工具解释用户命令以帮助创建 Ember.js 应用程序。每个用户命令都会被查找并执行。Ember CLI 依赖于包括 Bower、Lodash、Broccoli 和 Babel 在内的多个其他依赖项。

还有更多...

让我们来看看命令和别名。

命令

Ember CLI 安装后,我们将能够访问多个命令。以下是一些更重要命令的简短列表:

命令 目的
ember 这将打印出可用的命令列表
ember new <应用名称> 这将在 <应用名称> 目录中创建一个目录并创建应用程序结构
ember init 这将在当前目录中创建一个应用程序
ember build 这将在 /dist 文件夹中构建应用程序
ember server 这将启动一个网络服务器
ember generate <生成器名称> 这将运行一个生成器,为项目构建脚手架
ember destroy <生成器名称> 这将卸载由生成器创建的模块
ember test 这将使用 Testem 运行测试
ember install <插件名称> 这将安装插件

别名

请记住,对于每个命令,都有一个别名。这些别名使运行命令变得更快。假设您想构建一个新的项目。通常,您会输入以下内容:

$ ember build

这将正常工作且效果良好。它将生成一个新的项目和应用程序结构。您也可以使用别名。

$ ember b

这里有一些您可以使用的一些常用别名。这是可选的。

命令 别名
ember build ember b
ember generate ember g
ember init ember i
ember server ember s
ember destroy ember d
ember test ember t
ember version ember v

创建您的第一个项目

在这个菜谱中,我们将创建我们的第一个项目。

如何操作...

我们将首先使用 Ember CLI 工具来创建我们的第一个项目。

  1. 打开命令提示符并输入以下命令:

    $ ember new my-project
    
    

    这将创建一个全新的项目,名为 my-project。项目结构将包含我们开始所需的一切。

  2. 要显示此项目,我们可以简单地运行服务器命令:

    $ cd my-project
    $ ember server
    
    

    ember server 命令将在端口 4200 上启动一个网络服务器。您可以通过打开 http://localhost:4200 来访问此端口。您应该看到默认的 欢迎使用 Ember 网站

    小贴士

    在开发应用程序时保持 Ember 服务器运行是一个好主意。Ember CLI 使用一个名为 LiveReload 的工具在更改时刷新网络浏览器。这可以用来查看新更改如何影响您的应用程序。要运行 LiveReload,只需输入 ember server。这将启动带有 LiveReload 的服务器。

  3. 服务器命令默认为端口 4200。您可以使用 --port 参数轻松更改此端口:

    $ ember server --port 1234
    
    

    这将在端口 1234 上启动服务器,而不是默认的 4200。

  4. 另一个有用的选项是 --proxy 参数。这将把所有 Asynchronous JavaScript and XMLAjax)请求代理到指定的地址。假设我们有一个运行在端口 8080 上的节点服务器。我们可以按照以下方式运行服务器:

    $ ember server --proxy http://127.0.0.1:8080
    
    

    对于每个 Ajax 请求,Ember 现在将把这些请求发送到本地的端口 8080

    小贴士

    请记住,从 Ember 2.0 开始,Internet ExplorerIE)8 的支持已被取消。所有现代网络浏览器和 8 版本之后的 IE 版本都能正常工作。如果需要 IE 8 的支持,Ember.js 1.13 扩展了浏览器支持,应该可以与它兼容。

它是如何工作的...

ember server 命令创建一个 Node.js Express 服务器。此服务器使用 LiveReload,并在任何更改时刷新网页。服务器命令接受不同的参数,包括 --proxy--port

更多内容...

在运行服务器时,您可以访问测试。启动服务器后,您将能够访问 QUnit 接口。QUnit 是一个 JavaScript 单元测试框架。它用于运行您的集成和验收测试。要访问该接口,请将浏览器导航到 http://localhost:4200/tests。这将显示项目中的所有测试。从这里,您可以查看哪些测试通过,哪些失败。我们将在后面的章节中介绍这一点:

更多内容...

探索 pods 和文件夹布局

Ember CLI 将为我们创建文件夹结构。Ember.js 使用 模型-视图-控制器MVC)模式。您将在本食谱中看到文件夹结构是如何布局的,以及模型、控制器和视图(模板)是如何相互分离的。

准备工作

Ember CLI 依赖于 ES2015 模块。这意味着您可以使用明天的 JavaScript 语法编写今天的代码。这是通过 Ember Resolver 实现的。

小贴士

ES2015

ECMAScript 6,也称为 ES2015,是即将推出的 ECMAScript 编程语言版本。ES2015 包含几个新功能,包括模板字符串、解构、箭头函数、模块和类定义等。所有这些现在都在您的 Ember 项目中可用。

Pods

Ember pod 是一种不同类型的结构,它根据功能而不是类型来组织您的模块。随着您的项目增长,您可能希望按功能组织项目以帮助保持事物有序。Ember 解析器将首先查找 pod 结构,然后再查看传统结构。

要自动设置 pod 结构,您可以编辑项目目录根目录中的 .ember-cli 文件并添加以下行:

{
  "usePods": true
}

提示

下载示例代码

您可以从 www.packtpub.com 下载示例代码文件,以获取您购买的所有 Packt 出版物的所有内容。如果您在其他地方购买了此书,您可以访问 www.packtpub.com/support 并注册以将文件直接通过电子邮件发送给您。

这将设置默认结构,使其始终使用 pods。当使用 pods 时,设置所有 pods 所在的位置是一个好主意。为此,您需要编辑 config/environment.js 文件:

...
var ENV = {
  modulePrefix: 'pod-example',

..   podModulePrefix: 'pod-example/pods' 

podModulePrefix 属性设置 POD 路径,格式如下:{appname}/{poddir}。在先前的例子中,POD 目录现在设置为 app 文件夹中的 /pods。如果未设置位置,所有新的模块都将创建在 app/ 文件夹中。

如何做到这一点...

创建新项目后,将生成一个正常的文件夹布局。此布局由几种不同类型的模块组成。以下是每个目录的简要描述:

目录 执行的操作
app/adapters 适配器帮助扩展逻辑以与后端数据存储进行通信
app/components 组件用于帮助重用代码,并且其名称中必须包含破折号
app/helpers Helper 用于 HTML 重用
app/initializers 初始化器首先运行并帮助设置您的应用程序
app/mixins 这是一种用于多重继承的特殊类型的 Ember.Object
app/routes 路由帮助在不同应用程序状态之间移动
app/serializers 这用于序列化您的数据模型
app/transform Transform 用于反序列化和序列化模型属性
app/utils Utils 是小型实用类
app/models 模型存储数据存储
app/templates 模板使用 HTMLBars 向用户显示 HTML
app/templates/components 这些是在组件中使用的模板

一个带有默认布局的新项目 app 文件夹看起来类似于以下内容:

如何做到这一点...

每个模块都将有自己的目录。例如,templates 文件夹将存储所有模板,而 components 控制器将存储所有组件。

假设我们使用 pods 添加了一个新的帖子资源。以下命令将生成一个新的帖子模型、路由和模板,并将更新路由:

$ ember g resource posts

现在,文件系统将看起来像这样:

如何做...

Pods 根据功能对目录进行排序。帖子(posts)文件夹是功能,文件命名是根据它们所服务的功能来命名的。

它是如何工作的...

每个 Ember CLI 项目的目录结构都是经过设计的。当创建新项目或生成新脚手架时,CLI 会将文件放置在某个目录中,并使用 Ember Resolver 理解的 ES2015 格式进行命名结构。

Ember Resolver 负责查找应用程序中的代码并转换实际类文件中的命名约定。使用 Ember pods 时,解析器知道首先在默认结构之前查找那里。

资产编译

在这个菜谱中,我们将看看如何将资产添加到项目中。

如何做...

在你的应用程序中,在某个时刻,你可能想要添加资产并最小化或指纹化你的项目。这可以在项目的root文件夹中的ember-cli-build.js文件或asset文件夹中完成。

CSS 和资产

所有资产应放置在public/assets文件夹中。资产可以在程序中通过assets/images/{image file}进行引用。CSS 文件应放置在app/styles文件夹中。

压缩

默认情况下,CSS 和 JavaScript 文件在生产构建过程中会被压缩。有方法可以打开和关闭此功能。例如,假设你想关闭 CSS 和 JavaScript 的压缩。为此,我们可以简单地编辑ember-cli-build.js文件,并在// Add options here部分添加minifyCSSminifyJS部分:

module.exports = function(defaults) {
  var app = new EmberApp(defaults, {
    // Add options here
    minifyCSS: {
      enabled: false
    },
    minifyJS: {
      enabled: false
    }
  });

这将告诉编译器不要压缩 JavaScript 和 CSS。要在生产模式下构建应用程序,只需使用--environment参数:

$ ember build --enviroment=production

指纹化

默认情况下,所有文件在生产构建过程中都会进行指纹识别。这包括所有jscsspngjpggif资产。在这个过程中,所有这些文件都会在其文件名末尾附加一个 md5 校验和。在这个过程中,所有 HTML 和 CSS 文件都将被重写以包含这些新名称。

在对文件进行指纹化时,有几种选项可用。这全部都在ember-cli-build.js文件中控制。假设你想要禁用指纹化:

…
fingerprint: {
 enabled: false
}
…

另一个有用的选项是为所有静态文件添加一个域名。这可以通过使用prepend选项来完成。同样,这需要添加到application文件夹根目录下的ember-cli-build.js文件中:

…
fingerprint: {
   prepend: 'http://www.example.com'
}

现在,所有资产都将包含www.example.com域名。例如,一个普通的 JavaScript src文件将看起来像这样:

<script src="img/script.js">

这将被转换为以下内容:

<script src="img/script-12324adfasdf123234.js">

另一个有用的选项是 exclude。它接受一个字符串数组。exclude 数组中的任何文件名都不会被指纹化:

fingerprint: {
  exclude: ['fonts/12424']
}

ignore 选项也接受一个字符串数组。包含 ignore 数组中任何项目的任何文件名都不会被处理或指纹化:

fingerprint: {
  ignore: ['fonts/12424']
}

extension 选项默认为 'js''css''png''jpg''gif''map'。此选项可用于添加其他需要指纹化的文件类型:

fingerprint: {
  extension: ['r3','html']
}

replaceExtensions 选项默认为 'html''css''js'。如果需要,可以添加新文件类型以替换源代码和新的校验和文件名:

fingerprint: {
  replaceExtensions: ['html','htm']
}

它是如何工作的...

导入过程是通过 Broccoli 资产管道库完成的。这个构建工具执行所有指纹化、压缩和资产导入操作。此外,如果安装了适当的插件,Broccoli 还处理所有预处理程序。

资产清单位于项目文件夹根目录下的 ember-cli-build.js 文件中。你只能导入位于 bower_componentsvendor 目录中的资产。

依赖项管理

让我们看看依赖项管理以及我们如何在 Ember 项目中使用它。

如何做到这一点...

Bower 用于 Ember CLI 的依赖项管理。Bower 是一个前端工具,用于帮助获取和安装你可能需要的包。

  1. bower.json 文件位于你项目的 root 文件夹中。它包含所有依赖项。假设我们想要安装 Bootstrap 库

    $ bower install bootstrap --save
    
    

    此命令将在 bower_components 文件夹中安装 bootstrap 并将包信息保存到 bower.json 文件中。

    小贴士

    Ember 插件

    将第三方库添加到 Ember 的另一种流行方式是使用插件或插件,正如你有时看到的那样。插件是 Ember 在应用程序之间共享库的方式。有超过一千个这样的插件可供使用。

    你可以使用 Ember CLI 安装插件。例如,要安装 Bootstrap,你需要在项目目录中的命令行中输入以下内容:

    $ ember install ember-bootstrap

    你可以轻松在这些网站上找到插件列表:

    www.emberobserver.com

    www.emberaddons.com

    这将在第十一章 实时网络应用工作与创建插件 菜谱中更详细地讨论,实时网络应用

  2. 如果出于某种原因需要重新安装依赖项,可以单独运行 install 命令:

    $ bower install
    
    

    这将安装 bower.json 文件中列出的所有依赖项。

app.import 代码

Ember CLI 允许你加载 异步 模块定义AMD)和非 AMD 资产。这是一种定义代码模块及其依赖项的方式。

  1. 要加载非 AMD 资产,你需要使用 ember-cli-build.js 文件导入它:

    …
    app.import('bower_components/moment/moment.js');
    
  2. 这很有用,因为你可以使用 Bower 安装组件,然后使用app.import AMD 使其在程序中可用。你需要查阅包规范以了解如何使用它。

    提示

    关于 JSHint 的提示

    JSHint 是一个社区驱动的工具,用于检测 JavaScript 代码中的错误和潜在问题。它是 Ember CLI 内置的。当使用非 AMD 资产时,如果你有全局变量,你可能会遇到 JSHint 错误。为了解决这个问题,在模块页面的顶部添加/* global MY_GLOBAL */。在 moment 的例子中,它看起来像/* global moment */

  3. AMD 资产以类似的方式导入。你在第一个参数中添加路径,在第二个参数中添加导出和模块列表:

    app.import('bower_components/ic-ajax/dist/named-amd/main.js', {
      exports: {
        'ic-ajax': [
          'default',
          'defineFixture',
          'lookupFixture',
          'raw',
          'request',
        ]
      }
    });
    
  4. 要在你的应用程序中使用此资产,你可以按照以下方式导入:

    import { raw as icAjaxRaw } from 'ic-ajax';;
    

它是如何工作的...

依赖管理是通过 Bower 完成的。在依赖安装后,Broccoli 库被调用以将资产添加到管道中。这两个工具都是用 node 编写的,并且内置在 Ember CLI 中。

升级你的项目

Ember CLI 正在持续升级,每六周 Ember.js 就会有一个新的版本发布。保持你的构建工具和版本更新是很重要的。在这个菜谱中,我们将探讨如何做到这一点。

如何做到这一点...

要升级你的 Ember CLI 版本,你必须执行以下步骤:

  1. 首先,卸载旧的ember-cli

    $ npm uninstall –g ember-cli
    
    
  2. 清除 npm 缓存:

    $ npm cache clean
    
    
  3. 清除 Bower 缓存:

    $ bower cache clean
    
    
  4. 安装最新版本的ember-cli

    $ npm install –g ember-cli
    
    
  5. 如果需要,你可以指定由X.X.X表示的版本:

    $ npm install –g ember-cli@X.X.X
    
    

更新现有项目

在某些情况下,你可能想要将现有项目更新到最新版本的 Ember CLI。在这种情况下,你需要执行以下步骤:

  1. 首先,在项目文件夹的根目录中更改你想要升级的目录。删除这些临时开发目录:

    $ rm –rf node_modules bower_components dist tmp
    
    
  2. 使用以下命令更新package.json文件,以包含我们升级到的 Ember 版本:

    $ npm install ember-cli@X.X.X --save-dev
    
    

    X.X.X代表ember-cli的版本。--save-dev参数将信息保存到package.json文件中。

  3. 再次安装所有的 npm 和 Bower 包:

    $ npm install
    $ bower install
    
    
  4. 最后一步是运行init命令:

    $ ember init
    
    

    init命令将默认项目蓝图添加到你的目录中。

    提示

    init

    init命令将在你的项目目录中创建一个新的应用程序蓝图。按照提示并审查所有更改。你可能需要替换现有文件。按d进行文件差异比较并审查所做的更改。在开始升级过程之前,创建项目的备份。

请记住,在升级你的项目后,你可能会遇到许多新的弃用警告需要处理。当你运行ember server时,你会看到这些警告。每个都需要解决。

要解决这些弃用问题,查看应用程序提供的警告。例如,您可能会收到有关 Ember.View 的警告。警告将描述应使用 Ember.Component 代替。然后您需要将受影响的代码用 Ember 组件而不是 Ember 视图替换。

它是如何工作的...

当您升级工具时,您只是卸载节点包并重新安装最新版本。清除 Bower 和 Node 缓存也是一个好主意,这样 Node 和 Bower 就不会有任何冲突的包。

当我们更新现有项目时,我们首先必须确保所有现有模块和包都被删除。这是因为当我们安装 Ember CLI 的最新版本时,一些包可能会发生变化。在将 ember-cli 保存回包文件后,然后您可以再次安装 npm 和 Bower。

运行 ember init 在您所在的目录中生成应用程序结构。这很重要,因为自上次升级以来,一些文件可能已经更改。您始终可以按 d 键来比较更改。

部署

在创建您的应用程序后,您将需要能够部署它。让我们看看部署应用程序的一些方法。

如何操作...

  1. 在部署之前的第一基本步骤是构建您的项目。要构建应用程序,请运行 build 命令:

    $ ember build
    
    
  2. 此命令将在 /dist 文件夹中构建项目的所有内容。要为生产构建项目,您需要使用 -prod 参数:

    $ ember build -prod
    
    
  3. 如果需要,您可以指定默认输出文件夹:

    $ ember build –prod –o<directory>
    
    

构建用于生产的应用程序将自动压缩您的文件并对它们进行指纹识别。当您的环境设置为开发时,这不会发生,这是默认设置。

/dist 文件夹包含了您的 Web 服务器所需的一切。在这个时候,要部署您的应用程序,您只需要将 /dist 文件夹的内容复制到您的 Web 服务器上。

小贴士

Ember CLI Deploy

部署您的 Ember 应用程序的另一种极好方式是使用名为 Ember CLI Deploy 的 Ember 插件。此插件帮助您将 Ember 应用程序部署到多个不同的服务。它背后有一个非常活跃的社区,因此您可以期待频繁的更新。随着您的 Ember 应用程序的增长,您可能想看看这个。有关 Ember CLI 部署的更多信息,请参阅:github.com/ember-cli/ember-cli-deploy

部署到 Firebase

Firebase 是一种后端服务,可以处理数据存储、用户身份验证、静态托管等。在这个例子中,我们将使用 Firebase 作为托管我们的 Ember 应用程序的方式。

  1. 在 Firebase 上注册一个账户。这可以在 www.firebase.com 完成。

  2. 接下来,安装 firebase-tools

    $ npm install –g firebase-tools
    
    
  3. 在您创建项目并准备部署后,在文件夹根目录下运行 firebase init 命令:

    $ firebase init
    
    

    运行此命令后,您将需要回答几个问题。它首先会要求您登录到您的 Firebase 账户。输入您的凭证以继续。然后,它会要求您输入 Firebase 应用程序的名称。最后,它会要求您输入应用程序公共目录的名称。在大多数情况下,这应该是 /dist

  4. 编辑 firebase.json 文件并添加一些重写规则:

    {
      "firebase": "my-new-app",
      "public": "dist",
      "rewrites": [{
        "source": "**",
        "destination": "/index.html"
      }],
    }
    

    这是为了帮助在应用程序中进行导航。将 my-new-app 改为您的应用程序名称。

  5. 剩下的就是部署到 Firebase:

    $ firebase deploy
    
    

它是如何工作的...

Ember CLI 构建过程使用 Broccoli 资产管道和构建工具本身进行编译。它将所有文件合并,压缩,指纹化,并将它们组织在 /dist 文件夹中,以便可以部署。

许多服务可以托管静态文件。您可以使用 Firebase,或者构建生产版本后在自己的 Nginx 或 Apache 服务器上托管。

第二章. Ember.Object 模型

在本章中,我们将介绍以下食谱:

  • 与类和实例一起工作

  • 与计算属性一起工作

  • 在 Ember.js 中使用 Ember 观察者

  • 与绑定一起工作

  • 使用混合

  • 使用数组与可枚举一起

简介

Ember.Object 是几乎所有其他 Ember 对象的基类。路由、模型、视图和组件都继承自 Ember.Object。它无处不在,因此了解它的工作方式和如何在我们的应用程序中使用它非常重要。

标准的 JavaScript 对象在 Ember 中不常用。Ember 的对象模型建立在 JavaScript 对象之上,并添加了重要的功能,如观察者、混合、计算属性和初始化器。许多这些功能都与新的 ECMAScript 标准保持一致。

与类和实例一起工作

创建和扩展类是 Ember 对象模型的主要功能之一。在本食谱中,我们将探讨创建和扩展对象的工作方式。

如何做到...

  1. 让我们首先使用 extend() 创建一个非常简单的 Ember 类:

    const Light = Ember.Object.extend({
      isOn: false
    });
    

    这定义了一个新的名为 Light 的类,它有一个名为 isOn 的属性。Light 类从 Ember 对象继承属性和行为,例如初始化器、混合和计算属性。

    小贴士

    Ember Twiddle 小贴士

    在任何时候,你可能需要测试 Ember 代码的小片段。一个简单的方法是使用一个名为 Ember Twiddle 的网站。从这个网站,你可以创建一个 Ember 应用程序并在浏览器中运行它,就像使用 Ember CLI 一样。你甚至可以保存和分享它。它有类似 JSFiddle 的工具,但仅限于 Ember。在 ember-twiddle.com 上查看。

  2. 一旦你定义了一个类,你需要能够创建它的一个实例。你可以使用 create() 方法来做这件事。我们将创建一个 Light 的实例:

    constbulb = Light.create();
    

访问灯泡实例内的属性

  1. 我们可以使用 setget 访问器方法访问 bulb 对象的属性。让我们先获取 Light 类的 isOn 属性:

    console.log(bulb.get('isOn'));
    

    上述代码将从 bulb 实例获取 isOn 属性。

  2. 要更改 isOn 属性,我们可以使用 set 访问器方法:

    bulb.set('isOn', true)
    

    现在 isOn 属性将被设置为 true 而不是 false

初始化 Ember 对象

init 方法在创建新实例时被调用。这是一个放置任何可能需要为新实例编写的代码的好地方。

  1. 在我们的例子中,我们将添加一个警报消息,显示 isOn 属性的默认设置:

    const Light = Ember.Object.extend({
      init(){
        alert('The isON property is defaulted to ' + this.get('isOn'));
      },
      isOn: false
    });
    
  2. 一旦执行了 Light.create 这行代码,实例将被创建,并且屏幕上会弹出“isON 属性默认为 false”的消息。

    小贴士

    子类

    注意,你可以在 Ember 中创建你对象的子类。你可以重写方法并使用_super()关键字方法访问父类。这是通过创建一个新的对象来完成的,该对象使用父类的 Ember extend方法。

    另一个重要的事情是,如果你正在子类化一个框架类,例如Ember.Component,并且你重写了init方法,你需要确保调用this._super()。如果不这样做,组件可能无法正常工作。

重新打开类

在任何时候,你都可以重新打开一个类并在其中定义新的属性或方法。为此,请使用reopen方法。

在我们之前的示例中,我们有一个isON属性。让我们重新打开同一个类并添加一个color属性:

  1. 要添加color属性,我们需要使用reopen()方法:

        Light.reopen({
          color: 'yellow'
        });
    
  2. 如果需要,你可以使用reopenClass添加静态方法或属性:

    Light.reopenClass({
      wattage: 80
    });
    
  3. 你现在可以访问静态属性Light.wattage

它是如何工作的...

在前面的示例中,我们使用extend创建了 Ember 对象。这告诉 Ember 创建一个新的Ember类。extend方法在 Ember.js 框架中使用继承。Light对象继承了 Ember 对象的所有方法和绑定。

create方法也继承自 Ember 对象类,并返回该类的新实例。bulb对象是我们创建的 Ember 对象的新实例。

更多...

要使用前面的示例,我们可以创建自己的模块并将其导入到我们的项目中。

  1. 要这样做,在app文件夹中创建一个名为MyObject.js的新文件:

    // app/myObject.js
    import Ember from 'ember';
    export default function() {
        const Light = Ember.Object.extend({
          init(){
            alert('The isON property is defaulted to ' +this.get('isOn'));
          },
          isOn: false
        });
    
        Light.reopen({
          color: 'yellow'
        });
    
        Light.reopenClass({
          wattage: 80
        });
    
        const bulb = Light.create();
    
        console.log(bulb.get('color'));
        console.log(Light.wattage);
    }
    

    这是一个我们可以现在导入到我们的 Ember 应用程序中任何文件的模块。

  2. app文件夹中,编辑app.js文件。你需要在文件顶部添加以下行:

    // app/app.js
    import myObject from './myObject';
    
  3. 在导出之前,添加以下行:

    myObject();
    

    这将执行我们在myObject.js文件中创建的myObject函数。在运行ember server之后,你会看到isOn属性默认显示为false的弹出消息。

使用计算属性

在这个菜谱中,我们将查看计算属性以及它们如何用于显示数据,即使这些数据在应用程序运行时发生变化。

如何做到这一点...

让我们创建一个新的 Ember.Object 并给它添加一个计算属性:

  1. 让我们首先创建一个新的description计算属性。这个属性将反映isOncolor属性的状态:

    const Light = Ember.Object.extend({
      isOn: false,
      color: 'yellow',
    
      description: Ember.computed('isOn','color',function() {
        return 'The ' + this.get('color') + ' light is set to ' + this.get('isOn');
      })
    
    });
    
  2. 我们现在可以创建一个新的Light对象并获取计算属性description

    const bulb = Light.create();
    bulb.get('description'); //The yellow light is set to false
    

    前面的示例创建了一个依赖于isOncolor属性的计算属性。当调用description函数时,它返回一个描述灯的状态的字符串。

    计算属性将观察变化,并在它们发生时动态更新。

  3. 要看到这个功能的效果,我们可以修改前面的示例并将isOn属性设置为false。使用以下代码来完成此操作:

    bulb.set('isOn', true);
    bulb.get('description') //The yellow light is set to true
    

    描述已经自动更新,现在将显示The yellow light is set to true

连接 Light 对象

Ember 提供了一个很好的功能,允许计算属性存在于其他计算属性中。在先前的例子中,我们创建了一个 description 属性,该属性输出了有关 Light 对象的一些基本信息。

  1. 让我们添加另一个属性,该属性提供完整的描述:

    const Light = Ember.Object.extend({
      isOn: false,
      color: 'yellow',
      age: null,
    
      description: Ember.computed('isOn','color',function() {
        return 'The ' + this.get('color') + ' light is set to ' + this.get('isOn');
      }),
    
      fullDescription: Ember.computed('description','age',function() {
        return this.get('description') + ' and the age is ' + this.get('age')
      }),
    
    });
    
  2. fullDescription 函数返回一个字符串,该字符串将描述的输出与显示 age 的新字符串连接起来:

    const bulb = Light.create({age:22});
    bulb.get('fullDescription'); //The yellow light is set to false and the age is 22
    

    在这个例子中,在 Light 对象实例化期间,我们将 age 设置为 22。如果需要,我们可以覆盖任何属性。

别名

Ember.computed.alias 方法允许我们创建一个属性,该属性是另一个属性或对象的别名。

  1. getset 的任何调用都将表现得像是更改了原始属性:

    const Light = Ember.Object.extend({
      isOn: false,
      color: 'yellow',
      age: null,
      description: Ember.computed('isOn','color',function() {
        return 'The ' + this.get('color') + ' light is set to ' + this.get('isOn');
      }),
      fullDescription: Ember.computed('description','age',function() {
        return this.get('description') + ' and the age is ' + this.get('age')
      }),
     aliasDescription: Ember.computed.alias('fullDescription')
    });
    
    const bulb = Light.create({age: 22});
    bulb.get('aliasDescription');//The yellow light is set to false and the age is 22.
    
  2. aliasDescription 别名将显示与 fullDescription 相同的文本,因为它只是这个对象的别名。如果我们稍后对 Light 对象中的任何属性进行了更改,别名也会观察这些更改并正确计算。我们将在 使用绑定 的配方中进一步讨论这一点。

它是如何工作的...

计算属性建立在观察者模式之上。每当观察显示状态变化时,它都会重新计算输出。如果没有变化发生,则结果将被缓存。

换句话说,计算属性是当它们的依赖值中的任何一个发生变化时都会更新的函数。您可以使用它们的方式与您使用静态属性的方式几乎相同。它们在 Ember 及其代码库中很常见且很有用。

请记住,计算属性只有在它位于模板或正在使用的函数中时才会更新。如果函数或模板没有被调用,则不会发生任何事情。这将有助于提高性能。

在 Ember.js 中使用 Ember 观察者

观察者是 Ember 对象模型的基础。在下一个配方中,我们将使用我们的灯示例,添加一个观察者,并查看它是如何运行的。

如何做到这一点...

  1. 首先,我们将添加一个名为 isOnChanged 的新观察者。它只会在 isOn 属性更改时触发:

    const Light = Ember.Object.extend({
      isOn: false,
      color: 'yellow',
      age: null,
      description: Ember.computed('isOn','color',function() {
        return 'The ' + this.get('color') + 'light is set to ' + this.get('isOn')
      }),
      fullDescription: Ember.computed ('description','age',function() {
        return this.get('description') + ' and the age is ' + this.get('age')
      }),
      desc: Ember.computed.alias('description'),
     isOnChanged: Ember.observer('isOn',function() {
     console.log('isOn value changed')
     })
    });
    
    const bulb = Light.create({age: 22});
    
    bulb.set('isOn',true); //console logs isOn value changed
    

    Ember.observer isOnChanged 监视 isOn 属性。如果此属性发生任何更改,isOnChanged 将被调用。

    注意

    计算属性与观察者

    初看之下,观察者可能看起来与计算属性相同。实际上,它们非常不同。计算属性可以使用 getset 方法,并且可以在模板中使用。另一方面,观察者只是监视属性变化,不能在模板中使用或像属性一样访问。它们也不返回任何值。因此,请注意不要过度使用观察者。在许多情况下,计算属性是更合适的解决方案。

  2. 此外,如果需要,您可以为观察者添加多个属性。只需使用以下代码:

    Light.reopen({  
    isAnythingChanged: Ember.observer('isOn','color',function() {
        console.log('isOn or color value changed')
      })
    });
    
    const bulb = Light.create({age: 22});
    bulb.set('isOn',true); // console logs isOn or color value changed
    bulb.set('color','blue'); // console logs isOn or color value changed
    

    isOncolor属性变化时,isAnything观察者会被调用。观察者会触发两次,因为每个属性都发生了变化。

与 Light 对象和观察者的同步问题

观察者很容易变得不同步。例如,如果它观察的属性发生变化,它将按预期被调用。调用后,它可能会操作一个尚未更新的属性。由于所有事情都同时发生,这可能导致同步问题。

  1. 以下示例展示了这种行为:

      Light.reopen({
        checkIsOn: Ember.observer('isOn', function() {
          console.log(this.get('fullDescription'));
        })
      });
    
    const bulb = Light.create({age: 22});
    bulb.set('isOn', true);
    

    isOn发生变化时,不清楚计算属性fullDescription是否已经更新。由于观察者同步工作,很难判断已经触发和改变的内容。这可能导致意外的行为。

  2. 为了解决这个问题,最好使用Ember.run.once方法。这是 Ember run循环的一部分,是 Ember 管理代码执行的方式。重新打开Light对象,你会看到以下内容:

    Light.reopen({
        checkIsOn: Ember.observer('isOn','color', function() {
          Ember.run.once(this,'checkChanged');
        }),
        checkChanged: Ember.observer('description',function() {
          console.log(this.get('description'));
        })
    });
    const bulb = Light.create({age: 22});
    bulb.set('isOn', true);
    bulb.set('color', 'blue'); 
    

    checkIsOn观察者使用Ember.run.once调用checkChanged观察者。此方法在每个run循环中只运行一次。通常,checkChanged会被触发两次;然而,由于它是通过Ember.run.once调用的,它只输出一次。

它是如何工作的...

Ember 观察者是来自Ember.Observable类的混入。它们通过监控属性变化来工作。当任何变化发生时,它们会被触发。请注意,这些与计算属性不同,不能在模板中使用,也不能与获取器或设置器一起使用。

与绑定一起工作

大多数框架都包含某种绑定实现。Ember 也不例外,它有可以与任何对象一起使用的绑定。以下食谱解释了如何使用它们以及单向和双向绑定。

如何做到这一点...

在这个例子中,有一个教师和学生 Ember 对象。每个都有自己的属性集,并且它们都有 homeroom。我们可以通过为教师对象设置别名来共享 homeroom。

  1. 让我们从创建一个教师和学生Ember.Object开始:

    const Teacher = Ember.Object.extend({
      homeroom: '',
      age: '',
      gradeTeaching: ''
    });
    
    const Student = Ember.Object.extend({
      homeroom: Ember.computed.alias('teacher.homeroom'),
      age: '',
      grade: '',
      teacher: null
    });
    

    学生homeroomEmber.computed.alias,它将homeroom属性绑定到teacher.homeroom

  2. 接下来,我们将实例化teacherstudent对象:

    const avery = Teacher.create({
      age: '27',
      homeroom: '1075',
      gradeTeaching: 'sophmore'
    });
    
    const joey = student.create({
      age: '16',
      grade: 'sophmore',
      teacher: avery
    });
    

    joey对象将homeroom属性设置为avery,这是我们刚刚创建的teacher对象。

  3. 我们现在可以使用console.log来输出我们的发现:

    console.log(joey.get('age')); //16
    console.log(avery.get('homeroom')); //1075
    avery.set('homeroom','2423');
    console.log(joey.get('homeroom')); //2423
    

    如你所见,每当avery对象更改其homeroom时,学生joeyhomeroom也会改变。这是因为 joey 的 homeroom 是教师avery的别名。

  4. 你并不总是需要访问引用其他对象的属性。你可以绑定到任何东西:

    const myName = Ember.Object.extend({
      name: 'Erik Hanchett',
      otherName: Ember.computed.alias('name')
    });
    
    const erik = myName.create();
    
    console.log(erik.get('name')); //Erik Hanchett
    console.log(erik.get('otherName')); //Erik Hanchett
    

    别名指向name;因此,当打印到控制台时,它显示Erik Hanchett

    注意

    Ember 有一个名为 Ember.Binding 的类。这是一个具有与 Ember.computed.aliasEmber.computed.oneWay 非常相似的行为和功能的公共类。你应该使用 Ember.computed.alias,而不是 Ember.Binding。计算别名是 Ember 中绑定的首选方法。Ember.Binding 仍然存在,并可能在某个时候被弃用。

单向绑定

Ember 默认使用一种称为双向绑定的机制。这意味着当 UI 中的属性发生变化时,这些变化会更新回控制器或组件。另一方面,单向绑定只在一个方向上传播变化。

例如,假设我们有一个具有 firstNamelastNamenickName 属性的 User 对象。我们可以使用 Ember.computed.oneWayfirstName 属性创建一个单向绑定。

让我们看看当我们尝试修改它时会发生什么。创建一个新的用户对象并具有这些属性。实例化对象并尝试更改属性:

const User = Ember.Object.extend({
  firstName: null,
  lastName: null,
  nickName: Ember.computed.oneWay('firstName')
});

const user = User.create({
  firstName: 'Erik',
  lastName:  'Hanchett'
});

console.log(user.get('nickName'));              // 'Erik'
user.set('nickName', 'Bravo'); // 'Bravo'
console.log(user.get('firstName'));             // 'Erik'

你可以看到,即使用户已经更新,nickName 也没有改变。你可以将单向绑定视为使用 Ember.computed.alias。然而,它只允许你获取值,而不能设置值。使用 Ember.computed.oneWay 时,上游属性不会发生变化。

它是如何工作的...

Ember 绑定被用于 Ember 框架的许多部分。它们是从 Ember.computed 命名空间派生出来的。在这个命名空间中是计算别名方法。计算别名通过创建双向绑定来指定另一个对象的路径。

绑定对象不会立即更新。Ember 会等待所有应用程序代码运行完毕后再同步所有更改。这防止了在值更新时同步绑定产生不必要的开销。

单向绑定通过只单向传播信息来工作。信息不会在上游属性中更新。

使用混入

混入是 Ember 中重用和共享代码的绝佳方式。以下配方将介绍如何在代码中使用它们的一些基本操作。

如何做到这一点...

在这个配方中,我们将创建一个通用的混入对象。

  1. 创建一个具有一些属性和函数的 Ember 混入对象:

    const common = Ember.Mixin.create({
        property1: 'This is a mixin property',
        edit: function() {
          console.log('Starting to edit');
          this.set('isEditing', true);
        },
        isEditing: false
    });
    

    这个混入可以被添加到任何对象中。为了简单起见,这个混入所做的只是显示一些文本,并在调用 edit 函数时将 isEditing 属性设置为 true

  2. 让我们看看当我们把这个对象添加到另一个对象中时它看起来是什么样子:

    const obj = Ember.Object.extend(common,{
      objprop: 'This is an Ember object property'
    });
    
    const object = obj.create();
    

    Ember.Object 中存在的 extend 方法允许有一个或多个可选的 Ember.Mixin 类型的参数。在这个例子中,我们将通用混入添加到新的 Ember.Object 对象中。然后我们使用 create 实例化这个 Ember 对象。

  3. 剩下的就是输出内容。使用 console.log 来显示每个属性:

    console.log(object.get('objprop'));  //This is an Ember object property
    console.log(object.get('property1'));  //This is a mixin property
    console.log(object.get('isEditing'));  //false
    object.edit();  //Starting to edit
    console.log(object.get('isEditing')); //true
    

    这就是输出将看起来像什么。如您所见,我们可以访问 mixin 的任何属性或方法,就像 mixin 被包含在 Ember 对象本身中一样。这是在应用程序中重用代码的一种方便方式。

  4. 让我们创建另一个 mixin:

    const secondMixin = Ember.Mixin.create({
      secondProperty: 'This is the second mixin property'
    });
    
  5. 现在让我们看看如果我们将其添加到 Ember 对象中会是什么样子:

    const obj = Ember.Object.extend(common,secondMixin,{
      objprop: 'This is an Ember object Property'
    });
    
  6. 现在,我们可以在我们的对象中访问公共的secondMixin。我们可以使用console.log来输出secondProperty

    console.log(object.get('secondProperty'));//This is the   second mixin propety
    

使用 Ember CLI 的 mixin

Mixins 与 Ember CLI 配合得非常好。首先,使用 mixin 生成器创建一个。

  1. 确保你处于应用程序目录中,然后输入以下命令:

    $ ember generate mixin common
    installing mixin
     create app/mixins/common.js
    installing mixin-test
     create tests/unit/mixins/common-test.js
    
    

    generator命令会创建一个app/mixins文件夹和common.js文件。common.js文件是我们将放置 mixin 代码的地方。

  2. 我们将使用前面的例子中的 mixin 并将其添加到这个文件中:

    // app/mixins/common.js
    import Ember from 'ember';
    
    export default Ember.Mixin.create({
        property1: 'This is a mixin property',
        edit: function() {
          console.log('Starting to edit');
          this.set('isEditing', true);
        },
        isEditing: false
    });
    

    这个 mixin 与前面的例子完全相同;然而,现在它在一个我们可以导入到任何地方的模块中,包括组件或控制器。

    现在,我们将将其导入到app文件夹目录中的app.js文件中。

  3. 首先,我们需要在文件顶部添加import语句:

    import common from './mixins/common';
    

    这允许我们在app.js文件的任何地方使用公共 mixin。

  4. 我们将在app/app.js文件的底部添加以下代码:

    // app/app.js
    
    const obj = Ember.Object.extend(common,{
      objprop: 'This is an Ember object property'
    });
    
    const object = obj.create();
    
    console.log(object.get('objprop'));//This is an Ember object property
    console.log(object.get('property1'));//This is a mixin property
    console.log(object.get('isEditing'));//false
    object.edit();  //Starting to edit
    console.log(object.get('isEditing'));  //true
    

    如您所见,公共 mixin 中的所有属性和方法都可用于该对象。

  5. 如果我们将公共 mixin 添加到组件中,它可能看起来像以下代码。将此代码添加到common-example.js文件中:

    // app/components/common-example.js
    import Ember from 'ember';
    import common from '../mixins/common';
    
    export default Ember.Component.extend(common,{
        compprop: 'This is a component property',
        actions: {
          pressed: function(){
            this.edit();
          }
        }
    });
    

    和往常一样,我们必须首先将 mixin 导入到我们的组件中。路径始终相对于你所在的目录,因此,我们必须使用../mixins/common来找到它。

    在组件中,我添加了一个简单的动作pressed,它触发 mixin 的edit方法。如果动作被触发,我们将在控制台看到Starting to edit message。更多组件的例子请参考第六章,Ember 组件

它是如何工作的...

Ember.Mixin类允许创建可以将其属性和方法添加到其他类的 mixin。它们不能被实例化,但可以被添加或混合

计算机科学中的 mixin 是一个类,它通过组合而不是继承将其行为借用到借用类中。它鼓励代码重用,并避免了多重继承可能引起的歧义。

使用数组中的可枚举

当处理数组时,Ember.Enumerable方法非常重要。在这些菜谱中,我们将查看一些常见用例。

准备工作

要了解如何使用可枚举的,我们首先需要看看标准的 JavaScript 数组方法和它们使用可观察枚举的等效方法:

标准方法 可观察的等效方法
unshift unshiftObject
shift shiftObject
reverse reverseObjects
push pushObject
pop popObject

我们将在示例中使用一些这些方法,所以请记住它们的标准和可观察的等效方法。

Ember.Enumerable类有几个我们可以在我们的 Ember 应用程序中使用的方法。以下是更常见的方法列表以及它们的作用:

枚举方法 定义
forEach 这遍历枚举,对每个项调用传递的函数
firstObject 这返回集合中的第一个对象
lastObject 这返回集合中的最后一个对象
map() 这会将枚举中的所有项映射到另一个值,类似于 JavaScript 1.6 中的 map
mapBy() 与 map 类似,这返回枚举中所有项的命名属性的值
filter 这返回一个数组,包含枚举中传递的函数返回 true 的所有项
find 这返回方法返回的第一个数组项
findby 这返回第一个具有与传递的值匹配的属性的项
every 这仅在传递的函数对枚举中的每个项返回 true 时才返回 true
any 这仅在传递的函数对枚举中的任何项返回 true 时才返回 true

许多这些方法与它们的 JavaScript 对应方法类似。如果您知道如何使用 JavaScript 方法,您应该能够使用 Ember 的等效方法。

如何做到这一点...

Ember.Enumerables将 Ember 对象的全部良好特性添加到枚举中。我们将查看几个示例,说明如何做到这一点。所有这些食谱的内容都在app.js文件中的chapter2/example6文件夹中。

使用数组中的 forEach

枚举的一个非常常见的用例是使用forEach遍历数组。

  1. 创建一个学生数组:

    const students = ['Erik', 'Jim', 'Shelly', 'Kate'];
    
  2. 使用forEach枚举遍历数组:

    students.forEach(function(item, index) {
      console.log(`Student #${index+1}: ${item}`);
    });
    

    控制台输出将显示数组中的每个学生的姓名:

    Student #1: Erik
    Student #2: Jim
    Student #3: Shelly
    Student #4: Kate
    
    

    提示

    模板字面量

    Ember 与最新的 ECMAScript 2015 兼容。一个很酷的新特性被称为模板字面量或模板字符串。模板字面量是可以在多行中扩展并包含插值表达式的字符串字面量。您可以通过在字符串中包围变量来进行字符串插值,如下所示${}。每个变量将按前一个forEach示例中所示的方式显示在字符串中。

使用数组中的 map

map方法接受一个数组,映射每个项,并返回一个新的修改后的数组。假设我们想将学生姓名全部转换为大写。我们可以使用map来实现这一点。

  1. 创建一个students列表:

    const students = ['Erik', 'Jim', 'Shelly', 'Kate'];
    

    第一个字母需要大写;然而,我们希望所有的字母都大写。

  2. 使用map将每个项目转换为大写:

    const upperCaseStudent= students.map(function(item) {
      return item.toUpperCase();
    });
    

    数组中的每个项目都已转换为大写。新的upperCaseStudent数组包含所有新值。

  3. 使用forEach枚举遍历数组中的每个项目并显示其内容:

    upperCaseStudent.forEach(function(item, index) {
      console.log(`student #${index+1}: ${item}`);
    });
    

    输出显示了新 upperCaseStudent 数组中的每个名称:

    student #1: ERIK
    student #2: JIM
    student #3: SHELLY
    student #4: KATE
    
    

使用 mapBy 和一个数组

如果你的数组由对象组成,可以使用 mapBy 可枚举来提取每个对象的命名属性并返回一个新数组。

  1. 让我们创建一个教师和学生对象:

    const student = Ember.Object.extend({
      name: 'Erik Hanchett'
    });
    
    const teacher = Ember.Object.extend({
      name: 'John P. Smith'
    
    });
    

    每个对象都有一个名为 name 的属性:

  2. 接下来,我们将实例化每个对象。

    const t= teacher.create();
    const s = student.create();
    const people = [s, t];
    

    将每个对象放入 people 数组中:

  3. 我们可以使用 mapBy 来创建一个新数组。

    console.log(people.mapBy('name'));//['Erik Hanchett', 'John P.   Smith']
    

    这个新数组返回了两个对象 name 属性的值。

在数组中查找第一个和最后一个对象

如果需要,我们有一个简单的方法来获取数组中的第一个和最后一个对象。

  1. 我们首先创建一个学生数组:

    const students = ['Erik', 'Jim', 'Shelly', 'Kate', 'Jenny', 'Susan'];
    

    这个数组有六个不同的学生。

  2. 让我们获取数组中的最后一个对象:

    console.log(students.get('lastObject')); //Susan
    

    这将显示数组中的最后一个对象 Susan

  3. 现在让我们检索数组中的第一个对象:

    console.log(students.get('firstObject')); //Erik
    

    这将显示数组中的第一个项目 Erik

  4. 我们也可以将对象推送到数组中:

    students.pushObject('Jeff');
    
  5. 学生 Jeff 现在已经被添加到列表中:

    console.log(students.get('lastObject')); //Jeff
    

使用过滤器进行有趣的操作

一个非常常见的做法是取一个数组并返回一个过滤后的项目列表。

  1. 首先,创建一个数字数组:

    const array = [1,2,5,10,25,23];
    
  2. arrayfilter 结合起来,只返回那些大于 10 的数字:

    const newArray =array.filter(function(item, index, self) {
      return item > 10;
    })
    
  3. 使用 console.log 来显示新数组:

    console.log(newArray); //[25,23]
    

    这个新数组中只包含大于 10 的数字。

使用 filterBy 和一组对象

使用 filterBy,你可以通过某些属性过滤一组对象。

  1. 创建一个新的 student 对象,它有一个 namegrade

    const student = Ember.Object.extend({
      grade: null,
      name: null
    });
    
  2. 将学生添加到新数组中:

    const listOfStudents = [
      student.create({grade: 'senior', name: 'Jen Smith'}),
      student.create({grade: 'sophmore', name: 'Ben Shine'}),
      student.create({grade: 'senior', name: 'Ann Cyrus'})
    ];
    
  3. 使用 filterBy 来显示高年级学生:

    const newStudent = listOfStudents.filterBy('grade','senior');
    

    这将返回一个包含所有高年级学生的数组。

  4. 我们可以使用 forEach 来双重检查输出:

    newStudent.forEach(function(item,index){
      console.log(item.get('name'));
    });
    Jen Smith
    Ann Cyrus
    

使用 find 来获取第一个匹配项

find 可枚举与 filter 的工作方式非常相似,但它会在找到第一个匹配项后停止。

  1. 创建一个数字数组:

    const array = [1,2,5,10,25,23];
    
  2. 使用 array.find 来检索列表中第一个大于 10 的数字:

    const newArray =array.find(function(item, index){
      return item > 10;
    }); 
    
  3. 然后,我们将检查新数组的输出:

    console.log(newArray); //25
    

    答案是 25,因为它是列表中第一个大于 10 的数字。

使用 findBy 和集合

findBy 可枚举与 filterBy 的工作方式非常相似,但它会在找到第一个匹配项后停止。

  1. 创建一个新的 student 对象:

    const student = Ember.Object.extend({
      grade: null,
      name: null
    });
    
  2. 接下来,创建一个学生数组:

    const listOfStudents = [
      student.create({grade: 'senior', name: 'Jen Smith'}),
      student.create({grade: 'sophmore', name: 'Ben Shine'}),
      student.create({grade: 'senior', name: 'Ann Cyrus'})
    ];
    
  3. 使用 findBy 来匹配只有 gradesenior 的属性:

    const newStudent = listOfStudents.findBy('grade','senior');
    
  4. 这将返回第一个高年级学生:

    console.log(newStudent.get('name')); //Jen Smith
    

    Jen Smith 是第一个符合这个标准的学生,因此它被返回到 newStudent 数组中。

使用 every 可枚举来学习

every 可枚举只有在每个项目都符合某个特定条件时才会返回 true

  1. 开始创建一个数字数组:

    const array = [11,25,23,30];
    
  2. 使用 every 可枚举来检查数组中的每个项目是否大于 10

    console.log(array.every(function(item, index, self) {
      return item > 10;
    })); //returns true
    

    这返回 true,因为数组中的每个项目都大于 10

使用 any 来找到至少一个匹配项

any 可枚举将在至少有一个项目符合某个条件时返回 true

  1. 再次创建一个数字列表:

    const array = [1,2,5,10,25,23];
    
  2. 使用 any 可枚举来检查这个数组中的这些数字是否超过 10

    console.log(array.any(function(item, index, self) {
      return item > 10;
    })); //returns true
    

    这将返回 true,因为至少有一个数字超过了 10

它是如何工作的...

Ember.Enumerable 混合是 Ember 对 JavaScript 1.8 之前定义的数组 API 的实现。它在页面加载时自动应用,因此任何方法都是可用的。为了使 Ember 能够观察可枚举的变化,你必须使用 Ember.Enumerable

可枚举 API 尽可能遵循 ECMAScript 规范,以最小化与其他库的不兼容性。在可用的情况下,它使用原生的浏览器实现。

第三章. Ember 模板

在本章中,我们将涵盖以下菜谱:

  • 定义应用程序模板

  • 在模板中使用条件

  • 显示项目列表

  • 使用元素属性和类绑定

  • 在模板内部处理 HTML 链接

  • 处理 HTML 动作

  • 使用模板输入助手

  • 使用开发助手

简介

Ember 应用程序使用模板引擎向用户显示 HTML 和动态内容。在 Ember 中,这是通过 Handlebars 模板库完成的。这个库将 Handlebars 表达式渲染到屏幕上,使用数据绑定。

HTMLbars 是 Ember 使用的 Handlebars 的一个变体。它具有更好的性能,并以更有效的方式处理构建 DOM。请注意,在本章中,我们将将 HTMLbars 和 Handlebars 互换使用,因为它们本质上做的是同一件事。

在本章中,我们将介绍如何在我们的应用程序中使用模板。

定义应用程序模板

要与模板一起工作,我们需要了解属性如何与控制器和组件绑定的一些基础知识。以下是一些介绍如何完成此操作的菜谱。

准备工作

在我们开始之前,我们需要生成一个模板。

  1. 我们首先将使用 Ember CLI 创建一个新的应用程序:

    $ ember new HelloWorldApp
    $ cd HelloWorldApp
    
    

    此命令将生成一个我们可以用于此菜谱的新应用程序。

  2. 接下来,创建一个新的路由,用于添加一个新的模板:

    $ ember g route helloworld
    
    

    此命令将生成模板和路由文件以及单元测试。模板文件名为 helloworld.hbs,将在 app/templates 文件夹中生成。路由文件名为 helloworld.js,位于 app/routes 文件夹中。route.js 文件也将被修改以包含新的 helloworld 路由。我们将在第四章(Chapter 4)中更详细地讨论路由,Ember Router

  3. 然后,我们需要生成一个 controller

    $ ember g controller helloworld
    
    

    这将在 app/controller 文件夹中生成一个名为 helloworld.js 的新文件,并在 tests/unit/controllers 中生成一个单元测试。我们现在可以继续了。

如何操作...

让我们看看如何向我们的新模板文件添加属性。

  1. 首先,编辑 helloworld.hbs 文件。对于这个简单的例子,我们将创建一个包含姓氏和名字属性的字符串,如下所示:

    // app/templates/helloworld.hbs
    Hello World! My name is {{firstName}} {{lastName}}.
    {{outlet}}
    

    Handlebar 表达式被双大括号 {{ }} 包围,并有一个上下文。上下文是一个对象,Handlebar 表达式从中读取其属性。在这个例子中,上下文是控制器。{{outlet}} 将渲染任何嵌套路由的模板,这将在稍后更详细地讨论。

  2. 控制器需要具有 firstNamelastName 属性,以便它们可以在模板中显示:

    // app/controllers/helloworld.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
        firstName: 'Erik',
        lastName: 'Hanchett'
    });
    

    控制器的名称与模板相同。按照惯例,模板将从同名的控制器中检索属性。它们彼此绑定。如果数据有任何变化,其他值也会变化。

使用带有组件的模板

与控制器类似,我们可以创建一个可以作为模板上下文的组件。在组件中,我们可以设置模板稍后可以访问的属性。

  1. 要创建一个新的组件,使用generate component命令:

    $ ember g component hello-world
    
    

    所有组件的名称都必须包含一个连字符。此命令将在app/components/hello-world.js文件夹中创建hello-world.js文件,在app/components/hello-world.hbs文件中创建一个模板文件,并在tests/integration/components/hello-world-test.js中创建一个集成测试文件。

  2. 编辑hello-world.hbs文件并添加 hello world 字符串:

    // app/templates/components/hello-world.hbs
    Hello World! My name is {{firstName}} {{lastName}}.
    {{yield}}
    

    firstNamelastName参数是从组件中检索的。当组件以块形式存在时,使用yield表达式。我们将在第六章Ember 组件中更多地讨论这一点。

  3. 向组件文件hello-world.js添加两个属性,第一个是firstName,最后一个是lastName

    // components/hello-world.js
    import Ember from 'ember';
    
    export default Ember.Component.extend({
        firstName: 'John',
        lastName: 'Smith'
    });
    
  4. 对于最后一部分,我们只需要将刚刚创建的组件添加到我们的application.hbs文件之一:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    
    {{hello-world}}
    {{outlet}}
    

    {{hello-world}} Handlebars 表达式将组件添加到application.hbs文件中。然后hello-world模板将在这里渲染。{{outlet}}模板将渲染application路由下的嵌套路由。

  5. 启动 Ember 服务器并导航到http://localhost:4200

  6. 在 Ember 服务器启动后,在本地主机 4200 端口打开一个网页浏览器。屏幕上的消息将显示Hello World! My name is John Smith

  7. 导航到http://localhost:4200/helloworld,你会看到两条消息。屏幕上的消息将显示Hello World! My name is John SmithHello World! My name is Erik Hanchett

  8. helloworld路由被加载时,应用程序模板会显示。然后{{outlet}}模板会使用helloworld模板文件的內容进行渲染。这就是为什么两个消息都会显示。记住,所有路由都嵌套在application路由下。

它是如何工作的...

Ember.js 使用 Handlebars 模板库。这个库为你提供了一种在组件或控制器(也称为上下文)和模板之间进行数据绑定的方式。这种数据绑定是双向的。换句话说,组件或控制器中的数据变化将在模板中反映出来。模板中数据的变化将在控制器或组件中反映出来。

在前面的简单示例中,组件中的firstNamelastName属性在模板中通过双大括号{{}}进行访问。这被称为 Handlebars 表达式。模板只是普通的 HTML,其中嵌入了 Handlebar 表达式。Ember 在构建过程中稍后编译这些模板。

在模板中使用条件

使用条件是使用 Ember 模板引擎的基本方法。在下面的菜谱中,我们将查看条件以及它们如何与模板一起工作。

如何做到这一点...

让我们看看一个简单的示例,该示例在某个属性为真时显示文本。

  1. 创建一个新的项目并生成一个名为conditional的新控制器。在application文件夹的根目录下运行此命令以创建controllertemplate

    $ ember g controller conditional
    $ ember g template conditional
    
    

    这将创建条件控制器。

  2. router.js文件中更新新的conditional路由:

    // app/router.js
    …
    Router.map(function() {
      this.route('conditional');
    });
    

    这将添加一个新的conditional路由。要使用 Ember 服务器访问此路由,请打开网页浏览器并导航到http://localhost:4200/conditional

  3. 使用isHomeworkDone属性更新conditional控制器:

    // app/controllers/conditional.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
        isHomeworkDone: true});
    

    这将创建一个新的isHomeworkDone属性并将其默认设置为true

  4. 更新条件模板,使其在isHomeworkDonetrue时显示一条消息,如果不是则显示另一条消息:

    // app/templates/conditional.hbs
    Hello!
    {{#if isHomeworkDone}}
     Thanks for finishing the homework!
    {{else}}
     Please finish the homework
    {{/if}}
    

    注意

    {{if}}语句是一个助手,必须像任何其他 Handlebar 表达式一样被大括号{{}}包围。它以一个#符号开始,表示它是一种块调用形式。{{/if}}语句关闭该语句。

    之前的示例显示了两种语句,{{if}}{{else}},都是块形式。只有为真的语句将被显示。

  5. 如我们从早期的控制器中知道的那样,如果isHomeworkDonetrue,则在模板渲染后,将显示语句Thanks for finishing the homework!。另一方面,如果isHomeworkDonefalse,则将显示语句Please finish the homework

  6. 要测试此示例,请导航到http://localhost:4200/conditional路由。application.hbs中的{{outlet}}将渲染其内部的conditional模板。

使用模板与内联调用

内联调用可用于使用if语句在一行代码中显示数据。

  1. 我们将使用内联调用重新创建前面的示例。使用内联调用在app/templates文件夹中的condtional.hbs文件中编辑router.js文件,使用新的if语句:

    // app/templates/conditional.hbs
    Hello
    
    {{if isHomeworkDone 'Thanks for finishing the homework!' 'Please finish the homework'}}
    
  2. 当使用内联调用时,您不需要使用井号#或使用{{/if}}结束if块。一切都可以在一个表达式中写出来。

  3. isHomeworkDone之后,助手的第一个参数Thanks for finishing the homework!只有在isHomeworkDonetrue时才会显示。第二个参数Please finish the homework将在isHomeworkDonefalse时显示。

在模板中使用嵌套调用

嵌套调用是内联的,这意味着它们返回单个值。它们也可以在内联助手中接受多个嵌套的if语句。

  1. conditional控制器中,添加两个名为isHomeworkDoneisChoresDone的属性:

    // app/controllers/conditional.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
      isHomeworkDone: true,
      isChoresDone: true});
    

    这两个都默认为true

  2. 让我们使用嵌套调用只显示一个消息,如果isHomeworkDoneisChoresDone都是true。用新的嵌套if语句编辑condtional.hbs文件:

    // app/templates/conditional.hbs
    Hello
    
    {{if isHomeworkDone (if isChoresDone 'Thanks for finishing the homework!' )}}
    

    谢谢完成作业字符串只有在isChoresDoneisHomeworkDone都是true时才会显示。否则,不会显示任何内容。由于控制器将这两个值都设置为true,在模板渲染后,将显示谢谢完成作业!

if相反的是unless

另一个有用的助手是unless。它正好与if助手相反。它可以与所有三种调用方式一起工作——内联、块和嵌套。

我们将在conditional.hbs文件中创建一个unless块,如果它在我们不是真的,将显示一个字符串:

// app/templates/conditional.hbs
Hello

{{#unless isHomeworkDone}}
  Please finish the homework
{{else}}
  Thanks for finishing the homework!
{{/unless}}

在这个块中,如果isHomeworkDonefalseunless助手将显示请完成作业。另一方面,如果isHomeworkDonetrue,将显示消息谢谢完成作业!。这本质上与if助手相反。

在这个例子中,假设isHomeworkDonetrue,在模板渲染后,将显示字符串谢谢完成作业!

它是如何工作的...

ifunless条件是内置助手,它们由 Handlebars 模板引擎提供给我们。它们被大括号{{}}包围,这告诉 Handlebars 解释它们。{{if}}语句检查属性是否为true。JavaScript 值如undefinednull''[]和数字0将返回为false

这些条件助手可以通过三种不同的方式被调用——块、嵌套或内联。所有三种都会与ifunless助手一起工作。

显示物品列表

通常,你会有一个需要迭代的物品列表。我们可以使用each助手遍历这些项目。这个配方将介绍如何做到这一点。

如何做到这一点...

假设我们有一个学生列表,并想在模板中显示它们。我们将使用each助手来完成这项工作。

  1. 在一个新项目中,生成student``controllertemplate

    $ ember g template student
    $ ember g controller student
    
    

    这将创建我们示例所需的必要文件。

  2. 使用新的student路由更新router.js文件:

    // app/router.js
    …
    Router.map(function() {
      this.route('student');
    });
    

    这将添加一个新的conditional路由。要使用 Ember 服务器访问此路由,请打开网络浏览器并导航到http://localhost:4200/student

  3. 将学生控制器更新为具有students数组属性:

    // app/controllers/students.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
      students: [ {name: 'Erik'}, {name: 'Jim'}, {name: 'Jane'}]
    });
    

    这个数组有三个学生对象。

  4. 在我们的student.hbs模板中,我们将使用each助手遍历students数组:

    // app/templates/student.hbs
    
    {{#each students as |student|}}
      {{student.name}}<br>
    {{/each}}
    

    each辅助函数的第一个参数是要迭代的数组。在这种情况下,这是在student控制器中声明的students数组。|student|param是我们将用于遍历数组的内容。

    each辅助函数必须以块形式存在。在这个例子中,学生的每个值都会显示,并在其后使用 HTML 换行符。

  5. 渲染后的输出将如下所示:

    Erik<br>
    Jim<br>
    Jane<br>
    

    如果数组意外为空,可以使用{{else}}

  6. 向模板添加一个新的数组。这个数组可以是空的,甚至可能不存在:

    // app/templates/student.hbs
    
    {{#each emptyArray as |item|}}
      {{item}}
    {{else}}
      Empty Array
    {{/each}}
    

    只有当数组为空或不存在时,才会渲染else块。

查找数组的索引

如果需要,你还可以在第二个块param中访问数组的index

  1. 创建一个新的数组并添加indexparam

    // app/templates/student.hbs
    
    {{#each students as |student index|}}
    Student {{student.name}} is at index {{index}}<br>
    {{/each}}
    

    在每次迭代后,使用 HTML 换行元素显示nameindex。索引可以通过双大括号{{index}}访问。

  2. 假设我们正在使用本章前面提到的相同的学生数组,渲染的输出将如下所示:

    Student Erik is at index 0<br>
    Student Jim is at index 1<br>
    Student Jane is at index 2<br>
    

    请记住,index0开始,而不是1

它是如何工作的...

each辅助函数使用块参数来遍历数组。each辅助函数接受一个数组参数,而block param用于遍历列表中的每个单独的项目。如果数组不存在或为空,可以使用else来显示一条消息。

在本章的食谱中,students数组在controller中声明。它有几个学生对象,可以通过模板访问。模板使用了这个数组,并使用each辅助函数遍历它。

使用元素属性和类绑定

HTMLBars 的一个非常有用的功能是将元素绑定到你的 HTML 属性中。

如何做...

一个非常简单的例子是将元素绑定到img src标签。

  1. 在一个新项目中,生成index templateindex controller

    $ ember g template index
    $ ember g controller index
    
    

    这将生成此示例所需的文件。

  2. 创建一个新的index controller文件,其属性为urlsideClasssecondClass

    // app/controllers/index.js
    import Ember from 'ember';
    export default Ember.Controller.extend({
      url: 'http://placehold.it/350x200',
      sideClass: 'cc',
      secondClass: 'dd'
    
    });
    

    我们可以获取index路由和控制器,而无需为它们创建特定的路由。它的工作方式与应用程序路由相同,所有其他路由都继承自该路由。

  3. 创建一个新的模板并添加一个img标签。将显示url元素:

    // app/templates/index.hbs
    
    <img src="img/{{url}}"/>
    

    这将像url属性在img标签的src属性中一样被渲染。

  4. 模板将使用url属性如下渲染:

    <img src="img/350x200"/>
    

    我们基本上可以将这个添加到我们喜欢的任何标签中。

  5. 让我们在模板中创建一个div标签,并添加一些用于类的属性:

    // app/templates/index.hbs
    
    <div id="side" class="{{sideClass}} {{secondClass}}">Info</div>
    

    sideClasssecondClass都将添加到类属性中。由于这些属性被绑定,它们就像 Ember 中的任何其他属性一样。它们可以动态更改,并且模板将相应地渲染。

    小贴士

    内容安全策略

    在运行本书中的示例时,你可能会偶尔在控制台中看到关于内容安全违规的消息。它们通常会以大红色文本出现在你的控制台中。Ember 团队这样做是为了帮助提醒开发者你的应用程序可能存在的潜在安全问题。对于本书的目的,这些警告可以忽略。另一方面,你可以通过编辑 config/environment.js 文件和 contentSecurityPolicy 部分来修复这些警告。你可以在 content-security-policy.com/ 找到关于内容安全如何工作的示例。

它是如何工作的...

通过 HTMLBars 模板库在属性中绑定元素,该库基于 Handlebars 库。它查看每个具有属性的属性,并在屏幕上渲染它。这些属性绑定到可以在控制器或组件中访问的属性。

我们可以将任何属性绑定到任何属性上。唯一的例外是视图辅助函数。我们将在本章的稍后部分讨论这个问题。

在模板内处理 HTML 链接

Ember.js 提供的最有用的辅助函数之一是 link-to 辅助函数。我们将讨论如何在本次食谱中使用这个有用的功能。

如何做到...

link-to 辅助函数用于导航 Ember 应用程序。第一个参数始终是路由的名称。第二个是动态段。我们将在稍后讨论动态段。

使用 link-to 辅助函数的最简单方法之一是将其内联使用。

  1. 创建一个新的学生应用程序和路由。在 project 目录的根目录下运行此命令:

    $ ember g route students
    
    

    Ember CLI 将生成一个名为 students 的新路由。这将更新 router.js 文件以及添加模板和路由文件。

  2. app/templates 文件夹中打开 students.hbs 文件,并向其中添加此字符串:

    // app/templates/students.hbs
    Hi from the students route
    {{outlet}}
    

    在导航到 students 路由后,将显示此消息。

  3. 打开 application.hbs 文件。让我们添加一个 link-to 辅助函数:

    // app/templates/application.hbs
    {{#link-to 'students'}}Students{{/link-to}}<br>
    {{outlet}}
    

    link-to 辅助函数的第一个参数是 students。这是我们之前创建的 students 路由。这将渲染一个名为 Students 的 HTML 超链接,链接到 students 路由。{{outlet}} 告诉 Handlebars 模板库在哪里渲染路由的输出。

  4. link-to 的输出将显示一个 HTML 链接。当点击此链接时,它将显示我们在 students.hbs 文件中创建的 students 路由消息。这是由 {{outlet}} 渲染的:

    Students
    Hi from the students route
    

    Students 是指向路由 /students 的超链接。

    Ember.js 足够智能,可以记住点击链接后的历史记录。因此,如果用户不小心在网页浏览器上点击后退,它将返回到上一个路由。

  5. 如果需要,你可以通过向 link-to 辅助函数添加 replace=true 选项来覆盖此行为:

    // app/templates/application.hbs
    {{#link-to 'students' replace=true}}Students{{/link-to}}<br>
    {{outlet}}
    

    小贴士

    向视图辅助函数添加数据属性

    很不幸,像 link-toinput 这样的数据视图助手不允许自定义数据属性。换句话说,如果你正在使用 link-to,你不能在 link-to 助手末尾添加 data-toggle='dropdown'。然而,正常的属性如 class 将会工作。

    添加自定义属性的一种方法是为 link-toinput 助手重新打开 Ember.LinkComponentEmber.TextField。在 第二章 中讨论了重新打开类,The Ember.Object Model,所以首先查看那里。重新打开类后,你可以添加一个 attributeBindings 属性数组。数组中的每个元素都是你希望对 link-toinput 助手可用的数据属性。例如,要将 data-toggle 作为属性添加到你的 link-to 助手,它将看起来像 attributeBindings: ['data-toggle']。我们将在下一节中讨论更多关于 input 助手的内容。

    或者,你可以创建一个从 LinkComponent 扩展而来的组件,而不是普通的简单组件。然后你可以向它添加属性。确保不要将其命名为 link-to

可以使用 link-to 助手来链接动态段。动态段被添加到 link-to 助手的第二个参数中。在这个菜谱中,我们将创建一个带有动态段的 students 路由。

  1. 从项目根目录运行此命令以创建 studentsresource

    $ ember g resource students
    
    

    这将创建我们新的 students 路由所需的所有模型、路由和模板。

  2. 接下来,我们需要更新 router.js 文件并添加一个简单的动态段:

    // app/router.js
    import Ember from 'ember';
    import config from './config/environment';
    
    var Router = Ember.Router.extend({
      location: config.locationType
    });
    
    Router.map(function() {
      this.route('students',{path: '/students/:student_id'});
    });
    
    export default Router;
    

    在这里需要意识到最重要的是路径。这被称为动态段,由 :student_id 表示。按照惯例,students 路由将从学生模型中检索信息。如果用户导航到 /students/5,路由将检索具有 ID 为 5 的学生模型。更多关于动态段和路由的信息,请参阅 第四章,Ember Router

  3. app/routes 文件夹中创建一个新的 application.js 文件。为了简单起见,我们将让应用程序路由返回一个学生对象的数组,我们可以在模板中检索它:

    // app/routes/application.js
    
    import Ember from 'ember';
    
    export default Ember.Route.extend({
      model(){
        return [{id: 1,name: 'Erik Hanchett', age: 16, location: 'Reno'},{id: 2,name: 'Jeff Smith', age: 17, location: 'San Francisco'},{id: 3,name: 'Kate Smith',age: 19, location: 'Sparks'}];
      }
    });
    

    application 路由位于顶部,并且所有其他路由都会继承它。在这个例子中,我们返回了一个具有多个属性的对象列表。这个模型将能够在我们的学生模板中访问。

  4. 更新 app/templates 文件夹中的学生模板:

    // app/templates/students.hbs
    Student Route<br>
    {{model.name}}<br>
    {{model.age}}<br>
    {{model.location}}<br>
    

    这个模板将显示传递给它的模型中的 nameagelocation。确保所有值都以前缀 model 开头。

  5. 然后,我们将更新 application.hbs 文件,使用 {{each}} 助手和 link-to

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2><br>
    <br>
    {{#each model as |student|}}
      {{#link-to 'students' student}}{{student.name}}{{/link-to}} <br>
    {{/each}}
    {{outlet}}
    <br>
    <br>
    

    在这个例子中,我们有一个 each 助手,它遍历模型。link-to 助手有两个参数。第一个是路由,即 students。第二个是动态段,student。Ember 会将每个段替换为对应对象 ID 属性值。如果由于某种原因没有模型存在,您可以显式设置该值:

    {{#link-to 'students' 1}}Some Student{{/link-to}}
    

    这将链接学生的路由与具有 ID 为 1 的动态段。

    小贴士

    多个段

    可能会有嵌套多个段的路由。例如,一个博客可能有博客文章,每篇博客文章可能有评论。在这种情况下,您可以在 link-to 助手中指定多个段。您只需用空格将它们分开。例如,一个有多个评论的博客可能看起来像 {{#link-to 'blog.comment' 1 comment}}Comment{{/link-to}}1 是第一个动态段,comment 是第二个动态段。

  6. 渲染后,将显示三个链接,如下所示:

    <a href="/students/1">Erik Hanchett</a>
    <a href="/students/2">Jeff Smith</a>
    <a href="/students/3">Kate Smith</a>
    
  7. 点击任何链接将导航到具有该 ID 的学生路线。模板随后将在屏幕上显示学生的信息如下:使用动态段落的链接助手

它是如何工作的...

link-to 助手由模板引擎用于在整个应用程序中路由客户。它仅用于内部链接,不用于外部链接。

link-to 助手接受两个或更多参数。第一个是路由的名称。第二个用于动态段。

处理 HTML 动作

通常,在应用程序中,我们需要允许与影响应用程序状态的控件进行交互。为了实现这一点,我们将使用动作。

如何实现...

{{action}} 助手用于 HTML 元素,当用户点击元素时,将动作发送回模板对应的控制器或组件。让我们看看这个例子。

  1. 创建一个新的项目,导航到 application 目录的根目录,并输入以下命令以生成一个新的组件:

    $ ember g component action-component
    
    

    请记住,所有组件的名称都必须包含一个连字符。这将生成组件模板、JavaScript 文件和测试文件。

  2. 编辑 components 文件夹中的 action-component.js 文件。我们将添加动作 toggleText

    // app/components/action-component.js
    import Ember from 'ember';
    
    export default Ember.Component.extend({
      showText: true,
      actions: {
        toggleText(){
          this.toggleProperty('showText');
        }
      }
    });
    

    在这个例子中,我们有一个默认为 trueshowText 属性。当触发动作 toggleText 时,它会切换 showText 属性。toggleProperty 方法设置其当前属性的相反值。

  3. toggleText 动作现在可以添加到模板中。下一步是将动作添加到组件模板中,使用 {{action}} 助手:

    // app/templates/components/action-component.hbs
    {{#if showText}}
    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean dui est, auctor sit amet augue vel, mattis maximus libero. Praesent feugiat ex justo, vitae convallis nulla venenatis quis.
    {{/if}}<br>
    <button {{action 'toggleText'}}>{{if showText 'Hide Text' 'Show Text'}}</button>
    {{yield}}
    

    if辅助工具仅在showText属性为true时显示文本。底部的按钮有一个名为toggleText的动作附加到它上。每次按下此按钮时,相应的action-component组件将调用toggleText动作。为了保持清晰,按钮文本将显示为Hide Text(如果文本显示)和Show Text(如果文本隐藏)。

    action辅助工具可以添加到任何 HTML 元素上。一旦元素被点击,动作就会被触发。

  4. 好知道你可以将动作附加到任何 HTML 元素上,但并非所有元素都会响应。某些浏览器可能会忽略点击事件。在这种情况下,你可以使用这个css技巧作为解决方案:

    [data-ember-action] {
      cursor: pointer;
    }
    

    小贴士

    指定事件类型

    默认情况下,所有动作都会监听点击事件。当点击发生时,该动作将在上下文、组件或控制器中触发。你可以使用on选项指定替代点击事件。例如,一个具有双击动作的按钮将看起来像<button {{action 'toggleText' on='doubleClick'}}Show Text</button>。所有事件名称在分配给on时必须为驼峰式和小写。

  5. 现在,我们需要将组件添加到application模板文件中,以便它可以显示:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    <br>
    <br>
    {{action-component}}
    {{outlet}}
    <br>
    

    这将把我们的动作组件添加到我们的application模板中。

  6. 在使用 Ember 服务器加载应用程序后,它将看起来如下所示:如何操作...

    按下Hide Text按钮将隐藏文本。再次按下它将显示文本。

    小贴士

    允许修改键

    默认情况下,action辅助工具会在同时按下修改键(如AltCtrl)时忽略点击事件。如果需要,你可以指定一个allowedKeys选项。例如,一个允许键Alt的按钮将看起来像<button {{action 'toggleText' allowedKeys='alt'}}Show Text</button>

向动作事件添加参数

你可以在动作处理程序中添加参数,这些参数将被传递回上下文。action辅助工具的动作名称之后的所有内容都将作为参数传递给组件或控制器。

  1. 首先,我们将创建一个新的组件。在项目创建后,在project文件夹的根目录下运行此命令:

    $ ember g component param-component
    
    

    这将生成我们新的param-component所需的组件文件。

  2. 编辑param-component.js文件并添加一个名为pressed的新动作:

    // app/components/param-component.js
    import Ember from 'ember';
    
    export default Ember.Component.extend({
      actions: {
        pressed(text){
          alert(text);
        }
      }
    });
    

    在这个简单的示例中,pressed动作只有一个参数。当动作被触发时,将显示一个带有传入参数文本的alert框。

  3. 下一步是编辑模板并添加动作:

    // app/templates/components/param-component.hbs
    {{input text='text' value=hello}}
    <button {{action 'pressed' hello}}>Press Me</button>
    {{yield}}
    

    在这个模板中,我们有一个input辅助工具。input辅助工具将在使用模板输入辅助工具菜谱中更详细地讨论。按钮点击会触发pressed动作,并将输入辅助工具中的hello文本传递给动作。

    注意

    允许默认浏览器动作

    默认情况下,动作辅助工具会阻止 DOM 事件的默认浏览器行为。换句话说,当用户点击可能否则会重新加载页面的链接或按钮时,Ember 会阻止这种行为。如果需要,您可以使用preventDefault=false来关闭此行为。例如,我们可以向链接添加一个动作事件,并将其重定向到页面,同时触发一个事件<a href="thispage.htm" {{action 'pressed' preventDefault=false}}>按我</a>

  4. 下一步是将param-component添加到应用程序中:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    <br>
    <br>
    {{param-component}}
    
    {{outlet}}
    <br>
    

    在此代码中,我们向application模板添加了param-component

  5. 启动服务器后,将显示一个文本框。按下按我按钮将在弹出的警告框中显示文本。它应该如下所示:向动作事件添加参数

  6. 在某些情况下,我们可能不会使用input辅助工具。假设我们想要一个动作事件触发onblur。我们可以在action辅助工具中指定一个value选项:

    // app/templates/components/param-component.hbs
    <input type="text" value={{hello}} onblur={{action 'pressed' value='target.value'}} />
    

    输入text字段具有等于hello属性的value。每当元素失去焦点时,都会引发onblur事件。默认情况下,动作处理程序接收事件监听器的第一个参数。在这种情况下,它将是Event {}。我们必须指定value选项来指定目标值,使用target.value

由于 Ember 绑定值的方式,我们无法简单地只发送hello属性作为参数到动作。这就是为什么我们必须使用value选项。

您可以通过在文本框中输入文本并点击框外使其失去焦点来测试这一点,以便在弹出的警告框中显示正确的文本。

它是如何工作的...

action辅助工具附着到 HTML 元素上,以便允许用户交互。它向模板的相应上下文、组件或控制器发送命名事件。

默认情况下,action辅助工具会发送事件监听器的第一个参数。您可以在action事件之后发送任何您想要的参数。如果需要,您可以指定value选项并使用value.target,这将发送事件的目标。

使用模板输入辅助工具

要创建常见的表单控件,可以使用input辅助工具。本食谱将介绍如何在我们的 Ember 应用程序中使用它们。

如何操作...

最常见的input辅助工具是{{input}}。它围绕Ember.TextField视图,几乎与传统的<input>HTML 元素相同。

  1. 创建一个新的项目。在app/templates文件夹中,打开application.hbs文件并添加一个input辅助工具:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    <br>
    <br>
    {{input value='Hello World'}}
    

    这个input辅助工具非常简单;它所做的只是将文本框的值设置为hello world。它被双大括号包围,并支持正常的输入属性。

  2. 当渲染时,它将如下所示:

    <h2 id="title">Welcome to Ember</h2>
    <br>
    <br>
    <input type="text" value="Hello World"/>
    

    如果需要,我们可以将属性分配给input辅助工具。

  3. 创建一个新的application控制器。在根application文件夹中运行此命令:

    $ ember g controller application
    
    

    这将生成一个应用程序可以访问的新控制器。

  4. 打开控制器并添加一个新属性。我们将称之为 helloText

    // app/controllers/application.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
        helloText: 'Hello World'
    });
    
  5. 再次编辑 application.hbs 文件并将 value 设置为属性:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    <br>
    <br>
    {{input value=helloText}}
    

    helloText 属性现在绑定到输入值。具有引号值的属性将直接设置到元素。如果未加引号,则这些值将绑定到模板当前渲染上下文上的属性。在这种情况下,这是控制器。

  6. 让我们在 input 辅助器中添加一个简单的动作。这可以通过将破折号化的事件名称作为属性添加到 input 辅助器来完成:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    <br>
    <br>
    {{input value=helloText key-press='pressed'}}
    

    每当按下键时,组件或控制器中的 pressed 动作将被触发。

  7. 由于我们没有创建按键事件,我们将将其添加到我们的控制器中:

    // app/controllers/application.js
    …
      actions: {
        pressed(){
          console.log('pressed');
        }
      }
    

    每当在文本框中按下键时,都会将一条消息记录到控制台。

如何使用复选框辅助器

在上一个示例中,我们创建了一个简单的输入文本框。我们也可以以相同的方式创建复选框。这使用了 Ember.Checkbox 视图。

  1. 在一个新项目中,打开 app/templates 文件夹中的 application.hbs 文件。让我们添加一个新的复选框:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    <br>
    <br>
    {{input type='checkbox' checked=isChecked}}
    

    这与输入文本框非常相似。

  2. 生成 application 控制器。这将用于存储我们的 isChecked 属性:

    $ ember g controller application
    
    
  3. 使用新的 isChecked 属性更新控制器。将其设置为 true

    // app/controllers/application.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
        isChecked: true
    });
    

    此控制器只有一个布尔属性,isChecked

  4. isChecked 属性绑定到复选框。渲染后,它应该看起来如下:

    …
    <input type="checkbox" checked="true"/>
    

如何使用文本区域

要创建 textarea 元素,我们可以使用 textarea 辅助器。这会包装 Ember.TextArea 视图。

创建一个新的项目并编辑 app/templates 文件夹中的 application.hbs 文件。添加一个 textarea 辅助器:

// app/templates/application.hbs
<h2 id="title">Welcome to Ember</h2>
<br>
<br>
{{textarea value='hello world' cols='20' rows='10'}}

文本区域框将显示为 20 列和 10 行。渲染后看起来如下:

…
<textarea rows="6" cols="80">Hello World</textarea>

添加动作和属性的方式与 inputcheckbox 辅助器相同。

它是如何工作的...

输入、文本区域和复选框都是辅助器,使处理常见的表单控件变得更加容易。它们围绕 Ember.TextFieldEmber.CheckboxEmber.TextArea 包装。

使用这些辅助器,我们可以轻松地将元素和动作绑定到它们。

使用开发辅助器

调试模板是一项你经常会使用的任务。以下是执行此操作的步骤。

如何做到这一点...

调试 Ember 模板最基本的方法是使用 {{log}}{{debugger}}

  1. 创建一个新的 Ember 应用程序。创建一个名为 log-example 的新组件。在根 application 文件夹中运行以下命令:

    $ ember g component log-example
    
    

    这将创建一个新的组件模板和 JavaScript 文件。

  2. app/components 文件夹中打开 log-example.js 文件并添加一个名为 helloText 的新属性:

    // app/components/log-example.js
    import Ember from 'ember';
    
    export default Ember.Component.extend({
        helloText: 'Hello World'
    });
    

    这是一个只有一个属性的简单组件。

  3. app/templates/components 目录中打开 log-example.hbs 文件并添加 log

    // app/templates/components/log-example.hbs
    {{log 'Hello text is' helloText}}
    

    这将在浏览器的控制台窗口中显示一个字符串。

  4. 现在,我们可以将这个新组件添加到我们的 application.hbs 文件中:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    <br>
    <br>
    {{log-example}}
    

    文本 Hello text is Hello World 在渲染后将在控制台显示。

  5. 在这个相同的例子中,让我们添加 {{debugger}}。编辑 log-example.hbs 文件,并将其添加到底部:

    // app/templates/components/log-example.hbs
    {{log 'Hello text is' helloText}}
    {{debugger}}
    hi
    

    调试器等同于 JavaScript 的 debugger 关键字。它将停止代码的执行,并允许检查当前的渲染上下文。

  6. 如果我们启动服务器并加载网页,浏览器将在加载过程中暂停在调试语句处。此时,我们可以打开浏览器的控制台窗口,并使用 get 函数来查找 helloText 的当前值:

    > get('helloText')
    > "Hello World"
    
    

    get 命令可以检索上下文中的任何值。换句话说,它可以检索组件或控制器中的任何值。如果调试语句在 {{each}} 循环中,这也同样适用。

  7. 你也可以在控制台调试器中获取视图的上下文:

    > context
    
    

    小贴士

    Ember 检查器

    Ember 检查器是 Chrome 和 Firefox 网络浏览器的插件。它使得调试和了解你的 Ember 应用变得容易。当你使用此插件时,你可以看到关于你的应用的各种信息,包括路由、模型、模板、控制器和组件。你可以从 Firefox 或 Chrome 插件商店免费下载它。

它是如何工作的...

Handlebars 库使得调试模板变得容易。它们是与网络浏览器交互的辅助工具,用于将信息记录到控制台或停止其执行。

Ember 的 {{debugger}} 在 JavaScript 中的等效是 debugger。它们的工作方式非常相似。

第四章:Ember 路由器

在本章中,我们将介绍以下食谱:

  • 定义应用程序路由

  • 设置路由模型

  • 在路由中处理动态段

  • 使用模板定义路由

  • 使用路由进行重定向

  • 使用异步路由

  • 加载和错误处理

  • 使用查询参数

简介

Ember 中的路由器负责在用户执行操作时更改应用程序的状态。这可能包括用户更改 URL 或在应用程序中点击后退按钮。无论执行什么操作,路由处理程序都负责。它获取当前 URL 并将其映射到正确的路由,以便向用户显示。

路由处理程序负责渲染模板、加载模型以及从一个路由到另一个路由的重定向和过渡。它们还可以处理模型变化时发生的行为。

定义应用程序路由

当加载应用程序时,路由器会查看 URL 并将其与路由匹配。我们将介绍一些关于它是如何工作的基础知识。

如何操作...

路由图用于定义 URL 映射。让我们看看如何使用this.route添加新路由。

  1. 在新应用程序中,打开app文件夹中的router.js文件。首先,我们将创建一个名为about的新路由:

    // app/router.js
    import Ember from 'ember';
    import config from './config/environment';
    
    var Router = Ember.Router.extend({
      location: config.locationType
    });
    
    Router.map(function() {
      this.route('about');
    });
    
    export default Router;
    

    上一段代码中的Router.map处理程序的程序路由。this.route创建about路由。默认情况下,路由的名称将与路径相同。例如,about路由路径将位于/about。我们可以使用path来具体设置路径。

  2. 我们不希望所有请求都发送到/about,让我们更改路径,使它们发送到/me

    // app/router.js
    …
    this.route('about',{ path: '/aboutme' });
    …
    

    新的路由about现在将被映射到 URL /aboutme

  3. 为了测试这一点,我们可以创建一个新的模板并在应用程序路由中添加一个link-to助手。首先,我们将创建模板:

    $ ember g template about
    
    

    Ember CLI 为我们创建的模板。这将创建app/templates文件夹中的about.hbs文件。

  4. link-to助手添加到application.hbs文件中:

    // app/templates/about.hbs 
    <h2 id="title">Welcome to Ember</h2>
    
    {{outlet}}
    
    {{#link-to 'about'}}about template{{/link-to}}
    

    代码在主应用程序模板中创建了一个指向about模板的新链接。

  5. 向我们刚刚创建的about模板添加一条新消息:

    // app/templates/about.hbs
    <br>Hello from the about route!<br>
    

    当我们导航到这个新路由时,将只显示此文本。

  6. 我们现在可以运行服务器并检查输出。运行ember server并点击about 模板链接。about 路由将加载,如下所示:如何操作...

    小贴士

    应用程序路由

    当您的应用程序首次启动时,应用程序路由将被加载。就像任何其他路由一样,应用程序模板将默认加载。应用程序路由是免费提供的,不需要添加到app/router.js文件中。{{outlet}}将用于渲染所有其他路由。在这里放置页眉、页脚和其他装饰性内容是个好主意。

在您的应用程序中使用嵌套路由

有时,您可能需要多个级别的路由。您可能需要在其他模板中包含模板。这可以通过嵌套路由来实现。

  1. 假设我们有一个 about 路由,其中嵌套了 locationjob 路由:

    // app/router.js
    import Ember from 'ember';
    import config from './config/environment';
    
    var Router = Ember.Router.extend({
      location: config.locationType
    });
    
    Router.map(function() {
      this.route('about', function() {
        this.route('location');
        this.route('job');
      });
    });
    
    export default Router;
    
  2. 路由 map 有一个最高级别的路由称为 about。在这个路由之下是 locationjob。创建 locationjob 所需的两个模板:

    $ ember g template about/location
    $ ember g template about/job
    $ ember g template about
    
    

    这将在 app/templates/about 文件夹中创建正确的 location.hbsjob.hbs 文件,以及在 app/templates 文件夹中的 about.hbs 文件。

  3. 为了能够访问嵌套路由,我们需要编辑 about.hbs 并为 locationjob 嵌套路由添加 outlet

    // app/templates/about.hbs
    <br>Hello from the about route<br>
    {{#link-to 'about.location'}}location{{/link-to}}<br>
    {{#link-to 'about.job'}}job{{/link-to}}
    <br>{{outlet}}<br>
    

    注意 link-to 辅助函数如何路由到 about.location。您可以使用点符号来链接嵌套路由。locationjob 嵌套路由将在 {{outlet}} 中渲染。

  4. 为了使事情更有趣,我们将更新 joblocation 路由模板:

    // app/templates/about/location.hbs
    Hello from about/location route
    
    // app/templates/about/job.hbs
    Hi from the about/job route
    
  5. 最后,我们将在应用程序路由中添加一个 link-to 辅助函数:

    // app/templates/application.hbs
    <br>
    <br>
    
    {{#link-to 'about'}}Hello{{/link-to}}
    {{outlet}}
    

    link-to 辅助函数将路由到 about。这将渲染在 outlet 中。

  6. 运行 ember server 后,您将可以点击链接并在路由间切换。它应该看起来与以下图片相似:在您的应用程序中使用嵌套路由

    如果我们点击 工作 链接,URL 将更改为 http://localhost:4200/about/job。然后 about 模板中的 {{outlet}} 将显示 job 模板信息。

添加通配符

您可以在路由中使用通配符。这允许您创建匹配多个段的 URL。让我们为任何未找到的 URL 创建一个通配符。

  1. 在一个新项目中,更新 app 文件夹中的 router.js 文件:

    // app/router.js
    import Ember from 'ember';
    import config from './config/environment';
    
    var Router = Ember.Router.extend({
      location: config.locationType
    });
    
    Router.map(function() {
        this.route('page-not-found', {path: '/*wildcard' });
    });
    
    export default Router;
    

    /*wildcard 路径将捕获所有未定义的路由并将它们路由到 page-not-found

  2. 创建一个新的 page-not-found 模板:

    $ ember g template page-not-found
    
    // app/templates/page-not-found
    <br>Not Found</br>
    
    

    当用户导航到匹配 /* 且没有现有路由匹配的 URL 时,此路由将在 application.hbsoutlet 中渲染。

向我们的关于应用程序添加动态段

路由的一个重要责任是加载模型。在这个例子中,我们将在路由中创建一个简单的动态段,列出关于路由的多个工作。

  1. 在一个新项目中,编辑 router.js 文件并添加以下代码:

    // app/router.js
    import Ember from 'ember';
    import config from './config/environment';
    
    var Router = Ember.Router.extend({
      location: config.locationType
    });
    
    Router.map(function() {
        this.route('about', function(){
            this.route('location', {path: '/about/:location_id'});
        });
    });
    
    export default Router;
    
  2. 查看输出,我们可以看到路由图显示了 about 路由及其下方的嵌套 location 路由。location 路由是一个以 : 开头的动态段,后面跟着一个标识符。:location_id 标识符将从 location 模型检索模型信息。

    例如,如果用户导航到 /about/5,则路由将设置 location_id5,以便加载具有 5 ID 的 location 模型。我们将在下一节中更详细地介绍 about 路由。

    注意

    索引路由

    在嵌套的每一层,包括应用层,Ember.js 都会自动创建一个名为 index 的路由。你不需要在 router.js 中映射它。类似于应用路由,它已经存在了。索引路由将自动在其父模板的出口处渲染。例如,如果你在 app/templates 文件夹中创建了一个 index.hbs 文件,它将自动在 application.hbs 出口处渲染。创建路由时请记住这一点。

它是如何工作的...

Ember.js 中的路由定义在 app/router.js 文件中。路由映射用于定义每个路由,并告诉 Ember 应用程序应使用 URL 中的哪个路径。按照惯例,每个路由都有一个同名的模板。通配符和动态段可以使路由更加灵活,以便它们可以加载特定数据。

设置路由模型

有时,你需要从模型中检索数据以供模板使用。路由负责加载适当的模型。本教程将介绍如何进行此操作。

如何操作...

  1. 在一个新应用中,打开 router.js 文件并添加一个新的路由。我们将把这个路由称为 students

    // app/router.js
    import Ember from 'ember';
    import config from './config/environment';
    
    var Router = Ember.Router.extend({
      location: config.locationType
    });
    
    Router.map(function() {
      this.route('students');
    });
    
    export default Router;
    

    students 路由将从 students 路由处理程序中检索数据。

  2. 生成 students 路由。这将创建 students 路由处理程序和模板:

    $ ember g route students
    
    
  3. students.js 文件中,添加一个新的模型,该模型返回一个 JavaScript 对象:

    // app/routes/students.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        model() {
          return [1,2,3,4,5,6,7];
        }
    });
    

    model 钩子通常返回一个 Ember Data 记录。然而,它也可以返回任何承诺对象、纯 JavaScript 对象或数组。Ember 将等待数据加载或承诺解决后再渲染模板。

    在我们的示例中,为了简化,我们返回了一个数组。

  4. 在模板中创建一个简单的 each 循环来显示 model 中的数据:

    // app/templates/students.hbs
    {{#each model as |number|}}
        Number: {{number}}<br>
    {{/each}}
    

    each 循环将显示数组中的每个 number。模型数据是从我们之前创建的路由返回的。

  5. 运行 ember server 并在 http://localhost:4200/students 路由上加载。渲染后,它将看起来像以下图片:如何操作...

它是如何工作的...

路由的一个重要任务是加载模型。模型是代表应用程序可能向用户展示的数据的对象。路由可以返回 Ember Data 记录、数组或对象。

处理路由内的动态段

使用动态段和动态模型是路由的一个重要方面。以下教程将介绍如何实现这一点。

准备工作

在我们开始我们的食谱之前,我们需要设置一个名为Ember CLI Mirage的插件。Ember 插件,也称为附加组件,使得在应用程序之间共享通用代码变得容易。Ember CLI Mirage 插件使得创建模拟服务器变得容易,这样我们就可以开发、测试和原型化我们的数据。在本章中,我们不会过多地介绍这个附加组件。如果您想了解更多信息,您可以在github.com/samselikoff/ember-cli-mirage下载。

在这个例子中,我们将使用Ember Data 的 RESTAdapter,而不是新的JSON API适配器。

  1. 在一个新项目中,在application文件夹中运行以下安装命令:

    $ ember install ember-cli-mirage@0.1.11
    
    

    这将安装 Ember CLI Mirage 0.1.11版本、Bower 和npm包到应用程序中。我们将使用这个版本在本书的所有示例中。

  2. app/mirage文件夹中的config.js文件中打开。添加几个新的路由:

    // app/mirage/config.js
    export default function() {
    
    this.get('/students');
    this.get('/students/:id');
    }
    

    第一个模拟路由/students将返回我们 Mirage 内存数据库中的所有学生数据。第二个模拟路由/students/:id将只返回与 URL 中的 ID 匹配的数据。这将在我们尝试使用模型动态段时使用。

  3. students创建一组新的固定数据。在app/mirage/fixtures目录中创建一个名为students.js的新文件:

    // app/mirage/fixtures/students.js
    export default [
      {id: 1, name: 'Jane Smith', age: 15},
      {id: 2, name: 'Erik Hanchett', age: 14},
      {id: 3, name: 'Suzy Q', age: 17}
    ];
    

    这个文件名students.js与路由匹配,并将用于在 Mirage 内存数据库中加载数据。请注意,Mirage 还支持工厂。工厂是一个功能强大的特性,使得加载大量虚假数据变得极其容易。工厂也可以用于测试用例。

    为了简单起见,我们只使用固定数据。

  4. 为我们的应用程序加载一个新的场景。更新app/mirage/scenarios/default.js文件:

    // app/mirage/scenarios/default.js
    export default function( server ) {
    
        server.loadFixtures();
    }
    

    scenarios文件夹中的default.js文件用于在开发中初始化数据库。server.loadFixtures()方法加载所有固定数据,以便它可以被/students路由访问。

如何做到这一点...

我们模型中的数据可能永远不会改变。另一方面,这些数据可能会根据与用户的交互多次更改。这个食谱将介绍如何使用动态段与您的路由一起使用并从模型返回数据。

  1. 首先创建一个名为students的新资源。然后生成一个名为application的适配器,最后创建一个路由:

    $ ember g resource students
    $ ember g adapter application
    $ ember g route application
    
    

    这将生成本食谱所需的路线、适配器和模板文件。请注意,通过生成application路由,您将被提示覆盖application.hbs文件。当发生这种情况时,您可以选择n,即不覆盖。

  2. router.js文件中更新新的动态段路由:

    // app/router.js
    import Ember from 'ember';
    import config from './config/environment';
    
    var Router = Ember.Router.extend({
      location: config.locationType
    });
    
    Router.map(function() {
      this.route('students',  {path: '/students/:student_id'});
    });
    
    export default Router;
    

    这个新的路由路径为/students/:student_id。这个路由将从 URL 中提取:student_id并将其作为第一个参数传递给模型钩子。

    例如,假设我们有一个学生列表,并且我们希望能够通过访问 /students/1/students/2 来访问每个学生的数据。每个 URL 都将返回该学生的数据。

  3. 更新 app/adapters 文件夹中的 application.js 文件:

    import DS from 'ember-data';
    
    export default DS.RESTAdapter.extend({
    });
    

    这将创建一个新的 RESTAdapter,Ember 将用于此示例。这将在模型章节中更详细地介绍。

  4. 编辑 app/models 文件夹中的 students.js 文件。此文件是我们的模型,将用于从我们之前创建的 Mirage 模拟服务器检索数据:

     // app/models/student.js
    import DS from 'ember-data';
    
    export default DS.Model.extend({
        name: DS.attr('string'),
        age: DS.attr('number')
    
    });
    

    这创建了一个具有两个属性 nameage 的新模型。模型文件定义了数据将如何看起来。我们将在 第七章Ember 模型和 Ember Data 中更详细地介绍这一点。现在,我们将从模拟服务器检索这些数据。

  5. 更新 app/routes 文件夹中的 students.js 文件。添加 Ember Data 的 findRecord 方法:

    // app/routes/students.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        model(param) {
          return this.store.findRecord('student',param.student_id);
        }
    });
    

    此处的 model 钩子有一个参数,即 paramparam 参数是 student_id,它从路由的 URL 中传递过来。Ember Data 有一个 findRecord 方法,它接受两个参数。第一个参数是模型名称,第二个是 ID。

    此模型将返回传递给它的 ID 的学生记录。我们现在可以在模板中使用它。

  6. 编辑 app/templates 文件夹中的 students.hbs 文件。添加模型信息:

    // app/templates/students.hbs
    {{model.name}}
    {{model.age}}
    

    {{model.name}}{{model.age}} 属性将检索从路由传递到模板的模型信息。

    到目前为止,我们应该能够运行 ember server 并在访问 http://localhost:4200/students/1 时看到数据。为了使事情变得简单一些,我们将继续创建一个新的路由处理器用于主应用程序路由。

  7. 编辑 app/routes 文件夹中的 application.js 文件:

    // app/routes/application.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        model() {
          return this.store.findAll('student');
        }
    });
    

    注意事项

    多个模型

    有时,你可能想在一条路由中使用多个模型。这可以通过使用 Ember.RSVP.hash 来实现。该 hash 接收返回承诺的参数。当所有参数都解析完成后,Ember.RSVP.hash 才会解析。在模型中,它可能看起来像这样:return Ember.RSVP.hash({ students: this.store.findAll('student'), books: this.store.findAll('book')})。每个模型之间用逗号分隔。

    这将允许我们的应用程序从我们的学生模型中检索所有记录。

  8. 打开 app/templates 文件夹中的 application.hbs 文件。我们将添加一个 each 迭代器,它将链接到每个学生的信息:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Chapter 4</h2>
    
    {{#each model as |student|}}
        {{#link-to 'students' student.id}}{{student.name}}{{/link-to}}<br>
    {{/each}}
    
    {{outlet}} 
    

    在此模板中,我们正在迭代 student 模型中的所有记录。我们使用每个学生的名字作为链接到我们的动态段。student.id 参数传递给 link-to 辅助函数。

  9. 启动服务器后,你会看到一个学生列表以及每个学生信息的链接。点击学生的名字后,student.hbs 模板将加载并显示学生的信息。它看起来像以下图片:如何操作...

它是如何工作的...

动态模型允许数据根据用户操作而变化。这些数据将取决于router.js文件中设置的动态段。该段在router文件中定义,并从 URL 传递给模型钩子作为其第一个参数。Ember Data 使用findRecord方法查找正确的记录并将其返回到模板,以便可以使用。

使用模板定义路由

路由处理器的另一个任务是渲染适当的模板。以下是一个关于这一点的示例。

如何操作...

在这个示例中,我们将创建一些嵌套路由并检查它们在哪里渲染。

  1. 在一个新项目中,创建一个新的studentsschools路由:

    $ ember g route schools
    $ ember g route schools/students
    
    

    这将创建嵌套的studentsschools路由。

  2. 让我们看看router.js文件:

    // app/router.js
    import Ember from 'ember';
    import config from './config/environment';
    
    var Router = Ember.Router.extend({
      location: config.locationType
    });
    
    Router.map(function() {
      this.route('schools', {}, function() {
        this.route('students', {});
      });
    });
    
    export default Router;
    

    生成的命令已经创建了我们需要的路由。schools路由有一个嵌套的路由,称为students

    根据约定,路由将渲染与名称相同的模板。因此,schools路由将渲染到schools.hbs文件,而students路由将渲染到schools/students.hbs文件。

  3. 更新schools.hbs文件:

    // app/templates/schools.hbs
    This is the school route<br>
    {{outlet}}
    

    {{outlet}}将在schools文件夹中渲染students.hbs文件。每个模板都将渲染到其父路由模板的{{outlet}}中。

  4. 更新app/templates/schools文件夹中的students.hbs文件:

    // app/templates/schools/students.hbs
    This is the students route<br>
    {{outlet}}
    
  5. 运行ember server,你应该看到这个结果:如何操作...

  6. 通过访问http://localhost:4200/schools/students,将显示两个模板。应用程序{{outlet}}渲染学校模板。学校模板的{{outlet}}渲染学生模板。

  7. 如果需要,您可以更改路由的渲染位置。您不必在具有相同名称的模板中渲染,而是可以使用路由处理器中的renderTemplate()方法将其设置为任何内容:

    // app/routes/school.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
      renderTemplate() {
        this.render('anotherSchool');
      }
    });
    

    学校路由现在将渲染到anotherSchool模板。

它是如何工作的...

默认情况下,路由将根据路由的名称渲染模板。Ember 通过约定来实现这一点。另一方面,在路由处理器中使用renderTemplate可以改变这个默认设置。所有这些都是在 Ember API 的底层完成的。

使用路由进行重定向

路由的一个重要特性是重定向。这个示例将介绍如何使用transitionTo方法。

如何操作...

在我们的路由处理器中,我们有特殊的钩子,称为beforeModelafterModel。我们可以使用这些钩子在模型加载前或加载后执行操作。transitionTo方法可以用来将应用程序重定向到不同的路由。

  1. 在一个新应用中,创建一个新的students路由:

    $ ember g route students
    
    

    这将生成students路由和模板文件。

  2. 为了简化,我们将让路由处理器返回一个简单的数字数组:

    // app/routes/students.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        model() {
          return [1,2,3,4,5,6,7,8,9];
        }
    });
    
  3. 再次编辑routes/students.js文件。这次,我们将添加一个 before 钩子和一个过渡到它:

    // app/routes/students.js
    …
    beforeModel(){
      this.transitionTo('teachers');
    }
    

    transitionTo 方法将从一条路由重定向到另一条路由。此选项将在模型加载之前进行重定向并切换到教师的路由。

  4. 我们也可以使用 afterModel() 钩子在模型加载后进行转换:

    // app/routes/students.js
    …
    afterModel(){
      this.transitionTo('teachers');
    }
    

    这将在切换到新路由之前等待模型完全加载。如果需要,您可以在切换之前检查已加载的路由。

    小贴士

    重定向

    当转换到嵌套路由时,使用 redirect 方法而不是 afterModelbeforeModel 钩子是个好主意。这将防止在重定向后再次触发 beforeModelafterModel 和模型。在处理嵌套路由中的 transitionTo 时,请记住这一点。

它是如何工作的...

afterModelbeforeModel 钩子在模型加载之后或之前触发。transitionTo 方法用于从一条路由重定向到另一条路由。它可以在路由处理程序或应用程序的任何其他地方使用。

处理异步路由

路由器的一个更高级功能是处理异步逻辑。以下食谱使用承诺解释了这个概念。

如何操作...

在路由中,Ember 大量使用承诺。承诺是表示最终值的对象。我们可以在我们的模型中使用承诺。

  1. 为应用路由创建一个新的路由器:

    $ ember g route application
    
    

    如果提示覆盖模板,请输入 Y。这将生成默认应用路由的路由器文件。

  2. app/router 文件夹中的 application.js 文件中添加一个新的模型:

    // app/router/application.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        model() {
          return  new Ember.RSVP.Promise(function(resolve) {
            resolve({message: 'Resolved'});
          });
        } ,
        setupController(controller, model){
          this._super(controller, model);
          console.log(model.message);
        }
    });
    

    在路由器中,我们创建了一个新的模型。这个模型将可用于我们的应用程序模板。在这个模型中,我们返回 Ember.RSVP.Promise,这是 Ember 处理承诺的方式。它可以解决或拒绝。为了简单起见,我们让它返回一个消息。

    setupController 钩子用于设置当前路由的控制器。由于我们正在覆盖 setupController,它也会覆盖其默认行为。因此,我们必须在它上面调用 super。否则,它可能会影响其正常行为。我们可以使用 console.log 将模型消息输出到控制台。

    小贴士

    异步路由

    在转换过程中,模型钩子在路由器中触发。如果在转换过程中模型返回一个数组,它将立即返回。另一方面,如果模型返回一个承诺,它必须等待这个承诺得到解决或拒绝。路由器将任何定义了 then 方法的对象视为承诺。在承诺解决后,转换将从它离开的地方继续。可以链式多个承诺,因此下一个承诺或模型必须在转换完成之前得到解决。

  3. 让我们再次编辑应用路由器并将其设置为拒绝:

    // app/routes/application.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        model() {
          return Ember.RSVP.Promise.reject('error');
        },
        setupController(controller, model)
           this._super(controller, model);
           console.log(model.message);
        },
        actions: {
          error(reason){
            console.log(reason);
          }
        }
    });
    

    在前面的代码中,模型返回一个拒绝的承诺。如加载和错误处理配方中所述,有一个名为error的事件。这个事件仅在模型中发生错误时才会触发。然后我们可以将错误记录到控制台。

  4. 我们可以通过编辑app/templates文件夹中的application.hbs文件来测试这一点:

    // app/templates/application.hbs
    {{outlet}}
    
    Message: {{model.message}}
    

    如果承诺没有拒绝,将显示模型消息。如果模型拒绝,则不会显示任何内容;路由停止加载,控制台将显示错误信息。

    错误事件向上冒泡。在这种情况下,我们已经在应用程序路由上,并且无法进一步冒泡。如果我们处于另一个路由,我们可以返回 true,那么错误就会冒泡到应用程序错误事件。

  5. 再次编辑app/router文件夹中的application.js文件。让我们处理拒绝:

    // app/routers/application.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        model() {
          return new Ember.RSVP.Promise(function(resolve,reject) {
            reject('error');
          }).then(null, function() {
            return {message: 'Returned from rejection};
          });
        },
        setupController(controller, model){
          this._super(controller, model);
          console.log(model.message);
        },
        actions: {
          error(reason){
            console.log(reason);
          }
        }
    
    });
    

    在前面的代码中,RSVP承诺拒绝。然而,我们仍然在最后链式返回一个消息。这样,转换就不会停止,并且会继续。

  6. 运行ember server并打开一个网页。您应该看到这条消息:如何做...

    这条消息显示从拒绝返回,因为我们处理了承诺拒绝回调并仍然返回了一个消息。

它是如何工作的...

承诺是 Ember 处理异步逻辑的一种方式。承诺是一个表示最终值的对象。承诺可以拒绝或满足,即解决一个值。要检索值或处理拒绝,可以使用then方法,它可以接受两个不同的回调。第一个是用于满足,第二个是用于拒绝。例如,您可能使用拒绝来重试或返回不同的数据。

加载和错误处理

当路由正在加载或发生错误时,我们有能力让用户知道正在发生什么。这个配方将介绍如何做到这一点的基础知识。

如何做...

  1. 在一个新项目中,创建一个新的students路由:

    $ ember g route students
    
    

    ember generate命令将为students路由创建所有必要的文件。

  2. app/routes文件夹中编辑students.js文件。添加一个新的模型返回:

    // app/routes/students.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        model(){
          return new Ember.RSVP.Promise(function (resolve, reject) {
            Ember.run.later((function() {
              resolve( [1,2,3,4,5,6,7,8,9]);
            }), 2000);
          });
        }
    });
    

    在我们的路由文件中,我们返回一个 Ember RSVP承诺。这个承诺解决为一个简单的数组,将被返回。Ember.run.later是 Ember 的一个内置方法,它是 Ember 运行循环的一部分。它类似于 JavaScript 的setTimeout()。在这种情况下,我们设置了 2,000 毫秒的超时,以便我们可以模拟如果模型加载缓慢可能会发生的情况。两秒钟后,解决将返回数组。

  3. app/templates文件夹中添加一个加载子状态模板:

     // app/templates/students-loading.hbs
    <h1> Loading! Please wait! </h1>
    

    这个子状态将在学生路由加载时被加载。通过添加一个带有路由名和结尾破折号加载的模板来创建一个加载子状态。例如,在我们的示例中,我们调用students-loading.hbs子状态。加载子状态的应用程序将是application-loading.hbs

  4. 作为子状态的替代,我们可以在路由中使用加载事件。如果beforeModelmodelafterModel钩子不能立即解决,将触发加载事件。添加一个新的加载动作,在模型加载时显示弹窗,并过渡到应用程序路由:

    // app/routes/students.js
    …
        },
        actions: {
          loading(transition, originRoute) {
            alert('Sorry this page is taking so long to load!');
            this.transitionTo('application');
          }
        }
    …
    

    当路由加载时,将触发弹窗。

  5. 导航到http://localhost:4200/students,当模型加载时,你会看到弹窗。

创建错误子状态

错误子状态会在遇到错误时发生。它与加载子状态非常相似。

  1. 在一个新的应用程序中,创建一个teachers路由:

    $ ember g route teachers
    
    

    这将为teachers路由创建所有必要的文件。

  2. 编辑app/routes文件夹中的teachers.js文件。添加一个新的Ember.RSVP.Promise并拒绝:

    // app/routes/teachers.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        model(){
          return new Ember.RSVP.Promise(function (resolve, reject) {
            reject('error');
          });
        }
    

    在这个例子中,我们返回一个新的Ember.RSVP.Promise,它将拒绝。这将导致错误发生。

  3. app/templates文件夹中创建一个新的teachers-error.hbs文件。当teachers路由发生错误时,将显示此文件:

    // app/templates/teachers-error.hbs
    <h1>Error Loading!</h1>
    

    错误子状态,就像加载子状态一样,必须以带有结尾破折号的路由名命名。模板将在错误发生时显示,不需要执行任何其他逻辑。

  4. 或者,您也可以在路由中使用错误事件来显示错误并重定向到不同的路由:

    // app/routes/teachers.js
    …
       },
        actions: {
          error(error, transition) {
            alert('Sorry this page is taking so long to load!');
            this.transitionTo('application');
          }
        }
    

    使用错误事件只是处理错误的一种方式。我们当然可以创建一个错误路由来过渡到。

它是如何工作的...

Ember 路由具有内置的方法和事件来处理错误和加载。在加载数据时,模型钩子正在等待查询完成。在此期间,将立即和同步地过渡到一个以破折号加载结尾的模板。URL 不会受到影响。查询完成后,加载路由将退出,并继续原始路由。

在处理错误时,将加载错误模板。再次强调,URL 不会切换到错误路由。错误将作为模型传递给错误状态。

使用查询参数

查询参数允许您使用 URL 来表示应用程序状态。在这些菜谱中,我们将以几种不同的方式使用查询参数来展示其工作原理。

如何做到这一点...

查询参数是可选的关键字值对。它们将出现在 URL 中?的右侧。

  1. 在一个新的项目中,生成一个新的application控制器:

    $ ember g controller application
    
    

    application控制器将在app/controllers文件夹中生成。

  2. 更新应用程序控制器,为student添加一个新的queryParams

    / app/controllers/application.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
        queryParams: ['student'],
        student: null
    });
    

    这将在 URL 中的student查询参数和控制器中的student属性之间建立绑定。如果任一发生变化,另一个也会发生变化。

    如果student属性被设置为任何非 null 的值,则student属性将有一个默认值。这一点很重要,因为查询参数值会被转换为与默认值相同的数据类型。换句话说,如果学生属性默认为数字1,而你将 URL 更改为/?student=2,属性将被设置为数字2,而不是字符串"2"。此外,请记住,默认值不会在 URL 中序列化。

  3. 更新app/templates文件夹中的application.hbs文件。我们将添加student属性以进行测试:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    
    {{outlet}}
    student: {{student}}
    

    这是一个非常简单的模板,它只是显示控制器中的student信息。

  4. 启动 Ember 服务器并尝试更改 URL。导航到http://localhost:4200?students=Erik。模板将更新以显示新的学生信息:如何操作...

URL 中问号?之后的任何内容都可以用于查询参数。每个参数由一个与号&分隔。在这种情况下,学生属性被设置为 Erik。这将在模板中更新。

重要的是要意识到我们可以使用link-to辅助器传递查询参数。

  1. 在一个新项目中,创建一个新的application.js控制器:

    $ ember g controller application
    
    

    这将生成我们可以用于查询参数的application控制器。

  2. 编辑application控制器并添加一个新的查询参数:

    // app/controllers/application.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
        queryParams: ['student'],
        student: null
    });
    

    在这个例子中,我们创建了一个简单的名为student的查询参数。

  3. 更新app/templates文件夹中的application.hbs文件。添加student属性和带有查询参数的新link-to辅助器:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    
    {{outlet}}
    student: {{student}}<br>
    {{#link-to 'application' (query-params student='Jane')}}Jane Query{{/link-to}}
    

    您可以通过将查询参数用括号括起来并使用query-params子表达式辅助器来将其添加到link-to中。子表达式之后是键值对。在这种情况下,我们有一个student键。

  4. 启动 Ember 服务器并点击Jane Query链接。应该出现以下页面:添加带有查询参数的辅助器

    小贴士

    使用带有查询参数的transitionTo

    当使用transitionTo方法转换路由时,可以使用查询参数。您可以将查询参数作为对象键queryParmams的最后一个参数添加。例如,如果您需要转换到应用程序路由并需要传递学生查询参数,它可能看起来像这样:

    this.transitionTo('application', { queryParams: { student: 'Erik' }});

重置控制器的查询参数

默认情况下,查询参数是粘性的。换句话说,当你进入和离开路由时,它们会被保留。它们也会保留路由中加载的模型。

您可以通过几种方式来覆盖这种行为。一种方式是将默认查询参数传递给link-to辅助函数或使用transitionTo。另一种方式是使用Route.resetController钩子。

  1. 创建一个新的项目并生成一个名为route1的新路由:

    $ ember g route route1
    $ ember g controller route1
    
    

    这将创建一个新的路由和控制器route1

  2. 编辑app/controllers文件夹中的route1.js文件:

    // app/controllers/route1.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
        queryParams: ['student'],
        student: null,
    });
    

    就像我们之前的例子一样,我们使用了一个简单的查询参数student

  3. 编辑app/routes文件夹中的route1.js文件。向路由添加一个新的resetController钩子:

    // app/routes/route1.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        resetController: function (controller, isExiting, transition) {
            this._super(controller,isExiting,transition);
            if (isExiting) {
              controller.set('student', null);
            }
        }
    });
    

    在这个路由中,我们使用resetController钩子。这将在有人退出或从路由转换时触发。和之前一样,我们必须调用 super,这样我们就不阻止默认行为。isExiting参数只有在路由的模型发生变化时才会为 false,否则它将触发。

    controller.set方法是我们访问学生属性的一种方式。我们将它设置为 null,这样当我们离开route1时,它就不会被保留。

  4. 编辑app/templates文件夹中的application.hbs文件。将link-to辅助函数添加到新的route1路由中:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    {{#link-to 'route1'}}Route 1{{/link-to}}<br>
    {{outlet}}
    

    这个模板非常简单。我们只是创建了一个指向新的route1的链接。

  5. 编辑app/templates文件夹中的route1.hbs文件。添加student属性和一个返回主application路由的链接:

    Route 1<br>
    student: {{student}}<br>
    {{#link-to 'application'}}App{{/link-to}}<br>
    

    这个模板显示了我们可以通过查询参数设置的student属性。然后它有一个返回主application路由的链接。

  6. 运行ember server并加载应用程序。输入 URL,http://localhost:4200/route1?student=Erik。这将显示带有学生查询参数的route1。如果您点击应用程序链接,它将带您回到主应用程序。如果您再次点击Route 1链接,它将不会保留查询参数并重置。它看起来像以下图片:重置控制器查询参数

它是如何工作的...

查询参数是出现在应用程序 URL 中?右侧的键值对。它们通过在 URL 中序列化数据来定义额外的应用程序状态。它们在路由驱动控制器中设置。我们可以使用transitionTolink-to辅助函数轻松地导航到它们。

第五章。Ember 控制器

在本章中,我们将介绍以下食谱:

  • 存储应用程序属性

  • 处理控制器中的动作

  • 与转换一起工作

  • 管理控制器之间的依赖关系

简介

Ember.js 中的控制器与组件类似。它们可以封装代码,处理动作,并具有属性。此外,控制器可以转换到应用程序的不同部分,并从其他控制器检索信息。

在不久的将来,控制器将被组件完全取代。然而,直到这一发生,了解控制器的工作原理是很有帮助的。

存储应用程序属性

控制器的一个优点是存储属性。控制器具有存储在应用程序中的属性,这与模型不同,模型中的属性是保存到服务器的。在本食谱中,我们将查看几种不同类型的属性以及它们如何在模板中显示。

如何操作...

  1. 在新应用程序中,运行以下命令:

    $ ember g controller application
    
    

    在这个应用程序中,我们将使用application controller来存储所有属性。

  2. 更新应用程序控制器以添加新的动作和几个属性:

    // app/controllers/application.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
        prop2: 'test',
        prop3: true,
        actions: {
          enter(val){
            alert(this.get('prop1'));
            alert(this.getProperties('prop1','prop2').prop1);
            alert(val);
            this.toggleProperty('prop3');
          }
        }
    });
    

    此控制器有两个属性。第一个属性包含一个字符串。第二个具有附加的布尔值trueenter动作显示几个警告框。让我们更详细地看看enter动作:

        alert(this.get('prop1'));
        alert(this.getProperties('prop1','prop2').prop1);
        alert(val);
        this.toggleProperty('prop3');
    

    当在 Ember 中访问属性时,始终使用this.getthis.set。这保证了属性将在 Ember 中被正确读取或设置。此外,我们可以使用this.getProperties。这允许我们一次性获取多个属性。它返回一个包含这些属性及其值的对象。

    另一个有用的方法是toggleProperty。这将取一个布尔值并将其切换。换句话说,false的值将变为true,反之亦然。

    注意,您不必在控制器定义中声明每个属性。属性可以直接从模板中检索并进行操作。在上面的示例中,prop1属性是从模板中检索的。它从未在控制器中定义过。如果需要,您可以设置默认属性值。

  3. 更新应用程序的模板。显示属性:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    
    {{outlet}}
    
    <input type='text' value={{prop1}}>One way input<br>
    Property 1:{{prop1}}<br>
    Property 2:{{prop2}}<br>
    {{input type='text'  value=prop1}}Two way input helper<br>
    <button {{action 'enter' prop1}}>Value of prop1</button><br>
    
    Propert 3:{{prop3}}
    

    prop1属性绑定到括号输入和input辅助工具。默认情况下,括号输入标签是单向的。这意味着当模板渲染时,prop1属性是从控制器中检索的。它被复制,并且属性的变化不会反映回控制器。另一方面,input辅助工具是双向绑定的。对input辅助工具中的值所做的任何更改都会反映在控制器中。

  4. 启动 Ember 服务器并更新单向输入中的值:如何操作...

    即使括号输入标签的值设置为prop1,它也不会改变模板中其他地方的{{prop1}}属性。这是因为值被设置为仅以这种方式工作。在控制器中更改prop1值的唯一方法是将它作为从模板发送回控制器的操作。

    这是“数据向下,操作向上”的基础。数据从控制器或组件复制到模板。对其的任何更改都会随后通过操作发送回父组件或控制器。请记住这个概念,因为它在 Ember 中变得越来越流行。

  5. 更新双向输入助手框中的值:如何操作...

    双向input助手更新模板中的所有值,因为它链接回控制器。模板中属性的任何更改都会反映在控制器中,因为它双向绑定。

  6. 点击prop1 的值按钮:如何操作...

    当按钮被点击时,会触发一个操作。它会显示三个警告框。每个警告框都会显示prop1属性。然后它会将第三个布尔属性从true切换到false

计算属性或观察者也可以添加到控制器中。要了解更多信息,请查看第二章,Ember.Object 模型

它是如何工作的...

控制器可以使用属性向用户显示信息。与模型不同,它们不会被持久化到服务器。然而,它们被持久化在应用程序状态中。当与模板一起工作时,它们可以是单向或双向绑定。

在控制器中处理操作

操作对控制器很重要。它们由用户操作触发,可以用来改变应用程序状态。在这个菜谱中,我们将创建四种不同类型的操作,并看看它们如何对不同情况进行反应。

如何操作...

  1. 在一个新的应用中,生成以下文件:

    $ ember g controller application
    $ ember g route application
    
    

    我们将使用controllerapplication来存储操作。一些操作会冒泡到route,而一些则不会。

  2. 更新应用程序控制器并添加三个新操作:

    // app/controllers/application.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
        actions: {
          action1(){
            alert('Application controller action action1');
          },
          action2(){
            alert('Application controller action action2');
            return true;
          },
          action3(val){
            alert(`Value Passed: ${val}`);
          }
        }
    });
    

    让我们看看每个操作:

        action1(){
          alert('Application controller action action1');
        },
    

    这是一个正常的操作。它只是显示一个警告框:

        action2(){
          alert('Application controller action action2');
          return true;
        },
    

    这个操作有点更有趣。默认情况下,如果控制器中存在所有控制器操作,它们会return false。通过将值返回到true,这个操作将在显示警告框后冒泡到application路由。然后路由可以处理这个操作:

        action3(val){
          alert(`Value Passed: ${val}`);
        }
    

    操作会传递一个值给它。它会在警告框中向用户显示这个值。警告框中的文本正在使用一种称为 ES6 模板字符串的东西。这使得在文本中显示变量变得稍微容易一些。

  3. 更新应用程序路由并添加两个更多操作:

    // app/routes/application.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        actions: {
          enter(){
            alert('Application route action enter!');
          },
          action2(){
            alert('Application route action action2!');
          }
        }
    });
    

    每个动作都会显示一个警告框。你可能已经注意到 enter 动作在控制器中不存在。按照惯例,模板 actions 首先在控制器中查找。如果动作在控制器中未定义,它将向上冒泡到路由。

  4. 向应用程序模板添加四个按钮:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    
    <button {{action 'action1'}}>Action 1 in controller</button>
    <button {{action 'action2'}}>Action 2 in controller and route</button>
    <button {{action 'action3' 'Hello World'}}>Action 3 Passing Values</button>
    <button {{action 'enter'}}>Action enter only in route</button>
    

    每个 button 都绑定了一个 action。默认情况下,这是绑定到点击事件;如果需要,可以更改它。

  5. 运行 ember server。打开网页浏览器查看以下屏幕:如何操作...

    每个按钮代表不同类型的动作。

  6. 点击第一个按钮:如何操作...

    这显示了控制器中的消息。

  7. 点击第二个按钮:如何操作...

  8. 这个警告框首先从控制器中显示:如何操作...

    第二个警告框是从应用程序路由中显示的。

  9. 点击第三个动作:如何操作...

    这显示了带有传递值的控制器消息。

  10. 点击最后一个按钮:如何操作...

    这将显示应用程序路由中的消息,因为它在控制器中未定义。

它是如何工作的...

Ember 中的动作会冒泡,但它们依赖于用户动作。它们从控制器开始,然后移动到路由。我们可以在模板中添加动作并从模板传递值到我们的动作。

与过渡一起工作

当在路由内部时,你可以使用控制器过渡到另一个路由。我们将查看如何从一个路由过渡到另一个路由的示例。

如何操作...

  1. 在一个新应用程序中,生成以下文件:

    $ ember g route foo1
    $ ember g route foo2
    $ ember g controller foo1
    $ ember g controller foo2
    $ ember g template index
    
    

    这将为我们生成两个不同的路由——foo1foo2 路由。每个路由将有一个按钮,可以过渡到另一个路由。每个控制器将处理动作逻辑。

  2. 向 foo1 控制器添加一个动作:

    // app/controllers/foo1.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
        actions: {
          enter(){
            this.transitionToRoute('foo2');
          }
        }
    });
    

    这个控制器有一个名为 enter 的动作,它过渡到名为 foo2 的路由。this.transitionToRoute 方法用于在应用程序中过渡到不同的路由。它接受两个参数。第一个参数是路由的名称。第二个参数是可选的,你可以在其中输入模型。默认情况下,如果添加,它将在 URL 中序列化。

    trasintionToRoute 方法可以接受路由路径。例如,你可能有一个名为 foo2 的嵌套路由 foo3。你可以通过调用 this.trasitionToRoute('foo2.foo3') 来过渡到这个路由。

  3. 向 foo2 控制器添加一个动作:

    // app/controllers/foo2.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
        actions: {
          enter(){
            this.transitionToRoute('foo1');
          }
        }
    });
    

    当触发 enter 动作时,它将过渡到 foo1 路由。

  4. 更新 foo1 模板:

    // app/templates/foo1.hbs
    This is Foo1<br>
    
    <button {{action 'enter'}}>Move to route foo2</button>
    

    这个 button 触发了 foo1 控制器中的 enter action

  5. 更新 foo2 模板:

    // app/templates/foo2.hbs
    This is Foo2<br>
    <button {{action 'enter'}}>Move to route foo1</button>
    

    这个 button 触发了 foo2 控制器中的 enter action

  6. 在索引模板文件中为两个路由添加一个链接:

    // app/templates/index.hbs
    {{link-to 'Foo1 Route' 'foo1'}}<br>
    {{link-to 'Foo2 Route' 'foo2'}}<br>
    

    这使用非块形式的 link-to 辅助函数。第一个参数是显示的名称,第二个是路由的名称。

  7. 更新应用模板文件:

    // app/templates/application.hbs
    {{#link-to 'application'}}<h2 id="title">Welcome to Ember</h2>{{/link-to}}
    
    {{outlet}}
    

    应用模板文件顶部有一个链接返回到应用。

  8. 运行ember server,在打开网页浏览器后,你会看到以下屏幕:如何操作...

    这显示了每个路由的链接。

  9. 点击Foo1 路由链接。将显示以下页面:如何操作...

    这显示了 foo1 路由。

  10. 点击Foo2 路由按钮。将显示以下屏幕:如何操作...

    点击按钮后,显示 foo2 路由。

它是如何工作的...

要在应用中导航,我们可以在控制器中使用转换。trasitionToRoute方法用于从一个路由转换到另一个路由。它是Ember.Controller类的一部分。

管理控制器之间的依赖关系

经常,控制器需要访问其他控制器的属性和模型。当你有嵌套资源时,这尤为重要。在这个菜谱中,我们将查看一个需要访问父控制器模型和属性的嵌套控制器。

如何操作...

  1. 在一个新应用中,生成一些新的路由和模板:

    $ ember g route foo1
    $ ember g route foo1/foo2
    $ ember g controller foo1
    $ ember g controller foo1/foo2
    $ ember g template index
    
    

    这生成了foo1foo2路由和控制器。foo2路由是一个嵌套路由。索引模板将包含链接。

  2. router.js文件中验证是否已正确创建所有路由:

    // app/router.js
    import Ember from 'ember';
    import config from './config/environment';
    
    const Router = Ember.Router.extend({
      location: config.locationType
    });
    
    Router.map(function() {
      this.route('foo1', function() {
        this.route('foo2');
      });
    });
    
    export default Router;
    

    随着我们生成了路由,router.js应该已经为我们设置好了。正如你所见,foo2路由嵌套在foo1路由下。这意味着我们将必须访问/foo1/foo2 URL 来访问foo2路由。

  3. 向 foo1 路由添加一个新模型:

    // app/routes/foo1.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        model(){
          return ['abc','def','ghi'];
        }
    });
    

    此路由所做的只是返回一个包含简单字母数组的模型。

  4. 向 foo2 路由添加一个新模型:

    // app/routes/foo1/foo2.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        model(){
          return ['jkl','mno','pqr'];
        }
    });
    

    嵌套的foo2路由在其model钩子中也返回一个简单的字母数组。

  5. 向 foo1 控制器添加一个新属性:

    // app/controlers/foo1.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
        prop1: 'foo property'
    });
    

    此控制器有一个字符串属性。

  6. 在 foo2 控制器中创建一个可以访问foo1模型和属性的属性:

    // app/controllers/foo1/foo2.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
        foo1Controller:Ember.inject.controller('foo1'),
        foo1: Ember.computed.reads('foo1Controller.model')
    });
    

    Ember.inject.controller允许我们访问foo1控制器和属性。然后我们可以使用Ember.computed.reads方法设置foo1属性。这创建了一个只读的计算属性,我们可以在模板中使用它。

  7. 更新 foo1 模板文件:

    // app/templates/foo1.hbs
    <h3>Foo1</h3>
    
    {{#each model as |letters|}}
        {{letters}}<br>
    {{/each}}
    {{outlet}}
    

    foo1模板所做的只是显示模型中的列表。

  8. 更新 foo2 模板文件:

    // app/templates/foo1/foo2.hbs
    <h3>Foo2</h3>
    
    {{#each model as |letters|}}
        {{letters}}<br>
    {{/each}}
    <h3>Here is the model injected in from the foo1 controller</h3>
    {{#each foo1 as |letters|}}
        {{letters}}<br>
    {{/each}}
    
    <h3>Foo1 property</h3>
    {{foo1Controller.prop1}}
    

    foo2模板可以访问foo2foo1控制器的所有属性。在这个例子中,我们可以使用each辅助函数列出foo2模型中的所有字母。它还可以使用foo1计算属性列出foo1控制器中的所有字母。我们甚至可以使用foo1Controller访问单个属性。

  9. 向应用和索引模板文件添加一些基本链接:

    // app/application.hbs
    {{#link-to 'application'}}<h2 id="title">Welcome to Ember</h2>{{/link-to}}
    
    {{outlet}}
    

    应用文件顶部将有一个链接:

    // app/templates/index.hbs
    {{link-to 'Foo1 Route' 'foo1'}}<br>
    {{link-to 'Foo2 Route' 'foo1.foo2'}}
    

    这里的link-to辅助函数链接到foo1foo2路由。

  10. 启动 Ember 服务器,打开网页浏览器后,你会看到以下屏幕:如何操作...

    索引主页显示每个路由的链接。

  11. 点击Foo2 路由。以下窗口将显示:如何操作...

    如你所见,foo1 模板和 foo2 模板都会显示。foo2 模板可以访问 foo2foo1 属性和模型。

它是如何工作的...

控制器可以使用 Ember.inject.controller 访问其他控制器。这被称为依赖注入。当我们将一个对象注入到另一个对象中时,就会发生依赖注入。依赖注入在第十一章的使用依赖注入配方中进行了更详细的介绍,实时 Web 应用

第六章。Ember 组件

在本章中,您将学习以下菜谱:

  • 在应用中使用组件

  • 在组件中使用事件

  • 在组件中实现动作

  • 将属性传递给组件

  • 在组件中使用 yield

简介

组件是 Ember.js 的一个主要特性。使用组件,您可以封装代码并创建小部件和自定义标签。此外,您可以将属性传递给组件,处理事件和动作,并在其中包装内容。通常,它可以取代您的控制器。

组件与 W3C 自定义元素规范紧密对齐。尽管 W3C 规范仍在考虑中,但鉴于足够的时间,它可能会被 Web 所采用。

在应用中使用组件

组件可以在应用中以多种方式使用。在本菜谱中,我们将了解如何创建组件并将其添加到模板中。

如何操作...

首先,我们将创建一个简单的组件来显示学生信息。

  1. 在新应用中生成一个新的组件:

    $ ember g component student-info
    
    

    所有组件名称都必须包含一个连字符。这将生成 student-info 组件。此占位符将在 app/componentsapp/templates/components 文件夹中创建。

  2. 编辑 app/components 文件夹中的 student-info.js 文件。添加一些简单的属性:

    // app/components/student-info.js
    import Ember from 'ember';
    
    export default Ember.Component.extend({
        name: 'Erik',
        grade: 12,
        nickName: 'E'
    
    });
    

    在此组件中,我们添加了三个属性,namegradenickName。我们将在模板中稍后使用这些属性。

  3. 更新 app/templates/components 文件夹中的组件模板:

    // app/templates/components/student-info.hbs
    <br>student info:<br>
        {{name}}<br>
        {{grade}}<br>
        {{nickName}}<br>
    

    在此模板中,我们只是显示组件中的信息。

  4. 最后,让我们编辑 app/templates 文件夹中的 application.hbs 文件:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    {{student-info}}
    

    通过向其中添加 Handlebars 表达式 {{student-info}} 来将组件添加到应用模板中。这将自动注册内联 Handlebars 辅助函数并将 student-info.hbs 文件的内容渲染到 application.hbs 文件中。

    让我们更改此示例并添加块形式。

  5. 编辑 student-info.hbs 文件并添加 yield

    <br>student info:<br>
        {{name}}<br>
        {{grade}}<br>
        {{yield}}
        {{nickName}}<br>
    

    {{yield}} 表达式将是外部模板在组件以块形式渲染时的占位符。我们将在 使用 yield 与组件 菜谱中进一步讨论这一点。

  6. 使用新块形式组件更新 application.hbs 文件:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    {{#student-info}}
        This will render in the yield<br>
    {{/student-info}}
    

    组件名称前有一个哈希 (#) 符号。这是向 Handlebars 模板引擎发出信号,表示组件将以块形式存在。student-info.hbs 文件中的 yield 辅助函数将显示块中的内容。

  7. 运行 ember server 并查看此输出:如何操作...

    学生组件在此应用模板中以块形式显示。

    小贴士

    所有组件都是 div 标签。换句话说,默认创建的组件模板将在 div 标签中渲染。这可以通过 tagName 属性来更改。您只需将此属性添加到 components 目录中的组件 JavaScript 文件中即可。

动态创建学生组件

如果需要,你可以在运行时延迟选择组件。让我们看看一个例子,使用我们在早期部分创建的学生组件来做这件事。

  1. 在一个新项目中,创建一个新的student-info组件:

    $ ember g component student-info
    
    

    这将为student-info组件生成必要的文件。

  2. 编辑组件文件并添加以下信息:

    // app/components/student-info.js
    import Ember from 'ember';
    
    export default Ember.Component.extend({
        name: 'Erik',
        grade: 12,
        nickName: 'E',
    
    });
    

    此组件有几个简单的属性。

  3. 使用以下信息更新组件模板:

    // app/templates/components/student-info.hbs
    <br>student info:<br>
        {{name}}<br>
        {{grade}}<br>
        {{yield}}
        {{nickName}}<br>
        {{moreInfo}}<br>
    

    就像之前一样,我们正在显示从组件上下文中检索的一些简单属性。

  4. app/routes文件夹中创建一个新的应用程序路由。添加一个名为comp的新model方法:

    // app/routes/application.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        model() {
          return ['student-info'];
    
        }
    });
    

    此模型返回一个字符串数组。此数组将用作我们模板中要动态显示的组件的名称。

  5. 更新app/templates文件夹中的application.hbs文件。添加一个新的each助手,该助手将显示新的动态组件:

    <h2 id="title">Welcome to Ember</h2>
    {{#each model as |comp|}}
        {{component comp}}
    {{/each}}
    

    要显示动态组件,你必须使用{{component}}助手。助手的第一个参数是你想要使用的组件的名称。在这种情况下,{{comp}}被渲染为student-info。请注意,我们可以使用组件助手的内联形式或块形式。如果组件以内联形式渲染,则不使用yield

  6. 运行ember server后,模板将使用动态组件进行渲染:动态创建学生组件

它是如何工作的...

组件用于将数据封装成可以在整个应用程序中轻松重用的形式。每个组件可以是块形式或内联形式,默认情况下以div标签的形式渲染。组件有一个模板和 JavaScript 文件。

在组件中使用事件

当创建组件时,你可以将事件附加到它们上。让我们看看一个例子。

如何做到这一点...

  1. 在一个新项目中,生成一个名为student-info的新组件:

    $ ember g component student-info
    
    

    这将在component目录和templates/components文件夹中生成组件文件。

  2. 编辑app/components/student-info.js文件。添加一个新的click事件:

    // app/components/student-info.js
    import Ember from 'ember';
    
    const {$}=  Ember
    export default Ember.Component.extend({
        click() {
          $('html').fadeToggle( 'slow', 'linear');
          $('html').delay(250).fadeIn();
        }
    });
    

    在这个例子中,你首先会注意到我们正在使用 ES2015 解构赋值。解构赋值语法从数组或对象中提取数据。我无需在所有地方都键入Ember.$,只需键入$即可。

    Ember CLI 默认安装了 jQuery。我们正在使用 jQuery 语法来淡入 HTML 文档,并在组件被点击后淡出。

    虽然我们不仅仅限于click事件。Ember 还有几个可用的事件。

    • 触摸事件:

      touchStart

      touchMove

      touchEnd

      touchCancel

    • 键盘事件:

      keyDown

      keyUp

      keyPress

    • 鼠标事件:

      mouseDown

      mouseUp

      contextMenu

      click

      doubleClick

      mouseMove

      focusIn

      focusOut

      mouseEnter

      mouseLeave

    • 表单事件:

      submit

      change

      focusIn

      focusOut

      input

    • HTML5 拖放事件:

      dragStart

      drag

      dragEnter

      dragLeave

      dragOver

      dragEnd

      drop

  3. 对于最后一步,我们需要将组件添加到我们的应用程序模板中。目前不需要编辑组件模板。我们将只设置组件为块形式,以便任何对元素的点击都将触发事件:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    
    {{#student-info}}
        Student info block
    {{/student-info}}
    {{outlet}}
    

    student-info 组件是块形式的。点击块中的任何位置将触发 click 事件,导致 HTML 文档淡出。

    app 文件夹的根目录下有一个 index.html 文件。此文件包含默认的 HTML 和 head 标签。它还包含一些指向你的 CSS 和供应商文件的链接。你可能注意到有 {{content-for}} 辅助函数。这些与 Ember 插件一起使用,不应删除。

  4. 运行 ember server,模板应按以下方式渲染:如何做...

    如果点击学生信息块 div 的任何部分,HTML 文档将淡出。

它是如何工作的...

Ember 组件中的事件通过在组件中添加事件名称作为方法来工作。这些事件在组件被添加到的模板中触发。默认情况下,组件是 div 标签。因此,任何事件都必须在渲染的模板中的 div 标签的上下文中发生。

Ember 支持多种不同类型的事件,包括双击、HTML 5 拖放事件和触摸事件。可以使用 Ember.Application.customEvents 方法注册自定义事件。

在组件中实现动作

组件可以通过动作与变化进行通信。这些动作可以发送回父组件或在组件中处理。让我们看看几个展示这一点的示例。

如何做...

在这个示例中,我们将创建一个学生列表,然后对其进行操作。

  1. 在一个新项目中,生成一个 student-list 组件:

    $ ember g component student-list
    
    

    这将生成 student-list 组件和必要的文件。

  2. 更新 app/components 文件夹中的 student-list.js 文件。我们需要创建几个 actions 和一个新的数组:

    // app/components/student-list.js
    import Ember from 'ember';
    
    export default Ember.Component.extend({
        init() {
          this._super(...arguments);
          this.setup();
        }, 
        actions: {
          remove(){
            this.get('listOfStudents').popObject();
          },
          reset(){
            this.setup();
          }
    
        },
        setup(){
          let st = this.set('listOfStudents',[]);
          st.clear();
          st.pushObject('Erik');
          st.pushObject('Bob');
          st.pushObject('Suze');
    
        }
    
    });
    

    此组件的第一部分是 init 方法。该方法将在组件初始化时立即触发。这是一个私有方法,用于设置组件。由于我们正在重写此框架方法,我们必须调用 super 以确保组件正确创建。然后调用 setup 方法。

    setup 方法创建一个新的 listOfStudents 数组,将其清空,并通过将其弹出到数组中来创建三个新的字符串对象。由于我们在 init 方法中创建 listOfStudents 数组,它将属于此组件实例的局部变量。在 initdidInsertElement 方法中声明对象或属性是良好的实践。否则,如果对象或数组被声明为组件的属性,组件将不再拥有其独立的独立状态。我们将在本书的后面讨论 didInsertElement 钩子。

    列出了两个actionsremovereset。两者都将与组件模板中的动作相关联,我们将在稍后使用。remove动作从数组的顶部删除或弹出对象。reset方法调用setup,这使组件返回到其原始状态。

  3. 添加几个按钮,并使用each辅助函数在app/templates/components文件夹中的student-list.hbs文件中列出数组的内容:

    // app/templates/components/student-list.hbs
    <button {{action 'remove'}}>Remove</button>
    <button {{action 'reset'}}>Reset</button><br>
    {{#each listOfStudents as |student|}}
        {{student}}<br>
    {{/each}}
    

    {{action}}辅助函数将在点击removereset按钮时触发。action辅助函数的第一个参数始终是动作名称。each辅助函数列出listOfStudents Ember 数组的内容。

  4. 对于最后一部分,将student-list组件添加到应用程序模板中:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    {{student-list }}
    {{outlet}}
    

    这将显示student-list组件的内容。

  5. 启动ember server后,你的输出将如下所示:如何操作...

    点击移除按钮将逐个删除每个项目。重置按钮将数组重置。

使用我们的学生组件的闭包动作

Ember 为你提供了一种从父组件向子组件发送动作的方法。我们可以使用闭包动作来实现这一点。

  1. 在一个新项目中,生成一个新的application路由和一个student-list组件:

    $ ember g route application
    $ ember g controller application
    $ ember g component student-list
    
    

    这将在app/routes文件夹中生成application.js文件,application控制器和student-list组件。

  2. 在应用程序路由文件中,添加一个新的listOfStudents数组:

    // app/routes/application.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        listOfStudents: [],
        beforeModel(){
          this.reset();
        },
        model(){
    
          return this.get('listOfStudents');
        },
        reset(){
          let st = this.get('listOfStudents');
          st.clear();
          st.pushObject('Tiffany');
          st.pushObject('Zack');
          st.pushObject('George');
        },
        actions: {
          removeRoute(){
            this.get('listOfStudents').popObject();
          },
          resetRoute(){
            this.reset();
          }
        }
    });
    

    这可能让你想起了上一个示例。在这里,我们创建了一个名为listOfStudents的新数组。beforeModel钩子将在模型设置之前运行。在这个beforeModel中,调用了reset方法。这会将默认数据添加到数组中。

    模型钩子返回listOfStudents数组。此外,我们添加了两个动作removereset,分别用于删除一个项目或将项目重置回原始数组。

    这实际上与我们在组件中较早之前拥有的代码相同,但我们将其移动到了路由中。

  3. 编辑application.hbs文件并添加我们刚刚创建的组件:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    {{student-list onRemove=(action 'removeController' ) onReset=(action 'resetController')}}
    {{#each model as |student|}}
        {{student}}<br>
    {{/each}}
    {{outlet}}
    

    此模板将显示来自我们的模型和student-list组件的学生列表。onRemoveonReset属性设置为传递给组件的父控制器中的函数。这被称为闭包动作。所有闭包动作都必须在(action 'removeController')动作周围有括号。

  4. 向控制器添加两个新动作:

    // app/controllers/application.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
        actions:{
          removeController(){
            this.send('removeRoute');
          },
          resetController(){
            this.send('resetRoute');
          }
        }
    });
    

    这两个动作使用this.send将动作发送到我们之前定义的removeRouteresetRoute动作。你可以使用send方法从父路由或控制器触发动作。

  5. 更新组件模板文件:

    // app/templates/components/student-list.hbs
    <button {{action 'removeComponent'}}>Remove</button>
    <button {{action 'resetComponent'}}>Reset</button>
    

    此组件显示两个按钮,这些按钮与组件中定义的动作相关联。

  6. 更新组件 JavaScript 文件,并添加两个新动作:

    // app/components/student-list.js
    import Ember from 'ember';
    
    export default Ember.Component.extend({
        actions: {
          removeComponent(){
            this.get('onRemove')();
          },
          resetComponent(){
            his.attrs.onReset();
          }
        }
    });
    

    这些动作将从组件模板触发。此时,它们将触发从关闭操作传递给它的函数,即onRemoveonReset

    要调用这些方法,你可以使用this.getthis.attrs。在这种情况下,this.get方法和属性名onRemove将调用传递给它的方法。

    在另一种情况下,你可以使用this.attrs来访问属性上的属性以调用传递的函数,即this.attrs.onReset()

    动作将按此方式流动:应用模板 -> 组件 -> 控制器 -> 路由。最终结果是路由触发动作以删除或重置列表。

  7. 运行ember server,你应该会看到一个可以删除或重置的列表:使用我们的学生组件的关闭操作

  8. 点击删除重置按钮将触发从控制器传递进来的动作。这个动作将冒泡到路由以重置或从列表中删除一个项目。

它是如何工作的...

在组件中可以通过几种方式处理动作。动作可以添加到不同的 HTML 标签中,并在组件内部处理。动作还可以通过关闭操作或发送操作发送到父组件或控制器。

关闭操作使得传递动作变得更容易。我们可以将动作传递给组件,然后它们可以调用这些动作。这有助于在不同父路由、控制器和组件之间分离逻辑。

将属性传递给组件

组件默认情况下与其周围环境隔离。组件需要的任何数据都必须传递进来。在这个菜谱中,我们将创建一个学生列表。然而,我们将传递数据到组件以进行渲染。

如何实现...

  1. 在一个新应用中,生成一个新的组件和application路由:

    $ ember g route application
    $ ember g component student-list
    
    

    这将在routes文件夹中生成application.js文件以及从student-list组件必需的文件。

  2. 编辑app/routes文件夹中的application.js文件:

    // app/routes/application.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        model() {
          return ['Jim','Jeff','Jill']
        }
    });
    

    此模型将返回一个简单的数组。

  3. 更新app/templates/components文件夹中的student-list模板:

    // app/templates/components/student-info.hbs
    
    {{#each compModel as |student|}}
        {{student}}<br>
    {{/each}}
    

    这将使用compModel属性,并通过each辅助函数遍历它。

  4. 编辑app/templates文件夹中的application.hbs文件。添加新的student-info组件:

    // app/templates/application.hbs
    {{student-list compModel=model}}
    
    {{outlet}}
    

    student-list模板具有compModel属性。这个属性传递了在路由中之前设置的模型,即应用程序模型。请注意,compModel是从组件内部访问的。模型是从组件外部访问的。除非将其传递给它,否则组件无法访问模型。

  5. 运行ember server,你应该在模型中看到元素列表:如何实现...

它是如何工作的...

组件是独立的代码集合,无法访问外部世界。换句话说,组件必须通过传递给它任何所需的数据。您可以通过在 Handlebars 表达式中组件名称后添加属性来设置此操作。

使用组件的 yield

组件可以以块或内联形式设置。当以块形式时,组件可以提供信息。在这个菜谱中,我们将查看使用 yield 在模板中显示信息的示例。

如何操作...

  1. 在一个新项目中,创建一个新的学生组件:

    $ ember g component student-info
    
    

    这将创建学生组件。

  2. 编辑student-info模板文件并添加一些文本:

    // app/templates/components/student-info.hbs
    This is information before the yield<br>
    {{yield}}
    This is information after the yield
    

    组件中的{{yield}}表达式将是块中文本将被渲染的地方。

  3. 将新的student-info组件添加到应用程序模板文件中:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    
    {{#student-info}}
        Information in block<br>
    {{/student-info}}
    

    当以块形式时,由哈希符号#指定,块中的信息将显示在{{yield}}中。

  4. 运行ember server后,将显示以下屏幕:如何操作...

    如您所见,组件yield模板显示了块中的信息。

它是如何工作的...

{{yield}} Handlebars 表达式通过获取组件块中的信息并将其渲染出来来工作。块形式由哈希符号#指定。内联组件没有哈希符号,并且不提供信息。

第七章. Ember 模型和 Ember Data

在本章中,您将学习以下食谱:

  • 理解 Ember Data 的功能

  • 使用 Ember Data 创建、读取、更新和删除记录

  • 使用固定数据

  • 自定义适配器和序列化器

  • 处理关系

简介

模型是 Ember 中的对象,代表可以显示给用户的数据。它们是持久的,当用户关闭浏览器窗口时不会丢失。

许多模型是从存储在服务器数据库中的数据加载的。通常,数据以 JSON 表示的形式来回发送。这就是 Ember Data 发挥作用的地方。当您在 Ember 中创建应用程序时,Ember Data 默认包含。它有助于检索数据、在本地存储数据并将信息保存到服务器。

Ember Data 可以配置为与许多不同类型的数据库和服务器一起工作。如果使用得当,Ember Data 可以帮助您管理应用程序模型,而无需在应用程序中多次使用 Ajax 请求。

理解 Ember Data 的功能

Ember Data 使用一个单一的数据存储,可以在整个应用程序中访问。在这个例子中,我们将创建一个简单的应用程序,检索书籍列表并将其显示给用户。

准备工作

在我们开始之前,我们需要为我们的服务器生成模拟数据。Ember CLI 内置了一个模拟服务器,可以通过生成基本的 Express 服务器 来处理这种情况。然而,为了本食谱的目的,我们将继续使用 Ember CLI Mirage 插件。它具有更多功能且易于使用。您可以在 github.com/samselikoff/ember-cli-mirage 找到更多关于 Ember CLI Mirage 的信息。

  1. 首先,让我们创建一个新的应用程序:

    $ ember new BookExample
    
    
  2. 应用程序创建完成后,让我们安装插件:

    $ cd BookExample
    $ ember install ember-cli-mirage
    $ ember g factory book
    
    

    这将安装所需的最新 Bower 和 npm 软件包,并为 Mirage 创建 book 工厂。

    为了使这个食谱工作,我们需要模拟书籍数据。

  3. 编辑 app/mirage 文件夹中的 config.js 文件:

    // app/mirage/config.js
    export default function() {
    
        this.get('/books');
        this.get('/books/:id');
    }
    

    此配置文件将设置我们所需的数据的虚拟路由。/books 路由将返回所有书籍数据,而 /books/:id 路由将根据 URL 中传递的 ID 返回单个书籍。

  4. 更新 app/mirage/factories 文件夹中的 book.js 文件。添加以下属性:

    // app/mirage/factories/book.js
    import Mirage, {faker}  from 'ember-cli-mirage';
    
    export default Mirage.Factory.extend({
    
        title: faker.lorem.sentence,  // using faker
        author() {return faker.name.findName(); },
        year: faker.date.past
    });
    

    此文件设置了我们将用于模型的属性。title 属性指的是书籍的名称,author 指的是写这本书的人,而 year 是出版年份。为了使事情更简单一些,Ember CLI Mirage 包含了一个名为 faker 的库。这个库生成我们可以用来填充我们的内存数据存储的数据。

  5. 更新 app/mirage/scenarios 文件夹中的 default.js 文件:

    export default function( server ) {
    
        server.createList('book',10);
    }
    

    确保删除 server 附近的注释。此场景将在浏览器加载时每次生成十个新的 'book' 记录。浏览器加载后,书籍将通过工厂生成。

如何做到这一点...

  1. 首先创建一个用于书籍的模型文件、REST 适配器和路由:

    $ ember g model book title:string author:string year:date
    $ ember g adapter application
    $ ember g route books
    $ ember g route application
    
    

    此命令将生成一个名为 book 的新模型,并将 titleauthoryear 作为模型中的属性。generate adapter 命令将为我们的应用程序创建一个新的适配器,而最后的命令将为 bookapplication 生成路由。

  2. app/models 文件夹中打开 book.js 文件。它应该看起来如下:

    // app/models/book.js
    import DS from 'ember-data';
    
    export default DS.Model.extend({
        title: DS.attr('string'),
        author: DS.attr('string'),
        year: DS.attr('date')
    });
    

    models 文件是我们将要使用的数据的表示。我们可以使用三种不同类型的数据:stringnumberdate。这些数据将从我们的模拟服务器加载。

  3. app/adapters 文件夹中打开创建的 application.js 文件:

    // app/adapters/application.js
    import DS from 'ember-data';
    
    export default DS.RESTAdapter.extend({
    });
    

    Ember Data 有几个适配器可供使用。其中最容易使用的是 REST 适配器。

  4. REST 适配器期望服务器以这种格式提供数据:

    {
      "books": [
        {
          "id": 1,
          "title": "Some title",
          "author": "Authors name",
          "date": "1980-05-23"
        }
        {
          "id": 2,
          "title": "Some other title",
          "author": "Authors name 2",
          "date": "1985-05-23"
    
        }
      ]
    }
    

    前面的 JSON 列出了一个书籍数组。如果意外地只返回了一个记录,REST 适配器会期望数组被命名为 book 而不是 books。请记住,你应该将所有记录名称使用驼峰命名法,并且数据应该以 REST 适配器格式呈现。

  5. 我们需要能够从我们的数据存储中检索数据并将其展示给用户。编辑 app/routes 文件夹中的 application.js 文件。添加一个新的模型,它返回所有列出的书籍:

    // app/routes/application.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        model(){
          return this.store.findAll('book');
        }
    });
    

    如 第四章 中所述,Ember Router,路由的一个职责是返回模型数据。Ember Data 存储有一个名为 findAll 的方法,它将返回 book 模型的所有数据。按照惯例,Ember 应用程序将执行一个 HTTP GET 请求到 /book/ URL,并期望响应中包含 JSON 负载。由于此模型位于应用程序路由中,它可以在任何模板中访问。

  6. 更新 application.hbs 文件并显示来自模拟服务器的新的数据:

    // app/templates/application.hbs
    {{#link-to 'index'}}<h2 id="title">Welcome to Ember</h2>{{/link-to}}
    
    {{outlet}}
    
    {{#each model as |book|}}
        <br>
        title: {{#link-to 'books' book.id}}{{book.title}}{{/link-to}}<br>
    {{/each}}
    

    此模板使用 each 辅助函数遍历页面加载后从 model 钩子返回的所有数据。link-to 辅助函数将 book.id 作为参数传递到 URL 中。我们将使用书籍的 title 作为链接。

  7. 更新书籍路由,使其返回单个记录:

    // app/routes/books.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        model(params){
          return this.store.findRecord('book',params.book_id);
        }
    });
    

    model 钩子从 URL 接收一个参数。可以使用 Ember Data 的 findRecord 来查找单个记录。当 model 钩子被加载时,它将向 /books/:id URL 发送一个 HTTP GET 请求。第一个参数是数据存储,第二个参数是记录 ID。

  8. app 文件夹中的 router.js 文件中更新新的动态路由 books

    // app/router.js
    import Ember from 'ember';
    import config from './config/environment';
    
    var Router = Ember.Router.extend({
        location: config.locationType
    });
    
    Router.map(function() {
        this.route('books', {path:'/books/:book_id'});
    });
    
    export default Router;
    

    这个新的 books 路由的路径为 /books/:book_id。要访问 books 路由,必须在路径中提交一个 ID。

  9. 更新 books.hbs 模板:

    // app/templates/books.hbs
    {{outlet}}
    <br>
    <b>Author: {{model.author}}</b><br>
    <b>Year: {{model.year}}</b>
    <br>
    

    访问此路由将触发 model 钩子。这将渲染所选书籍的 authoryear

  10. 运行 ember server 并打开一个网页浏览器。您将看到所有标题的列表,每个标题都有一个链接指向每本书:如何操作...

    每篇文章都有一个唯一的 ID。点击链接将显示该书的标题和作者。

工作原理...

模型代表 Ember 中的数据。这些模型可以使用 Ember Data 将数据存储、更新和从服务器检索数据。从服务器发送的数据通常是 JSON 格式。Ember 为与来自服务器的数据接口提供了一组适配器。REST 适配器是最常用的。它期望数据以特定格式,以便 Ember 可以解析、存储并向用户显示。

Ember Data 在从服务器检索数据后缓存数据。这最小化了往返服务器的次数。然而,当从存储返回缓存的记录时,Ember Data 将在后台发出请求。如果数据已更改,它将在屏幕上重新渲染。

使用 Ember Data 创建、读取、更新和删除记录

在前面的配方中,我们从我们的模拟服务器检索了现有的书籍数据并将其显示给用户。Ember Data 还具有从数据存储创建、删除甚至更新记录的能力。我们将探讨这些方法以及更多。

准备工作

就像前面的例子一样,我们需要安装 Ember CLI Mirage。请参阅前面的配方以获取有关此过程的说明。我们将使用与书籍配方相同的工厂,并添加新的方法来添加、编辑和删除数据。

  1. mirage 文件夹中,打开 config.js 文件:

    // app/mirage/config.js
    export default function() {
    
        this.get('/books');
        this.get('/books/:id');
        this.put('/books/:id');
        this.post('/books');
        this.delete('/books/:id');
    }
    

    这将模拟我们的后端,并允许我们创建、更新和删除数据。这是通过使用 HTTP PUT、POST 和 DELETE 请求来完成的。我们将在程序中稍后使用这些。

  2. 更新 app/mirage 文件夹中的 book.js 文件:

    // app/mirage/factories/book.js
    import Mirage, {faker}  from 'ember-cli-mirage';
    
    export default Mirage.Factory.extend({
    
       title: faker.lorem.sentence,  // using faker
       author() {return faker.name.findName(); },
       year: faker.date.past
    });
    

    这个工厂将用于生成 Mirage 内部数据库将返回的假数据。

  3. 更新 scenarios 文件夹中的 default.js 文件:

    // app/mirage/scenarios/default.js
    export default function( server ) {
    
        server.createList('book',10);
    }
    

    每次应用程序加载时,服务器将创建 10book 对象。

如何操作...

对于这个配方,我们将向我们在 Understanding the functionalities of Ember Data 配方中已有的书籍示例中添加内容。

  1. 创建一个名为 new 的路由和一个名为 books 的控制器:

    $ ember g route new
    $ ember g controller books
    $ ember g controller new
    $ ember g controller application
    
    

    这将生成 new 路由和 books 控制器文件。

  2. 为新路由创建一个新的模板:

    // app/templates/new.hbs
    {{outlet}}
    <b>title: {{input value=info.title size='15'}}</b><br>
    <b>Author: {{input value=info.author size='15'}}</b><br>
    <b>Year: {{input value=info.year size='35'}}</b><br>
    <button {{action 'newText' }}>Submit Changes</button>
    <button {{action 'cancel' }}>Cancel</button><br>
    

    新路由将用于向存储库添加新书。在这个模板中,使用 input 辅助函数创建三个文本框。每个都将绑定到信息属性。底部的一个按钮将提交更改到 newText action 方法。cancel 按钮将触发 cancel 动作。

  3. 更新 new.js 控制器,添加新的 cancelnewText actions

    // app/controllers/new.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
        info: {},
        actions:{
          newText(){
            let inf = this.get('info');
            let newBook = this.store.createRecord('book',{
              title: inf.title,
              author: inf.author,
              year: new Date(inf.year)
            });
    
          newBook.save().then(()=>{
            this.transitionToRoute('application');
            this.set('info',{});
          },()=> {
            console.log('failed');
          });
          },
          cancel(){
            return true;
          }
    
        }
    });
    
  4. 这里有很多事情在进行中;让我们首先看看 newText 动作:

        newText(){
          let inf = this.get('info');
          let newBook = this.store.createRecord('book',{
            title: inf.title,
            author: inf.author,
            year: new Date(inf.year)
          });
    

    在这个操作中,我们获取之前声明的info属性。这个属性是一个对象,用于存储从模板中获取的值。存储库有一个名为createRecord的方法,它接受两个参数。第一个参数是模型。第二个是我们想要存储的对象。在这种情况下,我们想要向存储库添加一个新的book记录。我们使用inf对象设置titleauthoryear

  5. 使用createRecord方法不会持久化更改。save()方法用于将数据持久化回服务器:

        newBook.save().then(()=>{
          this.transitionToRoute('application');
          this.set('info',{});
        },()=> {
          console.log('failed');
        });
    

    save方法是一个承诺。它要么成功,要么失败。如果成功,我们使用transitionToRoute方法将路由切换回主应用程序。

    之后,我们将info属性设置回空对象。我们这样做是为了清除模板input助手的所有数据。如果它不成功,那么我们将输出错误到控制台:

        cancel(){
          return true;
        }
    

    cancel操作返回true。这意味着不是控制器处理它,而是将其冒泡到路由进行处理:

    小贴士

    使用 Ember 的 REST

    在使用 Ember 中的 REST 适配器时,save()方法将向服务器发送 PUT、DELETE、GET 或 POST HTTP 请求。PUT 请求将在更新期间发送。DELETE 请求用于删除记录。POST 用于添加新记录,而 GET 请求用于检索记录。这是由 Ember REST 适配器按照惯例完成的。

  6. 更新books.hbs模板文件,添加一个新操作以更新:

    // app/templates/books.hbs
    {{outlet}}
    <br>
    <b>title: {{input value=model.title size='15'}}</b><br>
    <b>Author: {{input value=model.author size='15'}}</b><br>
    <b>Year: {{input value=model.year size='35'}}</b><br>
    <button {{action 'updateText'}}>Submit Changes</button>
    <button {{action 'cancel'}}>cancel</button>
    <br>
    

    我们已更新书籍模板,使其与上一个示例的行为不同。在这个示例中,它将允许我们按以下方式编辑条目:

    如何做...

  7. 更新books.js控制器以处理新的updateTextcancel操作:

    // app/controllers/books.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
        actions: {
          updateText(){
            let book = this.get('model');
            book.set('year',new Date(book.get('year')));
            book.save();
            this.transitionToRoute('application');
          },
          cancel() {
            return true;
          }
        }
    });
    

    updateText操作获取当前书籍model,设置year,然后保存。之后,它过渡到application路由。如果需要,我们可以处理保存承诺失败时的错误条件。为了简单起见,我们将保持原样。cancel操作返回true,这意味着它将冒泡到书籍路由进行处理。

  8. 更新路由中的books.js文件:

    // app/routes/books.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        model(params){
          return this.store.findRecord('book',params.book_id);
        },
        actions:{
          cancel() {
            return true;
          }
        }
    });
    

    路由文件与上一个配方相同,但现在我们有一个cancel操作。这个cancel操作将在控制器返回true后触发。通过在这里返回true,操作再次冒泡到应用程序路由。

  9. 更新new.js路由文件:

    // app/routes/new.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        actions: {
          cancel() {
            return true;
          }
        }
    });
    

    此文件将接收来自新控制器的操作。它也返回true,这意味着cancel操作将由应用程序路由处理。

  10. 更新应用程序路由文件:

    // app/routes/application.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        model(){
          return this.store.findAll('book');
        },
        actions: {
          cancel(){
            this.transitionTo('application');
          }
        }
    });
    

    应用程序路由中的cancel操作处理新的和书籍路由的cancel操作。在任何情况下,它都会过渡到application路由。总之,操作的冒泡从新的控制器到新的路由,最后到应用程序路由。如果cancel操作未包含在控制器中,根据惯例,操作将自动向上冒泡。

  11. 我们需要更新应用程序模板并添加一个新选项来删除记录。使用新的delete操作更新application.hbs文件:

    // app/templates/application.hbs
    {{#link-to 'index'}}<h2 id="title">Welcome to Ember</h2>{{/link-to}}
    {{#link-to 'new'}}<h5>Add New Book</h5>{{/link-to}}
    
    {{outlet}}
    
    {{#each model as |book|}}
        <br>
        title: {{#link-to 'books' book.id}}{{book.title}}{{/link-to}} <br>
    <a href="" {{action 'delete' book}}>delete?</a><br>
    {{/each}}
    

    应用程序将显示每本书。每条记录底部还有一个delete操作按钮,该按钮传递book记录。

  12. 更新应用程序控制器以处理新的delete操作:

    // app/controllers/application.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
        actions:{
          delete(book){
            book.deleteRecord();
            console.log(book.get('isDeleted'));
            book.save();
          }
        }
    });
    

    书籍记录有一个名为deleteRecord的方法。这个方法会删除记录;然而,它不会在save()完成之前向服务器发送 HTTP 删除请求。另一个名为destroyRecord的方法会同时deletesave。在这个例子中,我们将使用deleteRecord

  13. 加载 Ember 服务器,你会看到一个记录列表。你可以点击每条记录并删除或编辑它:如何操作...

    小贴士

    不使用 Ember Data?

    当与后端数据存储一起工作时,Ember Data 是首选方法。然而,它不必是唯一的选择。在路由中定义模型信息时,你可以使用 Ajax 方法或定义自己的存储库。如果需要,你可以使用服务并将它们注入到你的应用程序的各个部分。这需要额外的大量工作,具体取决于你的设置,但这是一个选择。

它是如何工作的...

Ember Data 附带了一些适配器,可以用来从后端服务器检索数据。REST 适配器允许用户使用 HTTP GET、DELETE、PUT 和 POST 请求向后端服务器发送请求。默认情况下,它期望以 JSON 格式接收响应。

Ember Data 存储方法允许用户查找、删除和保存记录。Ember 的save()方法触发对服务器的响应。在保存完成之前,如果需要,可以回滚记录。

使用固定数据

固定数据是模拟数据的一种方式。它是静态数据,可以在测试我们的应用程序时用于我们的模型以显示给用户。在这个菜谱中,我们将看到如何使用 Ember CLI Mirage 设置它的一些基本知识。

准备工作

与我们其他许多示例一样,我们将使用 Ember CLI Mirage。我们将设置固定数据而不是使用工厂。

  1. 开始创建一个新的应用程序。然后添加 Ember CLI Mirage 插件并生成应用程序的模型和路由:

    $ ember install ember-cli-mirage
    $ ember g model student name:string age:number
    $ ember g route index
    $ ember g adapter application
    $ ember g fixture students
    
    

    这些命令将生成我们应用程序的基本结构。在这个应用程序中,固定数据将用于显示学生信息。为了简单起见,我们只会显示这些信息而不会对其进行操作。

  2. miragefixtures文件夹中,更新students.js文件并添加固定数据:

    // app/mirage/fixtures/students.js
    export default [
        {id: 1, name: 'John', age: 17},
        {id: 2, name: 'Jack', age: 18},
        {id: 3, name: 'Suze', age: 17},
        {id: 4, name: 'Jane', age: 18}
    ];
    

    固定数据有四个记录。每个记录都有一个不同的学生的 nameage。要使用 Ember CLI Mirage 与固定数据一起使用,你必须将其作为对象的数组输入。

  3. 更新 mirage 文件夹中的 config.js 文件。此文件用于设置 students 路由:

    // app/mirage/config.js
    export default function() {
    
        this.get('/students');
    
    }
    

    这将为 Ember Data 设置一个模拟服务器端点。按照惯例,Ember Data 将查找复数模型名称的 URL 路径。在这个例子中,我们的模型将是学生;因此,当 Ember Data 查找数据时,它将对服务器的 /students 执行 GET 请求。

  4. default.js 文件中添加一个新的场景用于固定数据:

    // app/mirage/scenarios/default.js
    export default function(server ) {
        server.loadFixtures();
    
    }
    

    loadFixtures() 命令将在内存中加载固定数据,以便它们可用于 students 路由。

如何操作...

  1. 之前,我们创建了模型文件。让我们先看看它,以确保一切设置正确:

    // app/models/student.js
    import DS from 'ember-data';
    
    export default DS.Model.extend({
        name: DS.attr('string'),
        age: DS.attr('number')
    });
    

    student 模型有两个属性,称为 nameage

  2. 更新 index.js 路由文件以返回学生模型:

    // app/routes/index.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        model(){
          return this.store.findAll('student');
        }
    });
    

    路由文件将使用 findAll 方法返回所有 student 记录。这将触发对服务器 /students 的 HTTP GET 请求。当你访问路由时,将触发 model 钩子。按照惯例,Ember 将缓存这些结果。

  3. 打开应用程序适配器。将其设置为 REST 适配器:

    // app/adapters/application.js
    import DS from 'ember-data';
    
    export default DS.RESTAdapter.extend({
    });
    

    RESTAdapter 将用于所有路由。它是一种假设通过 XHR 发送 JSON 数据的适配器类型。

  4. 编辑 index.hbs 文件。这将显示模型信息:

    // app/templates/index.hbs
    {{#each model as |student|}}
        Name: {{student.name}}<br>
        age: {{student.age}}<br>
    {{/each}}<br>
    

    在这个例子中,我们使用 each 辅助函数遍历所有记录。

  5. 运行 ember server,以下结果应该会显示:如何操作...

    页面加载后,将显示学生姓名和年龄的列表。这些数据是从我们之前设置的模拟服务器中检索的固定数据。

它是如何工作的...

固定数据在测试应用程序时使用。这是已知数据,可用于重复测试。Ember CLI Mirage 可以设置为使用固定数据。

我们将在测试章节中更详细地介绍使用固定数据与测试。

自定义适配器和序列化器

Ember Data 对如何访问数据有很强的意见。适配器对数据的外观有内置的假设。我们可以使用序列化和适配器来改变这些假设。

对于这个菜谱,我们将基于上一节中创建的学生应用程序进行构建。

准备工作

我们将使用之前菜谱中的相同应用程序。我们需要编辑 Ember CLI Mirage 以处理新的命名空间。

mirage 文件夹中的 config.js 文件中更新 students 路由:

// app/mirage/config.js
export default function() {

    this.get('api/v1/students');
}

这将更改端点为 api/v1/students 而不是仅仅 /students

如何操作...

  1. 在上一节中的学生应用程序中,编辑 adapters 文件夹中的 application.js 文件。

  2. 添加一个新的 namespace

    // app/adapters/application.js
    import DS from 'ember-data';
    
    export default DS.RESTAdapter.extend({
        namespace: 'api/v1'
    });
    

    namespace 属性用于在请求前添加特定的 URL 前缀。在这种情况下,所有请求都将添加 api/v1 作为前缀。

  3. 启动 Ember 服务器,你应该会看到请求发送到 /api/v1/students

Ember Data 中的可选定制

如果需要,Ember 提供了多种其他定制选项。以下是一些需要记住的重要选项。

主机定制

在适配器文件中,你可以添加 Ember Data 应该发送请求的新位置。这将覆盖本地服务器的默认位置:

// app/adapters/application.js
import DS from 'ember-data';

export default DS.RESTAdapter.extend({
host: 'https://api.example.com'
});

例如,现在所有请求都将发送到 api.example.com

Headers 定制

根据 API,你可能需要在每个 HTTP 请求中发送特定的 headers。你可以使用 headers 属性来添加这些 headers:

// app/adapters/application.js
import DS from 'ember-data';

export default DS.RESTAdapter.extend({
  headers: {
    'API_INFO:': 'key',
    'SECOND_HEADER': 'Some value'
  }
});

这将为每个发送的请求添加新的 headers。如果需要,你还可以通过创建计算属性在 headers 中使用动态信息。

与序列化器一起工作

当使用 Ember Data 时,序列化器格式化从后端数据存储发送和接收的数据。我们可以定制这些数据以适应后端的需求。

更新 IDs

默认情况下,Ember Data 期望每个记录都有一个 ID。我们可以使用主键属性来更改此名称。

  1. 生成一个新的 serializer 文件:

    $ ember g serializer application
    
    

    这将生成我们可以更新的 serializer 文件。

  2. 使用新的 ID 更新 application.js 序列化器:

    // app/serializers/application.js
    import DS from 'ember-data';
    
    export default DS.RESTSerializer.extend({
        primaryKey: '_id'
    });
    

    这将在序列化和反序列化数据时将 ID 属性转换为 _id。换句话说,当数据从服务器发送或接收时,它将有一个设置为 _id 的主键。

处理 JSON 负载时的 KeyForAttribute

有时,从服务器返回的数据可能不是正确的格式。例如,RESTAdapter 期望 JSON 负载属性名称为驼峰式。我们可以使用 keyForAttribute 属性来更改这一点:

// app/serializers/application.js
import DS from 'ember-data';

export default DS.RESTSerializer.extend({
    keyForAttribute(attr) {
      return Ember.String.decamelize(attr);
    }
});

例如,假设从服务器返回的数据是下划线而不是驼峰式。服务器返回 school_name 而不是 schoolName。这可以通过使用 keyForAttributedecamelize 来修复。这将模型名称 schoolName 转换为 school_name,以便与服务器返回的内容匹配。

它是如何工作的...

适配器用于 Ember Data 以帮助解释从服务器发送和检索的数据。它对数据的外观有一套内置假设。我们可以进行更改,以便我们可以适应不同类型的 API。例如,我们可以定制端点的路径命名空间以及如果需要的主机。

序列化器格式化从服务器发送和接收的数据。Ember Data 期望数据以特定的格式。我们可以更改此数据中的许多内容,包括主键和 JSON 负载数据中的键。这是通过向序列化器添加新属性来实现的。

与关系一起工作

当与数据存储一起工作时,你需要能够处理关系。在这个菜谱中,我们将介绍一些常见的关系,从一对一和一对多,以及如何使用 Ember Data 来实现。

准备工作

与其他菜谱一样,我们将使用 Ember CLI Mirage 来模拟我们的后端。在这个菜谱中,我们将创建一个简单的一对多和一对多关系。我们将模拟一个有教练和班级的学校。对于每个班级,都有一个教练。每个教练将有一个或多个班级。

  1. 创建一个新的 Ember 应用程序。在这个应用程序中,生成以下文件:

    $ ember install ember-cli-mirage
    $ ember g model instructor
    $ ember g model class
    $ ember g route index
    $ ember g helper addone
    $ ember g adapter application
    $ ember g fixture classes
    $ ember g fixture instructors
    
    

    这将创建我们需要的模型、路由、适配器和辅助函数。

  2. miragefixtures 文件夹中,更新这两个文件,classes.jsinstructors.js

    // app/mirage/fixtures/classes.js
    export default [
        {id: 1, subject: 'History',instructor:[1]},
        {id: 2, subject: 'Spanish',instructor:[1]},
        {id: 3, subject: 'Government',instructor:[3]},
        {id: 4, subject: 'English',instructor:[2]},
        {id: 5, subject: 'German',instructor:[2]},
        {id: 6, subject: 'Social Studies',instructor:[4]},
        {id: 7, subject: 'Math',instructor:[]}
    ];
    

    classes.js 文件包含班级和科目的列表。

  3. 创建 instructors.js 文件:

    // app/mirage/fixtures/instructors.js
    export default [
        {id: 1, name: 'John', age: 17, classes:[1,2]},
        {id: 2, name: 'Jack', age: 18, classes:[4,5]},
        {id: 3, name: 'Suze', age: 17, classes:[3]},
        {id: 4, name: 'Jane', age: 16, classes:[6]}
    ];
    

    如你所见,每位年轻教练都有一个他们教授的班级列表。每个班级只有一个,并且只有一个教练。

  4. 编辑 Mirage 的 config.js 文件。添加新的路由:

    // app/mirage/config.js
    export default function() {
    
        this.get('/instructors',['instructors','classes']);
        this.get('/classes',['instructors','classes']);
    }
    
  5. 每个这些端点都将返回 instructorclass 数据。这是通过 侧加载 实现的。以下是一个示例 JSON 响应侧加载:

    {
      "instructors": [
        {
          "id": 1,
          "name": "John",
          "age": "17",
          "classes": [1,2]
        },
        {
          "id": 2,
          "name": "Jack",
          "age": "18",
          "classes": [3,4]
    
        }
      ],
      "classes": [
        {
          "id": 1,
          "subject": "History",
          "instructor": [1]
        },
        {
          "id": 2,
          "subject": "Spanish",
          "instructor": [1]
    
        },
        {
          "id": 3,
          "subject": "Government",
          "instructor": [2]
        },
        {
          "id": 4,
          "subject": "English",
          "instructor": [2]
    
        },
      ]
    
    }
    

    如前例所示,instructorclass 数据都被返回了。这是 RESTAdapter 预期的默认行为。

    另一方面,我们可以使用异步关系返回数据。当这种情况发生时,服务器数据存储只返回一个模型的数据。然后 Ember 执行一个或多个 HTTP 请求以检索其他模型的数据。为了简化这个例子,我们假设数据是侧加载的。

  6. 最后,创建一个新的场景,用于加载我们使用的固定数据:

    // app/mirage/scenarios/default.js
    export default function( server ) {
    
      server.loadFixtures();
    }
    

    这将加载这两个固定数据,以便它们可以被返回给 Ember 客户端。

如何实现...

  1. 打开 adapter 文件夹中的 application.js 文件。将其设置为 RESTAdapter

    import DS from 'ember-data';
    
    export default DS.RESTAdapter.extend({
    });
    

    将使用 RESTAdapter 来实现这个菜谱。

  2. 编辑 models 文件夹中的 class.jsinstructor.js 文件。为模型添加新的属性:

    // app/models/class.js
    import DS from 'ember-data';
    
    export default DS.Model.extend({
      subject: DS.attr('string'),
      instructor: DS.belongsTo('instructor')
    });
    

    在这个例子中,我们需要确保班级有一个 instructor。这可以通过使用 DS.belongsTo 方法来实现。这告诉 Ember 查看该属性的 instructor 模型。

  3. 教练与班级模型之间存在一对一的关系。一位教练可以教授一个或多个班级。我们可以使用 DS.hasMany() 方法并指定模型名称来实现这一点:

    // app/models/instructor.js
    import DS from 'ember-data';
    
    export default DS.Model.extend({
        name: DS.attr('string'),
        age: DS.attr('number'),
        classes: DS.hasMany('class')
    });
    
  4. 更新 routes 文件夹中的 index.js 文件。指定它返回所有 instructor 数据:

    // app/routes/index.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        model(){
          return this.store.findAll('instructor');
        }
    });
    

    此路由使用 Ember Data 的 findAll 方法返回所有 instructor 数据。

  5. 更新 helper 文件:

    // app/helpers/addone.js
    import Ember from 'ember';
    
    export function addone(params) {
        return +params+1;
    }
    
    export default Ember.Helper.helper(addone);
    

    Ember 中的辅助函数用于操作模板数据。你可以传递信息给一个,并返回信息。在这个例子中,我们进行了一些简单的数学运算。

  6. 使用模型数据编辑 index.hbs 文件:

    // app/templates/index.hbs
    {{outlet}}
    {{#each model as |instructor|}}
    Name of instructor: <b>{{instructor.name}}</b><br>
    Teaches Classes:<br>
    {{#each instructor.classes as |class index|}}
        <b>{{addone index}}: {{class.subject}}</b><br>
    {{/each}}
    <br>    <br>
    {{/each}}
    

    在这个模板中,我们使用 each 辅助函数来显示讲师的姓名。为了访问课程信息,另一个 each 辅助函数遍历 instructor.classes。在每次迭代中,我们显示 subjectindex 课程。由于索引从零开始,我们可以将其传递给 addone 辅助函数。这个辅助函数增加传递给它的数字。

  7. 运行 ember server 并应看到从固定数据中显示的所有数据:如何操作...

    每位讲师都列出了每个课程

它是如何工作的...

Ember 使用 DS.hasManyDS.belongsTo 方法来表示一对多和一对一的关系。按照惯例,Ember 假设你正在使用 JSON API 适配器。在撰写本文时,JSON API 是 Ember Data 的默认适配器。它通过 XHR 通过定义良好的 JSON 与服务器通信。它的目标是便于在客户端和服务器端处理,同时支持广泛的用例,包括关系。在大多数情况下,REST 适配器表现良好。因此,我在书中包括了它而不是 JSON API 适配器。请注意,你可以使用任一适配器来实现你的目标。

这可以通过使用 RESTAdapter 来更改。RESTAdapter 假设所有键都是驼峰式命名,并且发送的数据都是侧加载的。这样做是为了帮助开发者轻松地将他们的后端 API 和数据存储与 Ember 集成。

第八章。日志、调试和测试

在这一章中,我们将介绍以下配方:

  • 使用 Ember.Logger

  • 使用 Ember 检查器

  • 验证弃用和使用 Ember 检查器的先进功能

  • 使用接受测试

  • 使用单元测试

  • 测试组件

  • 测试路由

  • 测试模型

  • 测试控制器

简介

测试是 Ember 框架的一个重要部分。Ember 允许三种不同的测试分类——接受单元集成

接受测试用于测试应用程序流程和交互。它模拟用户可能执行的动作。例如,这可能包括填写表单或导航到应用程序的不同部分。

单元测试用于测试功能的小块。这可能包括测试计算属性或检查不同元素标签的字段。

集成测试介于单元测试和接受测试之间。建议与组件测试一起使用。例如,集成测试与测试 UI 和控件效果良好。

为您的应用程序创建测试用例是一种良好的实践,特别是如果这个应用程序将在生产中使用。

使用 Ember.Logger

Ember.Logger 是 Ember 中一种强大的日志类型。它超越了 imports.console 的功能。在这个配方中,我们将查看一些示例,说明如何在您的应用程序中使用它。

如何做...

在这个项目中,我们将创建一个简单的程序,演示如何使用一些 Ember.logging 功能:

  1. 在一个新程序中,添加一个新的 index 路由:

    $ ember g route index
    
    

    这将创建一个新的 index 路由。

  2. 编辑 routes 文件夹中的 index.js 文件。添加一些新的日志:

    // app/routes/index.js
    import Ember from 'ember';
    const {Logger}= Ember;
    export default Ember.Route.extend({
        model(){
          Logger.log('log');
          Logger.info('info', 'more stuff');
          Logger.error('error');
          Logger.debug('debug');
          Logger.warn('warn');
          Logger.assert(true === false);
          return {};
        }
    });
    

    Ember.logging 提供了五种不同的日志选项。所有这些不同类型的 log 方法都接受一个或多个参数。当写入浏览器控制台窗口时,每个参数将被连接在一起,并用空格分隔:

    Logger.log('log');
    
  3. 这是 Ember 中日志的基本形式。它只是将值记录到浏览器控制台:

    Logger.info('info', 'more stuff');
    

    info 记录器将消息作为 info 消息记录到控制台。在 Firefox 和 Chrome 中,旁边会显示一个小的 I 图标:

    Logger.error('error');
    

    error 日志以带有 错误 图标的红色文本和堆栈跟踪的形式打印到控制台。

    Logger.debug('debug');
    

    debug 日志以蓝色文本打印到控制台。

    Logger.warn('warn');
    

    警告日志将以带有 警告 图标的格式打印到控制台。

    Logger.assert(true === false);
    

    如果返回值是 false,则 assert 语句将返回错误和堆栈跟踪。

  4. 启动 Ember 服务器并打开 控制台。这是它在 Chrome 中的外观截图:如何做...

它是如何工作的...

Ember.Logger 是一个更强大的控制台记录器。它是一个强大的日志工具,可以简化调试。Ember.Logger 是 Ember CLI 包内建的。

使用 Ember 检查器

Ember 检查器是您浏览器的一个插件,可以帮助您调试 Ember 应用程序。在这个配方中,我们将查看一些示例,说明如何使用它。

准备工作

在开始之前,你必须安装附加组件。它适用于 Chrome、Firefox 或 Opera。其他浏览器如 Internet Explorer 也可以通过 bookmarklet 使用。

  1. 安装 Ember Inspector 的浏览器附加组件:

  2. 为了我们食谱的目的,我们将使用 Ember CLI Mirage 返回一个简单的 school 模型。在创建新项目后,运行此命令:

    $ ember install ember-cli-mirage
    $ ember g factory school
    
    

    这将安装所有必要的文件,以便我们可以模拟 HTTP 服务器。

  3. 在安装了 Ember CLI Mirage 的附加组件后,更新 mirage 文件夹中的 config.js 文件:

    // app/mirage/config.js
    export default function() {
    
        this.get('/schools');
    }
    

    这将为我们的 Ember 客户端在检索学校信息时连接的 HTTP GET 路由添加。

  4. 更新 mirage/factories/ 文件夹中名为 school.js 的文件,用于学校信息:

    // app/mirage/factories/school.js
    import Mirage, {faker}  from 'ember-cli-mirage';
    
    export default Mirage.Factory.extend({
    
        name:faker.name.firstName,       // using faker
        city: faker.address.city,
    });
    

    此文件将用于生成我们虚构学校的假名字和城市数据。

  5. 更新 default.js 文件:

    // app/mirage/scenarios/default.js
    export default function( server ) {
    
        server.createList('school', 2);
    }
    

    这将为我们的应用程序从工厂中生成所需的数据。

  6. 生成 index 路由、model 和 REST adapter

    $ ember g route index
    $ ember g model school name:string city:string
    $ ember g adapter application
    
    

    这些文件将用于我们的应用程序,我们将在下一节中讨论。

如何操作...

此应用程序的目的是返回学校列表。我们将使用 Ember Inspector 来更详细地查看这些数据。

  1. 打开 adapters 文件夹中的 application.js 文件。验证 RESTAdapter 是否已设置:

    // app/adapters/application.js
    import DS from 'ember-data';
    
    export default DS.RESTAdapter.extend({
    });
    

    这告诉 Ember 使用 Ember Data 的 REST 适配器。回顾 第七章,Ember 模型和 Ember Data,以刷新 Ember Data 的知识。

  2. 编辑 models 文件夹中的 school.js 文件:

    // app/models/school.js
    import DS from 'ember-data';
    
    export default DS.Model.extend({
        name: DS.attr('string'),
        city: DS.attr('string')
    });
    

    此模型有两个属性,namecity

  3. 更新 app/templates 文件夹中的 index.hbs 文件:

    // app/templates/index.hbs
    {{outlet}}
    
    {{#each model as |schoolName|}}
        Name:{{schoolName.name}}<br>
        City:{{schoolName.city}}<br>
        <br>
    {{/each}}
    

    each 辅助函数将遍历返回的每个模型并显示学校信息。

  4. 更新 app/routes 文件夹中的 index.js 文件:

    // app/routes/index.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        model() {
          return this.store.findAll('school');
        }
    });
    
  5. 运行 ember server 并你会看到学校列表,如下图所示:如何操作...

  6. 让我们打开 Ember Inspector 看看它显示了什么。在 Chrome 中,你可以通过打开控制台并点击 Ember 选项卡来访问它:如何操作...

    Ember Inspector 显示 视图树路由数据弃用信息

  7. 在 Ember Inspector 中点击 Info如何操作...

    信息显示在此应用程序中加载的所有库信息。这有助于了解所有内容使用的是哪个版本。

  8. 在 Ember 检查器中点击视图 如何操作...

    视图树显示了有关应用程序的各种信息。它显示了当前的路由、模板、模型等。这有助于确定当前屏幕上加载了什么。

  9. 在 Ember 检查器中点击路由如何操作...

    路由显示所有可用的路由、控制器和模板。一些路由可能尚未定义,但将显示,例如加载

  10. 点击index路由旁边的$E如何操作...

    在检查器中,您可以通过点击$E来分配实例变量。

  11. 打开控制台并使用实例变量$E添加新记录:如何操作...

  12. 您可以在浏览器的控制台窗口中键入以下内容:

    $E.store.createRecord('school',{name: 'Test School', city: 'Reno'});
    
    

    这将在您的数据存储中添加一条新记录。页面将自动更新此信息。这非常有价值。您可以在任何地方使用它来帮助调试问题。

  13. 点击数据以查看所有模型数据:如何操作...

    可选地,您可以点击这些数据中的任何一个并验证其属性。您也可以进行更改,并且它将自动更新到屏幕上。

验证弃用和使用 Ember 检查器的高级功能

在本食谱中,我们将使用 Ember 检查器的先进功能来探索弃用。

如何操作...

弃用警告将在弃用部分显示。请检查此选项以确保您的应用程序中没有弃用内容。如果列出弃用,它将有一个您可以点击的链接,以查看您需要做什么来修复它:

如何操作...

工作原理...

Ember 检查器旨在使创建和调试 Ember 应用程序变得更加容易。它被创建为大多数现代网络浏览器的插件。它显示您当前应用程序的信息、路由、模板、数据等。

它是一个开源插件,并有一个活跃的开发者社区支持。新功能正在不断添加。要请求您自己的功能,请查看以下 GitHub 页面:github.com/emberjs/ember-inspector

使用接受测试

接受测试通常有助于测试工作流程并模拟用户交互。在本食谱中,我们将探讨创建几个简单的接受测试。

如何操作...

  1. 在新应用程序中,创建一个名为book-shelf的新组件和一个名为add-book-test.js的新接受测试:

    $ ember g component book-shelf
    $ ember g acceptance-test add-book
    
    

    这将为book-shelf组件和add-book接受测试创建必要的代码。请注意,将为book-shelf组件生成一个集成测试。在本示例中,我们不会更新集成测试。

  2. 使用新的books数组和新的操作更新组件文件:

    // app/components/book-shelf.js
    import Ember from 'ember';
    
    export default Ember.Component.extend({
        books: Ember.A([{name: 'Moby Dick'}]),
        actions: {
          add(val) {
            this.get('books').addObject({name:val});
          }
        }
    });
    

    此组件使用 books 属性来跟踪书架上的书籍。books 属性是一个 Ember 对象数组。add 动作将另一个对象添加到数组中。使用 Ember.A 声明 Ember 数组。

  3. 更新 book-shelf.hbs 组件模板文件:

    // app/templates/components/book-shelf.hbs
    {{input value=val}}
    <button {{action 'add' val}}>Push Me</button><br>
    <ul>
    {{#each books as |book|}}
        <li>{{book.name}}<br></li>
    {{/each}}
    </ul>
    

    组件列出所有书籍。它还有一个 input 辅助器和 button。按钮有一个名为 add 的动作,在点击事件上被触发。它将 input 辅助器的值作为参数传递给 action

  4. book-shelf 组件添加到应用程序文件:

    <h2 id="title">Welcome to Ember</h2>
    {{book-shelf}}
    {{outlet}}
    

    此代码将 book-shelf 组件添加到应用程序模板中。

  5. 将测试代码添加到 add-book-test.js 文件:

    // app/tests/acceptance/add-book-test.js
    import { test } from 'qunit';
    import moduleForAcceptance from 'example3/tests/helpers/module-for-acceptance';
    
    moduleForAcceptance('Acceptance | add book');
    
    test('visiting / and adding book', function(assert) {
        visit('/');
        fillIn('input','My new book');
        click('button');
        andThen(function() {
          assert.equal(currentURL(), '/');
          assert.equal(find('li:last').text(),'My new book');
    
        });
    
    });
    

    这个验收测试访问应用程序的根目录 /。然后,它向 input 辅助器添加新文本并点击 button。然后检查 URL 和模板,以确保文本已被添加。

    顶部的代码大多是样板代码。测试在底部,可以按步骤进行。visitfillInclickandThen 辅助器都是异步测试辅助器。

    注意

    以下是一个所有异步和同步测试辅助器的列表:

    • click(selector): 这将点击一个元素并触发相应的动作,并在异步行为完成后返回一个承诺

    • fillIn(selector, value): 这将使用给定的值填充选定的输入,并在所有异步行为完成后返回一个承诺

    • keyEvent(selector, type, keyCode): 这在元素上模拟 keypresskeydownkeyup

    • triggerEvent(selector, type, options): 这将在由 selector 标识的元素上触发给定的事件

    • visit(url): 这将访问由 URL 给定的路由,并在所有异步行为完成后返回一个承诺

    • currentPath(): 这将返回当前路径

    • currentRouteName(): 这将返回当前激活的路由

    • currentURL(): 这将返回当前 URL

    • find(selector, context): 这将从应用程序的根元素开始查找元素;可选地,您可以添加一些上下文

  6. 运行 ember server 并访问 /tests URL。此 URL 将显示所有正在运行的测试。查找添加书籍的验收测试:如何操作...

  7. 这个验收测试表明一切通过。或者,您也可以在命令行上运行测试:

    $ ember test –server
    
    
  8. 这将弹出一个屏幕,以便您可以在控制台中运行测试。要使用此功能,您必须首先导航到 localhost7357 端口。然后,此屏幕将刷新以显示通过测试的数量:如何操作...

    每个测试都将进行检查,并且任何失败的测试都将显示在此屏幕上。在文件更改后,测试将被重新运行。

它是如何工作的...

接受测试用于测试用户交互和流程。这是通过 Ember 的 QUnit 测试框架完成的,尽管可以使用第三方插件支持其他测试框架。您可以导航到 /tests URL 或在服务器上运行 ember test 来执行测试用例。

使用单元测试

单元测试用于测试较小的功能块。在这个菜谱中,我们将看到一个例子。

如何操作...

在这个例子中,我们将创建一个简单的 Ember.Object,并带有计算属性。我们将测试这个计算属性并断言返回的值是否正确。

  1. 在一个新项目中,在 models 文件夹中创建一个新的 first-last.js 文件:

    // app/models/first-last.js
    
    import Ember from 'ember';
    
    export default Ember.Object.extend({
        firstName: 'John',
        lastName: 'Smith',
        fullName: Ember.computed('firstName', 'lastName', function() {
          const firstName = this.get('firstName');
          const lastName= this.get('lastName');
          return `Full Name: ${firstName} ${lastName}`;
        })
    });
    

    在此文件中,我们有两个属性,firstNamelastName。计算属性 fullName 将这两个属性组合起来并返回一个全名。如果这些属性中的任何一个发生变化,计算属性将会触发。

  2. 创建一个新的单元测试来检查 Ember.Objectcomputed 属性:

    $ ember g model-test first-last
    
    

    生成的 model-test 单元测试将在 /tests/unit/models 目录中创建一个新的测试文件。

  3. 更新 first-last-test.js 文件,添加一个新的单元测试来检查计算属性是否返回正确的值:

    // tests/unit/models/first-last-test.js
    
    import { moduleFor, test } from 'ember-qunit';
    
    moduleFor('model:first-last', 'Unit | Model | fullName', {
        unit: true
    });
    
    test('check computed property fullName', function(assert) {
        const firstLast= this.subject();
        firstLast.set('firstName','Erik');
        firstLast.set('lastName','Hanchett');
        assert.equal(firstLast.get('fullName'), 'Full Name: Erik Hanchett');
    });
    

    moduleFor 是由 ember-qunit 提供的单元测试辅助工具。这有助于我们获取用于查找和实例化的模型。unit: true 属性将测试用例标记为单元测试。由于我们使用 moduleFor,我们可以使用 this.subject() 实例化 firstLast 对象。然后测试设置计算属性的 firstNamelastName 并进行 assert 以确保它们相等。

  4. 运行 ember server 并检查 /tests URL 的输出:如何操作...

    输出显示单元测试已通过

  5. 更新 first-last-test.js 文件,使用错误的值:

    // tests/unit/models/first-last-test.js
    …
    test('check computed property fullName', function(assert) {
        const firstLast= this.subject();
        firstLast.set('firstName','Erik');
        firstLast.set('lastName','Hanchett');
        assert.equal(firstLast.get('fullName'),'Full Name: Erik wrong');
    });
    

    我们更新了代码,所以现在测试将失败,因为文本不匹配。

  6. 导航到 /tests 文件夹并查看输出:如何操作...

    现在测试失败,因为文本不匹配。您可以通过导航到 /tests 文件夹或从命令行运行 ember test 来查看此输出。

工作原理...

单元测试用于测试小块代码或功能。它们是 Ember QUnit 库的一部分。单元测试可以运行您应用程序中的几乎所有内容,包括模型、组件或控制器。

测试组件

应该使用集成测试来测试组件。在这个菜谱中,我们将查看一个简单示例,该示例是一个改变文本大小的组件。

如何操作...

  1. 在一个新应用中,创建一个名为 font-sizer 的新组件:

    $ ember g component font-sizer
    
    

    这将生成一个名为 font-sizer 的新组件。此组件将用于调整文本大小。

  2. 更新 components 文件夹中的 font-sizer.js 文件:

    // app/components/font-sizer.js
    import Ember from 'ember';
    
    export default Ember.Component.extend({
        textInfo: 'Hello World',
        attributeBindings: ['style'],
        style: Ember.computed('size',function() {
          const size = this.get('size');
          return new Ember.Handlebars.SafeString("font-size: "+ size);
        })
    
    });
    

    所有组件都在模板内部以div标签的形式渲染。如果需要,我们可以向这些div标签添加不同的属性。在这种情况下,attributeBindings属性将添加一个style标签。style标签将是一个计算属性,每当size值更改时都会更新。Ember.Handlebars.SafeString让 Ember 知道字符串应该以不转义的形式显示,并且它是安全的。

  3. 使用新组件更新application.hbs文件:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    
    {{#font-sizer size="38px" }}
        Test
    {{/font-sizer }}
    {{outlet}}
    

    font-sizer组件是块形式的。测试文本将在div块中。

  4. 更新font-sizer-test.js文件,以便我们可以检查确保属性已被添加:

    // tests/integration/components/font-sizer-test.js
    import { moduleForComponent, test } from 'ember-qunit';
    import hbs from 'htmlbars-inline-precompile';
    
    moduleForComponent('font-sizer', 'Integration | Component | font sizer', {
        integration: true
    });
    
    test('check attributes', function(assert) {
    
        this.render(hbs`{{font-sizer size=val}}`);
        this.set('val','38px');
        assert.equal(this.$('div').attr('style'),'font-size: 38px', 'size is set to 38px');
    
    });
    

    当我们创建组件时,此代码会自动为我们生成。moduleForComponent辅助函数用于组件。integration: true标签将此测试标记为集成测试。通过这样做,我们能够渲染组件并将size属性传递给它。使用assert.equal方法来检查font-size是否设置正确。

  5. 运行ember server并检查/tests如何操作...

    测试用例通过

在字体大小组件中测试操作

我们可以模拟操作并测试结果以验证预期结果。

  1. 使用现有应用程序,更新font-sizer.js文件以添加新操作:

    // app/components/font-sizer.js
    …
        textInfo: 'Hello World',
        }),
        actions: {
          updateText(){
            this.set('textInfo','Hi');
          }
        }
    
    });
    

    此新操作将textInfo属性设置为'Hi'

  2. 更新components文件夹中的font-sizer.js文件:

    // app/templates/components/font-sizer.hbs
    <div id='info'>{{textInfo}}</div><br>
    {{yield}}<br>
    <button {{action 'updateText'}}>Update Text</button>
    

    在这个模板中,我们创建了一个新的div标签,其 ID 为info,围绕textInfo属性。为button点击添加了一个新的updateText操作。此操作更新textInfo属性。

  3. font-sizer-test.js文件添加一个新测试,以便它可以检查添加的新操作:

    // tests/integration/components/font-sizer-test.js
    …
    test('check action', function(assert) {
    
        assert.expect(2);
        this.render(hbs`{{font-sizer}}`);
        assert.equal(this.$('#info').text(), 'Hello World', |'starting text is hello world');
        this.$('button').click();
        assert.equal(this.$('#info').text(),'Hi', 'text changed   to hi');
    
    });
    

    通过设置assert.expect(2),测试必须有两个断言,否则将失败。我们首先使用this.render辅助函数渲染组件。下一个语句检查this.$('#info').text()返回的值是否等于Hello World。然后我们模拟点击按钮。最后一个语句检查this.$('#info').text()是否等于Hi

  4. 运行ember server并导航到/tests。所有测试都将通过:在字体大小组件中测试操作

工作原理...

组件通过moduleForComponent辅助函数使用集成测试。Ember 的 QUnit 功能利用了 Ember 实际查看组件的方式。您可以测试绑定值以及返回的操作。

测试路由

测试路由可以通过验收测试或单元测试来完成。在这个例子中,我们将为路由创建一个简单的单元测试。

如何操作...

  1. 在一个新应用程序中,生成一个新的students路由:

    $ ember g route students
    
    

    此命令将为学生生成路由代码。

  2. 编辑学生路由信息并添加一个新属性:

    // app/routes/students.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        someText: 'someText'
    });
    

    此路由有一个名为someText的属性。

  3. 编辑tests/unit/routes文件夹中的students-tests.js文件:

    // tests/unit/routes/students-test.js
    import { moduleFor, test } from 'ember-qunit';
    
    moduleFor('route:students', 'Unit | Route | students', {
        // Specify the other units that are required for this test.
    });
    
    test('check prop and route exists', function(assert) {
        let route = this.subject();
        assert.expect(3);
        assert.equal(route.get('someText'),'someText');
        route.set('someText','otherText');
        assert.equal(route.get('someText'),'otherText');
        assert.ok(route);
    });
    

    在这个例子中,我们正在检查 someText 属性的输出。第一个 assert.equal 获取属性并检查它是否与 someText 值匹配。路由实例也可以设置属性。下一个断言检查新值是否已设置。最后的断言确保路由可用。

  4. 运行 ember server 并导航到 /tests如何做...

    这表明所有测试都通过了

它是如何工作的...

在你的 Ember 应用程序中,路由有几个不同的功能。它可以存储你的模型数据,并具有属性和操作。当测试路由时,我们可以使用它进行更通用的验收测试或作为单独的单元测试。

测试模型

当测试模型时,你可以使用 Ember Data 来帮助。在这个菜谱中,我们将创建一个模型并测试以确保它正确地创建数据。

如何做...

  1. 在一个新应用程序中,生成一个新的学生模型:

    $ ember g model student.js
    
    

    这将生成学生模型所需的所有文件。

  2. 更新学生模型,添加两个属性:

    // app/models/student.js
    import DS from 'ember-data';
    
    export default DS.Model.extend({
        firstName: DS.attr('string'),
        lastName: DS.attr('string')
    });
    

    这个模型有两个属性,firstNamelastName。两者都持有 string 类型的值。

  3. 为新模型添加一个新的单元测试,以测试新属性:

    // tests/unit/models/student-test.js
    import { moduleForModel, test } from 'ember-qunit';
    
    moduleForModel('student', 'Unit | Model | student', {
        // Specify the other units that are required for this test.
        needs: []
    });
    
    test('it exists', function(assert) {
        let model = this.subject();
        assert.ok(!!model);
    });
    
    test('Test model data', function(assert) {
        assert.expect(2);
        let model = this.subject({firstName: 'Erik', lastName: 'Hanchett'});
        assert.equal(model.get('firstName'),'Erik', 'first Name is Erik');
        assert.equal(model.get('lastName'),'Hanchett', 'last Name is Erik');
    });
    
  4. 这个测试使用 moduleForModel 辅助函数。第一个测试检查模型是否正常存在。第二个测试检查属性:

    …
    test('Test model data', function(assert) {
        assert.expect(2);
        let model = this.subject({firstName: 'Erik', lastName: 'Hanchett'});
        assert.equal(model.get('firstName'),'Erik', 'first Name is Erik');
        assert.equal(model.get('lastName'),'Hanchett', 'last Name is Erik');
    });
    

    在创建 model 实例时,你可以传递 model 属性的值。在这种情况下,在存储中创建了一个 {firstName: 'Erik', lastName: 'Hanchett'} 对象。我们可以使用 model.get 方法访问这些值。assert.equal 方法与模型进行比较,以确保值匹配。

  5. 运行 ember server 并导航到 /tests,你会看到通过测试的测试:如何做...

    这表明两个测试都通过了

它是如何工作的...

moduleForModel 辅助函数用于访问 Ember 的模型信息。这是通过 Ember Data 实现的,以便可以测试模型。Ember 的 QUnit 提供了一种完全测试模块的方法。

测试控制器

应该在你的应用程序中测试控制器。在这个菜谱中,我们将测试一些来自控制器的基本操作。

如何做...

  1. 创建一个新的 index 控制器:

    $ ember g controller index
    
    

    这创建了一个名为 index 的新控制器。

  2. 在索引控制器中,添加一个新的属性和操作:

    // app/controllers/index.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
        myValue: 'value',
        actions:{
          pressed(value){
            this.set('myValue',value);
          }
        }
    });
    

    这个控制器有一个名为 myValue 的属性。另一个名为 pressed 的操作将 myValue 的值更改为函数中传入的任何值。

  3. 更新索引单元测试。为操作和属性添加一些测试:

    // tests/unit/controllers/index-test.js
    import { moduleFor, test } from 'ember-qunit';
    
    moduleFor('controller:index', 'Unit | Controller | index', {
        // Specify the other units that are required for this test.
        // needs: ['controller:foo']
    });
    
    // Replace this with your real tests.
    test('it exists', function(assert) {
        let controller = this.subject();
        assert.ok(controller);
    });
    
    test('check property', function(assert) {
        assert.expect(2);
        let controller = this.subject();
        assert.equal(controller.get('myValue'),'value');
        controller.send('pressed','newValue');
        assert.equal(controller.get('myValue'),'newValue');
    });
    
  4. 这里使用 moduleFor 辅助函数来测试控制器。第一个测试确保控制器存在:

    …
    test('check property', function(assert) {
        assert.expect(2);
        let controller = this.subject();
        assert.equal(controller.get('myValue'),'value');
        controller.send('pressed','newValue');
        assert.equal(controller.get('myValue'),'newValue');
    });
    

    这个测试使用this.subject创建了一个控制器实例。检查初始值以确保其正确性。要向控制器发送动作,调用controller.send方法。发送方法可以接受一个或多个参数。第一个是触发动作的名称。之后是应该传递给方法的任何值。

  5. 运行ember server并导航到/tests。这将显示通过测试:如何操作...

    这条消息显示所有测试都通过了

它是如何工作的...

控制器测试与我们之前讨论的单元测试非常相似。它们使用 QUnit 的moduleFor辅助函数。在一个控制器中,我们可以测试属性或动作,并确保结果符合预期。

第九章. 使用 Ember.js 的实际任务

在本章中,我们将涵盖以下食谱:

  • 使用服务与组件

  • 管理基本认证

  • 使用 OAuth2 与 Ember Simple Auth

  • 使用 Liquid Fire 创建过渡

  • 与 HTML5 拖放一起工作

  • 使用 Ember.js 学习 Bootstrap

简介

在开发 web 应用程序时,你可能会遇到一些棘手的情况。你可能需要设置认证或动画或添加过渡。你可能需要弄清楚如何在你的应用程序中使用 Bootstrap。这些场景经常发生。

在本章中,我们将介绍一些更常见的实际任务和一些你可以用来使使用 Ember.js 的工作更轻松的食谱。

使用服务与组件

在 Ember.js 中,一个服务是一个单例对象,它持有状态。换句话说,它可以在 Ember 应用程序中共享并且不会改变。例如,会话数据、与服务器通信的 API 和 WebSockets 是很好的服务候选者。

在这个食谱中,我们将创建并将一个服务注入到组件中。

小贴士

依赖注入

服务和依赖注入密不可分。依赖注入DI)发生在我们将对象在实例化过程中注入到其他对象中时。这意味着我们取一个服务并将其注入到我们的路由、控制器和组件中。这是一个重要的框架概念,不应过度使用。注入过多的服务会破坏关注点分离的设计原则。

如何操作...

  1. 在一个新应用程序中,生成以下文件:

    $ ember g service start
    $ ember g component comp-info
    $ ember g initializer init
    
    

    这些文件将被用来创建我们的应用程序。该服务将包含一个属性和一个返回数据的方法。

  2. 开始编辑 start.js 服务:

    // app/services/start.js
    import Ember from 'ember';
    
    export default Ember.Service.extend({
        isOn: false,
        importantInfo(){
          return "Important Info is " + this.get('isOn');
        }
    });
    

    这是 services 文件。它有一个 isOn 属性和一个名为 importantInfo 的方法,该方法返回一个字符串。在这个例子中,我们想在我们的组件 comp-info 中访问这些信息,以便它可以被显示。

  3. 编辑组件 comp-info.js 文件并添加一个新的动作,该动作使用 start 服务的信息:

    // app/components/comp-info.js
    import Ember from 'ember';
    
    export default Ember.Component.extend({
        start: Ember.inject.service(),
        message: null,
        actions: {
          pressMe() {
            this.start.toggleProperty('isOn');
            this.set('message',this.start.importantInfo());
            console.log(this.start.isOn);
          }
        }
    
    });
    

    在组件中,最重要的是 start 属性。我们可以使用 Ember.inject.service()start 服务注入到组件中。按照惯例,属性的名称必须与注入的服务名称匹配。在我们的例子中,start 服务将被注入。

    pressMe 动作切换 start 服务的 isOn 属性。然后我们在 message 属性中 setimportantInfo 方法返回的文本,以便可以在模板中显示。

  4. 向组件的模板信息中添加 button

    // app/templates/components/comp-info.hbs
    <button {{action "pressMe"}}>push me</button><br>
    {{message}}
    

    在组件中,我们只是给按钮添加了一个动作并显示了一条消息。

  5. comp-info 组件添加到应用程序模板文件中:

    <h2 id="title">Welcome to Ember</h2>
    
    {{outlet}}
    {{comp-info}}
    

    现在模板将显示刚刚创建的组件。

  6. 启动 Ember 服务器,它将如下所示:如何操作...

    按下按钮将切换isOn属性。正如您从本例中可以看到的,组件通过服务访问了信息并将其显示到模板中。

  7. 创建一个initializer,将服务注入到所有组件中:

    // app/initializer/init
    export function initialize(app) {
        app.inject('component', 'start', 'service:start');
    }
    
    export default {
        name: 'init',
        initialize
    };
    

    Ember.js 初始化器在应用程序启动时创建。这是一个预加载数据或设置应用程序状态的不错的地方。在initialize函数中的app参数也被称为Ember.Application。它作为依赖声明的注册表。可以注册和注入到应用程序中的工厂(类)。service:start是我们之前创建的start服务的键。一旦注册了工厂,它就可以在应用程序的任何地方注入。由于start服务已经创建,因此不需要注册它。

    app.inject接受三个参数。第一个是要注入的类型。第二个是服务的name,即start。最后,创建service:start工厂。

  8. 更新组件,使其不再注入start服务,因为它已经通过依赖注入可用:

    // app/components/comp-info.js
    …
    export default Ember.Component.extend({
        //start: Ember.inject.service(),
        message: null,
    …
    

    start服务已被注释掉,因此不再可用。组件的其他部分保持不变,因为仍然可以使用this.get('start')检索服务。这是由于我们在初始化器中将它注入到了所有组件中。

  9. 再次运行服务器,加载的模板将保持相同,并且具有相同的功能。

它是如何工作的...

服务是长期存在的 Ember 对象,可以在应用程序的不同部分使用。它们与会话、WebSockets、地理位置、日志记录等一起使用时很好。可以使用Ember.inject.service将它们提供给应用程序的其他部分,这是一个可以检索服务并使其可用的方法。

DI 可以用来将服务注入到 Ember 应用程序的许多不同部分。Ember 的架构使用由Ember.Application注册的工厂。我们可以使用Application.inject方法将服务注入到所有路由、组件和控制台中。

管理基本认证

在任何实际的 Ember 应用程序中,在某个时候,您将需要处理认证。例如,用户可能需要向服务器发送他们的凭据以识别自己,或者认证用户可能需要访问应用程序的保护部分。

认证的一个重要方面是基于登录用户保护信息。这可以通过使用令牌创建会话来完成。在本食谱中,我们将使用 Express 服务器创建一个简单的基于令牌的认证。这将帮助我们理解基础知识。在下一节中,我们将介绍使用 OAuth2 与 Ember Simple Auth。

如何操作...

  1. 在一个新应用程序中,生成以下文件:

    $ ember g service session
    $ ember g adapter application
    $ ember g controller login
    $ ember g model student name:string age:number
    $ ember g route login
    $ ember g route students
    $ ember g template index
    $ ember g server index
    $ npm install body-parser –save-dev
    
    

    这将生成我们应用程序所需的所有脚手架。students 路径将被保护。它将只在用户认证后从服务器获取信息。

    ember g server index 命令将为我们生成一个 node Express 模拟服务器。在前面的章节中,我们使用 Ember CLI Mirage 来进行所有模拟测试。由 Ember 生成的 Express 服务器并不那么强大。然而,它将更容易设置我们的模拟服务器和认证示例。请注意,当将 Ember 应用程序部署到生产环境时,Ember 服务器将不会包含在内。然而,当运行 ember serve 命令时,它将自动启动。

  2. 创建一个名为 session 的新服务,该服务将处理我们的认证并跟踪已认证的用户:

    // app/services/session.js
    import Ember from 'ember';
    
    export default Ember.Service.extend({
        token: null,
        authenticate(log, pass) {
          return Ember.$.ajax({
            method: 'POST',
            url: '/token',
            data: { username: log, password: pass}
          }).then((info)=>{
            this.set('token',info.access_token);
          });
        }
    
    });
    

    此服务将被注入到我们的登录控制器中。这将跟踪已认证的用户并向服务器发送 Ajax 请求。authenticate 方法接受登录名和密码。这些值将使用 HTTP POST 方法发送到 /token。如果登录信息正确,将返回并保存令牌。如果不正确,将返回错误。我们将在登录控制器中稍后处理此错误。

    将使用 token 属性来跟踪用户是否已认证。

  3. 更新 Express 服务器 index.js 文件。为 token 添加一个路由:

    // server/index.js
    /*jshint node:true*/
    
    const bodyParser = require('body-parser');
    
    module.exports = function(app) {
        app.post('/token', function(req, res) {
    
          if (req.body.username === 'erik' && req.body.password === 'password') {
            res.send({ access_token: "secretcode" });
          } else {
            res.status(400).send({ error: "invalid_grant" });
          }
    
        });
    };
    

    这是我们的 node Express 服务器。它将在我们启动 Ember 服务器时运行。当向服务器发送 HTTP POST /token 请求时,它将检查正文 usernamepassword。对于此示例,我们将直接将它们硬编码为 'erik''secretcode'。如果这些匹配,它将返回 access_token。如果不匹配,它将返回无效消息。

    访问令牌将被保存在 session 服务中。我们可以使用此令牌来认证对服务器的未来请求。

  4. 更新 application.js 文件。添加一个新的授权头:

    // app/adapters/application.js
    import DS from 'ember-data';
    import Ember from 'ember';
    
    export default DS.RESTAdapter.extend({
        namespace: 'api',
        session: Ember.inject.service(),
        headers: Ember.computed('session.token',function(){
          return {
            'Authorization': `Bearer ${this.get('session.token')}`
          };
        })
    });
    

    在我们的应用程序中,访问学生路由将触发对服务器的请求。服务器只会对已认证的用户做出响应。服务器期望每个 Ember Data 请求都带有授权 bearer 头。我们可以使用 headers computed 属性,并返回来自服务的 Authorization: Bearer 头和密钥令牌。使用 Ember Data 向服务器发送的每个请求都将发送此头。

  5. 更新 Express 服务器索引信息,以便在令牌匹配时将信息返回给应用程序:

    // app/server/index.js
    …
        app.use(bodyParser.urlencoded({ extended: true }));
    
        app.get('/api/students', function (req, res) {
    
          if (req.headers.authorization !== "Bearer secretcode") {
            return res.status(401).send('Unauthorized');
          }
    
          return res.status(200).send({
            students: [
              { id: 1, name: 'Erik', age: 24 },
              { id: 2, name: 'Suze', age: 32 },
              { id: 3, name: 'Jill', age: 18 }
            ]
    
          });
        });
    …
    

    app.post 之上,你可以为 students 路径添加 app.get。每当 Ember 进入 /students 路径时,它将触发这个 HTTP GET 请求。服务器将检查请求头是否包含密钥代码。如果匹配,它将返回 students 路径的正确 JSON 数据。如果不匹配,它将返回 401 错误。

  6. 验证学生路由和模型信息:

    // app/models/student.js
    import DS from 'ember-data';
    
    export default DS.Model.extend({
        name: DS.attr('string'),
        age: DS.attr('number')
    });
    
  7. 之前,我们使用两个属性 nameage 生成此文件:

    // app/routes/students.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        model() {
          return this.store.findAll('student');
        }
    });
    
  8. 学生路由将向 /students 发送一个 HTTP GET 请求以检索学生模型。由于我们定义应用程序使用 REST 适配器,Ember 将期望数据以 REST 格式:

    // app/templates/students.hbs
    Secret Student Information
    {{#each model as |student|}}
        {{student.name}}<br>
        {{student.age}}<br>
    {{/each}}
    

    学生模板使用 each 辅助函数遍历从服务器返回的模型。它显示每个 nameage

  9. 将登录用户名和属性信息添加到登录模板中:

    // app/templates/login.hbs
    <h3>Login</h3>
    
    {{input value=loginName placeholder='Login'}}<br>
    {{input value=password placeholder='Password' type='password'}}<br>
    <button {{action 'authenticate'}}>Login</button>
    

    此模板使用 input 辅助函数为 loginNamepassword 属性。按钮触发 authenticate 动作。

  10. 更新登录控制器以处理 authenticate 动作:

    // app/controllers/login.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
        loginName: null,
        password: null,
        session: Ember.inject.service(),
        actions: {
          authenticate(){
            this.get('session').authenticate(this.get('loginName'), this.get('password')).then( ()=>{
              alert('Great you are logged in!');
              this.transitionToRoute('students');
            },(err)=>{
              alert('Error! Problem with token! '+ err.responseText);
            });
          }
        }
    });
    

    控制器有几个属性。它检索会话信息服务并使用 authenticate 方法将登录信息发送到服务器。authenticate 方法返回一个承诺。如果成功,应用程序将过渡到 students 路由。如果不成功,将在警告框中显示错误。在此示例中,我们使用了 ES6 箭头函数。()=> 箭头函数比使用函数表达式要短一些,并且它还词法绑定此变量。

  11. 在索引中添加一个链接以登录:

    // app/templates/index.hbs
    Welcome to my app! Login {{#link-to 'login'}}here{{/link-to}}
    

    这只是一个简单的登录路由链接。

  12. 启动服务器并导航到登录路由。您将看到以下图像:如何操作...

  13. 要登录,请输入用户名 'erik' 和密码 'password'。点击 登录 后,将带有用户名和密码信息的 HTTP POST 请求发送到服务器。我们之前设置的 Express 服务器将响应令牌,会话服务将保存它。然后,应用程序将过渡到学生路由。以下屏幕将显示:如何操作...

  14. 当此路由加载时,将向 /students 发送一个 HTTP GET 请求。Express 服务器将检查以确保授权载体头包含正确的密钥代码。然后,它将响应 Ember 将显示的 JSON 数据。

  15. 如果用户名或密码不匹配,将显示以下警告框:如何操作...

它是如何工作的...

基于令牌的认证要求客户端向服务器发送凭据。如果授权,服务器随后将发送一个令牌,客户端将其保存并用于随后的服务器请求。如果令牌不存在,服务器可能不会向客户端返回数据。

此配方是使用认证的简单示例。它缺少适当的错误处理和会话持久性。尽管如此,它为您提供了认证是如何工作的一个想法。

使用 OAuth2 和 Ember Simple Auth

OAuth2 为网络应用程序指定了授权流程。我们可以使用它与 Ember 保护我们的应用程序,并只为授权用户提供数据。在此配方中,我们将查看如何使用 OAuth2 与 Ember Simple AuthESA),这是一个强大的 Ember 扩展。

ESA 将处理我们的客户端会话和认证,并将请求发送到服务器。它非常可定制和可扩展。尽管它可能很复杂,就像我们上一个食谱一样,我们将创建一个只能由授权用户访问的保护学生路由。

准备工作

为了本例的目的,我们需要一个 OAuth2 服务器。设置 OAuth2 服务器超出了本食谱的范围。有多个 OAuth2 库可供使用来设置一个服务器。我推荐以下:

如何操作...

  1. 在一个新的 Ember 应用程序中,运行生成器命令以创建所需的文件:

    $ ember g adapter application
    $ ember g component login-comp
    $ ember g controller login
    $ ember g controller students
    $ ember g model student name:string age:number
    $ ember g route students
    $ ember g route application
    $ ember g route login
    $ ember g template index
    $ ember install ember-simple-auth
    
    

    这将生成我们开始应用程序所需的脚手架。最后一个命令安装了 ESA 的插件。

  2. 我们将首先设置 Ember Simple Auth 认证器和授权器以支持 OAuth2。我们需要设置它,以便用户可以与服务器进行认证。在app文件夹中创建两个新的目录,分别命名为authenticatorsauthorizers

  3. authenticators目录中添加一个名为oauth2-custom.js的新文件,并在authorizers文件夹中添加application.js。添加以下代码:

    // app/authenticators/oauth2-custom.js
    import Authenticator from 'ember-simple-auth/authenticators/oauth2-password-grant';
    import Ember from 'ember';
    
    export default Authenticator.extend({
        makeRequest(url, data) {
    
          var client_id = '123';
          var client_secret = 'secret';
          data.grant_type = 'password';
    
          return Ember.$.ajax({
            url: this.serverTokenEndpoint,
            type: 'POST',
            data: data,
            dataType: 'json',
            contentType: 'application/x-www-form-urlencoded',
            crossDomain: true,
            headers: {
              Authorization: "Basic " + btoa(client_id + ":" + client_secret)
            }
          });
        }
    });
    

    当用户登录时,ESA 会使用authenticators文件。如果需要,我们可以覆盖认证器中的任何内容。makeRequest方法用于向服务器发送消息。默认情况下,ESA 将使用包含用户名和密码的表单字段的 HTTP POST请求发送到/token

  4. 不幸的是,许多 OAuth2 服务器在首次使用服务器进行认证时需要名为Authorization Basic的头部,其中包含秘密客户端 ID 和客户端密钥。为了解决这个问题,我们可以通过我们的自定义 Ajax 请求扩展makeRequest方法。这将在我们登录时使用:

    // app/authorizers/application.js
    import OAuth2Bearer from 'ember-simple-auth/authorizers/oauth2-bearer';
    
    export default OAuth2Bearer.extend();
    

    authorizers文件由 ESA 使用,以告诉我们使用哪种类型的认证。在本例中,我们使用OAuth2Bearer.extend()定义的 Oauth2。

  5. 更新适配器并将 ESA 数据适配器混入添加到adapters文件夹中的application.js文件:

    // app/adapters/application.js
    import DS from 'ember-data';
    
    import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin';
    
    export default DS.RESTAdapter.extend(DataAdapterMixin, {
        namespace: 'api',
        authorizer: 'authorizer:application'
    });
    

    适配器告诉 Ember 将所有请求发送到/api命名空间。ESA 的DataAdapterMixin用于定义应用程序将使用的授权器。在这种情况下,所有 Ember Data 请求都将使用我们之前定义的 OAuth2 应用程序授权器。换句话说,任何使用 Ember Data 发送到服务器的请求,如果存在,都将包含会话数据令牌。

  6. 让我们更新我们的login-comp组件模板:

    // app/templates/components/login-comp.hbs
    <h2>Login page</h2>
    
    <form {{action 'authenticate' on='submit'}}>
        {{input value=login placeholder='Login'}}<br>
        {{input value=password placeholder='Password' type='password'}}<br>
        <button type="submit">Login</button>
    </form>
    

    这将提交loginpassword到我们在组件中设置的authenticate操作。

  7. 更新登录页面组件的authenticate操作:

    // app/components/login-comp.js
    import Ember from 'ember';
    
    export default Ember.Component.extend({
    
        auth: Ember.inject.service('session'),
        login: null,
        password: null,
        actions: {
          authenticate() {
            this.get('auth').authenticate('authenticator:oauth2-custom', this.get('login'),this.get('password')).then(() => {
              alert('Thanks for logging in!');
              this.get('transition')();
              }, () => {
                alert('Wrong user name or password!');
              });
             }
          }
    
    });
    

    由于我们使用欧空局(ESA),我们可以访问一个 session 服务。这个 session 服务有一个 authenticate 方法,它使用我们之前创建的 authenticator。在上面的代码中,我们使用 this.get() 方法从我们的模板中获取 loginpassword。然后我们在我们的服务上调用 authenticate 方法,并传入我们的 authenticator

    如果服务器返回成功消息,那么我们调用 transition 方法,这是一个传递给组件的方法。如果不成功,则弹出一个警告框告诉用户他们的登录不成功。

  8. 将登录页面组件添加到登录模板中,并更新登录控制器:

    // app/templates/login.hbs
    {{login-comp transition=(action 'loggedIn')}}
    

    这调用登录组件并传入父级动作 loggedIn

    // app/controllers/login.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
        actions: {
          loggedIn(){
            this.transitionToRoute('students');
          }
        }
    });
    

    这个动作将应用过渡到 students 路由。它仅在成功登录时触发。它也是传递给登录页面组件的动作的名称。

  9. 更新学生控制器、路由和模板:

    // app/templates/students.hbs
    <h2>Students</h2>
    {{#each model as |student|}}
        <h3>Student: {{student.name}} </h3>
        <h3>Age: {{student.age}} </h3>
    {{/each}}
    
    <button {{action 'logout'}}>Log Out</button>
    

    模板使用 each 辅助函数显示来自服务器的信息。一个 logout 按钮动作将用户登出:

    // app/controllers/students.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
        auth: Ember.inject.service('session'),
        actions: {
          logout(){
            this.get('auth').invalidate();
          }
        }
    });
    
  10. logout 动作使会话无效。使会话无效将吊销令牌,使其不再可用:

    // app/routes/students.js
    import Ember from 'ember';
    import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
    
    export default Ember.Route.extend(AuthenticatedRouteMixin,{
        model(){
          return this.store.findAll('student');
        }
    });
    

    这个路由返回 student 模型的所有信息。你会注意到添加了 AuthenticatedRouteMixin。这告诉 Ember 只有在服务器认证后,这个路由才是可用的。如果没有认证,它将路由回应用。

  11. 将应用程序混合添加到应用程序路由:

    // app/routes/application.js
    import Ember from 'ember';
    
    import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';
    
    export default Ember.Route.extend(ApplicationRouteMixin);
    

    欧空局(ESA)的 ApplicationRouteMixin 将捕获任何错误并过渡到登录路由。

  12. 使用链接到索引模板中的登录路由:

    // app/templates/index.hbs
    Hello! Want to login? Click {{#link-to 'login'}}here!{{/link-to}}
    

    link-to 辅助函数链接到 login 路由。

  13. 启动 Ember 服务器和 OAuth2 服务器:

    $ ember serve –-proxy http://localhost:3000
    
    

    --proxy 参数告诉 Ember 将所有服务器请求代理到 localhost3000 端口。在这个例子中,我们将假设 OAuth2 服务器正在你的本地机器上的 3000 端口上运行。

    成功登录看起来像这样:

    如何做...

    然后,它将重定向到学生路由。这个路由将向服务器发送一个带有正确令牌的授权携带请求。它将接收学生数据,以便将其显示给用户:

    如何做...

    如果未登录就访问此路由,将导致重定向到登录页面。

它是如何工作的...

Ember Simple Auth 插件管理会话、身份验证、授权、持久化和与服务器的通信。它有一个内置的会话服务,这使得管理变得容易。

OAuth2 是在 Web 应用中进行身份验证时的一种流类型的规范。由于 Ember 是单页应用,应用端的安全性不高。它必须依赖于服务器来进行身份验证和管理令牌。ESA 通过处理与服务器发送和通信所需的所有工作来实现这一点。

使用 Liquid Fire 创建过渡

Ember Liquid Fire 是一个处理动画和转换的 Ember 插件。它是一种工具包,将责任分割为模板标题、转换映射和转换。

在这个菜谱中,我们将创建几个转换来查看这个插件是如何工作的。

如何做到这一点...

  1. 在一个新的 Ember 应用程序中,生成以下文件:

    $ ember g route tut1
    $ ember g route tut2
    $ ember g template index
    $ ember install liquid-fire
    
    

    这将为 tut1tut2 路由生成脚手架,并安装 liquid-fire 插件。

  2. app 文件夹的根目录中创建一个新的 transitions.js 文件。添加一些转换:

    // app/transitions.js
    export default function(){
        this.transition(
          this.fromRoute('tut1'),
          this.toRoute('tut2'),
          this.use('toRight'),
          this.reverse('toLeft')
    
        );
        this.transition(
          this.fromRoute('index'),
          this.toRoute('tut1'),
          this.use('crossFade'),
          this.reverse('fade',{duration: 500})
    
        );
    
    }
    

    Liquid Fire 需要一个转换映射文件。有多个预定义的转换可供使用:

    • toLeft

    • toRight

    • toUp

    • toDown

    • crossFade

    • fade

    每一个都表现得像您预期的那样。toLeft 转换将创建一个从左到右移动页面的转换动画。toRight 转换则正好相反。如果需要,您也可以创建自己的动画。

    这个映射告诉我们从一条路由移动到另一条路由时应该使用哪些转换。

  3. 使用 Liquid Fire 输出更新应用程序模板:

    // app/templates/application.hbs
    {{#link-to 'application'}}<h2 id="title">Welcome to Ember</h2>{{/link-to}}
    {{liquid-outlet}}
    

    要使用 Liquid Fire 转换,我们必须使用 liquid-outlet。这用于在路由之间进行转换。以下是所有可用的模板助手:

    • {{#liquid-outlet}}:在路由之间进行转换

    • {{#liquid-with}}:在单个路由中在模型或上下文之间进行转换

    • {{#liquid-bind}}:这更新到简单的绑定值

    • {{#liquid-if}}:在 #if 语句中在 true 和 false 分支之间切换

    • {{#liquid-spacer}}:这提供了一个平滑增长/缩小的容器,当包含的 文档对象模型 (DOM) 发生变化时,它会进行动画处理

  4. 在索引模板文件中,添加一个链接到 tut1 路由:

    // app/templates/index.hbs
    {{#link-to 'tut1'}}First Transition{{/link-to}}<br>
    {{liquid-outlet}}
    

    link-to 助手转换到 tut1 路由。当渲染时,液体输出将显示 tut1 路由。

  5. 更新 tut1tut2 路由模板:

    // app/templates/tut1.hbs
    {{#link-to 'tut2'}}Tutorial 2{{/link-to}}<br>
    <div class="demo">
        <p>
        Lorem ipsum dolor sit amet, consectetur adipiscing elit.In non aliquet quam. Vivamus egestas mi sapien, augue.
        </p>
    </div>
    
    {{liquid-outlet}}
    

    所有这些只是有一个链接到第二个路由 tut2

    // app/templates/tut2
    {{#link-to 'tut1'}}Tutorial 1{{/link-to}}<br>
    
    <div class='demo'>
    <p>
    Quisque molestie libero vel tortor viverra. Quisque eu posuere sem. Aenean ut arcu quam. Morbi orci dui, dictum ut libero in, venenatis tempor nulla. Nullam convallis mauris ante, sed venenatis augue auctor in. Morbi a mi a sapien dictum interdum. Quisque faucibus malesuada risus eget pretium. Ut elementum sapien ut nunc eleifend, at dapibus enim dignissim.
    </p>
    </div>
    {{liquid-outlet}}
    

    这有一个链接回到 tut1

  6. 运行服务器,您将在点击链接时看到转换:如何做到这一点...

    这是在转换过程中使用交叉淡入从应用程序路由到 tut1 路由时的样子。

它是如何工作的...

Liquid Fire 是 Ember 的一个多功能插件,它为 Ember 带来了生命力和动画。它使用简单的转换映射和模板助手来简化操作。在底层,Liquid Fire 使用许多技巧来实现这些动画。它是可扩展的,因此您可以创建自己的转换。

使用 HTML5 拖放

拖放是 HTML5 标准的一部分。它允许用户在 DOM 中抓取对象并将它们拖放到不同的位置。如果浏览器支持,任何元素都可以被拖放。大多数现代浏览器都支持。

在这个菜谱中,我们将看到一个例子,将 IMG 文件拖动到屏幕上的拖放区域。

如何做到这一点...

  1. 在一个新的应用程序中,生成以下文件:

    $ ember g component drag-drop-zone
    $ ember g component drag-drop
    
    

    drag-drop-zone组件将代表每个项目将被放置的区域。drag-drop组件将是将被放置的项目。

  2. 编辑drag-drop-zone.js文件:

    // app/components/drag-drop-zone.js
    import Ember from 'ember';
    
    export default Ember.Component.extend({
        classNames: ['draggable-dropzone'],
        classNameBindings: ['dragClass'],
        dragClass: 'deactivated',
        dragLeave(event) {
          event.preventDefault();
          return this.set('dragClass', 'deactivated');
        },
        dragOver(event) {
          event.preventDefault();
          return this.set('dragClass', 'activated');
        },
        drop(event) {
          var data;
          this.set('dragClass', 'deactivated');
          data = event.dataTransfer.getData('text/data');
          event.target.appendChild(document.getElementById(data));
        }
    
    });
    

    此组件附加了一些特殊事件。Ember 内置了dragLeavedragOverdrop事件。每当项目被拖到组件上时,这些事件都会触发。记住,所有组件都以div标签的形式渲染。我们可以使用classNames属性添加更多类。

    classNameBindings属性允许将类添加到组件中,就像它们是属性一样。换句话说,dragClass可以在组件中动态设置。我们将使用它来改变当项目被拖过时drop区域的颜色。当项目被放置时,将触发drop事件。

  3. 更新拖放组件:

    // app/components/drag-drop.js
    import Ember from 'ember';
    
    export default Ember.Component.extend({
        tagName: 'img',
        classNames: ['draggable-item'],
        attributeBindings: ['draggable','src'],
        draggable: 'true',
        src: 'http://www.programwitherik.com/content/images/2015/02/eriksmall3-1.png ',
        dragStart(event){
          event.dataTransfer.setData('text/data', event.target.id);
        }
    });
    

    如前所述,通常,组件以div标签的形式渲染。然而,我们可以使用tagName属性来改变这一点。在drag-drop组件中,我们正在创建一个image标签。在 Ember 中可用dragStart事件。在这个例子中,我们将数据设置为目标 ID。

    要在 HTML5 中拖动项目,必须在标签上有一个draggable属性。它也必须设置为true。我们将使用attributeBindings来实现这一点。

  4. 更新app.css文件:

    // app/styles/app.css
    .draggable-dropzone {
        border: 1px solid black;
        width: 200px;
        height:200px;
    
    }
    
    .activated {
        border: 4px solid red;
    }
    

    这是一些基本的css,它为drop区域创建了一个边框,并在项目即将被放置时将颜色改为red

  5. 最后一步是将组件添加到应用程序模板文件中:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    
    <br>
    {{drag-drop-zone}}
    
    <br>
    <br>
    <br>
    {{drag-drop}}
    {{outlet}}
    

    这将渲染两个组件到应用程序模板中。

  6. 渲染页面,您将看到图片和dropzone如何操作...

    您可以将图片拖入框中:

    如何操作...

    在项目放置之前,框会变成红色,在放置后恢复为黑色。

它是如何工作的...

HTML5 标准允许拖放元素。Ember 在组件和控制器中提供了几个内置事件,我们可以使用这些事件来捕获事件以允许拖放。dragLeavedragOverdropdragStart方法都可以用来捕获事件。

使用 Ember.js 学习 Bootstrap

Bootstrap,以前称为 Twitter Bootstrap,是一个流行的、免费的、开源的工具集合,用于创建网站和应用。它包含多个模板,用于排版、表单、按钮和导航。

您可以使用 Bootstrap 创建漂亮且简单的用户界面。在这个菜谱中,我们将使用它来创建一个下拉菜单。

如何操作...

  1. 在一个新的 Ember 应用程序中,使用 Bower 安装 Bootstrap 的最新版本:

    $ bower install bootstrap --save-dev 
    
    

    这使用 Bower 的前端包管理器来安装 Bootstrap。它将被保存为bower.json文件中的开发依赖项。

  2. 更新ember-cli-build.js文件并添加 Ember Bootstrap 库:

    // ember-cli-build.js
    /* global require, module */
    var EmberApp = require('ember-cli/lib/broccoli/ember-app');
    
    module.exports = function(defaults) {
        var app = new EmberApp(defaults, {
          // Add options here
        });
    
        app.import('bower_components/bootstrap/dist/js/bootstrap.js');
        app.import('bower_components/bootstrap/dist/css/bootstrap.css');
        return app.toTree();
    };
    

    app.import语句将资产路径作为第一个也是唯一的参数。这对于非 AMD 资产是标准的。一旦加载,我们就可以在应用程序的任何地方使用 Bootstrap。

  3. 将下拉按钮添加到应用程序模板中:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    
    {{outlet}}
    
    <!-- Single button -->
    <div class="btn-group">
    <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
    Action <span class="caret"></span>
    </button>
    <ul class="dropdown-menu">
    <li><a href="#">Action</a></li>
    <li><a href="#">Another action</a></li>
    <li><a href="#">Something else here</a></li>
    <li role="separator" class="divider"></li>
    <li><a href="#">Separated link</a></li>
    </ul>
    </div>
    

    这将添加一个下拉按钮。

  4. 启动服务器,你将看到渲染后的按钮:如何操作...

    点击按钮后,菜单将会显示。

  5. 让我们安装 Bootstrap Ember 附加组件,并在 ember-cli-build.js 文件中取消注释 app.imports

    $ ember install ember-bootstrap
    
    

    Ember Bootstrap 是一个附加组件,它包含了项目中所有的正常 css 和图标资产。它还包括一组本地的 Ember 组件。它不使用 Bootstrap JavaScript 文件:

    // ember-cli-build.js
    …
    //app.import('bower_components/bootstrap/dist/js/bootstrap.js');
    //app.import('bower_components/bootstrap/dist/css/bootstrap.css');
    …
    

    由于我们正在使用附加组件,我们必须在 Bootstrap 文件中取消注释。它们已经被包含在内。

  6. 使用新的 Ember Bootstrap 组件更新应用程序模板文件:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    
    <nav class="navbar navbar-default navbar-static">
        <div class="container-fluid">
          <ul class="nav navbar-nav">
            {{#bs-dropdown tagName="li"}}
            {{#bs-dropdown-toggle}}Example Dropdown <span class="caret"></span>{{/bs-dropdown-toggle}}
              {{#bs-dropdown-menu}}
                <li>{{#link-to "info1"}}Info 1{{/link-to}}</li>
                <li>{{#link-to "info2"}}Info 2{{/link-to}}</li>
              {{/bs-dropdown-menu}}
            {{/bs-dropdown}}
          </ul>
        </div>
    </nav>
    
    {{outlet}}
    

    所有 Ember Bootstrap 组件都以 bs 开头。{{bs-dropdown}} 组件创建一个下拉菜单,显示用户链接。

    使用 Ember Bootstrap 可能会比使用 Bower 安装 Bootstrap 更干净、更简单。

  7. 加载服务器,你将看到以下图片:如何操作...

    这是在使用 Ember Bootstrap 创建菜单。

它是如何工作的...

Bootstrap 是一套多功能的工具集,可以帮助你快速设计前端。Ember 使用名为 Broccoli 的库来接受资产。Broccoli 是一个资产管道——它帮助构建应用程序。app.import 语句用于将 AMD 和非 AMD 资产引入应用程序。

另一方面,Ember Bootstrap 库也可以使用。它内置了易于使用的组件,使得添加按钮和菜单变得简单。

第十章. 使用 Ember 的精彩任务

在这一章中,我们将介绍以下菜谱:

  • 使用 Ember 验证

  • 使用 D3.js 与 Ember.js

  • 使用 Ember 与 Sockets

  • 使用 Ember 与 Firebase

  • 使用服务器端渲染

简介

在这一章中,你将学习从验证表单数据到查看 Ember 的服务器端渲染的每一件事。每个菜谱都将向你展示 Ember 为创建雄心勃勃的应用程序所提供的强大功能和可能性。

使用 Ember 验证

数据表单验证在 Web 开发中是一个非常常见的用例。当你在应用程序中创建验证规则时,最终用户会立即知道他们是否犯了错误。

在这个菜谱中,我们将查看两个验证示例。在第一个示例中,我们将使用一个组件并根据一些简单的验证规则检查数据。在第二个示例中,我们将使用一个流行的 Ember 插件来简化操作。

如何操作...

  1. 在新应用中,创建两个组件并安装验证插件:

    $ ember g component val-example
    $ ember g component val-example2
    $ ember install ember-cp-validations
    
    

    val-example 组件将是第一个示例。val-example2 组件将使用 ember-cp-validations 插件作为第二个示例。

  2. 更新 val-example.hbs 组件模板文件:

    // app/templates/components/val-example.hbs
    Enter Age:<br>
    {{input value=num}}
    <button {{action 'check'}}>Check</button><br>
    {{error}}<br>
    

    此组件要求输入年龄。如果点击按钮,将触发 'check' 动作。使用简单的 input 辅助器来捕获输入。{{error}} 属性显示错误文本。

  3. val-example.js 组件文件中,添加一个 check 动作和简单的 validation 方法:

    // app/components/val-example.js
    import Ember from 'ember';
    
    export default Ember.Component.extend({
        num: null,
        actions:{
          check(){
            if(this.validation(this.get('num'))){
              this.set('error','');
            }
            else{
              this.set('error','Error in box!');
            }
    
            console.log(this.getProperties('num'));
          }
        },
        validation(value){
          return Number(parseInt(value))==value;
        }
    
    });
    

    check 动作调用 validation 方法。validation 方法的唯一目的是如果值是数字则返回 true,如果值不是数字则返回 false。这个结果用于设置模板中将要使用的 error 属性。

    这是一个相当简单的示例。它展示了你可能需要验证字段的内容。另一个示例可能是创建一个新的计算属性,使其依赖于 num 属性。无论哪种方式都行得通。

  4. 将组件添加到 application.hbs 模板文件:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    
    {{outlet}}
    
    {{val-example}}
    

    val-example 组件将在 {{val-example}} 处渲染。

  5. 运行 ember server 并在文本框中输入一个非数字值。以下窗口将显示:如何操作...

    框内错误 消息显示是因为没有输入数字。这是在点击 检查 按钮后触发的。

    对于下一个示例,我们将使用一个 Ember 插件。

  6. 更新 val-example2.hbs 模板文件,使其能够接受电子邮件和数字:

    // app/templates/components/val-example2.hbs
    Enter Age:<br>
    {{input value=num}}<br>
        <div style='color: red'>
          {{message}}<br>
        </div>
    Enter Email:<br>
    {{input value=email}}<br>
        <div style='color: red'>
          {{emailMessage}}<br>
        </div>
    <button {{action 'check'}}>Check</button><br>
    

    第二个组件比第一个复杂一些。我们将验证两个表单字段,一个数字字段和一个电子邮件字段。此外,我们将在一个 div 标签中包围消息,该标签将用于帮助设置文本的 color。在按下 check 按钮后,这两个字段都将被验证。

  7. components 文件夹中创建一个新的 validations.js 文件:

    // app/components/validations.js
    import { validator, buildValidations } from 'ember-cp-validations';
    
    export default buildValidations({
        num: [
        validator('number',{
          allowString: true,
          integer: true,
          message: 'Error! This is not an integer!'
        }),
        validator('presence', true)
        ],
        email: [
          validator('format', {
            type: 'email',
            message: 'This is not an email address!'
          }),
        ],
    });
    

    此文件是 ember-cp-validations 插件所需的。在这个文件中,我们定义我们的验证规则。我们首先命名每个属性并定义所需的验证。我们可以验证许多不同类型的输入。这些验证的列表可以在 offirgolan.github.io/ember-cp-validations/docs/index.html 找到。

    validator 插件附带了一些预构建的消息。我们可以通过设置 message 属性来覆盖这些消息。在上面的代码中,numemail 验证器有自定义消息。num 验证器检查字段中是否有任何值,并且它是否是一个数字。电子邮件字段检查值是否为电子邮件地址的格式。

  8. 向组件添加一个新的 check 动作:

    // app/components/val-example2.js
    Import Ember from 'ember';
    import Validations from './validations';
    
    export default Ember.Component.extend(Validations,{
        num: null,
          email: null,
          message: '',
          emailMessage: '',
          actions: {
            check(){
              this.set('message','');
              this.set('emailMessage','');
              this.validate().then(({model, validations})=>{
    
                if(validations.get('isValid')){
                  this.set('message','');
                  this.set('emailMessage','');
                }
                else{
    
                  if(model.get('validations.attrs.num.isInvalid')){
                    this.set('message',model.get('validations.attrs.num.messages'));
                  }
                  if(model.get('validations.attrs.email.isInvalid')){
                    this.set('emailMessage',model.get('validations.attrs.email.messages'));
                  }
                }
    
              },(errors)=>{
                console.log(errors);
              });
    
            }
          }
    });
    
  9. 在设置好 validations 文件后,你可以将其作为混合添加到组件中。添加 validations 混合后,你将能够访问 validate() 方法。这是一个在验证字段后返回的承诺:

    …
        this.validate().then(({model, validations})=>{
    
          if(validations.get('isValid')){
            this.set('message','');
            this.set('emailMessage','');
          }
    …
    

    validations 有一个 isValid 属性。只有当 numemail 这两个属性都通过了 validation,它才会返回 true

  10. 我们也可以检查每个单独的验证:

    …
     else{
    
        if(model.get('validations.attrs.num.isInvalid')){
          this.set('message',model.get('validations.attrs.num.messages'));
        }
        if(model.get('validations.attrs.email.isInvalid')){
          this.set('emailMessage',model.get('validations.attrs.email.messages'));
    …
    

    我们可以在组件中访问 model 属性。这个 model 将包含 num 和电子邮件 properties。我们可以使用 validations.attrs.num.isInvalid 来检查验证是否失败。如果失败了,我们可以使用 validations.attrs.num.messages 设置我们之前创建的 message

    在上面的代码中,如果验证无效,那么在触发 check 动作后,我们将设置要在模板中显示的错误消息。

    此插件非常灵活。如果需要,我们可以创建自己的自定义验证。

  11. 使用第二个组件更新 application.hbs 文件:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    
    {{outlet}}
    
    {{val-example2}}
    

    这将在我们的应用模板中显示第二个组件示例。

  12. 启动 Ember 服务器,并输入无效的年龄和电子邮件值,然后点击 检查 按钮。以下图像将被显示:如何操作...

    在点击 检查 按钮后,将触发一个动作。我们之前创建的验证器将检查文本并返回文本是否有效。在这个例子中,文本无效,因此显示了一个错误消息。

它是如何工作的...

数据表单验证是任何网络应用的一个极其重要的功能。任何具有任何类型用户表单的 Ember 应用都需要验证数据。Ember.js 可以从模板中检索数据并进行验证。其他属性可以用来切换或设置要显示的错误消息。

Ember CP 验证插件使这个过程更容易。你可以创建自己的验证或使用一些内置的验证。此代码使用计算属性和其他方法来验证并向用户报告问题。

在 Ember 中还有其他几个流行的验证插件可用。查看此网站获取更多信息:emberobserver.com/categories/validations

使用 Ember.js 与 D3.js

D3.js 是一个用于操作文档数据的 JavaScript 库。它可以用来创建形状、动画和强大的可视化。它使用 Web 标准,如 HTML、SVG 和 CSS 来实现其目标。

Ember.js 可以通过使用 Bower 或作为插件来导入 D3。我们将使用 Bower 来尝试它。然而,你也可以安装流行的 ember-cli-d3 包(ember install ember-cli-d3)并获取一些额外的功能。

如何操作...

  1. 在新应用程序中,在 application 文件夹中运行以下命令:

    $ bower install d3 –save
    $ ember g component d3-code
    
    

    bower 命令将安装 D3 并将其保存到 bower.json 文件中。组件最终将包含我们所有的 D3 代码。

  2. 编辑 ember-cli-build.js 文件并添加 d3 文件:

    // ember-cli-build.js
    /*jshint node:true*/
    /* global require, module */
    var EmberApp = require('ember-cli/lib/broccoli/ember-app');
    
    module.exports = function(defaults) {
        var app = new EmberApp(defaults, {
          // Add options here
    });
    
    app.import('bower_components/d3/d3.js');
    return app.toTree();
    };
    

    这个文件是我们添加所有第三方库的地方。要添加 D3,我们必须添加指向 D3 库目录的 app.import 语句。此时,D3 将在应用程序的任何地方可用。

  3. 打开 d3-code.js 模板文件并添加一个 div 标签:

    <div id='holder'></div>
    

    这将是我们在稍后创建的动画的占位符。

  4. 编辑 components 文件夹中的 d3-code.js 文件。添加一个新的圆圈动画:

    // app/components/d3-code.js
    import Ember from 'ember';
    
    export default Ember.Component.extend({
        didInsertElement() {
    
          let svgContainer = d3.select('#holder').append('svg').attr('width',700)
          .attr('height',700);
    
          svgContainer.append('circle').attr('cx',250)
          .attr('cy',250)
          .attr('r', 100)
          .transition()
          .attr('cx',500)
          .attr('cy',450)
          .duration(2000)
          .style('fill','red');
    
        }
    });
    

    这个组件的目的是使用 D3 创建一个新的 svg 标签和一个新的 circle 对象。为了实现这一点,我们希望在组件加载后将其渲染到屏幕上。Ember.js 视图(components)有 didInsertElementwillInsertElementwillDestroyElement 钩子。这些都与组件的应用程序生命周期中的不同点相对应。

    willInsertElement 钩子在视图渲染后但插入到 DOM 之前发生。didInsertElement 钩子在视图被插入到 DOM 之后发生。当处理第三方库如 D3 时,这是最有用的钩子。willDestroyElement 钩子在元素从 DOM 中移除之前被调用。这是一个放置代码的好地方,这些代码可以移除你可能添加的事件处理器。

    svgContainer 变量创建 svg 标签并将其附加到我们之前创建的 div 持有器中。circle 变量附加了将被动画化的 circle 标签。

  5. 将组件添加到应用程序模板中:

    // app/templates/application.hbs
    
    {{d3-code}}
    

    这会将组件添加到应用程序模板中,以便它可以被渲染。

  6. 运行 ember server,你会看到圆圈从屏幕的一侧移动到另一侧:如何操作...

    页面加载后,圆圈将从左上角移动到右下角。这发生在组件完全渲染到屏幕上之后。

它是如何工作的...

D3 使用网络标准生成强大的可视化。Ember 可以使用内置的 Broccoli 库导入任何第三方库。一旦加载了库,就可以在整个应用程序中访问它。我们可以使用视图中的didInsertElement钩子来操作 DOM。D3 将在屏幕上绘制一个圆并对其动画化。

使用 Ember 和 Sockets

WebSocket使与服务器建立通信会话成为可能。Ember.js 本身不处理这个功能。然而,有一些易于使用的扩展包可以用来实现这一点。

如何做...

  1. 在一个新应用程序中,生成一个新的路由并安装 WebSocket 扩展包:

    $ ember install ember-websockets
    $ ember g component w-s
    $ ember g route ws
    
    

    这将安装我们开始所需的组件路由和扩展包。

  2. components模板文件夹中,编辑w-s.hbs文件:

    // app/templates/components/w-s.hbs
    Welcome Chat!<br><br>
    
    Received Message: {{message}}<br>
    <button id="sendButtonPressed" {{action "sendButtonPressed"}}>Press Me</button>
    

    在这个模板中,我们有一个message属性和发送操作的按钮,即sendButtonPressed。当操作被触发时,会向服务器发送一条消息。message属性将显示从服务器返回的任何消息。

  3. 更新w-s.js组件文件,使其能够处理来自模板的操作:

    // app/components/w-s.js
    import Ember from 'ember';
    
    export default Ember.Component.extend({
        websockets: Ember.inject.service(),
        socket: null,
        init() {
          this._super();
          let socket = this.get('websockets').socketFor('ws://localhost:7000/');
          this.set('socket',socket);
          socket.on('open', this.myOpenHandler, this);
          socket.on('message', this.myMessageHandler, this);
          socket.on('close', (event)=> {
            console.log('closed');
          }, this);
        },
        message: '',
    
        myOpenHandler(event) {
          console.log('On open event has been called: ' + event);
        },
    
        myMessageHandler(event) {
          console.log('Message: ' + event.data);
          this.set('message',event.data);
        },
    
        actions: {
          sendButtonPressed() {
            this.get('socket').send('Hello Websocket World');
          }
        }
    });
    

    扩展包在应用程序中安装了一个名为websockets的服务。可以通过使用Ember.inject.service将其注入到组件中来访问它。由于属性名与服务名匹配,我们不需要在注入语句中指定服务名。

  4. 首先,我们将设置init函数:

    …
        init() {
          this._super();
          let socket = this.get('websockets').socketFor('ws://localhost:7000/');
          this.set('socket',socket);
          socket.on('open', this.myOpenHandler, this);
          socket.on('message', this.myMessageHandler, this);
          socket.on('close', (event)=> {
            console.log('closed');
          },this);
        },
    …
    

    组件中的init函数在对象实例化时被调用。你可以通过设置自己的init来覆盖它。为了确保不会发生奇怪的事情,我们必须调用this._super(),这样 Ember 就可以正确设置组件。

    socket.on方法创建了三个事件,分别称为openmessageclose。在实例化过程中,创建了这三个事件处理器。第一个处理器处理与服务器建立连接时发生的事件。

    可以使用socketFor方法设置 WebSocket 服务。这告诉服务服务器所在的位置。

    …
        message: '',
    
        myOpenHandler(event) {
          console.log('On open event has been called: ' + event);
        },
    …
    

    当连接建立时,会在控制台记录一条消息。

  5. 第二个事件是在从服务器接收到消息时触发的:

    …
        myMessageHandler(event) {
          console.log('Message: ' + event.data);
          this.set('message',event.data);
        },
    …
    
  6. 接收消息后,它被设置为message属性。当与服务器断开连接时,触发最后一个事件:

    …
        socket.on('close', (event)=> {
          console.log('closed');
        }, this);
    ..
    

    这将在控制台记录一条消息。

  7. sendButtonPressed操作创建一条消息并将其发送到服务器:

    …
        actions: {
          sendButtonPressed() {
            this.get('socket').send('Hello Websocket World');
          }
        }
    …
    

    操作被触发后,我们使用现有的socket属性向服务器发送消息。

  8. w-s组件添加到ws.hbs路由:

    // app/templates/ws.hbs
    {{w-s}}
    

    组件将在{{w-s}}处渲染。

  9. 加载服务器并导航到/ws路由的w-s。你会看到以下消息:如何做...

    这条消息显示了来自服务器的值。按下按钮会将消息发送到服务器。

    你可以看到我们如何从这个简单的例子中创建一个完整的聊天服务器。

它是如何工作的...

WebSockets 有助于促进服务器和浏览器之间的通信。浏览器可以通过套接字发送和接收数据。这可以与事件驱动的消息或 API 一起使用。

还有更多...

实现 WebSockets 服务器有许多方法。一种很好的方法是使用 ws 库。在以下步骤中,我们将创建一个基本的 WebSockets 服务器,用于本菜谱:

  1. 创建一个新的空目录并运行此命令:

    $ npm init
    
    

    按照提示创建你的项目。

  2. 安装 ws 包:

    $ npm install ws –save
    
    

    这将安装 ws npm 包并将其保存到 package.json 文件中。

  3. 创建一个新的 server.js 文件。创建一个简单的 WebSockets 服务器:

    // server.js
    var WebSocketServer = require('ws').Server;
    var ws = new WebSocketServer({port: 7000});
    
    ws.on('connection', function connection(ws) {
    ws.on('message', function incoming(message) {
        console.log('received: %s', message);
        ws.send('Hey! Welcome to my websocket challenge!');
    });
    
    });
    
  4. 启动节点服务器:

    $ node ./server.js
    
    

    这个服务器在 7000 端口上创建了一个 port。然后它等待连接。如果它收到一条消息,它将输出到控制台并返回一条新消息。这将显示在 Ember 模板文件中并由消息处理程序接收。

使用 Ember 与 Firebase

Firebase 是一个后端即服务提供商。它只需几行代码即可存储数据和认证你的用户。它与许多不同的框架很好地集成,包括 Ember.js。

在这个菜谱中,我们将通过创建一个博客应用来查看 Firebase 的几个功能。这个应用将允许用户创建、编辑或删除帖子。用户可以使用 Twitter 登录并完成认证。

准备工作

在开始之前,我们必须在 Firebase 上创建一个账户,网址为 www.firebase.com。由于 Google 拥有 Firebase,所以这应该非常简单。实际上,你应该可以直接使用你的 Google 凭证登录。

登录后,你需要创建一个新的应用并设置一个新的提供者。按照以下步骤操作:

  1. 在 Firebase 控制台中创建一个新的应用。它应该看起来像以下图片。记下应用 URL以备后用:准备工作

  2. 创建应用后,点击管理,然后点击左侧的登录与认证。点击Twitter并输入你的Twitter API 密钥Twitter API 密钥准备工作

    要获取这些密钥,你需要通过 Twitter 创建一个新的应用。为此,请访问 apps.twitter.com 并点击创建新应用。遵循屏幕上的说明。确保将回调 URL 设置为 https://auth.firebase.com/auth/twitter/callback

    这就足够了。确保你记下 Firebase 创建的 URL 名称。你稍后设置 environment.js 文件中的所有内容时需要它。

如何做到这一点...

  1. 在一个新应用中,生成并安装这些文件:

    $ ember install emberfire
    $ ember install torii
    $ ember install ember-bootstrap
    $ ember install ember-cli-showdown
    $ ember g route new
    $ ember g route posts
    $ ember g route application
    $ ember g controller new
    $ ember g controller posts
    $ ember g model post title:string body:string author:string titleURL:string
    $ ember g template index
    $ ember g util clean
    
    

    生成的文件将是我们的应用程序的骨架。两个 install 命令将使用名为 torii 的附加组件安装 Firebase 认证所需的文件。

  2. 验证应用程序适配器是否已为 Firebase 设置:

    // app/adapters/application.js
    import Ember from 'ember';
    import FirebaseAdapter from 'emberfire/adapters/firebase';
    
    const { inject } = Ember;
    
    export default FirebaseAdapter.extend({
      firebase: inject.service(),
    });
    

    此适配器在我们安装 emberfire 扩展时自动为我们生成。它将 Firebase 服务注入到我们的应用程序和数据存储中。

  3. 通过编辑 environment.js 文件来配置 emberfire 适配器:

    // config/environment.js
    …
        firebase: 'https://testemberfire.firebaseio.com/',
        torii: {
          sessionServiceName: 'session'
        },
    …
    

    要使用 Firebase,您必须将 firebase 属性设置为之前创建的 Firebase URL。确保 torii 属性也已设置,这样我们就可以在我们的应用程序中使用 session 对象。

  4. 在名为 torii-adapters 的新文件夹中添加 application.js 文件:

    // app/tori-adapters/application.js
    import Ember from 'ember';
    import ToriiFirebaseAdapter from 'emberfire/torii-adapters/firebase';
    export default ToriiFirebaseAdapter.extend({
        firebase: Ember.inject.service()
    });
    

    Torii 是 Ember.js 的认证抽象类型。这将使我们在程序中使用会话变量成为可能。

  5. 更新 utils 文件夹中的 clean.js 文件:

    // app/utils/clean.js
    export default function clean(title) {
        title = title.replace(/ /g, '_');
        return title.replace(/[^a-zA-Z0-9-_]/g, '');
    }
    

    此文件仅用于清理 URL 并返回它。换句话说,它移除了除破折号和字符 a-z 之外的所有内容。我们将在稍后为我们的 URL 使用此功能。

  6. 让我们看一下模型文件,确保它看起来 OK

    // app/models/post.js
    import DS from 'ember-data';
    
    export default DS.Model.extend({
        title: DS.attr('string'),
        body: DS.attr('string'),
        author: DS.attr('string'),
        titleURL: DS.attr('string')
    
    });
    

    模型包含我们将用于应用程序中每个帖子的所有数据。这应该是在之前生成的。

  7. 使用 titleURL 作为路径来更新 router.js 文件:

    // app/router.js
    import Ember from 'ember';
    import config from './config/environment';
    
    const Router = Ember.Router.extend({
        location: config.locationType
    });
    
    Router.map(function() {
        this.route('posts', {path: '/:titleURL'}, function() {
    });
        this.route('new');
    });
    
    export default Router;
    

    其中一些是在我们创建帖子和新路由时为我们生成的。然而,我们想确保 titleURL 被设置为每个单独帖子的路径。我们通过传递 :titleURL 动态段到路径来实现这一点。

  8. 将每个单独帖子的查询添加到帖子路由中:

    // app/routes/posts.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        model(param) {
          return this.store.query('post', {
            orderBy: 'titleURL',
          equalTo: param.titleURL });
        }
    });
    

    当用户导航到 /posts URL 时,模型将期望传递一个参数。例如,如果您导航到 /posts/my_post,则 my_post 段将作为参数传递,可以在路由中访问。我们将在 Firebase this.store.query 方法中使用此参数。第一个参数是模型的名称。然后我们可以使用 orderByequalTo 来指定我们正在寻找的确切帖子。

    小贴士

    唯一性

    如您所想象,在创建新帖子时,标题可能唯一,也可能不唯一。this.store.query 方法将所有结果作为数组返回给模型。我们可以在 Firebase 中通过使 titleURL 唯一来强制执行唯一性。另一种可能性是在创建帖子时检查标题的唯一性。无论如何,在这个例子中,我们将假设所有标题 URL 都是唯一的。

  9. 编辑应用程序路由文件并添加模型和几个操作:

    // app/routes/application.js
    import Ember from 'ember';
    
    export default Ember.Route.extend({
        model(){
          return this.store.findAll('post');
        },
        actions:{
          login(){
            this.get('session').open('firebase', { provider: 'twitter'}).then((data)=> {
            });
          },
          logout(){
            this.get('session').close();
          }
        }
    });
    

    我们希望主应用程序能够访问模型路由,以便我们可以使用 findAll 方法检索所有帖子。这基本上与我们在之前的食谱中使用的 Ember Data 方法相同。

    有两个操作,loginlogout。正如我们通过 torii 注入会话到程序中一样,我们可以从任何地方访问它。通过调用 this.get('session'),我们可以 打开关闭 会话。Firebase 有几个内置的认证器,包括 Twitter 和 Facebook。此例中的 login 操作将 打开 一个到 twitter 的窗口,以便用户可以进行认证。

    小贴士

    Firebase 安全性

    对于任何 JavaScript 浏览器应用程序,安全性可能比较复杂。Firebase 让这变得容易一些。Firebase 跟踪已认证的用户。在 Firebase 控制台中,你可以设置规则,使得只有已认证的用户才能接收数据。这超出了本食谱的范围。然而,使用第三方认证器(如 Twitter 或 Facebook)可以与 Firebase 一起确保数据安全。

  10. 打开应用程序模板文件。在顶部添加一个导航栏,并添加登录、登出和添加新帖子的按钮:

    //app/templates/application.hbs
    <nav class="navbar navbar-inverse navbar-fixed-top">
        <div class="container-fluid">
        <div class="navbar-header" href="#">
        {{#link-to 'index' class='navbar-brand'}}My New Blog{{/link-to}}
        </div>
        <ul class="nav navbar-nav">
        {{#if session.isAuthenticated}}
        <li>{{#link-to 'new'}}Add New Post{{/link-to}}</li>
        {{/if}}
        </ul>
        <ul class="nav navbar-nav navbar-right">
        {{#unless session.isAuthenticated}}
        <li><a href="#" {{action 'login' }}>Login</a></li>
        {{else}}
        <li><a href="#" {{action 'logout' }}>Logout</a></li>
        {{/unless}}
        </ul>
        </div>
    </nav>
    <br>
    <br>
    <br>
    {{outlet}}
    

    由于我们已安装了 ember-bootstrap 扩展包,我们可以创建一个非常简单的顶部导航栏。loginlogout 按钮被 if 辅助器包围。在每一个模板中,你都可以访问 session 属性。这个属性有一个名为 isAuthenticated 的方法,如果用户已登录则返回 true,如果用户未登录则返回 false。我们可以使用这个方法仅在用户未登录时显示 login 按钮。如果用户已登录,他们将看到 logout 按钮。

    我们没有应用程序控制器,所以这些动作将冒泡到应用程序路由,在那里它们将被处理。

  11. 现在更新 index.hbs 文件,为每个单独的帖子添加链接:

    // app/templates/index.hbs
    
    <div class = 'row'>
        <div class='col-md-4'>
          <h1>Posts</h1>
            {{#each model as |post|}}
              <br>{{#link-to 'posts' post.titleURL}}{{post.title}}{{/link-to}}
            {{/each}}
        </div>
    
    </div>
    

    model 会遍历每个帖子,并在屏幕上显示 title。每个 titleURL 都作为参数传递给帖子路由。

  12. 在新模板中,添加一些文本框,以便用户可以添加帖子。添加一个预览帖子的部分:

    // app/templates/new.hbs
    <br><br>
    <div class='col-md-4 border' >
        <h1>New Post</h1>
        <form {{action 'save' on="submit"}}>
          <dl>
            <dt>Title:<br> {{textarea value=title cols="40" |rows="1" placeholder='Title'}}</dt>
            <dt>Body:<br> {{textarea value=body cols="40" rows="6" placeholder='Body'}}</dt>
          </dl>
          <button type='submit' class='btn btn-primary'>Add</button>
        </form>
    </div>
    <div class='col-md-4 border'  >
        <h1>Preview</h1>
        <h3>{{title}}</h3>
        <h4>{{markdown-to-html markdown=body}}</h4>
    </div>
    

    新模板将用于创建新帖子。textarea 辅助器创建了两个文本框。表单有一个 save 动作,当表单提交时将被触发。

    在设置项目时,我们安装了一个 markdown 扩展包。这允许我们在帖子的正文中使用 markdown。Markdown 是一种文本到 HTML 的转换工具。它使得在文本中编写 HTML 更加容易。

  13. 在帖子模板中,显示每个帖子,并展示编辑帖子的方式:

    // app/templates/posts.js
    {{#each model as |model|}}
    <div class='row'>
        {{#if isEditing}}
          <div class='col-md-4 border'>
            <form {{action 'save' on='submit'}}>
              <dl>
                <dt>Title:<br> {{textarea value=model.title cols='40' rows='1'}}</dt>
                <dt>Body:<br> {{textarea value=model.body cols='40' rows='6'}}</dt>
              </dl>
          <div class = 'row'>
            <button type='submit' class = 'btn btn-primary'>Done</button>
          </div>
            </form>
          </div>
        {{/if}}
        <div class='col-md-4 border'>
          <h1>{{model.title}}</h1>
          <h3>{{markdown-to-html markdown=model.body}}</h3>
          <h4>-{{model.author}}</h4>
          {{#if session.isAuthenticated}}
            <form {{action 'edit' }}>
            <button type='submit' class='btn btn-primary'>Edit</button>
                <button type='delete' class= 'btn btn-primary' {{action 'delete'}}>Delete</button>
            </form>
          {{/if}}
        </div>
    </div>
    {{/each}}
    

    这会显示每个单独的帖子。如果用户已认证,他们可以删除或编辑帖子。

    再次使用 textarea 模板辅助器来显示文本框。表单有一个附加的 edit 动作,它将 isEditing 属性设置为 true,以便可以编辑帖子。delete 动作会删除帖子。

  14. save 动作添加到新控制器中:

    // app/controllers/new.js
    import Ember from 'ember';
    import cleanURI from '../utils/clean';
    
    export default Ember.Controller.extend({
        actions: {
          save(){
    
            const titleURL= cleanURI(this.get('title'));
            const post = this.store.createRecord('post',{
            title: this.get('title'),
            body: this.get('body'),
            author: 'test',
            titleURL: titleURL
            });
            post.save();
            this.set('title','');
            this.set('body','');
            this.transitionToRoute('index');
          }
        }
    });
    

    save 动作用于将数据保存到 Firebase。首先,它获取帖子的标题,并使用 cleanURI 工具去除所有特殊字符和空格。Firebase 有一个名为 createRecord 的函数,用于创建新记录。然后我们将记录保存到存储中,并将值 set 回默认。最后,应用程序返回到索引页面。

  15. 在帖子控制器中,添加 editdeletesave 动作:

    // app/controllers/posts.js
    import Ember from 'ember';
    import cleanURI from '../utils/clean';
    
    export default Ember.Controller.extend({
        actions: {
          edit(){
            this.set('isEditing', true);
          },
          delete(){
            this.get('model').forEach(model=>{
              model.deleteRecord();
            });
            this.get('model').save();
            this.set('isEditing', false);
            this.transitionToRoute('index');
          },
          save(){
            this.get('model').forEach(model=>{
              const titleURL = cleanURI(model.get('title'));
              model.set('titleURL', titleURL);
              model.save();
            });
            this.set('isEditing',false);
            this.transitionToRoute('index');
          }
        }
    });
    

    让我们更详细地分解一下:

    …
        edit(){
          this.set('isEditing',true);
        },
    …
    

    编辑函数将isEditing属性设置为true。帖子模板使用此属性来显示或隐藏编辑窗口:

    …
        delete(){
          this.get('model').forEach(model=>{
            model.deleteRecord();
          });
          this.get('model').save();
          this.set('isEditing',false);
          this.transitionToRoute('index');
        },
    …
    

    delete操作删除记录。为此,我们必须在我们的model上使用forEach方法。在路由中,我们使用了query方法,它返回一个数组。因此,我们必须遍历返回的每个记录,并删除它。再次假设每个标题都是唯一的,并且只有一个记录。请记住始终.save(),以便在 Firebase 中持久化记录。记录被删除后,我们将过渡到 index 路由:

    …
        save(){
          this.get('model').forEach(model=>{
            const titleURL = cleanURI(model.get('title'));
            model.set('titleURL',titleURL);
            model.save();
          });
          this.set('isEditing',false);
          this.transitionToRoute('index');
        }
    …
    

    save函数获取标题,清理它,设置它,并保存模型。在这个例子中,我们必须使用forEach方法遍历数组。之后,我们将isEditing属性设置回false,并过渡回index

  16. 运行应用程序,将显示以下屏幕:如何操作...

    这显示了屏幕的左上角。由于我们还没有添加帖子,所以没有列出帖子:

    如何操作...

  17. 屏幕右上角将显示登录按钮。我们需要通过按下此按钮来登录。这将弹出一个请求我们 Twitter 账户凭证的窗口。登录后,将显示添加新帖子文本:如何操作...

  18. 点击添加新帖子将显示以下屏幕:如何操作...

  19. 您可以看到预览将 Markdown 转换为 HTML。添加新帖子后,它将被列出:如何操作...

  20. 点击标题将带我们到编辑屏幕。在这里,我们可以编辑删除帖子:如何操作...

  21. 点击编辑按钮将弹出编辑屏幕:如何操作...

    从这里,我们可以进行任何更改并将其保存回来。每次保存时,帖子都会在 Firebase 中持久化。

它是如何工作的...

Firebase 通过emberfiretorii插件与其后端服务通信。EmberFire 是官方的 Ember Data 适配器。它具有许多与其他流行适配器相同的特性。它可以相对容易地保存、删除、编辑和查询数据。其中一个目的是使其能够非常容易地持久化和保存数据,而无需设置自己的后端。

Firebase 还有可以连接的认证提供者。Firebase 处理提供者与应用程序之间的所有认证。这只需要在 Firebase 中设置提供者。

使用服务器端渲染

Ember.js 在浏览器中运行。它使用 JavaScript 来处理所有路由和数据渲染。它仅在初始页面加载时与服务器通信,以检索 JSON 数据。这可能会有些限制。较大的应用程序在较慢的连接上可能加载时间更长,并且仍然存在一些关于搜索引擎优化的担忧。

为了帮助缓解这些担忧,Ember 团队创建了 FastBoot。FastBoot 是一个 Ember CLI 插件,允许 Ember.js 在服务器上渲染和提供应用程序。截至撰写本文时,它仍在开发中,有一些限制。它不建议用于生产,并且不与 jQuery 或 didInsertElement 一起工作。它有望在 Ember v2.4 中准备好用于生产。

尽管如此,它正在改进,并且是 Ember 的重要插件。

如何操作...

  1. 在一个新应用程序中,运行以下命令:

    $ ember install ember-cli-fastboot
    $ rm –rf bower_components
    $ bower install --save ember#canary
    
    

    FastBoot 需要 Ember 的 canary 版本来运行。在安装之前,我们必须删除 bower_components 文件夹。在安装过程中,你可能会收到一条消息,表明 Bower 找不到 Ember 的合适版本。这是正常的;请确保从列表中选择 ember#canary

  2. 为生产构建应用程序:

    $ ember build –prod
    
    

    这将构建生产服务器并压缩所有文件。

  3. 运行 Ember FastBoot 服务器:

    $ ember fastboot --serve-assets --port 4200 --environment production
    
    

    这将运行 FastBoot 服务器。让我们分析这些参数:

    • --serve-assets:这将从 dist 文件夹中提供资源。

    • --port 4200:这指定了端口。默认是 3000。在这种情况下,我们可以使用 4200 来匹配我们通常使用的测试服务器。

    • --environment production:默认是开发环境。使用生产环境。它效果更好,因为 Ember FastBoot 使用压缩文件运行得更快。

  4. 在端口 4200 打开 localhost 以查看网页加载:如何操作...

    页面看起来没有不同。然而,如果你查看浏览器控制台,你会注意到 Ember 应用程序在浏览器中运行时没有下载所有通常需要的 JavaScript。

它是如何工作的...

Ember FastBoot 是一个正在开发中的 Ember 插件。截至撰写本文时,它有很多限制。话虽如此,它最终将允许你在服务器上渲染你的 Ember.js 应用程序,或者至少是应用程序的一部分,用于初始页面加载。这将显著减少页面加载时间。要了解更多关于 Ember FastBoot 的信息,请访问他们的网站 github.com/tildeio/ember-cli-fastboot

第十一章。实时网络应用程序

在本章中,我们将涵盖以下食谱:

  • 使用依赖注入

  • 与应用程序初始化器一起工作

  • 构建聊天应用程序

  • 创建和使用插件

  • 学习 Ember 运行循环

简介

随着你使用 Ember 的技能提升,你将学习如何处理实时应用程序。在这些类型的应用程序中,你将处理来自服务器的更新。你需要能够处理这些事件并在需要时通知用户。

在 Ember 中,我们可以使用诸如依赖注入和 WebSocket 服务之类的功能来处理来自服务器的实时事件。在本章中,我们将探讨这些概念,包括插件。

使用依赖注入

依赖注入模式用于声明和实例化对象的类以及处理它们之间的依赖关系。在 Ember 中,我们可以将对象或服务注入到路由、控制器或组件中。

在这个食谱中,我们将使用依赖注入将记录器对象注入到我们的控制器中。

如何做到这一点...

  1. 在一个新应用程序中安装 moment 库并创建一个新的 initializer

    $ bower install moment –save
    $ ember g initializer application
    
    

    这将安装 Bower Moment.js 库。我们将使用它来创建自定义记录器。

  2. 将 Moment 库导入 Ember 项目:

    // ember-cli-build.js
    /*jshint node:true*/
    /* global require, module */
    var EmberApp = require('ember-cli/lib/broccoli/ember-app');
    
    module.exports = function(defaults) {
        var app = new EmberApp(defaults, {
          // Add options here
        });
    
        app.import('bower_components/moment/min/moment.min.js');
          return app.toTree();
    };
    

    app.import 语句将 moment 库添加到应用程序中。

  3. initializers 文件夹中的 application.js 文件中添加一个新的记录器:

    // app/initializers/application.js
    /* global moment */
    import Ember from 'ember';
    
    export function initialize( application) {
        let MyLogger = Ember.Object.extend({
          log(info){
            let time = moment().format();
            Ember.Logger.debug(`(${time}):`,info);
          }
        });
        application.register('myLogger:zzz', MyLogger);
        application.inject('controller','myLogger','myLogger:zzz');
    }
    
    export default {
        name: 'application',
        initialize
    };
    

    这创建了一个名为 myLogger 的新记录器。它使用之前安装的内置 Ember.Logger 并添加了一个时间戳。

    让我们更详细地看看:

        application.register('myLogger:zzz', MyLogger);
    

    application.register 方法注册了一个新的工厂。第一个参数是注册键。注册键总是由冒号 : 分隔的两个部分组成。第一部分是工厂的类型,第二部分是工厂的名称。工厂的类型可以是模板、componentcontrollerservice,或者你可以创建自己的。在这个例子中,我将其命名为 myLogger。第二个参数是你想要注册的对象,MyLogger

        application.inject('controller','myLogger','myLogger:zzz');
    

    这个应用程序注入使新的 myLogger:zzz 工厂在所有控制器中可用。myLogger 的值来自 myLogger:zzz 工厂。

  4. 创建一个新的应用程序控制器并添加一个新的使用新的 myLogger 调试器的记录动作:

    // app/controllers/application.js
    import Ember from 'ember';
    
    export default Ember.Controller.extend({
        actions: {
          press(){
            this.myLogger.log('Hello World!');
    
        }
    });
    

    当发生新的 press 动作时,它会在控制台记录 Hello World

  5. action 添加到应用程序模板:

    <h2 id="title">Welcome to Ember</h2>
    
    {{outlet}}
    <button {{action 'press'}}>Button</button>
    

    当按钮被点击时,press 动作被触发。

  6. 运行 ember server,你将看到以下屏幕:如何做到这一点...

    按下按钮,你将在控制台看到以下内容:

    如何做到这一点...

    每次点击按钮时,它都会在控制台记录一个调试语句。

它是如何工作的...

依赖注入发生在我们实例化时将对象注入到其他对象中的过程。Ember 可以通过application.inject方法来实现这一点。要在 Ember 中完成此操作,我们必须创建工厂。工厂只是返回其他对象的简单对象。

Ember 将这些工厂注册到Ember.Application中。Ember.Application充当一种注册表,其中包含不同的工厂。注册后,它们可以被注入到 Ember 应用的其它部分,如组件或控制器。

使用应用初始化器

应用初始化器可以在应用启动时配置您的应用。这是设置应用中依赖注入的主要位置。

在这个例子中,我们将检查应用初始化器何时运行。

如何操作...

  1. 在新应用中,创建initializer

    $ ember g initializer application
    
    

    这将创建一个新的应用initializer。它将在应用启动时立即运行。

  2. 在初始化器中添加一个警告框:

    // app/initializers/application.js
    export function initialize( application ) {
        alert('loading application');
    }
    
    export default {
        name: 'application',
        initialize
    };
    

    这将在应用加载时立即加载一个alert框。

  3. 运行ember server,您应该在应用加载前看到一个警告框:如何操作...

    在显示此警告框之前,应用中还没有加载任何其他内容。

  4. 如果需要,我们也可以在初始化器中registerinject服务。它可能看起来如下:

    // app/initializer/application.js
    export function initialize(app) {
        app.inject('component', 'start', 'service:start');
    }
    
    export default {
        name: 'init',
        initialize
    };
    

    这会将名为 start 的服务注入到所有组件中。您可以在第九章 使用 Ember.js 的实战任务 中看到更多此类示例。

    小贴士

    应用实例初始化器

    应用实例初始化器在实例加载时运行。它是随着 Ember 的 FastBoot 添加的,以便更容易地并发运行多个请求。在启动过程中,首先运行应用初始化器,然后是实例初始化器。如果需要,您可以在实例初始化器中查找已在应用初始化器中注册的工厂。

    在大多数情况下,您只会使用实例初始化器进行某些 A/B 测试配置、配置初始状态以及与 Ember FastBoot 服务器一起工作时。要生成实例初始化器,请运行ember g instance-initializer <name>

工作原理...

应用初始化器在应用启动时立即运行。这是配置依赖注入到您应用中的主要位置。尽量保持初始化器尽可能轻量。向初始化器中添加更多复杂性可能会造成应用加载延迟。像异步加载条件这样的东西在服务或路由钩子中运行会更好。

构建聊天应用

在这个菜谱中,我们将结合您从初始化器和依赖注入中学到的知识来创建一个聊天室。聊天室将使用 WebSockets 与主机通信。

如何操作...

  1. 在新应用中,生成以下文件:

    $ ember g service sockjs
    $ ember g component chat-room
    $ ember g initializer application
    $ bower install sockjs --save
    
    

    这些将生成项目所需的文件。chat-room组件将包含我们创建的聊天室的逻辑。

  2. SockJS库导入到应用程序中:

    // ember-cli-build.js
    …
    app.import('bower_components/sockjs/sockjs.min.js');
    …
    

    这将导入库,以便我们可以在应用程序的任何地方使用全局变量sockjs

  3. SockJS创建一个新的服务:

    // app/services/sockjs.js
    /* global SockJS */
    import Ember from 'ember';
    const {run} = Ember;
    
    export default Ember.Service.extend(Ember.Evented,{
        socket: null,
        init() {
          this._super();
          let socket = new SockJS('http://localhost:7000');
          socket.addEventListener('message', run.bind(this, (event)=> {
            this.trigger('messageReceived', event.data);
            console.log(event.data);
          }));
          this.set('socket',socket);
        },
        sendInfo(message) {
          this.get('socket').send(message);
    
        }
    
    });
    

    让我们更详细地看看这个:

    /* global SockJS */
    

    这一行是必需的,这样JSHint就不会对 SockJS 全局变量提出抱怨。JSHint 是 Ember CLI 的内置库,用于检测程序中的错误:

    export default Ember.Service.extend(Ember.Evented,{
    

    这将Ember.Evented混入添加到服务中。这个混入允许 Ember 对象订阅和发射事件。这对于我们在本例中需要做的事情来说非常合适:

    init() {
        this._super(…arguments);
    },
    

    init方法是在 SockJS 套接字设置和事件监听器创建的地方。这个方法将在服务初始化后触发。this._super方法确保init方法被正确设置:

    let socket = new SockJS('http://localhost:7000');
    

    上一行在本地主机的端口7000创建了一个新的套接字服务器:

    socket.addEventListener('message', run.bind(this, (event)=> {
        this.trigger('messageReceived', event.data);
        console.log(event.data);
    }));
    this.set('socket',socket);
    

    这创建了一个在收到消息时触发的事件监听器。run.bind方法是 Ember run循环的一部分,我们将在本章后面描述它。这确保了所有请求都在run循环中得到妥善处理。

    this.triggerEvent.Evented类的一部分。trigger方法创建一个名为messageReceived的新事件。我们可以订阅这个事件,以便在收到消息时触发 Ember 中的其他方法。最后,我们将event.data中的信息log到控制台,并设置socket属性:

        sendInfo(message) {
          this.get('socket').send(message);
        }
    

    这个方法接受message并将其发送到我们之前定义的socket服务器。在这里访问socket属性。

  4. 将新服务注入到应用程序的所有组件中:

    // app/initializers/application.js
    export function initialize( application ) {
        application.inject('component', 'sockjs', 'service:sockjs');
    }
    
    export default {
        name: 'websockets',
        initialize
    };
    

    初始化器接受名为sockjs的服务并将其注入到所有组件中。这将在程序首次启动时运行。我们使用这个方法,这样我们就不必将sockjs服务特别注入到每个组件中。

  5. 为聊天室创建一个新的组件:

    // app/components/chat-room.js
    import Ember from 'ember';
    const {$} = Ember;
    
    export default Ember.Component.extend({
        message: '',
    
        init() {
          this._super(…arguments);
          this.sockjs.on('messageReceived',this, 'messageReceived');
        },
    
        messageReceived(message){
          $('#chat-content').val((i, text)=>
          `${text}${message}\n`;
          );
          this.set('message',message);
        },
        actions: {
          enter(info,username) {
            this.sockjs.sendInfo(`${username}: ${info}`);
    
          }
        }
    
    });
    

    让我们将这个操作分解成更小的部分:

        init() {
          this._super(…arguments);
          this.sockjs.on('messageReceived',this, 'messageReceived');
        },
    

    这个init方法在初始化时触发,并设置组件。然后我们可以使用on订阅在服务中之前创建的事件。第一个参数是事件的名称。第二个是绑定。最后一个是指回调函数的名称。因此,在本例中,每当服务中收到消息时,这个组件中的messageReceived回调将被触发:

        messageReceived(message){
          $('#chat-content').val((i, text)=>
          `${text}${message}\n`
          );
          this.set('message',message);
    

    这是messageReceived回调。它使用一点 jQuery 来查找chat-content ID,并使用 ES6 字符串插值将现有消息连接到它。此外,设置message属性:

        actions: {
          enter(info,username) {
            this.sockjs.sendInfo(`${username}: ${info}`);
    
          }
        }
    

    这个操作将infousername发送到套接字。这样,任何连接的其他客户端都会收到通知。

  6. 为组件创建chat-room.hbs模板文件:

    // app/templates/components/chat-room.hbs
    
        <textarea id="chat-content" style="width:500px;height:300px" ></textarea><br/>
        {{input type='text' placeholder='User Name' value=uname}}
        {{input type='text' placeholder='Chat Message' value=mess}}
        <button {{action 'enter' mess uname}}>Send</button><br>
    
    Message received:{{message}}
    

    这段代码显示来自服务器的消息。input 辅助器捕获用户名和消息。当点击“发送”按钮时,每个值都会传递给 enter 动作。

  7. 将组件添加到 application.hbs 文件中:

    // app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    
    {{outlet}}
    {{chat-room}}
    

    这将组件添加到应用程序中。

  8. 启动节点服务器。然后启动 Ember 应用程序。你会看到以下屏幕:如何操作...

    随着每个客户端的连接,他们可以向服务器发送消息。每个客户端将接收到这些消息并在聊天框中显示它们。

它是如何工作的...

聊天室由多个客户端与服务器进行通信组成。服务器的任务是当收到消息时通知所有其他已连接的客户端。在这个例子中,使用 SockJS 和 WebSocket 来实现。SockJS 库有消息事件,我们可以在 Ember 中设置这些事件。当收到消息时,它会被发送到一个组件,该组件会更新其模板以显示消息。

还有更多...

要使用前面的示例,你需要设置一个 WebSocket 服务器。以下是创建简单的 Node.js SockJS 服务器步骤。要了解更多关于 SockJS 的信息,请查看他们的 GitHub 页面 github.com/sockjs/sockjs-node

  1. 在一个新目录中,运行 npm init 命令:

    $ npm init
    $ npm install sockjs –save
    
    

    这将生成 package.json 文件并在其中安装 SockJS 服务器。

  2. 为 WebSocket 服务器创建一个新的 app.js 文件:

    // app.js
    var http = require('http');
    var sockjs = require('sockjs');
    
    var clients = {};
    
    function broadcast(message){
        for (var client in clients){
          clients[client].write(message);
        }
    }
    
    var socketServer = sockjs.createServer({ sockjs_url: 'http://cdn.jsdelivr.net/sockjs/1.0.1/sockjs.min.js' });
    socketServer.on('connection', (conn)=> {
        clients[conn.id] = conn;
    
        conn.on('data', (message)=> {
          console.log('received ' + message);
          broadcast(message);
        });
        conn.write("hello from the server thanks for connecting!");
        conn.on('close', ()=> {
          delete clients[conn.id];
        });
        console.log("connected");
    });
    
    var server = http.createServer();
    socketServer.installHandlers(server);
    server.listen(7000, '0.0.0.0');
    

    这个服务器使用 SockJS 库创建一个新的 socket 服务器。当一个新的客户端连接时,它会被添加到一个数组中。当它接收到数据时,它会使用这个函数将数据广播到所有通过此连接的其他服务器:

    function broadcast(message){
        for (var client in clients){
          clients[client].write(message);
        }
    }
    

    这个函数向所有连接的其他客户端发送一个包含刚刚接收到的 messagebroadcast message。当 Ember 收到这个信息时,它会被写入聊天框。

创建和使用插件

Ember 使用一种称为 Ember Addons(也称为插件)的方式来共享代码。Ember 插件使得与其他应用程序一起分发可重用库变得容易。任何人都可以创建插件。你可以将它们发布到 NPM 或你自己的私有 Git 仓库。

请记住,你还可以使用 Bower 来安装前端依赖项。这是通过 Bower 软件包管理器完成的。查看 第一章,Ember CLI 基础,了解更多关于如何操作的信息。

在这个菜谱中,我们将从上一节中的聊天程序创建一个插件。

如何操作...

  1. 创建一个名为 sockjs-chat 的新插件。生成以下文件:

    $ ember addon sockjs-chat
    $ cd sockjs-chat
    $ ember g component sockjs-chat
    $ ember g service sockjs
    $ ember g blueprint sockjs-chat
    $ npm install ember-cli-htmlbars --save
    
    

    ember addon 命令为插件生成文件夹结构。我们将在稍后更详细地讨论文件夹结构。blueprint 命令创建一个新的蓝图,称为 sockjs-chat。蓝图用于生成代码片段。这是安装 SockJS 库所需的。如果我们做任何与模板相关的事情,我们需要添加 ember-cli-htmlbars

  2. 创建 sockjs-chat 蓝图,以便安装 SockJS 库:

    // blueprints/sockjs-chat/index.js
    /*jshint node:true*/
    module.exports = {
        normalizeEntityName() {
    },
    
        afterInstall() {
          return this.addBowerPackageToProject('sockjs-client', '~1.0.3');
        }
    };
    

    afterInstall 钩子用于添加 Bower 包。默认情况下,蓝图文件将在插件安装期间运行。这保证了 sockjs-client 库通过 Bower 包管理器安装。

  3. 更新根 index.js 文件,以便导入 SockJS 库:

    // index.js
    /* jshint node: true */
    'use strict';
    
    module.exports = {
        name: 'sockjs-chat',
          included(app) {
            this._super.included(app);
            app.import(app.bowerDirectory + '/sockjs-client/dist/sockjs.min.js');
          }
    };
    

    JavaScript SockJS 库在蓝图中被安装。然而,我们仍然需要将其导入 Ember。这可以在根文件夹的 index.js 文件中完成。此文件是应用程序的入口点。included 钩子用于将 Bower 组件导入应用程序。导入按它们出现的顺序添加到应用程序中。

  4. 使用正确的项目信息设置 package.json 文件:

    // package.json
    {
        "name": "sockjs-chat",
        "version": "1.0.0",
        "description": "EmberJS Sockjs Chat Addon",
    …
    "repository": "https://github.com/ErikCH/sockjs-chat",
    …
    "author": "Erik Hanchett",
    …
        "keywords": [
          "ember-addon",
          "sockjs",
          "ember websockets"
    …
    

    确保你的 package.json 文件至少更新了 namedescriptionrepositoryauthorkeywords。如果你计划开源你的插件并将其发布到 NPM,这非常重要。没有这些信息,你的插件将很难找到。

  5. 在生成的服务文件中,添加一个新的 setupsend 方法:

    // addon/services/sockjs.js
    /* global SockJS */
    import Ember from 'ember';
    var {run} = Ember;
    
    export default Ember.Service.extend(Ember.Evented,{
        socket: null,
        setupSockjs(url) {
          let socket = new SockJS(url);
          socket.addEventListener('message', run.bind(this, (event)=> {
            this.trigger('messageReceived', event.data);
            console.log(event.data);
          }));
          this.set('socket',socket);
        },
        sendInfo(message) {
          let socket= this.get('socket');
          if(socket != null){
            socket.send(message);
          }
        }
    
    });
    

    这看起来可能很熟悉。这几乎是我们上一次配方中创建的相同服务。然而,这次我们有一个新的 setupSockjs 方法,它接受 url 作为参数。url 参数用于设置新的套接字监听器:

          socket.addEventListener('message', run.bind(this,(event)=> {
            this.trigger('messageReceived', event.data);
            console.log(event.data);
          }));
    

    当接收到新的 message 时,会触发此 event。在新的 message 到达后,将调用一个新的触发器 messageReceived

        sendInfo(message) {
          let socket= this.get('socket');
          if(socket != null){
            socket.send(message);
          }
    

    只要 socket 不是 nullmessage 就会被发送到 WebSocket 服务器。

  6. 设置 sockjs-chat.js 组件:

    // addon/components/sockjs-chat.js
    import Ember from 'ember';
    import layout from '../templates/components/sockjs-chat';
    const {typeOf} = Ember;
    export default Ember.Component.extend({
    
        sockjs: Ember.inject.service('sockjs'),
        layout,
        message:'',
    
        init() {
          this._super(...arguments);
          this.get('sockjs').setupSockjs(this.attrs.url);
          this.get('sockjs').on('messageReceived',this,(message)=>{
            this.set('message',message);
            this._actionHandler('receiveAction',message);
          });
        },
        _actionHandler(actionName, ...args) {
    
          if(this.attrs && typeOf(this.attrs[actionName]) === 'function'){
            this.attrsactionName;
          } else {
            this.sendAction(actionName,...args);
    
          },
    
          actions: {
            enter(info,username) {
              this._actionHandler('sendAction',info,username);
    
            }
          }
    
    });
    

    组件的目的是让某人能够轻松地将聊天功能添加到他们的应用程序中,而无需了解我们之前创建的服务内部结构。要使用此组件,模板必须是块或非块形式,并具有以下属性:

    {{sockjs-chat
    url='http://localhost:7000'
    receiveAction=(action 'receiveMessage')
    sendAction=(action 'sendMessage') }}
    

    url 属性是 WebSocket 的位置。receiveAction 方法是父组件的 action 名称。每当接收到消息时,它都会被触发。sendAction 方法是父组件用于发送消息的 action 名称。

    让我们更详细地看看这个组件:

        layout,
        message:'',
        init() {
          this._super(...arguments);
          this.get('sockjs').setupSockjs(this.attrs.url);
          this.get('sockjs').on('messageReceived',this,(message)=>{
            this.set('message',message);
            this._actionHandler('receiveAction',message);
          });
        },
    

    layout 属性与 layout: layout 相同。这是 ES6 的一部分。init 钩子在组件初始化时运行。每次你 extend 内置方法时,总是运行 this._super 的好主意。这确保了组件被正确设置。…arguments 数组是新的 ES6 语法的一部分。它被称为 Rest 参数,表示数组中的不定数量的参数。我们将在本组件中多次使用它。

    super 运行后,我们将 url 属性传递给我们的服务中的 setupSockjs 方法。this.attrs.url 获取传递给组件的 url 属性。

    由于我们使用 Ember.Event 混合,我们可以订阅服务并监视 messageReceived 触发器。当 messageReceived 触发时,我们将内部消息 this.message 属性设置为接收到的消息。然后我们将消息传递给一个新的方法 _actionHandler

        _actionHandler(actionName, ...args) {
    
          if(this.attrs && typeOf(this.attrs[actionName]) === 'function'){
            this.attrsactionName;
          } else {
            this.sendAction(actionName,...args);
          }
        },
    

    actionHandler 的目的是接收由 receiveActionsendAction 属性传递的 action 并调用它。然而,我们需要确保我们可以处理通过闭包动作传递的动作,如 第六章 所述,Ember 组件,或者只是一个命名的动作。如果是一个闭包动作,例如 (action 'receiveMessage'),那么我们只需使用 this.attrsactionname 来调用它。如果不是,那么我们使用 sendAction,这将动作发送到父组件:

        actions: {
          enter(info,username) {
            this._actionHandler('sendAction',info,username);
    
          }
        }
    

    enter 动作调用动作处理程序并传递 infousername。由于我们在 _actionHandler 中使用 Rest 参数 (…arguments),我们可以传递所需数量的参数。

  7. 更新 sockjs-chat.hbs 组件模板:

    // addon/templates/components/sockjs-chat.hbs
    {{#if hasBlock}}
        {{yield this}}
    {{else}}
    
        <textarea id="chat-content" style="width:500px;height:300px" ></textarea><br/>
        {{input type='text' placeholder='User Name' value=uname}}
        {{input type='text' placeholder='Chat Message' value=mess}}
        <button {{action 'enter' mess uname}}>Send</button><br>
    
    {{/if}}
    

    这在使用此插件时给用户提供了几个选择。他们可以使用块形式使用组件,这将类似于我们在上一章中创建的服务,或者他们可以设计自己的。如果用户以块形式添加组件,hasBlock 辅助函数将返回 true。如果组件不是以块形式添加的,那么它将显示正常的聊天窗口。

    在此模板中,一个重要的方面是 {{yield this}}。当处于块形式时,这将使块能够访问组件本身。在测试插件时,我们将查看这一点。

测试 sockjs-chat 插件

插件中的 /tests 文件夹是所有测试用例所在的地方。这与其他任何 Ember 应用程序非常相似。然而,插件还包括测试文件夹中的 dummy 文件夹。这个文件夹通常是插件制作者创建测试应用程序的地方。这个文件夹中的程序将能够访问插件,尽管你可能需要手动安装任何 Bower 依赖项。

  1. 运行此命令在 add-on 文件夹中安装 sockjs-client 以进行测试:

    $ bower install sockjs-client –-save-dev
    
    

    这将在 bower.jsondevDependencies 部分安装 sockjs-clientbower.json 文件仅用于 /tests/dummy/ 文件夹中的应用程序。

  2. 使用 SockJS bower_component 更新 ember-cli-build.js 文件:

    // ember-cli-build.js
    /*jshint node:true*/
    /* global require, module */
    var EmberAddon = require('ember-cli/lib/broccoli/ember-addon');
    
    module.exports = function(defaults) {
        var app = new EmberAddon(defaults, {
          // Add options here
        });
    
    /*
    This build file specifes the options for the dummy test app of this
    addon, located in `/tests/dummy`
    This build file does *not* influence how the addon or the app using it
    behave. You most likely want to be modifying `./index.js` or app's build file
    */
    
        app.import('bower_components/sockjs-client/dist/sockjs-0.3.4.js');
        return app.toTree();
    };
    

    这将把 sockjs-client 库添加到我们的 /tests/dummy 应用程序中。

  3. /tests/dummy 文件夹中,以非块形式添加插件中的组件:

    // tests/dummy/app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    
    {{sockjs-chat
    url='http://localhost:7000'
    receiveAction=(action 'receiveMessage')
    sendAction=(action 'sendMessage') }}
    

    这将添加我们的新组件附加组件到应用程序中。url 属性将被传递到服务中,以便它可以连接到端口 7000 的 WebSocket 服务器。receiveActionsendAction 属性指向闭包操作。这将在我们收到消息或想要发送消息时触发。

  4. 在应用程序控制器中定义发送和接收操作:

    // tests/dummy/app/controllers/application.js
    
    import Ember from 'ember';
    const {$} = Ember;
    export default Ember.Controller.extend({
        sockjs: Ember.inject.service('sockjs'),
        actions:{
          receiveMessage(message){
            $('#chat-content').val((i, text)=>
            `${text}${message}\n`
            );
            this.set('message',message);
    
          },
          sendMessage(message, username){
            console.log(username);
            console.log(message);
            var send = this.get('sockjs');
            send.sendInfo(`${username}: ${message}`);
    
           }
        }
    });
    

    这些 actions 处理消息的发送和接收。receive 方法使用一点 jQuery 将最新消息附加到聊天窗口。send 方法使用附加组件中的服务来发送消息。

  5. 运行 ember server 命令并测试附加组件:

    $ ember server
    
    

    您可以直接在 add-on 文件夹中运行服务器命令。这将提供 /tests/dummy/ 文件夹中的文件。请确保同时启动 WebSockets 服务器。查看如何在 Node.js 中创建 WebSocket 服务器的最后一个配方。

  6. 打开一个网页浏览器并输入一条消息:测试 sockjs-chat 附加组件

    这个聊天框是从附加组件中的模板生成的。在这里输入的消息将通过控制器中创建的操作发送。

  7. 使用块形式的组件并创建自己的聊天框:

    // tests/dummy/app/templates/application.hbs
    <h2 id="title">Welcome to Ember</h2>
    
    {{outlet}}
    
    <h2>Alternative</h2>
    {{#sockjs-chat
    url='http://localhost:7000'
    receiveAction=(action 'receiveMessage')
    sendAction=(action 'sendMessage') as |sockjs|}}
    
        <textarea id="chat-content" style="width:300px;height:300px"></textarea><br/>
        {{input type='text' placeholder='User Name' value=uname}}
        {{input type='text' placeholder='Chat Message' value=mess}}
        <button {{action 'enter' mess uname target=sockjs}}>Send</button><br>
        {{sockjs.message}}
    
    {{/sockjs-chat}}
    

    这个模板使用块形式的附加组件。这次,我们创建了一个更小的聊天室,而不是使用附加组件默认创建的聊天室:

    {{#sockjs-chat
    url='http://localhost:7000'
    receiveAction=(action 'receiveMessage')
    sendAction=(action 'sendMessage') as |sockjs|}}
    

    当一个组件以 hash # 开头时,它被认为是块形式。要访问组件本身,我们在末尾添加 |sockjs|。现在 sockjs 可以访问组件中的所有属性:

    <button {{action 'enter' mess uname target=sockjs}}>Send</button><br>
    

    由于我们可以在块中访问组件,我们可以将此 actiontarget 设置为 sockjs。我们还可以在任何需要的地方显示消息:

        {{sockjs.message}}
    

    这将在组件中显示 message 属性。

  8. add-on 文件夹中再次运行 ember server 并打开一个网页浏览器。输入一条消息:测试 sockjs-chat 附加组件

    如您所见,这个新的聊天窗口看起来略有不同。然而,它的行为与之前相同,并且使用的是相同的附加组件。

  9. 创建一个新的应用程序并将其链接到附加组件以进行测试:

    $ cd sockjs-chat
    $ npm link
    $ cd ..
    $ ember new chat 
    $ cd chat
    $ npm link sockjs-chat
    
    

    我们首先做的事情是导航到包含我们的新附加组件的 sockjs-chat 文件夹。然后我们运行 npm link 命令。这将在本地 NPM 缓存和附加组件项目之间生成符号链接。要访问附加组件,我们必须然后在我们的新应用程序中运行 npm link sockjs-chat。这创建了一个指向附加组件的链接。

  10. 将附加组件添加到聊天测试应用程序的 package.json 文件中:

    // chat/package.json
    …
        "devDependencies": {
        "sockjs-chat": "*"
    …
    

    这是将附加组件链接到测试时的最后一步之一。Ember 必须在 devDependencies 中有这段代码,才能看到附加组件。

  11. 运行安装并添加蓝图:

    $ npm install
    $ ember g sockjs-chat
    
    

    在更新 package.json 文件后,我们必须使用 npm install 安装新包。最后,运行 ember g sockjs-chat 将运行默认蓝图,该蓝图将在应用程序中安装 sockjs-client。蓝图在安装新的 Ember 插件时自动运行。但是,如果我们使用 npm link 技术,我们必须手动运行它。

    我们现在可以在应用程序中使用此插件。请注意,我们需要实现与在模拟应用程序中相同的控制器,以便使此插件正常工作。

发布 sockjs-chat 插件

有两种方式可以发布新的插件。我们可以使用 NPM 或 Git。

  1. 将您的插件发布到私有 Git 仓库:

    $ cd sockjs-chat
    $ git add .
    $ git commit –m "first commit"
    $ git remote add origin git@yourserver:username/sockjs-chat.git
    $ git push origin master
    
    

    要私有发布,您需要设置一个私有 Git 仓库。然后将插件推送到此仓库。在此情况下,将 yourserver:username 替换为您的私有 Git 仓库的 serverusername

  2. 在新应用程序中从 Git 仓库安装插件:

    $ cd my-app
    $ ember install git+ssh://git@yourserver:username/sockjs-chat.git 
    
    

    这将在应用程序中安装插件。请确保存储库的名称与插件的名称匹配,否则您将收到一条消息,表明找不到插件。

  3. 将您的插件发布到 NPM:

    $ cd sockjs-chat
    $ npm adduser
    $ npm publish
    
    

    这将使您成为 npm.org 网站的新用户。只要 package.json 文件设置正确,您就可以发布 npm。稍后,如果需要,您可以使用 npm 版本来提升插件版本。

  4. 在新应用程序中安装您的插件:

    $ cd my-app2
    $ ember install sockjs-chat
    
    

    这将在 my-app2 应用程序中从 npm 安装 sockjs-chat 应用程序。

它是如何工作的...

Ember 使用插件系统在应用程序之间共享代码。每个插件都有自己的包,可以添加到任何应用程序中。与 Bower 包管理器不同,这些库可能更复杂,并且可以封装 Ember 代码。

Ember 插件可以通过 NPM 或私有 Git 服务器访问。这可以用于在应用程序之间共享信息。

相关内容

使用 Ember 插件可以真正加快开发过程。有数千个插件可供选择。查看以下两个网站:

这两个网站都列出了插件并对它们进行排名。请将它们用于您的应用程序。您不会后悔的。

学习 Ember run 循环

Ember 的 run 循环是 Ember 内部极其重要的部分。run 循环用于批量、排序并以最有效的方式在 Ember 应用程序中工作。在本食谱中,我们将创建一个简单的计时器,并查看 run 循环是如何工作的。

准备工作

在我们继续到我们的食谱之前,我们需要了解 Ember run 循环的一些基础知识。

理解 Ember run 队列

Ember 的 run 循环由以下六个队列组成:

  • sync: 此队列包含绑定同步任务。

  • actions: 此队列包含一般工作和承诺。

  • routerTransitions: 此队列包含路由器中的转换任务。

  • render:这个队列包含旨在渲染的任务,通常用于更新 DOM。

  • afterRender:这个队列在所有之前安排的渲染任务完成后运行。这个队列通常用于第三方应用程序。

  • destroy:这个最后的队列负责拆解对象。

这些队列不是一直运行的。它们只在响应某些用户和定时器事件时运行。这样,责任就交回到了用户手中;否则,浏览器会挂起。

你应该在何时更改 Ember 的run循环?

你应该在特定情况下更改 Ember 的run循环:

  • 异步回调

  • 处理定时器

  • Ajax 回调

  • 某些类型的测试

  • WebSockets

  • PostMessagemessageChannel事件处理器

大多数时候,Ember 的run循环会处理一切,你不需要去触碰它。

与 Ember 的run命名空间一起工作

Ember 的run命名空间为我们提供了在处理run循环时可以使用的方法。最流行的方法如下:

  • Ember.run.bind:这对于在第三方库中使用非常棒,并且将函数的执行添加到 Ember 的run循环中

  • Ember.run.later:这个方法在指定的时间后运行传递的目标/方法

  • Ember.run.schedule:这个方法在循环结束时运行传递的目标/方法以及可选参数到命名队列

还有更多方法可用,但当你需要操作run循环时,这些是你最常用的方法。

如何实现...

  1. 在一个新应用程序中,生成一个新的time-checker组件:

    $ ember g component time-checker
    $ bower install moment –save
    
    

    这将生成一个名为time-checker的新组件。此外,我们还将使用 Moment 库来跟踪我们的计时器。

  2. 将 Moment 库导入到应用程序中:

    // ember-cli-build.js
    /*jshint node:true*/
    /* global require, module */
    var EmberApp = require('ember-cli/lib/broccoli/ember-app');
    
    module.exports = function(defaults) {
        var app = new EmberApp(defaults, {
          // Add options here
        });
    
        app.import('bower_components/moment/min/moment.min.js');
    
        return app.toTree();
    };
    

    这将把 Moment 库添加到应用程序中。

  3. 更新新的time-checker.js组件文件。向其中添加两个属性——一个用于显示组件开始的时间,另一个用于显示当前时间:

    // app/components/time-checker.js
    /* global moment */
    import Ember from 'ember';
    
    export default Ember.Component.extend({
        startTime: null,
        currentTime:null,
        init(){
          this._super(...arguments);
          this.set('startTime',moment());
          this.startWatchingTime();
    
        },
        startWatchingTime(){
          this.set('currentTime', moment());
          Ember.run.later(()=>{
            this.startWatchingTime();
          }, 1000);
        },
        diff: Ember.computed('startTime','currentTime', function(){
          return this.get('currentTime').diff(this.get('startTime'),'seconds');
    
        })
    });
    

    这个组件的目的是显示组件加载的时间和当前时间。它还显示了这两个时间之间的差异。让我们更详细地看看:

        init(){
          this._super(...arguments);
          this.set('startTime',moment());
          this.startWatchingTime();
    
        },
          startWatchingTime(){
            this.set('currentTime', moment());
            Ember.run.later(()=>{
              this.startWatchingTime();
            }, 1000);
    

    init函数在组件实例化后立即运行。它设置当前时间并调用startWatchingTime方法。这个方法使用Ember.run.later等待一秒后再继续。每秒,它会调用自己,然后再次计算新的日期和时间。与setTimeout相比,使用这个方法更好,因为它可能会在 Ember 的run循环中引起问题:

        diff: Ember.computed('startTime','currentTime', function(){
          return  this.get('currentTime').diff(this.get('startTime'),'seconds');
    
        })
    

    diff计算属性在currentTime变化时更新。它返回两个时间之间的秒数差异。

  4. 更新time-checker.hbs组件文件的模板:

    // app/templates/components/time-checker.hbs
    Startup time: {{startTime}}<br>
    Current time: {{currentTime}}<br>
    Difference: {{diff}}
    

    这将显示开始时间、当前时间和时间差。

  5. 更新应用程序模板并添加组件:

    <h2 id="title">Welcome to Ember</h2>
    
    {{time-checker}}
    

    这将把time-checker组件添加到应用程序中。

  6. 运行应用程序,你会看到两个时间。第二个时间将每秒更新一次:如何实现...

    Ember 的 run 循环使得这成为可能。

它是如何工作的...

Ember 的 run 循环帮助在 Ember 应用程序中安排工作。它会在有用户操作或特定的时间事件发生时运行。它由六个不同的队列组成。每个队列负责应用中的不同功能。

在大多数情况下,你不需要担心 run 循环。然而,在某些情况下,例如处理异步回调或时间事件时,你需要使用它们。在未来的编程过程中请记住这一点。

posted @ 2025-09-26 22:10  绝不原创的飞龙  阅读(4)  评论(0)    收藏  举报