[MobX State Tree数据组件化开发][2]:实例-TodoList
栏目: JavaScript · 发布时间: 5年前
内容简介:上一篇文章MST基础中简单地介绍了MST的几个基本概念和相关API,本文将带大家搭配React实现一个TodoList。为了省去枯燥的项目搭建过程,本文选择使用
上一篇文章MST基础中简单地介绍了MST的几个基本概念和相关API,本文将带大家搭配React实现一个TodoList。
准备工作
为了省去枯燥的项目搭建过程,本文选择使用 stackblitz
平台来编辑我们的代码。
同学们可以点击上面的地址fork一个starter项目,项目中已经配置好MST以及React相关的依赖,并且包含了一个简单的Counter demo,后面将在这个starter的基础上进行开发。
项目结构&规范说明
从上面的地址进入后,你会得到一个包含以下目录结构的初始项目。
其中,目录 components
用于存放React组件,目录 models
用于存放MST Model。
整个应用的Root Model在 models/index.ts
文件中定义:
import { types } from 'mobx-state-tree'; import { Counter } from './Counter'; export const Root = types .model('Root', { counter: types.optional(Counter, {}), }); 复制代码
定义好的Root Model会在项目的入口 index.tsx
文件中被引入,并创建实例对象,然后使用 mobx-react
提供的 Provider
组件将Root Model的实例对象传递到应用的 Context
中:
import React from 'react'; import { render } from 'react-dom'; import { Provider } from 'mobx-react'; import { Root } from './models'; import { ModelInjector } from './components/ModelInjector'; import './style.css'; import { Counter } from './components/Counter'; const root = Root.create({}); const ConnectedCounter = () => ( <ModelInjector> {(root) => <Counter model={root.counter}/>} </ModelInjector> ); function App () { return ( <Provider root={root}> <ConnectedCounter/> </Provider> ); } render(<App />, document.getElementById('root')); 复制代码
项目提供了一个名为 ModelInjector
的组件, index.tsx
代码中,使用 ModelInjector
组件对 Counter
组件进行了一个包装,将 root.counter
这个节点Model作为props传给了 Counter
组件。在本文以及后续的文章中,将会沿用这样的方式在组件与Model之间建立连接。
这样做的好处是,可以让TypeScript的静态类型约束覆盖到整个应用,开发过程中可以享受到类型带来的便利:
ModelInjector
组件的实现比较简单,可以在项目中自行查看。
确定目标
在开始动手编码之前,必须明确要做的这个东西是什么样的。
我们要做的这款TodoList大家应该比较熟悉:
这款TodoList来自TodoMVC。
由于本文的主题不在UI的实现上,我们可以复用他的DOM结构和CSS,这会省去不少功夫。
定义Model
明确要做什么之后,就可以着手开始分析这个应用的状态结构了。
TodoItem
从最基础的开始。TodoList的基本单位就是TodoItem,TodoItem具备的属性是他的 id
(用于编辑、删除时进行跟踪)、 title
,以及是否完成的标识 done
,所以可以得出:
// models/TodoItem.ts import { types, Instance } from 'mobx-state-tree'; export const TodoItem = types .model('TodoItem', { id: types.string, title: types.string, done: types.boolean, }) .actions(self => ({ switchDone(done?: boolean) { if (typeof done === 'boolean') { self.done = done; } else { self.done = !self.done; } } })); export type TodoItemInstance = Instance<typeof TodoItem>; 复制代码
新建 models/TodoItem.ts
文件,写入上面的代码。
细心的同学会发现,上面代码中还export了一个type定义 TodoItemInstance
。这个type表示的是 TodoItem
这个Model的实例类型,可以在定义React组件的props类型时使用。
TodoForm
应用还需要一个输入框,在新增的时候输入新TodoItem的title;以及一个可隐藏的输入框用来编辑已有TodoItem的title。
这两个输入框的功能相似,都是维护输入框的值并处理值的更新。不同的是 编辑输入框
会与某一个TodoItem关联,而 新增输入框
没有关联对象。
可以使用一个 TodoForm
的Model来维护两个输入框的状态:
// models/TodoForm.ts import { types, Instance } from 'mobx-state-tree'; import { TodoItemInstance } from './TodoItem'; export const TodoForm = types .model('TodoForm', { value: types.optional(types.string, ''), targetTodoId: types.optional(types.maybeNull(types.string), null), }) .views(self => ({ get trimedValue () { return self.value.trim(); }, get valid() { return this.trimedValue.length > 0; } })) .actions(self => ({ setTarget(target: TodoItemInstance) { self.value = target.title; self.targetTodoId = target.id; }, update(value: string) { self.value = value; }, reset() { self.value = ''; self.targetTodoId = null; } })); export type TodoFormInstance = Instance<typeof TodoForm>; 复制代码
TodoForm
中,使用 value
维护输入框的值, targetTodoId
表示当前关联的 TodoItem
的id。并提供了用于关联 TodoItem
的 setTarget
方法,更新值的 update
方法以及重置状态的 reset
方法。
这里使用了 types.optional
为状态设置了初始值。
另外还提供了两个计算值: trimedValue
以及 valid
。
这里需要注意的是,在 valid
的定义中, trimedValue
引用的是 this
而不是 self
,这是由于 valid
以及 trimedValue
两者的定义写在同一个 views
方法中, 在 views
方法结束前,TypeScript的类型系统并不能观察到 self
对应的类型中包含 valid
或者 trimedValue
,所以需要使用 this
来代替 self
。
除了 views
之外, actions
或者 volatile
也需要注意上面这个问题。
TodoList
完成上面的两个Model之后,剩下的都是一些与列表相关的状态了。将TodoItem与TodoForm进行组合,构成整个TodoList应用的基本Model:
// models/TodoList.ts import { types } from 'mobx-state-tree'; import { TodoItem } from './TodoItem'; import { TodoForm } from './TodoForm'; export const TodoList = types .model('TodoList', { adderForm: types.optional(TodoForm, {}), editorForm: types.optional(TodoForm, {}), list: types.array(TodoItem), }); 复制代码
其中 adderForm
与 editorForm
分别表示 新增Todo
与 编辑Todo
的表单Model, list
用于管理Todo列表。
仔细观察目标的成品图,他还包括三个筛选按钮 All
、 Active
、 Completed
,用于筛选展现的Todo列表的类型,这里可以将三种类型定义为枚举 TodoFilterType
,新建 enums.ts
文件,输入代码:
// enums.ts export enum TodoFilterType { All = 'All', Active = 'Active', Completed = 'Completed' } 复制代码
然后为 TodoList
新增一个 filterType
的状态:
// models/TodoList.ts import { types } from 'mobx-state-tree'; import { TodoItem } from './TodoItem'; import { TodoForm } from './TodoForm'; import { TodoFilterType } from '../enums'; export const TodoList = types .model('TodoList', { adderForm: types.optional(TodoForm, {}), editorForm: types.optional(TodoForm, {}), list: types.array(TodoItem), filterType: types.optional(types.string, TodoFilterType.All), }); 复制代码
有了这几个基础状态,就可以得到其他几个衍生状态:
// models/TodoList.ts ... export const TodoList = types .model('TodoList', { ... }) .views(self => ({ // 已完成的Todo列表 get doneList() { return self.list.filter(i => i.done); }, // 未完成的Todo列表 get activeList() { return self.list.filter(i => !i.done); }, // 是否全部完成 get isAllDone() { return this.doneList.length === self.list.length; }, // 剩余未完成的Todo数量 get activeCount() { return this.activeList.length; }, // 当前展现的Todo列表 get showList() { switch (self.filterType) { case TodoFilterType.Active: return this.activeList; case TodoFilterType.Completed: return this.doneList; default: return self.list; } }, // 是否显示主体UI(没有Todo数据的时候只显示一个新增输入框) get isShowMain() { return self.list.length > 0; }, // 是否包含已完成的Todo,用于控制右下角[Clear completed]按钮的展现和隐藏 get hasDoneTodos() { return this.doneList.length > 0; } })) 复制代码
最后,再补上更新状态的actions:
// models/TodoList.ts import { types, cast, Instance } from 'mobx-state-tree'; import uuid from 'uuid/v1'; ... export const TodoList = types .model('TodoList', { ... }) .views(self => ({ ... }) .actions(self => ({ // 切换全部完成/全部未完成 switchAllDone(done?: boolean) { if (typeof done !== 'boolean') { done = !self.isAllDone; } self.list.forEach(item => { item.switchDone(done); }); }, // 切换列表过滤类型 setFilterType(filterType: TodoFilterType) { self.filterType = filterType; }, // 新增Todo addTodo() { if (self.adderForm.valid) { self.list.push(cast({ id: uuid(), title: self.adderForm.trimedValue, done: false })); self.adderForm.reset(); } }, // 更新Todo updateTodo() { if (self.editorForm.valid) { const item = self.list.find(i => i.id === self.editorForm.targetTodoId); if (item) { item.title = self.editorForm.trimedValue; } self.editorForm.reset(); } }, // 删除Todo removeTodo(todoId: string) { const index = self.list.findIndex(i => i.id === todoId); if (index >= 0) { self.list.splice(index, 1); } }, // 清除已完成Todos clearDone() { self.list = cast(self.list.filter(i => !i.done)); } })); export type TodoListInstance = Instance<typeof TodoList>; 复制代码
上面的代码在给一些状态赋值的时候,用到了MST提供的 cast
方法,这个方法仅在TypeScript中有意义,因为他仅仅是将入参的类型转换成对应的状态的类型,使得代码的类型能通过TypeScript的检测(因为在TypeScript看来,没有cast的时候,等号左侧和右侧的两个值并不是类型匹配的)。
另外,在新增Todo的时候,使用了 uuid
库提供的方法生成Todo的唯一id。注意在项目中安装 uuid
依赖。
本实例中还依赖了 classnames
库,也需要一并安装。由于 uuid
以及 classnames
库都不包含类型定义文件(*.d.ts),在项目中新增了一个 modules.d.ts
文件,代码如下:
// modules.d.ts declare module 'classnames'; declare module 'uuid/v1'; 复制代码
更新Root
要在应用中使用上面定义的Model,还需要将他们加入到状态树中,更新 models/index.ts
文件:
// models/index.ts import { types } from 'mobx-state-tree'; import { TodoList } from './TodoList'; export const Root = types .model('Root', { todoList: types.optional(TodoList, {}), }); 复制代码
至此,这个TodoList实例的状态树就构造完成了。
实现UI组件
UI方面并不是本系列文章的重点,并且本文TodoList的UI实现比较简单,套用了TodoMVC的DOM结构和CSS,本文中只对几个关键的点做一下说明,完整的代码见文末。
尽可能地使用observer装视组件
使用 mobx-react
包提供的 observer
装饰器装饰后的组件能响应observable的变化,并做了诸多的性能优化,尽可能为你的组件加上observer,除非你想要自定义 shouldComponentUpdate
来控制组件更新时机。
尽可能地编写无状态组件
有的同学可能看到过类似这样的说法:
用Redux管理全局状态,用组件State管理局部状态。
笔者不认同这种说法,根据笔者的经验来看,当项目复杂到一定的程度,使用State管理的状态会难受到让你抓狂: 某个深层次的组件State只能通过改变上层组件传递的props来进行更新 。
更何况,现在无状态(state less)组件越来越受到大家的认可,react hooks的出现也顺应了组件无状态的这个发展趋势。
当应用都由无状态组件构成,应用的状态都存储在触手可及的地方(如Redux或MST),想要在某些时刻修改某个状态值就变得轻而易举。
这也是上文中,将输入框的值维护在 TodoForm
中的一个重要原因。
在线运行&完整代码
完整代码可点击此处查看,或者直接查看运行结果。
小结
本文使用MST搭配React构建了一个完整的TodoList应用,不知道同学们有没有体会到MST的魅力:
- 简单
- 最小化State
- 可组合
- 可复用
当然,本文只是一个开胃菜,还有更多优雅的特性等待后面的文章中慢慢去挖掘。
喜欢本文欢迎关注和收藏,转载请注明出处,谢谢支持。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 组件化之路—集成组件SDK
- Android组件化入门:一步步搭建组件化架构
- Android快速开发框架,基础库,样式库,组件化,组件集成
- Android组件化方案及组件消息总线modular-event实战
- 组件化实践
- 组件化架构漫谈
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。