利用 Flux 库构建应用
1. 异步操作的分发
异步操作创建器的实现颇具挑战性。因为通常需要让存储(stores)知晓异步操作即将发生,以便更新用户界面(UI)。例如,点击发送 AJAX 请求的按钮时,可能要在实际发送请求前禁用该按钮,防止重复请求。在 Flux 中,实现此功能的唯一方法是分发操作(dispatch an action),因为一切都是单向的。
某些库在一定程度上能提供帮助。例如,可将请求前操作和成功/错误响应操作抽象成更易用的形式,这是常见模式。但即便如此,仍存在一些问题,比如组装请求以获取给定操作所需的所有数据、同步响应并将其传递给存储,以便存储将其转换为视图所需的数据。
或许将异步问题置于 Flux 范围之外是个不错的选择。例如,Facebook 推出的 GraphQL 语言,能简化从后端服务构建复杂数据的过程,且仅返回存储实际需要的数据。这一切都在一次响应中完成,还能节省带宽和延迟。不过,这种方法并非适用于所有人,Flux 实现者需自行选择处理异步性的方式,只要客户端的单向数据流保持完整即可。
2. 存储的分区
在 Flux 架构中,存储分区不当可能是面临的最大设计风险之一。通常情况是,最初存储大致平衡,但随着系统发展,所有新功能都集中到一个存储中,而其他存储的职责变得不明确,导致存储失衡。持有大部分应用状态的存储会变得过于复杂,难以维护。
存储分区的另一个潜在问题是过于细化。虽然单个存储管理的状态足够简单,但复杂之处在于所有这些存储之间的依赖关系。即使依赖关系不多,考虑的存储越多,在思考问题时就越难记住足够的状态。当相关状态集中在一处时,更容易预测会发生什么。
一些 Flux 库,如 Redux,采取了激进的方法,只允许使用单个存储,从而消除了所有可能的混淆源。实际上,这避免了存储分区等设计问题。后续会看到,Redux 使用归约函数(reducer functions)来转换单个存储的状态。
3. 使用 Alt.js
3.1 Alt.js 的核心思想
Alt.js 是一个 Flux 库,它为我们实现了许多样板代码。它完全遵循 Flux 的概念和模式,让我们能从应用的角度关注架构,而不必担心操作常量和 switch 语句。
Alt.js 作为一个用于生产应用的 Flux 库,有以下几个目标:
-
合规性
:Alt.js 并非借鉴 Flux 的思想,而是真正为 Flux 系统设计的。例如,存储、操作和视图的概念都适用,并且严格遵循 Flux 架构的原则,强制执行同步更新回合和单向数据流。
-
自动化样板代码
:Alt.js 能很好地处理一些与实现 Flux 相关的繁琐编程任务,如自动创建操作创建函数和操作常量,还会为我们处理存储操作处理方法,减少对长 switch 语句的需求。
-
无需调度器
:我们的代码无需与调度器交互。调用操作创建函数时,操作会在幕后自动分发给所有存储。存储之间的依赖管理直接在存储内部处理。
3.2 创建存储
下面创建一个简单的应用,为用户显示两个列表:一个是待办事项列表,另一个是已完成事项列表。我们将使用两个存储,每个列表对应一个存储。
3.2.1 Todo 存储
import alt from '../alt';
import actions from '../actions';
class Todo {
constructor() {
// 用于创建新待办事项的输入元素的状态
this.inputValue = '';
// 初始待办事项列表
this.todos = [
{ title: 'Build this thing' },
{ title: 'Build that thing' },
{ title: 'Build all the things' }
];
// 设置当相应操作分发时要调用的处理方法
this.bindListeners({
createTodo: actions.CREATE_TODO,
removeTodo: actions.REMOVE_TODO,
updateInputValue: actions.UPDATE_INPUT_VALUE
});
}
// 使用操作的 "payload" 作为标题创建新的待办事项
createTodo(payload) {
this.todos.push({ title: payload });
}
// 根据索引(作为操作的 payload 传入)移除待办事项
removeTodo(payload) {
this.todos.splice(payload, 1);
}
// 更新用户当前在待办事项输入框中输入的值
updateInputValue(payload) {
this.inputValue = payload;
}
}
// "createStore()" 函数将我们的存储类与所有相关的操作分发机制挂钩,返回存储的实例
export default alt.createStore(Todo, 'Todo');
这里的状态是类的任何实例变量,在这个例子中,是
inputValue
字符串和
todos
数组。
bindListeners()
方法用于将操作映射到方法,
createStore()
函数实例化存储类并连接分发机制。
3.2.2 Done 存储
import alt from '../alt';
import actions from '../actions';
import todo from './todo';
class Done {
constructor() {
// "done" 状态保存已完成事项的数组
this.done = [];
// 绑定此存储的唯一监听器
this.bindListeners({
createDone: actions.CREATE_DONE
});
}
// 此操作的 payload 是 "todo" 存储中某个事项的索引。当该事项被点击时调用此方法,将该事项添加到 "done" 数组中
// 注意,此操作处理方法不会改变 "todo" 存储的状态,因为这是不允许的
createDone(payload) {
const { todos } = todo.getState();
this.done.splice(0, 0, todos[payload]);
}
}
// 创建存储实例,并将其与 Alt 的分发机制挂钩
export default alt.createStore(Done, 'Done');
这个存储在将事项标记为已完成时,会使用 Todo 存储复制项目数据,但不会改变 Todo 存储,以免违反单向数据流。
这些存储类不是事件发射器,状态改变时不会显式发出任何信号。例如,添加待办事项时视图如何知道状态已改变?由于
createTodo()
方法会自动调用,方法执行完成后,通知机制也会自动触发。
3.3 声明操作创建函数
Alt 可以生成我们需要的函数以及存储中
bindListeners()
调用使用的常量。以下是操作模块的示例:
import alt from './alt';
// 导出一个包含接受 payload 参数的函数的对象。这些是操作创建函数。
// 还会根据传递给 "generateActions()" 的名称创建操作常量
export default alt.generateActions(
'createTodo',
'createDone',
'removeTodo',
'updateInputValue'
);
这将导出一个包含操作创建函数的对象,函数名与传递给
generateActions()
的字符串相同,并生成存储使用的操作常量。由于操作创建函数非常相似,
generateActions()
非常实用,减少了大量样板代码。不过,对于涉及异步操作的更复杂情况,可能需要更多代码。
3.4 监听状态变化
使用 AltContainer React 组件可以将存储数据传递给其他 React 组件。以下是应用主模块的示例:
import React from 'react';
import { render } from 'react-dom';
import AltContainer from 'alt-container';
import todo from './stores/todo';
import done from './stores/done';
import TodoList from './views/todo-list';
import DoneList from './views/done-list';
// 渲染 "AltContainer" 组件。这里将存储与视图绑定在一起
// "TodoList" 和 "DoneList" 组件是 "AltContainer" 的子组件,因此它们将 "todo" 和 "done" 存储作为属性接收
render(
<AltContainer stores={{ todo, done }}>
<TodoList/>
<DoneList/>
</AltContainer>,
document.getElementById('app')
);
AltContainer 组件接受一个
stores
属性,它会监听每个存储,并在任何存储的状态改变时重新渲染其子组件。这是让视图监听存储所需的唯一设置,无需到处手动调用
on()
或
listen()
方法。
3.5 渲染视图和分发操作
3.5.1 TodoList 组件
import React from 'react';
import { Component } from 'react';
import actions from '../actions';
export default class TodoList extends Component {
render() {
// 从 "todo" 存储中获取要渲染的相关状态
const { todos, inputValue } = this.props.todo;
// 渲染用于输入新待办事项的输入框和当前待办事项列表
// 用户输入并按下回车键时,创建新的待办事项
// 用户点击待办事项时,将其移动到 "done" 存储中
return (
<div>
<h3>TODO</h3>
<div>
<input
value={inputValue}
placeholder="TODO..."
onKeyUp={this.onKeyUp}
onChange={this.onChange}
autoFocus
/>
</div>
<ul>
{todos.map(({ title }, i) =>
<li key={i}>
<a
href="#"
onClick={this.onClick.bind(null, i)}
>{title}</a>
</li>
)}
</ul>
</div>
);
}
// 点击活动的待办事项时,将其索引作为 payload 传递给 "createDone()" 操作,然后传递给 "removeTodo()" 操作
onClick(key) {
actions.createDone(key);
actions.removeTodo(key);
}
// 如果用户输入了文本并按下回车键,使用 "createTodo()" 操作创建新事项,然后使用 "updateInputValue()" 操作清空输入框
onKeyUp(e) {
const { value } = e.target;
if (e.which === 13 && value) {
actions.createTodo(value);
actions.updateInputValue('');
}
}
// 文本输入值改变时,更新存储
onChange(e) {
actions.updateInputValue(e.target.value);
}
}
这里不能直接在按下回车键时清空
e.target.value
,因为这违背了 Flux 将状态保存在存储中的原则。如果应用的其他部分需要知道文本输入值,只需依赖 Todo 存储即可。
3.5.2 DoneList 组件
import React from 'react';
import { Component } from 'react';
export default class DoneList extends Component {
render() {
// 从 "done" 存储中获取唯一需要的状态 "done" 数组
const { done } = this.props.done;
// 设置已完成事项的样式为删除线
const itemStyle = {
textDecoration: 'line-through'
}
// 渲染已完成事项列表,并将 "itemStyle" 应用到每个事项
return (
<div>
<h3>DONE</h3>
<ul>
{done.map(({ title }) =>
<li style={itemStyle}>{title}</li>
)}
</ul>
</div>
);
}
}
这个组件比 TodoList 组件简单,因为没有事件处理。
以下是 Alt.js 应用的整体流程图:
graph LR
A[用户操作] -->|触发操作| B[操作创建函数]
B -->|分发操作| C[存储]
C -->|状态更新| D[视图]
D -->|用户交互| A
4. 使用 Redux
4.1 Redux 的核心思想
Redux 是用于实现 Flux 架构的库,与 Alt.js 不同,它并不追求完全符合 Flux 规范。Redux 的目标是借鉴 Flux 的重要思想,摒弃繁琐的部分。尽管它没有按照官方文档中指定的方式实现 Flux 组件,但如今已成为 React 架构的首选解决方案,证明了简单性总是优于高级功能。
Redux 有以下核心思想:
-
无需调度器
:和 Alt.js 一样,Redux 从其 API 中去除了调度器的概念。这些 Flux 库不暴露调度器组件,说明 Flux 只是一组思想和模式,而非具体实现。Alt 和 Redux 都能分发操作,只是不需要调度器来完成。
-
单一存储
:Redux 摒弃了 Flux 架构需要多个存储的观念,使用单个存储来保存整个应用的状态。乍一看,这可能会让人觉得存储会变得过大、难以理解,但多个存储也可能出现同样的问题,唯一的区别是应用状态被分割到不同的模块中。
-
直接分发到存储
:当只需要关注一个存储时,可以做出一些设计上的妥协,比如将存储和调度器视为同一概念。Redux 正是这样做的,它直接将操作分发到存储。
-
纯归约函数
:多个 Flux 存储的思想是将应用状态分割成几个逻辑上分离的领域。使用 Redux 也能实现这一点,不同之处在于,Redux 使用归约函数将状态分割成不同领域。这些函数负责在操作分发时转换存储的状态,它们是纯函数,因为它们返回新数据,避免引入任何副作用。
4.2 归约函数和存储
下面使用 Redux 实现与 Alt.js 相同的简单待办事项应用。两个库有很多重叠之处,特别是 React 组件本身,不需要做太多更改。Redux 与 Alt 和一般 Flux 的不同之处在于它使用单个存储和改变存储状态的归约函数。
首先创建 Redux 存储的初始状态模块:
import Immutable from 'immutable';
// Redux 存储的初始状态
// 应用状态的 "形状" 包括两个领域 - "Todo" 和 "Done"。每个领域都是 Immutable.js 结构
const initialState = {
Todo: Immutable.fromJS({
inputValue: '',
todos: [
{ title: 'Build this thing' },
{ title: 'Build that thing' },
{ title: 'Build all the things' }
]
}),
Done: Immutable.fromJS({
done: []
})
};
export default initialState;
状态是一个简单的 JavaScript 对象,单个存储通过
Todo
和
Done
两个主要属性进行组织,类似于多个存储,但它们在一个对象中。每个存储属性都是 Immutable.js 数据结构,因为需要将传递给归约函数的状态视为不可变的,这个库能轻松实现不变性。
4.2.1 Todo 归约函数
import Immutable from 'immutable';
import initialState from '../initial-state';
import {
UPDATE_INPUT_VALUE,
CREATE_TODO,
REMOVE_TODO
} from '../constants';
export default function Todo(state = initialState, action) {
switch (action.type) {
// 当 "UPDATE_INPUT_VALUE" 操作分发时,设置 Immutable.Map 的 "inputValue" 键
case UPDATE_INPUT_VALUE:
return state.set('inputValue', action.payload);
// 当 "CREATE_TODO" 操作分发时,将新事项添加到 Immutable.List 的末尾
case CREATE_TODO:
return state.set('todos',
state.get('todos').push(Immutable.Map({
title: action.payload
}))
);
// 当 "REMOVE_TODO" 操作分发时,从 Immutable.List 中删除给定索引的事项
case REMOVE_TODO:
return state.set('todos',
state.get('todos').delete(action.payload));
default:
return state;
}
}
这里使用的
switch
语句模式很熟悉,实际上这个函数就像一个存储,但有两个主要区别:一是它是一个函数而不是类,这意味着不是设置状态属性值,而是返回新状态;二是 Redux 处理监听存储和调用归约函数的机制,使用类时需要自己编写很多这样的代码。
4.2.2 Done 归约函数
import Immutable from 'immutable';
import initialState from '../initial-state';
import { CREATE_DONE } from '../constants';
export default function Done(state = initialState, action) {
switch (action.type) {
// 当 "CREATE_DONE" 操作分发时,将新事项插入到 Immutable.List 的开头
case CREATE_DONE:
return state.set('done',
state.get('done')
.insert(0, Immutable.Map(action.payload))
);
// 无需操作,原样返回状态
default:
return state;
}
}
这些归约函数不能修改
state
参数,因此使用 Immutable.js 库,方便通过创建新数据来转换现有状态。虽然不是必须使用 Immutable.js 来转换 Redux 存储状态,但它有助于简化代码。
以下是 Redux 应用的整体流程图:
graph LR
A[用户操作] -->|触发操作| B[操作创建函数]
B -->|分发操作| C[单一存储]
C -->|归约函数转换状态| C
C -->|状态更新| D[视图]
D -->|用户交互| A
综上所述,Alt.js 和 Redux 都是优秀的 Flux 库,它们各有特点。Alt.js 严格遵循 Flux 规范,能帮助我们减少样板代码;Redux 则采取了更简洁的方式,使用单一存储和纯归约函数。在选择使用哪个库时,需要根据项目的具体需求和团队的技术栈来决定。
5. Alt.js 与 Redux 的对比
5.1 架构设计对比
| 对比项 | Alt.js | Redux |
|---|---|---|
| 存储设计 | 支持多个存储,每个存储管理特定部分的状态,可能会存在存储间依赖关系复杂的问题 | 仅使用单个存储来管理整个应用的状态,避免了多存储的依赖问题,但可能使存储变得庞大 |
| 调度器 | 隐藏了调度器的概念,在幕后自动处理操作分发到存储的过程 | 去除了调度器的概念,直接将操作分发到存储 |
| 状态管理 | 状态是存储类的实例变量,通过方法直接修改 | 状态通过纯归约函数进行转换,不直接修改原状态,而是返回新状态 |
5.2 代码实现对比
5.2.1 存储定义
-
Alt.js
:通过类定义存储,使用
bindListeners方法将操作映射到处理方法,createStore函数实例化存储并连接分发机制。示例代码如下:
import alt from '../alt';
import actions from '../actions';
class Todo {
constructor() {
this.inputValue = '';
this.todos = [
{ title: 'Build this thing' },
{ title: 'Build that thing' },
{ title: 'Build all the things' }
];
this.bindListeners({
createTodo: actions.CREATE_TODO,
removeTodo: actions.REMOVE_TODO,
updateInputValue: actions.UPDATE_INPUT_VALUE
});
}
createTodo(payload) {
this.todos.push({ title: payload });
}
removeTodo(payload) {
this.todos.splice(payload, 1);
}
updateInputValue(payload) {
this.inputValue = payload;
}
}
export default alt.createStore(Todo, 'Todo');
- Redux :使用纯函数(归约函数)来定义存储的状态转换逻辑。示例代码如下:
import Immutable from 'immutable';
import initialState from '../initial-state';
import {
UPDATE_INPUT_VALUE,
CREATE_TODO,
REMOVE_TODO
} from '../constants';
export default function Todo(state = initialState, action) {
switch (action.type) {
case UPDATE_INPUT_VALUE:
return state.set('inputValue', action.payload);
case CREATE_TODO:
return state.set('todos',
state.get('todos').push(Immutable.Map({
title: action.payload
}))
);
case REMOVE_TODO:
return state.set('todos',
state.get('todos').delete(action.payload));
default:
return state;
}
}
5.2.2 操作创建
-
Alt.js
:使用
generateActions方法自动生成操作创建函数和操作常量。示例代码如下:
import alt from './alt';
export default alt.generateActions(
'createTodo',
'createDone',
'removeTodo',
'updateInputValue'
);
- Redux :通常手动创建操作创建函数,返回包含操作类型和可选负载的对象。示例代码如下:
export const createTodo = (payload) => ({
type: 'CREATE_TODO',
payload
});
5.3 应用场景对比
- Alt.js :适用于对 Flux 规范要求严格,需要遵循传统 Flux 架构,且项目规模相对较小,存储间依赖关系可以较好管理的应用。
- Redux :更适合大型项目,尤其是需要可预测状态管理、易于调试和测试,以及需要与 React 紧密结合的应用。
以下是 Alt.js 和 Redux 对比的总结流程图:
graph LR
A[应用需求] -->|小型、遵循规范| B[选择 Alt.js]
A -->|大型、可预测性强| C[选择 Redux]
B -->|多存储管理| D[Alt.js 架构特点]
C -->|单存储管理| E[Redux 架构特点]
6. 实际项目中的选择建议
6.1 项目规模
- 小型项目 :如果项目规模较小,功能相对简单,团队对 Flux 规范有一定要求,且希望快速搭建项目,可以选择 Alt.js。它的样板代码自动化功能可以减少开发时间,让开发者更专注于业务逻辑。
- 大型项目 :对于大型项目,状态管理复杂,需要可维护性和可测试性强的架构,Redux 是更好的选择。其单一存储和纯归约函数的设计,使得状态变化可预测,便于调试和维护。
6.2 团队技术栈
- 熟悉传统 Flux :如果团队成员对传统 Flux 架构比较熟悉,且希望继续遵循 Flux 规范进行开发,Alt.js 可能更容易上手。
- 倾向函数式编程 :如果团队成员对函数式编程有一定了解,并且喜欢使用纯函数来处理数据,Redux 的纯归约函数会更符合团队的技术偏好。
6.3 性能和可扩展性
- 性能敏感 :在对性能要求较高的场景中,Redux 的单一存储和纯函数特性可能会带来更好的性能表现,因为状态变化可预测,减少了不必要的重新渲染。
- 扩展性需求 :如果项目需要不断扩展功能,Redux 的模块化设计和易于集成的特点,使其更适合应对未来的变化。
以下是选择库的决策列表:
1. 评估项目规模和复杂度。
2. 考虑团队成员的技术栈和偏好。
3. 分析项目对性能和可扩展性的要求。
4. 根据以上因素综合选择 Alt.js 或 Redux。
7. 总结
在构建基于 Flux 架构的应用时,Alt.js 和 Redux 是两个非常实用的库。Alt.js 严格遵循 Flux 规范,通过自动化样板代码帮助开发者更高效地实现 Flux 架构;Redux 则采取了更简洁和可预测的方式,使用单一存储和纯归约函数来管理应用状态。
在实际项目中,需要根据项目规模、团队技术栈、性能和可扩展性等因素来选择合适的库。无论选择哪个库,都能借助 Flux 架构的优势,实现单向数据流和可预测的状态管理,提高应用的可维护性和可测试性。希望通过本文的介绍,能帮助开发者更好地理解 Alt.js 和 Redux 的特点和适用场景,从而做出更合适的选择。
超级会员免费看
7853

被折叠的 条评论
为什么被折叠?



