Blaze 中的可重用组件

UI/UX 文章 中,我们讨论了创建可重用组件的优点,这些组件以清晰简洁的方式与其环境交互。

虽然 Blaze 只是一个简单的基于模板的渲染引擎,它不像 React 和 Angular 等其他框架那样强制执行很多这些原则,但你可以通过在编写 Blaze 组件时遵循一些约定来享受大多数相同的好处。本节将概述编写可重用 Blaze 组件的一些“最佳实践”。

以下示例将参考 Todos 示例应用程序中的 Lists_show 组件。

验证数据上下文

为了确保你的组件始终获取你期望的数据,你应该验证提供给它的数据上下文。这就像验证任何 Meteor 方法或发布的论据一样,让你可以在一个地方编写验证代码,然后假设数据是正确的。

你可以在 Blaze 组件的 onCreated() 回调中执行此操作,如下所示

1
2
3
4
5
6
7
8
9
Template.Lists_show.onCreated(function() {
this.autorun(() => {
new SimpleSchema({
list: {type: Function},
todosReady: {type: Boolean},
todos: {type: Mongo.Cursor}
}).validate(Template.currentData());
});
});

我们在这里使用 autorun() 来确保数据上下文在发生变化时重新验证。

将数据上下文命名为模板包含

你可能很想只将你感兴趣的对象作为模板的整个数据上下文提供(例如 {{> Todos_item todo}})。最好显式地为其命名({{> Todos_item todo=todo}})。这样做有两个主要原因

  1. 在子组件中使用数据时,更清楚地知道你在访问什么;{{todo.title}}{{title}} 更清楚。
  2. 它更灵活,以防将来需要向组件提供更多参数。

例如,在 Todos_item 子组件的情况下,我们需要提供两个额外的参数来控制项目的编辑状态,如果项目使用单个 todo 参数,添加这些参数将很麻烦。

此外,为了提高清晰度,始终明确地向包含提供数据上下文,而不是让它继承渲染它的模板的上下文

1
2
3
4
5
<!-- bad: inherits data context, who knows what is in there! -->
{{> myTemplate}}

<!-- explicitly passes empty data context -->
{{> myTemplate ""}}

