[MobX State Tree数据组件化开发][2]:实例-TodoList

栏目: JavaScript · 发布时间: 5年前

内容简介:上一篇文章MST基础中简单地介绍了MST的几个基本概念和相关API,本文将带大家搭配React实现一个TodoList。为了省去枯燥的项目搭建过程,本文选择使用

上一篇文章MST基础中简单地介绍了MST的几个基本概念和相关API,本文将带大家搭配React实现一个TodoList。

准备工作

为了省去枯燥的项目搭建过程,本文选择使用 stackblitz 平台来编辑我们的代码。

TS+React+MST Starter

同学们可以点击上面的地址fork一个starter项目,项目中已经配置好MST以及React相关的依赖,并且包含了一个简单的Counter demo,后面将在这个starter的基础上进行开发。

项目结构&规范说明

从上面的地址进入后,你会得到一个包含以下目录结构的初始项目。

[MobX State Tree数据组件化开发][2]:实例-TodoList

其中,目录 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的静态类型约束覆盖到整个应用,开发过程中可以享受到类型带来的便利:

[MobX State Tree数据组件化开发][2]:实例-TodoList

ModelInjector 组件的实现比较简单,可以在项目中自行查看。

确定目标

在开始动手编码之前,必须明确要做的这个东西是什么样的。

我们要做的这款TodoList大家应该比较熟悉:

[MobX State Tree数据组件化开发][2]:实例-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。并提供了用于关联 TodoItemsetTarget 方法,更新值的 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),
  });
复制代码

其中 adderFormeditorForm 分别表示 新增Todo编辑Todo 的表单Model, list 用于管理Todo列表。

仔细观察目标的成品图,他还包括三个筛选按钮 AllActiveCompleted ,用于筛选展现的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
  • 可组合
  • 可复用

当然,本文只是一个开胃菜,还有更多优雅的特性等待后面的文章中慢慢去挖掘。

喜欢本文欢迎关注和收藏,转载请注明出处,谢谢支持。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

雷军

雷军

蔡艳鹏 / 2012-12 / 29.80元

《雷军:人因梦想而伟大》内容简介:人生充满着期待,梦想连接着未来。雷军一直有个梦,就是建一个受世人尊敬的企业。他不仅建立了属于自己的受人尊敬的企业,也在帮助别人实现心中的梦想。雷军可以说是创业者、职场人奋斗的榜样,从他在金山的不折不挠,在投资界的百投百中,到小米的成功……无不充满传奇,让无数人争相效仿。一起来看看 《雷军》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

URL 编码/解码
URL 编码/解码

URL 编码/解码

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具