渣翻译,请看原文链接
React 和 Flux
尽管你可以一直都在组件中写另种逻辑,但最终这样做将是痛苦的.Flux 应用架构能让我们的React应用清爽起来.它不是唯一的解决方案,但却是一个不错的起点.
Flux让我们分离我们应用的数据和程序状态.这帮助我们保持它们的清晰和应用的可维护性.Flux是为了大型团队设计的.因此你可能会觉得它很详细.但我们可以直接使它到工作中提升我们的效率.
Flux简介
到目前为至,我们一直工作在视图中.Flux架构引入了几个新概念.它们是actions(行为),dispatcher(调度器)和stores(存储).与其它流行框架,如Angular或Ember相比,Flux实现的是数据单向流动的,尽管双向数据绑定带来了方便,但它是有代价的.我们很难理解发生了什么和为什么是这样的.
Actions(行为)和Stores(存储)
对我们而言,我们将建模NoteActions
和NoteStore
. NoteActions
提供操作我们数据的具体方法.例如,我们有NoteActions.create({task: 'Learn React'})
.
Dispatcher(调度器)
当我们触发了一个操作.调度器会得到通知.它能处理存储间依赖关系.调度器可能让我们在某些行为需要执行前执行其它行为.
最简单的方式,actions只是原样把消息传递给dispatcher.它也可以触发异步查询并且跟据最终结果进行调度.这使我们能处理接收的数据和可能的错误.
一旦调度程序处理一个行为,存储将监听它触发,对我们而言,NoteStore
获得通知.最终它会更新内部状态.这之后,它会把这个新状态通知到合适的监听器.
Flux数据流
通常,这个单向的过程会形成一个循环.接下来的示例图是一个常见的流.它与之前是相同的,只是增加了一个反回.最终,通过这个循环过程,将会刷新我们依赖在store的相关的组件.
这听起来像是有许多步骤,例如实现一个简单的新建Note
.但这种方法有它的好处,考虑到数据总是单向流动的,跟踪测试会很容易.如果发现问题,则必定出现在周期的某一环节上.
Flux的优势
虽然这听起来有点复杂,但是同样是灵活的.例如,我们能实现API通信,缓存,和视图的国际化.这种方式将保持逻辑的整洁,使应用程序更容易理解.
实现Flux架构会增加你的应用程序的代码量.要明白写最少的代码不是Flux的目标.它是被设计在团队中提高我们的生产力的工具.
使用哪种Flux实现?
选择哪个架构,归跟结底是你的个人喜好,你将考虑到如API,功能,文档,和技术支持,用比较流行的架构是一个不错的主意,等到你开始理解这个架构,可以让你更好的选择它. voronianski/flux-comparison提供了多种流行框架的比较.
移植到Alt
在这章,我们将使用一个知名的库Alt.它是一个灵活的,全功能的实现.
在Alt,你需要处理actions和stores,而隐藏了调度器,但你仍可以在需要时访问它.对比其它的实现,Alt隐藏了大量的样板.有指定的功能,允许你保存和还原应用程序状态.这对实现持续和通用的渲染来说是方便的.
配置Alt实例
我们从一个Alt实例开始.它保持跟踪actions和stores及其通信.让事情简单一些.我们处理所有Alt组件作为一个单例模式.
app/libs/alt.js
import Alt from 'alt';
//import chromeDebug from 'alt-utils/lib/chromeDebug';
const alt = new Alt();
//chromeDebug(alt);
export default alt;
Webpack会缓存这个模块.所以下次你引入Alt时.它将再反回你同样的实例.
如果你没有使用npm-install-webpack-plugin插件.记住要手动安装alt和其它工具到你的工程中
npm i alt alt-container alt-utils node-uuid -S
.
定义Notes
的CRUD操作
下一步,我们需要定义一些基本的API来操作便签数据.考虑到读是隐含的,我们不需要这个.我们能模拟其它行为,Alt提供了一个generateActions
.我们可以这样使用:
app/actions/NoteActions.js
import alt from '../libs/alt';
export default alt.generateActions('create', 'update', 'delete');
定义Notes
的Store
store的唯一来源,是源于你应用程序状态的一部分.在本例中,我们需要一个维护便签状态,我们将通过bindActions
函数来连接所有我们之前定义的行为(actions).
我们将把原先在App
中store的逻辑移到NoteStore
中.
设置骨架
第一步,我们能设置我们store的骨架,我们会在之后填充需要的方法.Alt使用标准的ES6类,即与我们之前所看到React组件相同的语法.下面是一个开始:
app/stores/NoteStore.js
import uuid from 'node-uuid';
import alt from '../libs/alt';
import NoteActions from '../actions/NoteActions';
class NoteStore {
constructor() {
this.bindActions(NoteActions);
this.notes = [];
}
create(note) {
}
update(updatedNote) {
}
delete(id) {
}
}
export default alt.createStore(NoteStore, 'NoteStore');
我们通过它的名字调用bindActions
映射每一个行为(action).之后我们触发这个适当的逻辑在每个方法.最终.Alt使用alt.createStore
连接存储.
注意指配一个标签给存储(在这个例子中为NoteStore
)并不是必需的.但它是一个好的实践,特别是我们反复使用这个数据时,它会是重要的.
实现create
对比早期的逻辑,create
将自动生成一个Note
的id,这个细节能被隐藏在存储内部:
app/stores/NoteStore.js
import uuid from 'node-uuid';
import alt from '../libs/alt';
import NoteActions from '../actions/NoteActions';
class NoteStore {
constructor() {
...
}
create(note) {
leanpub-start-insert
const notes = this.notes;
note.id = uuid.v4();
this.setState({
notes: notes.concat(note)
});
leanpub-end-insert
}
...
}
export default alt.createStore(NoteStore, 'NoteStore');
为了保持清爽.我们使用了this.setState
.它是Alt的特性,让我们表示我们将要改变store的状态.Alt会把这个事件发给可能的监听器.
实现update
update
与之前的写法大同小异.最重要的是我们通过this.setState
提交新状态.
app/stores/NoteStore.js
...
class NoteStore {
...
update(updatedNote) {
leanpub-start-insert
const notes = this.notes.map(note => {
if(note.id === updatedNote.id) {
// Object.assign is used to patch the note data here. It
// mutates target (first parameter). In order to avoid that,
// I use {} as its target and apply data on it.
//
// Example: {}, {a: 5, b: 3}, {a: 17} -> {a: 17, b: 3}
//
// You can pass as many objects to the method as you want.
return Object.assign({}, note, updatedNote);
}
return note;
});
// This is same as `this.setState({notes: notes})`
this.setState({notes});
leanpub-end-insert
}
delete(id) {
}
}
export default alt.createStore(NoteStore, 'NoteStore');
我们还剩下最后的操作,delete
.
上面的{notes}是ES6的功能.被称为property shorthand.它等价于
{notes: notes}
.
实现delete
delete
很简单.找到它之后删除.像之前一样.记着最后提交这个改变:
app/stores/NoteStore.js
...
class NoteStore {
...
delete(id) {
leanpub-start-insert
this.setState({
notes: this.notes.filter(note => note.id !== id)
});
leanpub-end-insert
}
}
export default alt.createStore(NoteStore, 'NoteStore');
现在我们几乎整合了Flux在我们的应用程序中.我们设置了几个行为API到操作Notes
数据.我们还有一个store用于实际的数据操作.我们还差了整合我们的视图.它会监听store并且能触发行为完成这个循环周期.
粘合在一起
这有一点点复杂,需要考虑的地方有很多.处理actions是容易的.例如,创建Note,我们需要触发NoteActions.create({task: 'New task'})
.这将导致store改变,并且引发所有监听到它组件的改变.
我们的NoteStore
将提供了两个方法重要的方法.是NoteStore.listen
和NoteStore.unlisten
.它允许视图订阅状态的改变.
在上一章你可能还记得,React提供了生命周期钩子的设置.我们可以在视图中的componentDidMount
和componentWillUnmount
中订阅NoteStore
.记得注释监听,避免内存泄露的可能.
基于这个思想我们可以在App
中组合NoteStore
和NoteActions
:
app/components/App.jsx
leanpub-start-delete
import uuid from 'node-uuid';
leanpub-end-delete
import React from 'react';
import Notes from './Notes.jsx';
leanpub-start-insert
import NoteActions from '../actions/NoteActions';
import NoteStore from '../stores/NoteStore';
leanpub-end-insert
export default class App extends React.Component {
constructor(props) {
super(props);
leanpub-start-delete
this.state = {
notes: [
{
id: uuid.v4(),
task: 'Learn Webpack'
},
{
id: uuid.v4(),
task: 'Learn React'
},
{
id: uuid.v4(),
task: 'Do laundry'
}
]
};
leanpub-end-delete
leanpub-start-insert
this.state = NoteStore.getState();
leanpub-end-insert
}
leanpub-start-insert
componentDidMount() {
NoteStore.listen(this.storeChanged);
}
componentWillUnmount() {
NoteStore.unlisten(this.storeChanged);
}
storeChanged = (state) => {
// Without a property initializer `this` wouldn't
// point at the right context because it defaults to
// `undefined` in strict mode.
this.setState(state);
};
leanpub-end-insert
render() {
const notes = this.state.notes;
return (
<div>
<button className="add-note" onClick={this.addNote}>+</button>
<Notes notes={notes}
onEdit={this.editNote}
onDelete={this.deleteNote} />
</div>
);
}
leanpub-start-delete
deleteNote = (id) => {
this.setState({
notes: this.state.notes.filter(note => note.id !== id)
});
};
leanpub-end-delete
leanpub-start-insert
deleteNote(id) {
NoteActions.delete(id);
}
leanpub-end-insert
leanpub-start-delete
addNote = () => {
this.setState({
notes: this.state.notes.concat([{
id: uuid.v4(),
task: 'New task'
}])
});
};
leanpub-end-delete
leanpub-start-insert
addNote() {
NoteActions.create({task: 'New task'});
}
leanpub-end-insert
leanpub-start-delete
editNote = (id, task) => {
// Don't modify if trying set an empty value
if(!task.trim()) {
return;
}
const notes = this.state.notes.map(note => {
if(note.id === id && task) {
note.task = task;
}
return note;
});
this.setState({notes});
};
leanpub-end-delete
leanpub-start-insert
editNote(id, task) {
// Don't modify if trying set an empty value
if(!task.trim()) {
return;
}
NoteActions.update({id, task});
}
leanpub-end-insert
}
这个应用程序应该与之前运行效果相同.当我们通过actions改变NoteStore
,会通过setState
级联我们的App
state.这将会使组件render
.这就是Flux的单向流动模式.
我们现在比之前的代码量增加了,但这没问题.App
变的更简洁且以后开发会更容易.更重要的是我们已经成功的在我们的应用中实现了Flux架构.
在localStorage
上实现持久化
我们修改我们NoteStore
的实现,对数据的改变进行保存。这样做我们刷新时不会丢失我们的数据。一种方式是使用localStorage.它是一个好的功能让我们持久化数据到浏览器。
理解localStorage
localStorage
有一个兄弟是sessionStorage
。但sessionStorage
在浏览器关闭时会丢失数据。它们两个有下面相同的API:
storage.getItem(k)
- 使用给定的键,反回一个存储的字符串值。storage.removeItem(k)
- 删除这个键对应的数据。storage.setItem(k,v)
- 存储这个键值对。storage.clear()
- 清空这个storage中的内容。
注意,使用浏览器的开发人员工具能很方便的操作它。例如,在Chrome中你能在Resources标签页中看这个状态。Console标签页中你能直接操作数据。你还可以使用storage.key
和storage.key = 'value'
快速修改。
localStorage
和sessionStorage
存储数据不能超过10MB。虽然它很好用,但有些情况下会失效,如在IE中发生内存溢出时和在Safari的私有模式下。
实现localStorage
的封装
为了更易于管理,我们可以实现对storage
的简单的封装。
我们使用JSON.parse
和JSON.stringify
进行序列化。在下面的实现里只需要storage.get(k)
和storage.set(k,v)
:
app/libs/storage.js
export default {
get(k) {
try {
return JSON.parse(localStorage.getItem(k));
}
catch(e) {
return null;
}
},
set(k, v) {
localStorage.setItem(k, JSON.stringify(v));
}
};
使用FinalStore
持久化应用
Alt专为这个意图,提供了一个内置的存储叫做FinalStore
。我们可以使用FinalStore
,引导和快照,持久我们全部的应用状态。FinalStore
是一个监听所有已有存储的存储。每当有存储改变,FinalStore
将了解它,这使得它非常适合持久化。
每当FinalStore
改变,我们能取得整个应用的状态的快照并推它到localStorage
。alt.bootstra
让我们设置所有存储的状态,这个方法不能发出事件,使我们的存储填充正确的状态,我们需要在组件渲染完成后调用它。在这个例子中,我们将取得数据从localStorage
并引用它填充我们的存储。
为了整合这个思路到我们的应用,我们将需实现一个小的模块管理它。我们考虑可能的初始数据和触发新的逻辑。
app/libs/persist.js,将设置一个FinalStore
,处理引导(恢复数据)和快照(保存数据)。
app/libs/persist.js
import makeFinalStore from 'alt-utils/lib/makeFinalStore';
export default function(alt, storage, storeName) {
const finalStore = makeFinalStore(alt);
try {
alt.bootstrap(storage.get(storeName));
}
catch(e) {
console.error('Failed to bootstrap data', e);
}
finalStore.listen(() => {
if(!storage.get('debug')) {
storage.set(storeName, alt.takeSnapshot());
}
});
}
最后,我们需要在初始化时调用这个持久化函数.我们需要传递相关数据到Alt实例,存储,存储名.
app/index.jsx
...
leanpub-start-insert
import alt from './libs/alt';
import storage from './libs/storage';
import persist from './libs/persist';
persist(alt, storage, 'app');
leanpub-end-insert
ReactDOM.render(<App />, document.getElementById('app'));
如果现在你刷新浏览器,程序将会记住这个状态.如果我们增加更多的存储到系统中,这个方案应该会是最省力的.即使整合一个真实的后端也不会有问题.
例如,你能传递初始值到你的HTML(全局渲染),加载它,然后持久化数据到后端,并且如要你想,你能使用localStorage
作为一个备份.
Universal rendering(全局渲染?服务端渲染?)是有力的技术让你在使用React时改善应用的性能,并受SEO欢迎.不是所有组件都需要在前端进行渲染,我们会在后端执行一部分.我们在后端渲染初始化应用程序所需要的,并把它呈现给用户.同样还可以包含一些初始数据到你的应用中而不用执行额外的查询.
使用AltContainer
AltContainer
封装会大大简化我们的连接逻辑.下面的实现展示怎么绑定所有在一起.注意我们删了多少代码:
app/components/App.jsx
leanpub-start-insert
import AltContainer from 'alt-container';
leanpub-end-insert
import React from 'react';
import Notes from './Notes.jsx';
import NoteActions from '../actions/NoteActions';
import NoteStore from '../stores/NoteStore';
export default class App extends React.Component {
leanpub-start-delete
constructor(props) {
super(props);
this.state = NoteStore.getState();
}
componentDidMount() {
NoteStore.listen(this.storeChanged);
}
componentWillUnmount() {
NoteStore.unlisten(this.storeChanged);
}
storeChanged = (state) => {
// Without a property initializer `this` wouldn't
// point at the right context (defaults to `undefined` in strict mode).
this.setState(state);
};
leanpub-end-delete
render() {
leanpub-start-delete
const notes = this.state.notes;
leanpub-end-delete
return (
<div>
<button className="add-note" onClick={this.addNote}>+</button>
leanpub-start-delete
<Notes notes={notes}
onEdit={this.editNote}
onDelete={this.deleteNote} />
leanpub-end-delete
leanpub-start-insert
<AltContainer
stores={[NoteStore]}
inject={{
notes: () => NoteStore.getState().notes
}}
>
<Notes onEdit={this.editNote} onDelete={this.deleteNote} />
</AltContainer>
leanpub-end-insert
</div>
);
}
...
}
AltContainer
让我们绑定数据到直接的子节点.在本例中,它注入notes
属性到Notes
节点.这个模式让我们设置任意连接到多个存储并管理它们,你能在附录中找到其它的用法.
整合AltContainer
绑到Alt组件中.如果你想的更远.你能把它封装到一个你自已的组件中.这个模式可以让你在以后用别的东西替换它.
调度器在Alt
尽管你可以一直不使用Flux的调度器,但理解它是有帮助的.Alt提供两种方式使用它.如果你想记录通过alt
实例推进的所有事情,你可以使用一个代码段,如alt.dispatcher.register(console.log.bind(console))
.或者,你可以触发this.dispatcher.register(...)
在一个存储的构造函数中.这个机制让你实现有效的日志记录.
其它Flux的实现
尽管我们最终在我们的应用中使用了Alt,它不是响应一可选的.为了比较另种架构.我使用不同技术实现了这个相同的应用.下面是一些比较:
- Redux 是一个Flux风格的架构.主要特点是热加载.Redux操作基于单一的状态树.状态树是使用reducers管理的.尽管有一些重复代码.Redux迫使你去钻研函数式编程.实现与Alt很接近.
- 对比Redux,Cerebral起点不同.它支持.应用程序如何改变它的状态.
- Mobservable 可以使你的数据结构可观测.
Relay?
对比Flux,Facebook的Relay改善数据获取部分.它让你推送必要的数据到视图层.它可以单独使用或在Flux中使用.
总结
在这章你学会了如何使用Flux架构开发一个简单的应用.在这个过程中学习了Flux的基本概念.现在我们将在应用中加入更多的功能.