我们将继续从Ember.js的官方指南中发布该教程的翻译。 本教程包括两部分,这是本教程第一部分的后半部分。 我们提醒您,您可以在此链接阅读前半部分
本教程涵盖的主题列表建议:
- 使用Ember CLI
- Ember应用程序文件和文件夹导航
- 在页面之间创建和链接
- 模板和组件
- 自动化测试
- 处理服务器数据
- 路线中的动态路段
- 灰烬中的服务
- 灰烬数据库
- 适配器和序列化器
- 提供者组件模式
坐下,打开终端,在计算机上找到项目,然后继续。 请记住,如果遇到任何困难,可以随时在Discord社区频道 (在俄语频道#lang-russian )以及俄语电报频道ember_js中寻求帮助。
组件详细信息
现在是时候终于在租赁清单上工作了:

通过编译此出租物业列表,您将了解:
- 组件生成
- 使用名称空间组件组织代码
- 使用
...attributes
转发HTML ...attributes
- 确定适当的测试范围
组件生成
让我们从创建<Rental>
组件开始。 这次,我们将使用组件生成器为我们创建模板和测试文件:
$ ember generate component rental installing component create app/components/rental.hbs skip app/components/rental.js tip to add a class, run `ember generate component-class rental` installing component-test create tests/integration/components/rental-test.js
生成器为我们创建了两个新文件: app/components/rental.hbs
的组件模板和tests/integration/components/rental-test.js
的组件测试文件。
我们将从编辑模板开始。 让我们对一个租用对象的详细信息进行硬编码 ,然后将其替换为服务器中的真实数据。

然后,我们将编写测试以确保所有详细信息都存在。 我们将替换我们自己的团队为我们生成的模板测试,就像我们之前对<Jumbo>
组件所做的那样:

测试必须通过。

最后,让我们向索引模板添加一个新组件来填充页面。

在这种情况下,我们应该在页面上看到<Rental>
组件三次显示我们的Grand Old Mansion:

一切工作看起来都不错!
使用名称空间组织代码
接下来,添加用于出租物业的图像。 我们将再次使用组件生成器:
$ ember generate component rental/image installing component create app/components/rental/image.hbs skip app/components/rental/image.js tip to add a class, run `ember generate component-class rental/image` installing component-test create tests/integration/components/rental/image-test.js
这次我们以/
的名称命名。 这导致在app/components/rental/image.hbs
创建一个组件,可以将其称为<Rental::Image>
。
类似的组件称为名称空间组件。 命名空间允许我们根据组件的用途将组件组织到文件夹中。 这是完全可选的,但很方便,尤其是在团队中开发大型应用程序时。
使用...attributes
转发HTML ...attributes
让我们编辑组件模板:

我们没有在<img>
为src
和alt
属性设置特定的值,而是选择了关键字...attributes
,有时也称为“ splattributes”。 这样,您可以在调用此组件时传递任意HTML属性,例如:

我们在此处指定了HTML属性src
和alt
,这些属性将传递给组件并附加到元素,其中...attributes
应用于组件模板。 您可能会认为这类似于{{yield}}
,但仅适用于HTML属性而不适用于显示内容。 实际上,当我们将class
属性传递给<LinkTo>
时,我们早已使用了此函数。

因此,我们的<Rental::Image>
组件与该站点上的任何特定租赁财产均未关联。 当然,所有内容也都与我们一起进行了硬编码,但是我们会尽快处理。 同时,我们将整个硬代码限制为一个组件,以便在继续提取实际数据时更容易清除它。
通常,将...attributes
添加到组件中的根元素是一个好主意。 这将提供最大的灵活性,因为发起者可能需要传递样式类或ARIA属性以提高可访问性。
现在让我们为新组件编写一个测试!

确定适当的测试范围
最后,我们还需要更新<Rental>
组件的测试,以确保我们成功调用了<Rental::Image>
。

由于我们已经编写了与<Rental::Image>
相关的测试,因此在这里我们可以省略细节并最小化检查。 因此,每当对<Rental::Image>
进行更改时,我们也不必更新<Rental>
测试。

互动组件
在本章中,您将向页面添加交互性,该交互性使用户可以单击图像来放大或缩小图像:

在这种情况下,您将了解:
- 向具有类的组件添加行为
- 从模板访问实例状态
- 具有跟踪属性的状态管理
- 在模板中使用条件语法
- 响应用户与操作的交互
- 如何调用元素修饰符
- 用户交互测试
向具有类的组件添加行为
到目前为止,我们编写的所有组件都只是呈现性的-它们只是可重用标记的片段。 这当然很棒,但是在Ember中,组件可以做的更多!
有时您想将某些行为与组件关联,以便它们可以做更多有趣的事情。 例如, <LinkTo>
可以通过更改URL并移至另一页面来响应单击。
在这里,我们将做类似的事情! 我们将实现«View Larger»
和«View Smaller»
,这将使我们的用户可以在家中单击图像,查看较大的版本,然后再次单击以返回较小的版本。
换句话说,我们需要一种在两个状态之一之间切换图像的方法。 为此,我们需要一种组件存储两种可能状态并知道其当前处于哪种状态的方法。
Ember还允许我们为此目的将JavaScript代码与组件相关联。 我们可以通过运行组件生成器为<Rental::Image>
组件添加一个JavaScript文件:
$ ember generate component-class rental/image installing component-class create app/components/rental/image.js
此命令在app/components/rentals/image.js
生成了一个与我们的组件模板同名的JavaScript文件。 它包含一个从@glimmer/component
继承的JavaScript类。
佐伊解释...
@glimmer/component
或Glimmer组件是可以使用的几种组件之一。 当您要向组件添加行为时,它们是一个很好的起点。 在本教程中,我们将仅使用Glimmer组件。
通常,应尽可能使用微光组件。 但是,您还可以看到在较旧的应用程序中使用的@ember/components
(经典组件)。 您可以通过查看导入路径来区分它们(由于它们具有不同且不兼容的 API,因此在搜索适当的文档时很有用)。
每当调用我们的组件时,Ember都会实例化该类。 我们可以使用该实例存储状态:

在这里,在组件构造函数中 ,我们将实例变量 this.isLarge
初始化为 false
,因为这是我们希望组件使用的默认状态。
从模板访问实例状态
让我们更新模板以使用刚刚添加的状态:

在模板中,我们可以访问组件的实例变量。 条件语法{{#if ...}}...{{else}}...{{/if}}
允许您根据条件(在本例中为this.isLarge
实例this.isLarge
)显示不同的内容。 结合这两个功能,我们可以分别形象化图像的小版本和大版本。
我们可以通过临时更改JavaScript文件中的初始值来进行验证。 如果我们更改app/components/rental/image.js
来初始化this.isLarge = true
; 在构造函数中,我们应该在浏览器中看到较大版本的属性图像。 哇!

检查之后,我们可以将this.isLarge
更改回false
。
由于构造器中实例变量的这种初始化模式相当普遍,因此语法较短:

功能相同,但短得多!
当然,我们的用户无法编辑我们的源代码,因此我们需要一种从浏览器切换图像大小的方法。 特别是,我们希望每当用户单击我们的组件时切换this.isLarge
的值。
使用跟踪属性进行状态管理
让我们通过添加一个切换大小的方法来更改类:

我们在这里做了一些事情,所以让我们弄清楚。
首先,我们将@tracked
装饰器添加到 @tracked
实例isLarge
。 此注释告诉Ember跟踪此变量以进行更新。 每当此变量的值更改时,Ember都会自动重新绘制任何依赖于其值的模板。
在我们的例子中,每当我们给this.isLarge
一个新值时, this.isLarge
注释@tracked
强制Ember重新评估模板中的条件{{#if this.isLarge}}
并分别在两个块之间切换。
佐伊解释...
不用担心! 如果您在模板中引用变量,但忘记添加@tracked
装饰器,则在开发模式下更改其值时会出现明显的错误!
我们处理用户行为
然后,向类中添加toggleSize
方法,该方法切换this.isLarge
而不是其当前状态( false
变为true
或true
变为false
)。
最后,我们在方法中添加了@action
装饰器。 这向Ember表示我们打算在模板中使用此方法。 否则,该方法将无法作为事件处理功能(在这种情况下为单击处理程序)正常运行。
佐伊解释...
如果忘记添加@action
装饰器,则在开发模式中单击按钮时也会收到错误消息!
现在是时候在模板中使用它了:

我们已经改变了两件事。
首先,由于我们要使组件具有交互性,因此将包含的标签从<div>
切换为<button>
(这对于可访问性而言很重要)。 使用正确的语义标签,我们还将获得“自由”的焦点和键盘交互。
然后,我们使用{{on}}
修饰符将this.toggleSize
附加为按钮单击处理程序。
因此,我们创建了第一个交互式组件。 尝试在浏览器中如何工作!

用户互动测试
最后,让我们为这个新行为编写一个测试:


测试结果

让我们在继续之前清理模板。 当我们将条件表达式插入模板时,我们添加了很多重复项。 如果仔细观察,这两个块之间唯一的区别是:
1) <button>
存在一个"large"
CSS类。
2)文字«»
和«»
。
这些更改隐藏在许多重复的代码中。 我们可以使用{{if}}
表达式来减少重复:

表达式{{if}}
带有两个参数。 第一个参数是条件 。 第二个参数是一个表达式,如果条件为true,则必须执行该表达式。
可选地, {{if}}
可以将条件为假时必须执行的表达式作为第三个参数。 这意味着我们可以如下重写按钮标签:

这是否可以提高代码的清晰度,这取决于口味。 无论如何,我们都大大减少了代码中的重复,并使重要的逻辑部分更加可见。
最后一次运行测试,以确保我们的重构不会破坏任何内容,我们将为下一个挑战做好准备!

重用组件
组件中剩余的未实现部分是显示房屋位置的地图,我们将继续对其进行处理:

添加地图时,您将了解:
- 应用程序级别配置管理
- 带参数的组件的参数化
- 访问组件参数
- 插值模板中的值
- 覆盖...属性中的HTML属性
- 使用吸气剂和自动跟踪进行重构(自动跟踪)
- 在测试上下文中检索JavaScript值
应用程序级别配置管理
我们将使用Mapbox API为我们的出租物业创建地图。 您可以免费注册 ,而无需使用信用卡。
Mapbox提供了静态地图图像 API,该API以PNG格式提供地图图像。 这意味着我们可以为所需的参数生成适当的URL,并使用标准的<img>
显示地图。 上课!
如果您有兴趣,可以使用交互式沙箱探索Mapbox中可用的选项。
在注册服务后,获取您的公共令牌 (默认公共令牌)并将其粘贴到config/environment.js
:


顾名思义, config/environment.js
用于配置我们的应用程序并存储此类API密钥。 可以从我们应用程序的其他部分访问这些值,并且根据当前环境(可以是开发,测试(测试)或生产(生产)),它们可以具有不同的值。
佐伊解释...
如果需要,可以创建不同的 Mapbox访问令牌以用于不同的环境。 为了使用Mapbox静态图片API,每个令牌至少必须具有“样式:图块”范围。
将更改保存到我们的配置文件后,我们将需要重新启动开发服务器以接收这些文件更改。 与我们编辑的文件不同, config/environment.js
不会自动重新启动。
您可以通过查找运行ember server
的终端窗口来停止服务器,然后按Ctrl + C,即在按住“ Ctrl”键的同时按键盘上的“ C”键。 停止后,您可以使用相同的ember server
命令再次启动它。
$ ember server building... Build successful (13286ms) – Serving on http://localhost:4200/
创建包含组件类的组件
将Mapbox API密钥添加到应用程序后,让我们为地图生成一个新组件。
$ ember generate component map --with-component-class installing component create app/components/map.js create app/components/map.hbs installing component-test create tests/integration/components/map-test.js
由于并非每个组件都必然具有与之关联的特定行为,因此默认情况下,组件生成器不会为我们创建JavaScript文件。 如前所述,我们总是可以在以后使用组件生成器添加它。
但是,对于我们的<Map>
组件,我们非常确定我们将需要一个JavaScript文件来处理尚未定义的某些行为! 因此,我们可以将--with-component-class
标志--with-component-class
生成器,以便从一开始就拥有所需的一切。
佐伊建议...
输入太多? 使用ember g component map -gc
。 -gc
标志表示Glimmer组件( g limmer c组件),并生成类( g generate c lass)
使用参数对组件进行参数化
让我们从我们的JavaScript文件开始:

在这里,我们从配置文件导入访问令牌,然后从getter token
返回它。 这使我们可以在MapComponent
类内部和组件模板中访问令牌this.token
。 如果令牌包含任何对URL不安全的特殊字符,则对令牌进行编码也很重要。
模式中的值插值
现在,让我们从JavaScript文件移至模板:

首先,我们有一个样式容器元素。
然后,我们有<img>
用于从Mapbox请求并渲染静态地图图像。
我们的模板包含几个尚不存在的值- @lat
, @lng
, @zoom
, @width
和@height
。 这些是<Map>
组件的参数 ,我们将在调用它们时提供给它。
使用参数对组件进行参数化,我们创建了一个可重用的组件,可以从应用程序的不同部分调用该组件,并对其进行自定义以满足这些特定上下文的需求。 前面使用<LinkTo>
组件时,我们已经看到了这一点。 我们需要指定@route
参数, @route
它知道要转到哪个页面。
我们根据@lat
和@lng
的值为alt
属性提供了合理的默认值。 您可能会注意到,我们直接将值插值到alt
属性的值中。 Ember会自动将这些内插值组合成一个字符串值,包括执行任何必要的转义HTML。
将HTML属性覆盖为...attributes
然后,我们使用...attributes
以允许调用方进一步自定义<img>
,例如,传递其他属性(例如class
,并使用更具体或更人性化的属性替换默认的alt
属性。
订单很重要! 灰烬按属性出现的顺序应用它们。 通过首先为属性分配默认的alt
值(在应用...attributes
之前),我们明确为调用者提供了根据必要的用例提供更专业的alt
属性的机会。
由于传递的alt
属性(如果存在)将出现在我们的属性之后,因此它将覆盖我们指定的值。 另一方面,重要的是在...属性之后分配src
, width
和height
,以免调用者意外覆盖它们。
src
属性会将所有必要的参数插入到Mapbox 静态图片 API的URL格式中,包括URL安全令牌this.token
。
最后,由于我们使用的是@2x
“视网膜”图像,因此必须指定width
和height
属性。 否则, <img>
显示将是我们预期的两倍!
我们已经为一个组件添加了很多代码,所以让我们编写一些测试! 特别是,我们需要确保我们对上面讨论过的HTML属性的行为进行了测试。



请注意,来自qunit-dom
的hasAttribute
helper hasAttribute
支持使用正则表达式 。 , , src
https://api.mapbox.com/
( , ). , , .
… .

, ! , ? , <Map>
<Rental>
:

!

...
, , config/environment.js
MAPBOX_ACCESS_TOKEN
. ! , Ember-.
<Rental>
, <Map>
.

- (auto-track)
<Map>
src
<img>
, . — JavaScript .
JavaScript API this.args.*
. , URL .
...
this.args
— API, Glimmer. (, «» ) (legacy) , API JavaScript .


! !

, @tracked
. , , Ember .
, , , . , src
lat
, lng
, width
, height
zoom
this.args
. , , , {{this.src}}
.
Ember , . @tracked
, Ember , (invalidate) , «» . (auto-track). , this.args
( , this.args.*
), @tracked
Glimmer. , (Just works).
JavaScript
, :



API this.setProperties
, .
, . ( !).
this
, render
. «» . .
!

<Rental>
. , :

:
- (model hook)
- (mocking) JSON
- (remote)
{{#each}}
<Rental>
. , , , , . .
, , . , .
, Ember — . , , .
. app/routes/index.js
:

, , . Route . , .
Route IndexRoute, , .
. ? model()
. .
, . Ember , . , , ( ).
, . , async
. await
. ( : , Promise Javascript )
. , , JavaScript ( POJO (Plain Old Javascript Object)).
, , , . @model
. POJO, .
, , title
:

, .

太好了!
, , , , , ! <Rental>
, .
.
-, <Rental>
@rental
. <h1>
, , :

@model
<Rental>
@rental
, «Grand Old Mansion» <Rental>
! , @rental
.


, «Grand Old Mansion», , .

, : , .
, , , , .
<Rental>
. , setProperties
, .

, <Rental>
render
@rental
. , !

(mocking) JSON
, , , , , !
, , , API. , API . JSON . , JSON HTTP- — , API, — - . 好酷!
? , JSON .zip. public
.
, public
:
public ├── api │ ├── rentals │ │ ├── downtown-charm.json │ │ ├── grand-old-mansion.json │ │ └── urban-living.json │ └── rentals.json ├── assets │ └── images │ └── teaching-tomster.png └── robots.txt 4 directories, 6 files
, , URL http://localhost:4200/api/rentals.json
.

«» JSON. !
(remote)
. , .

?
, Fetch API JSON API /api/rentals.json
, URL, .
, . Fetch API , fetch
async
, . , await
.
Fetch API . , ; , JSON, json()
. , await
.
, , .


JSON:API , , .
-, JSON:API , "data"
, . ; , , , — , .
, , . type
id
, (!). , , attributes
.
, , , , : , , type
, - . type
"Standalone" "Community", , <Rental>
.
JSON:API. .
:

(parsing) JSON attributes
, type
, . , , - .
! .
(helper) {{#each}}
, , — index.hbs
, <Rental>
. @rental
@model
. , @model
— ()! , , .
.

{{#each}}...{{/each}}
, . — — . — <Rental>
, <li>
.
{{rental}}
. rental
? , ! as |rental|
each
. - , as |property|
, {{property}}
.
, .

万岁! , . fetch
. , ?
, !

( 1.1 1.2)
Ember!
Ember, .
#Emberjs. Discord , ember_js
当您返回时,我们将依靠我们在第一部分中学到的知识,并迈向新的高度!
更新:拆卸感谢用户MK1301的错误纠正。