赵走x博客
网站访问量:151519
首页
书籍
软件
工具
古诗词
搜索
登录
49、Flux:理念、回顾Whinepad
48、lint、Flow、测试与复验:测试
47、lint、Flow、测试与复验:Flow
46、lint、Flow、测试与复验:ESLint
45、lint、Flow、测试与复验:package.json
44、构建实例应用:<Whinepad>
43、构建实例应用:应用配置
43、构建实例应用:<Excel>:改进的新版本
42、构建实例应用:组件:对话框
41、构建实例应用:组件:Actions
39、构建实例应用:表单:Form
38、构建实例应用:表单:<FormInput>“工厂组件”
37、构建实例应用:表单:Rating组件
36、构建实例应用:表单:Suggest
35、构建实例应用:Button组件
34、构建实例应用:组件
33、构建实例应用:Whinepad v.0.0.1
32、发布
31、开始构建
30、安装必备工具
29、为应用开发做准备:一个模板应用
28、JSX 和表单
27、JSX 和HTML 的区别
26、在JSX 中返回多个节点
25、展开属性
24、HTML 实体
23、JSX入门
22、Excel:一个出色的表格组件:下载表格数据
21、Excel:一个出色的表格组件:即时回放
20、Excel:一个出色的表格组件:搜索
19、Excel:一个出色的表格组件:编辑数据
18、Excel:一个出色的表格组件:排序
17、Excel:一个出色的表格组件
16、 PureRenderMixin
15、 性能优化:避免组件更新
14、 生命周期示例:使用子组件
13、组件生命周期示例:使用mixin
12、组件:生命周期方法
11、中途改变属性
10、从外部访问组件
9、在初始化state 时使用props:一种反模式
8、 props 与state
7、关于DOM 事件的说明
6、组件:带状态的文本框组件
5、组件的state
4、组件的propTypes
3、组件的属性
2、组件的基础
1、Hello World
50、Flux:Store
50、Flux:Store
资源编号:76104
书籍
React快速上手开发
热度:100
Flux 架构允许你创建多个Store(比如一个关于用户数据,另一个关于应用设置等),但目前我们只关注单个CRUD Store 的情况。这个Store 全权负责处理一个记录列表;在这个例子中,是关于酒类及其评价的记录。 这个CRUDStore 和React 本身没有联系,只需使用一个简单的JavaScript 对象即可实现
首先复制一份现有的代码: ``` $ cd ~/reactbook $ cp -r whinepad2 whinepad3 $ cd whinepad3 $ npm run watch ``` 接下来,创建一个新目录用于放置Flux 模块(以便与React 的UI 组件区分开)。目前只需创建Store 和Actions 两个模块: ``` $ mkdir js/source/flux $ touch js/source/flux/CRUDStore.js $ touch js/source/flux/CRUDActions.js ``` Flux 架构允许你创建多个Store(比如一个关于用户数据,另一个关于应用设置等),但目前我们只关注单个CRUD Store 的情况。这个Store 全权负责处理一个记录列表;在这个例子中,是关于酒类及其评价的记录。 这个CRUDStore 和React 本身没有联系,只需使用一个简单的JavaScript 对象即可实现: ``` /* @flow */ let data; let schema; const CRUDStore={ getData():Array
{ return data; }, getSchema():Array
{ return schema; } }; export default CRUDStore; ``` 如你所见,这个Store 负责维护单一的数据来源,比如本地模块变量data 和schema,并且可以把这些变量返回给任何需要的地方。此外,Store 还允许更新data 变量(但不更新scheam 变量,因为这是一个贯穿整个应用的常量): ``` setData(newData:Array
,commit:boolean=true){ data=newData; if(commit && 'localStorage' in window){ localStorage.setItem('data',JSON.stringify(newData)); } emitter.emit('change'); }, ``` 在上述代码中,除了更新本地的data 变量以外,Store 还更新了持久化存储中的数据;在这个例子中数据存储在localStorage,而在实际情况中还有可能是向服务器发起XHR 请求。这通常只会在“提交”情况下发生,因为没有必要总是更新持久化存储。比如在搜索时,你总是想要取得最新的数据,因此没有必要永久地保存这些数据。但是假如在调用setData() 后停电,你丢失了除搜索结果外的所有数据呢? 最后,你会看到一个change 事件被触发。(稍后你将了解更多内容。) Store 还可以提供一些有用的方法,比如获取数据的总行数,以及获取某一行记录: ``` getCount():number{ return data.length; }, getRecord(recordId:number):?Object{ return recordId in data?data[recordId]:null; } ``` 在应用初始化时,你需要初始化Store。在此之前初始化工作通过app.js 进行,但现在Store 是处理数据的唯一地方,因此将数据初始化交给Store 自行处理: ``` init(initialSchema:Array
){ schema=initialSchema; const storage="localStorage" in window?localStorage.getItem('data'):null; if(!storage){ data=[{}]; schema.forEach(item=>data[0][item.id]=item.sample); }else{ data=JSON.parse(storage); } }, ``` 现在app.js 可以通过以下方式启动应用: ``` // ... import CRUDStore from './flux/CRUDStore'; import Whinepad from './components/Whinepad'; import schema from './schema'; CRUDStore.init(schema); ReactDOM.render(
{/* 更多JSX代码 */}
{/* ... */} ); ``` 如你所见,一旦Store 被初始化,
组件就不需要再接收任何属性了。组件所需的数据可以通过CRUDStore.getData() 方法获取,而数据的描述可以通过CRUDStore.getSchema() 获取。 你可能会问:为何Store 不能自己读取数据,而是依赖于外部传入的schema对象?其实,你当然可以直接在Store 中导入schema 模块,但让应用自身处理schema 的来源更为合理。试想schema 是否为一个模块?若直接导入是否属于硬编码?既然是模块,是否应该由用户定义? # 1、Store 事件 还记得之前在Store 更新数据时调用的emitter.emit('change'); 方法吗?这个方法用于通知对数据感兴趣的UI 模块,以便模块在数据发生变化时从Store 中读取最新的数据,进行自我更新。那么,这个事件触发机制到底是如何实现的呢? 实现事件订阅的模式有很多。从本质上说,这些模式需要收集数据的一系列相关者(订阅者),然后在事件发生时“推送”消息,调用每个订阅者的回调函数(订阅者在订阅事件时,需要提供这个回调函数)。 为简单起见,我们使用一个名为fbemitter 的小型开源库来实现事件订阅功能: ``` $ npm i --save-dev fbemitter ``` 更新.flowconfig 文件: ``` [ignore] .*/fbemitter/node_modules/.* # 省略部分代码 [include] node_modules/classnames node_modules/fbemitter # 省略部分代码 ``` 在Store 模块的开端,需要导入并初始化事件emitter: ``` /* @flow */ import {EventEmitter} from 'fbemitter'; let data; let schema; const emitter = new EventEmitter(); const CRUDStore = { // ... }; export default CRUDStore ``` 这个emitter 需要负责两项任务: • 收集订阅; • 通知订阅者(正如之前看到的那样,在setData() 方法中调用emitter.emit('change'))。 在Store 中,还可以把收集订阅功能作为一个方法暴露出来,让调用者无需了解其实现细节: const CRUDStore = { // ... addListener(eventType: string, fn: Function) { emitter.addListener(eventType, fn); }, // ... }; 至此,这个CRUDStore 的功能已经完备了。 8.3.2 在
中使用Store 借助Flux 实现
组件相当简单,因为其中的大部分功能将迁移到CRUDActions 中实现(稍后介绍),但CRUDStore 也发挥了不少作用。我们将不再需要维护this.state. data。之前需要维护它的原因仅仅是需要把状态传递给
组件,但现在
组 件可以通过Store 取得数据了。事实上,
组件甚至不需要处理Store。我们不妨 添加一个需要用到Store 的功能:在搜索域中显示记录总数(如图8-2 所示)。 Flux | 171 图8-2:在搜索区域中显示记录总数 之前,在
组件的constructor() 构造函数中,需要对state 进行初始化: this.state = { data: props.initialData, addnew: false, }; 现在不再需要data 属性了,但你仍需一个count 变量记录数据总计数。因此在初始化时, 需要从Store 中读取数据: /* @flow */ // ... import CRUDStore from '../flux/CRUDStore'; // ... class Whinepad extends Component { constructor() { super(); this.state = { addnew: false, count: CRUDStore.getCount(), }; } /* ... */ } export default Whinepad 此外,在构造函数中还需要订阅Store 的数据变化事件,以便在数据变化时有机会更新 this.state 中的数据总计数: constructor() { super(); this.state = { 172 | 第8 章 addnew: false, count: CRUDStore.getCount(), }; CRUDStore.addListener('change', () => { this.setState({ count: CRUDStore.getCount(), }) }); } 以上就是组件和Store 之间需要进行的所有交互。无论Store 中的数据在任何时候、通过 任何方式发生变化(包括调用CRUDStore 中的setData() 方法时),Store 都会触发一个 change 事件。
组件会监听这个change 事件并更新自身状态。你已经知道,设 置状态将会导致组件重新渲染,因此render() 方法会再次被调用。该方法中的逻辑和以往 一样,仅仅是基于state 和props 组合UI: render() { return ( {/* ... */}
{/* ... */} ); } 此外,还可以通过在
组件中实现shouldComponentUpdate() 方法来优化性能。 由于数据的改变不一定会影响数据的总条数(比如编辑一条记录或者编辑记录中的某个字 段),组件在这种情况下不需要重新渲染: shouldComponentUpdate(newProps: Object, newState: State): boolean { return ( newState.addnew !== this.state.addnew || newState.count !== this.state.count ); } 最后,
组件不再需要把data 和schema 传递到
组件中了。同样也不需 要设置onDataChange 回调函数,因为现在所有的数据改变都可以从Store 的change 事件中 获知。因此,
组件中的render() 方法只需要这样写: render() { return ( {/* ... */}
Flux | 173
{/* ... */} ); } 8.3.3 在
中使用Store 和
组件类似,
组件也不再需要接收属性了。构造函数从Store 中读取 schema,并赋值给this.schema。事实上,this.state.schema 和this.schema 之间并没有实 质区别,只不过state 意味着变量可能会发生某些变化,而schema 是一个常量。 至于数据,现在只需要在初始化时从Store 中读取并记录在this.state.data 中即可,不再 通过属性接收。 最后,构造函数订阅了Store 的change 事件,以便state 中的数据保持最新(并触发重新 渲染): constructor() { super(); this.state = { data: CRUDStore.getData(), sortby: null, // schema.id descending: false, edit: null, // {row index, schema.id}, dialog: null, // {type, idx} }; this.schema = CRUDStore.getSchema(); CRUDStore.addListener('change', () => { this.setState({ data: CRUDStore.getData(), }) }); } 得益于Store,
组件需要做的就是这么简单。render() 方法和之前一样,只需从 this.state 中读取并呈现数据即可。 也许你会感到疑惑: 为何需要把Store 中的数据复制到this.state 中? 是否可以在 render() 方法中直接访问Store 中的数据呢?虽然理论上是可行的,但这样做会使组件会 变得“不纯”。还记得2.15 节提到过纯渲染组件(pure render component)的概念吗?这意 味着组件渲染的内容仅由props 和state 决定。在render() 方法中,任何函数调用看起来 都是不可靠的——你不能确定在一个额外的函数调用中会返回什么内容。应用还会变得难 以调试、难以预测:“为什么state 中的数据是1,而渲染出的内容是2 呢?哦,原来是在 render() 方法中进行了函数调用。” 174 | 第8 章 8.3.4 在
中使用Store 在此之前,表单组件同样需要获取schema(用于获取fields 属性)以及defaultValues 属 性(用于预填充表单或者显示一份只读版本)。目前这两者都已经转移到了Store 中。因此 现在表单只需要接收一个recordId 属性,并根据该属性在Store 中查找具体数据: /* @flow */ import CRUDStore from '../flux/CRUDStore'; // ... type Props = { readonly?: boolean, recordId: ?number, }; class Form extends Component { fields: Array
; initialData: ?Object; constructor(props: Props) { super(props); this.fields = CRUDStore.getSchema(); if ('recordId' in this.props) { this.initialData = CRUDStore.getRecord(this.props.recordId); } } // ... } export default Form 表单组件不需要注册Store 的change 事件,因为在编辑表单时不需要监听数据的变化。但 是可能出现这样的场景:另一个用户在同时进行编辑;同一个用户在不同的标签页中打开 了这个应用,并在两边都编辑了数据。如果需要处理这些情况,你就需要监听数据的变 化,并提醒用户:数据在其他地方正在被编辑。 8.3.5 界定 该如何界定是使用Flux Store 还是使用借助属性的非Flux 实现呢? Store 为所有的数据需 求提供了一站式服务,使你从属性传递中解脱出来,但与此同时也降低了组件的可重用 性。现在,你不能在另一个完全不同的场景中直接重用Excel 组件了,因为在组件中从 CRUDStore 获取数据的逻辑已经被硬编码。即便如此,只要新的场景中使用了类似CRUD 的逻辑(这是有可能的,否则你为何需要可编辑的数据表格呢?),你还是可以让组件从 Store 中获取数据。谨记一点:在应用中可以根据需要使用任意数量的Store。 Flux | 175 对于那些底层组件,比如按钮和表单输入元素,最好不要使用Store。因为对于这些组 件来说,使用传递属性的方式更加方便。那些处于两个极端间的组件类型都属于灰色 地带[从最简单的按钮(比如
)到最顶层负责管理所有内容的父组件(比如
)],由你来界定是否使用Flux。
组件是应该像之前那样连接到CRUD Store 还是应该和Store 隔离使其可重用呢?你可以根据手头上的任务以及将来重用该组件 的可能性,作出最合适的选择。