更喜欢 {{#each .. in}}

出于与上述类似的原因,最好使用 {{#each todo in todos}} 而不是旧的 {{#each todos}}。第二个将所有子节点的数据上下文设置为单个 todo 对象,并使难以访问块外部的任何上下文。

不使用 {{#each .. in}} 的唯一原因是它使难以在事件处理程序中访问 todo 符号。通常,对此的解决方案是使用子组件来渲染循环的内部

1
2
3
{{#each todo in todos}}
{{> Todos_item todo=todo}}
{{/each}}

现在你可以在 Todos_item 事件处理程序和助手内部访问 this.todo

将数据传递到助手

与其通过 this 在助手内部访问数据,不如直接从模板中传递参数。因此,我们的 checkedClass 助手接受 todo 作为参数并直接检查它,而不是隐式地使用 this.todo。我们这样做是为了类似于我们始终向模板包含传递参数的原因,以及因为“模板变量”(例如 {{#each .. in}} 助手的迭代器)在 this 上不可用。

使用模板实例

虽然 Blaze 的简单 API 不一定鼓励组件化方法,但你可以使用模板实例作为存储内部功能和状态的便捷位置。可以在 Blaze 的生命周期回调内部通过 this 访问模板实例,并在事件处理程序和助手内部作为 Template.instance() 访问。它还作为事件处理程序的第二个参数传递。

我们建议在这些上下文中将其命名为 instance,并在每个相关助手的顶部将其分配。例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Template.Lists_show.helpers({
todoArgs(todo) {
const instance = Template.instance();
return {
todo,
editing: instance.state.equals('editingTodo', todo._id),
onEditingChange(editing) {
instance.state.set('editingTodo', editing ? todo._id : false);
}
};
}
});

Template.Lists_show.events({
'click .js-cancel'(event, instance) {
instance.state.set('editingTodo', false);
}
});

使用响应式字典存储状态

The reactive-dict 包让你定义一个简单的响应式键值字典。它是将内部状态附加到组件的便捷方法。我们在 onCreated 回调中创建 state 字典,并将其附加到模板实例

1
2
3
4
5
6
7
Template.Lists_show.onCreated(function() {
this.state = new ReactiveDict();
this.state.setDefault({
editing: false,
editingTodo: false
});
});

创建状态字典后,我们就可以从助手内部访问它,并在事件处理程序中修改它(参见上面的代码片段)。

将函数附加到实例

如果你有一个模板实例的通用功能需要从多个事件处理程序中抽象或调用,那么将其作为函数直接附加到 onCreated() 回调中的模板实例是明智的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {
updateName,
} from '../../api/lists/methods.js';

Template.Lists_show.onCreated(function() {
this.saveList = () => {
this.state.set('editing', false);

updateName.call({
listId: this.data.list._id,
newName: this.$('[name=name]').val()
}, (err) => {
err && alert(err.error);
});
};
});

然后你可以从事件处理程序内部调用该函数

1
2
3
4
5
6
Template.Lists_show.events({
'submit .js-edit-form'(event, instance) {
event.preventDefault();
instance.saveList();
}
});

将 DOM 查找范围限定到模板实例

直接使用 jQuery 的全局 $() 在 DOM 中查找内容是一个坏主意。很容易选择页面上与当前组件无关的元素。此外,它限制了你渲染主文档之外的选项(参见下面的测试部分)。

相反,Blaze 提供了一种将查找范围限定在当前模板实例内部的方法。通常你可以在 onRendered() 回调中使用它来设置 jQuery 插件(通过 Template.instance().$()this.$() 调用),或者从事件处理程序中直接调用 DOM 函数(通过 Template.instance().$() 调用,或者使用事件处理程序的第二个参数,如 instance.$())。例如,当用户单击添加待办事项按钮时,我们希望聚焦 <input> 元素

1
2
3
4
5
Template.Lists_show.events({
'click .js-todo-add'(event, instance) {
instance.$('.js-todo-new input').focus();
}
});

使用 .js- 选择器用于事件映射

当你在 JS 文件中设置事件映射时,你需要“选择”模板中事件附加到的元素。最好不要使用与样式化元素相同的 CSS 类名,而是使用专门为这些事件映射添加的类名。一个合理的约定是使用以 js- 开头的类名来表示它由 JavaScript 使用。例如上面的 .js-todo-add

将 HTML 内容作为模板参数传递

如果你需要将内容传递给子组件(例如模式对话框的内容),可以使用 自定义块助手 来提供内容块。如果你需要更多灵活性,通常只提供组件名称作为参数就可以了。然后子组件就可以使用以下方法渲染该组件

1
{{> Template.dynamic templateName dataContext}}

这或多或少是 kadira:blaze-layout 包的工作方式。

传递回调

如果你需要向上通信组件层次结构,最好为子组件传递一个回调以供其调用。

例如,一次只能有一个待办事项项目处于编辑状态,因此 Lists_show 组件管理哪个项目处于编辑状态。当你聚焦一个项目时,该项目需要告诉列表的组件使其成为“编辑”的项目。为此,我们将一个回调传递到 Todos_item 组件中,当子组件需要更新父组件中的状态时,它会调用该回调

1
{{> Todos_item (todoArgs todo)}}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Template.Lists_show.helpers({
todoArgs(todo) {
const instance = Template.instance();
return {
todo,
editing: instance.state.equals('editingTodo', todo._id),
onEditingChange(editing) {
instance.state.set('editingTodo', editing ? todo._id : false);
}
};
}
});

Template.Todos_item.events({
'focus input[type=text]'() {
this.onEditingChange(true);
}
});

onRendered() 用于第三方库

如上所述,onRendered() 回调通常是调用期望预渲染 DOM 的第三方库(如 jQuery 插件)的正确位置。onRendered() 回调在组件首次渲染并附加到 DOM 之后触发一次。

有时,你可能需要等待数据准备就绪,然后再附加插件(虽然通常在这种情况下最好使用子组件)。为此,你可以在 onRendered() 回调中设置 autorun。例如,在 Lists_show_page 组件中,我们希望等到列表的订阅准备就绪(即待办事项已渲染)之后,再隐藏启动屏幕

1
2
3
4
5
6
7
8
Template.Lists_show_page.onRendered(function() {
this.autorun(() => {
if (this.subscriptionsReady()) {
// Handle for launch screen defined in app-body.js
AppLaunchScreen.listRender.release();
}
});
});
在 GitHub 上编辑