[MobX State Tree数据组件化开发][3]:选择正确的types.xxx

👉系列文章目录👈

定义Model时,须要正确地定义props中各字段的类型。本文将对MST提供的各类类型以及类型的工厂方法进行简单的介绍,方便同窗们在定义props时挑选正确的类型。react

前提

定义props以前,有一个前提是,你已经明确地知道这个Model中状态的数据类型。后端

若是Model用于存放由后端API返回的数据,那么必定要和后端确认返回值在全部状况下的类型。好比,某个字段在没有值的时候你觉得会给一个'',然后端却给了个null;或者某个数组你觉得会给你一个空数组[],他又甩你一个null若是你不打算本身编写一个标准化数据的方法,那必定要和数据的提供方肯定这些细节。数组

基础类型

types.string

定义一个字符串类型字段。安全

types.number

定义一个数值类型字段。app

types.boolean

定义一个布尔类型字段。ide

types.integer

定义一个整数类型字段。post

注意,即便是TypeScript中也没有“整数”这个类型,在编码时,传入一个带小数的值TypeScript也没法发现其中的类型错误。如无必要,请使用types.numberui

types.Date

定义一个日期类型字段。编码

这个类型存储的值是标准的Date对象。在设置值时,能够选择传入数值类型的时间戳或者Date对象。spa

export const Model = types
    .model({
        date: types.Date 
    })
    .actions(self => ({
        setDate (val: Date | number) {
            self.date = date;
        }
    }));
复制代码

types.null

定义一个值为null的类型字段。

types.undefined

定义一个值为undefined的类型字段。

复合类型

types.model

定义一个对象类型的字段。

types.array

定义一个数组类型的字段。

types.array(types.string);
复制代码

上面的代码定义了一个字符串数组的类型。

types.map

定义一个map类型的字段。该map的key都为字符串类型,map的值都为指定类型。

types.map(types.number);
复制代码

可选类型,types.optional

根据传入的参数,定义一个带有默认值的可选类型。

types.optional是一个方法,方法有两个参数,第一个参数是数据的真实类型,第二个参数是数据的默认值。

types.optional(types.number, 1);
复制代码

上面的代码定义了一个默认值为1的数值类型。

注意,types.array或者types.map定义的类型自带默认值(array为[],map为{}),也就是说,下面两种定义的结果是同样的:

// 使用types.optional
types.optional(types.array(types.number), []);
types.optional(types.map(types.number), {});

// 不使用types.optional
types.array(types.number);
types.map(types.number);
复制代码

若是要设置的默认值与types.arraytypes.map自带的默认值相同,那么就不须要使用types.optional

自定义类型,types.custom

若是想控制类型更底层的如序列化和反序列化、类型校验等细节,或者根据一个class或interface来定义类型,可使用types.custom定义自定义类型。

class Decimal {
    ...
}

const DecimalPrimitive = types.custom<string, Decimal>({
    name: "Decimal",
    fromSnapshot(value: string) {
        return new Decimal(value)
    },
    toSnapshot(value: Decimal) {
        return value.toString()
    },
    isTargetType(value: string | Decimal): boolean {
        return value instanceof Decimal
    },
    getValidationMessage(value: string): string {
        if (/^-?\d+\.\d+$/.test(value)) return "" // OK
        return `'${value}' doesn't look like a valid decimal number`
    }
});
复制代码

上面的代码定义了一个Decimal类型。

联合类型,types.union

实际开发中也许会遇到这样的状况:一个值的类型多是字符串,也多是数值。那咱们就可使用types.union定义联合类型:

types.union(types.number, types.string);
复制代码

联合类型能够有任意个联合的类型。

字面值类型,types.literal

字面值类型能够限制存储的内容与给定的值严格相等。

好比使用types.literal('male')定义的状态值只能为'male'

实际上,上面提到过的types.null以及types.undefined就是字面值类型:

const NullType = types.literal(null);
const UndefinedType = types.literal(undefined);
复制代码

搭配联合类型,能够这样定义一个性别类型:

const GenderType = types.union(types.literal('male'), types.literal('female'));
复制代码

枚举类型,types.enumeration

枚举类型能够看做是联合类型以及字面值类型的一层封装,好比上面的性别可使用枚举类型来定义:

const GenderType = types.enumeration('Gender', ['male', 'female']);
复制代码

方法的第一个参数是可选的,表示枚举类型的名称。第二个参数传入的是字面值数组。

在TypeScript环境下,能够这样搭配TypeScript枚举使用:

enum Gender {
    male,
    female
}

const GenderType = types.enumeration<Gender>('Gender', Object.values(Gender));
复制代码

可undefined类型,types.maybe

定义一个可能为undefined的字段,并自带默认值undefined

types.maybe(type)
// 等同于
types.optional(types.union(type, types.literal(undefined)), undefined)
复制代码

可空类型,types.maybeNull

types.maybe相似,将undefined替换成了null

types.maybeNull(type)
// 等同于
types.optional(types.union(type, types.literal(null)), null)
复制代码

不可不类型,types.frozen

frozen意为“冻结的”,types.frozen方法用来定义一个immutable类型,而且存放的值必须是可序列化的。

当数据的类型不肯定时,在TypeScript中一般将值的类型设置为any,而在MST中,就须要使用types.frozen定义。

const Model = types
    .model('Model', {
        anyData: types.frozen()
    })
    .actions(self => ({
        setAnyData (data: any) {
            self.anyData = data;
        }
    }));
复制代码

在MST看来,使用types.frozen定义类型的状态值是不可变的,因此会出现这样的状况:

model.anyData = {a: 1, b: 2}; // ok, reactive
model.anyData.b = 3; // not reactive
复制代码

也就是只有设置一个新的值给这个字段,相关的observer才会响应状态的更新。而修改这个字段内部的某个值,是不会被捕捉到的。

滞后类型,types.late

有时候会出现这样的需求,须要一个Model A,在A中,存在类型为A自己的字段。

若是这样写:

const A = types
    .model('A', {
        a: types.maybe(A), // 使用mabe避免无限循环
    });
复制代码

会提示Block-scoped variable 'A' used before its declaration,也就是在A定义完成以前就试图使用他,这样是不被容许的。

这个时候就须要使用types.late

const A = types
  .model('A', {
    a: types.maybe(types.late(() => A))
  });
复制代码

types.late须要传入一个方法,在方法中返回A,这样就能够避开上面报错的问题。

提纯类型,types.refinement

types.refinement能够在其余类型的基础上,添加额外的类型校验规则。

好比须要定义一个email字段,类型为字符串但必须知足email的标准格式,就能够这样作:

const EmailType = types.refinement(
    'Email',
    types.string,
    (snapshot) => /^[a-zA-Z_1-9]+@\.[a-z]+/.test(snapshot), // 校验是否符合email格式
);
复制代码

引用与标识类型

拿上一篇文章中的TodoList做为例子,咱们在对Todo列表中的某一个Todo进行编辑的时候,须要经过id跟踪这个Todo,在提交编辑结果时,经过这个id找到对应的Todo对象,而后进行更新。

这种须要跟踪、查找的需求很常见,写多了也以为麻烦。

好在MST提供了一个优雅的解决方案:引用类型和标识类型。

这二者须要搭配使用才能发挥做用:

定义标识,types.identifier

标识就是数据对象的惟一标识字段,这个字段的值在库中保持惟一,也就是primary_key。

好比上一篇文章中的TodoItem,能够改造为:

export const TodoItem = types
  .model('TodoItem', {
    id: types.identifier,
    title: types.string,
    done: types.boolean,
  });
复制代码

使用引用类型进行跟踪,types.reference

改造TodoList:

export const TodoList = types
  .model('TodoList', {
    ...
    list: types.array(TodoItem),
    editTarget: types.reference(TodoItem),
    ...
  });
复制代码

而后在建立Model实例,或者applySnapshot的时候,能够将editTarget的值设定为正在编辑的TodoItem的id值,MST就会自动在list中查找id相同的TodoItem:

const todoList = TodoList.create({
   list: [
       {id: '1', title: 'Todo 1', done: true},
       {id: '2', title: 'Todo 2', done: true},
       ...
   ],
   editTarget: '1'
});

//此时的editTarget就是list中id为'1'的TodoItem对象
console.log(todoList.list[0] === todoList.editTarget); // true

todoList.editTarget = todoItem2; // todoItem2为id为'2'的TodoItem对象
console.log(getSnapshot(todoList).editTarget === '2'); // true

todoList.editTarget = '2' as any;
console.log(getSnapshot(todoList).editTarget === '2'); // true
复制代码

上面的代码说明,reference类型的字段本质上维护的是目标的标识字段值,而且,除了将目标对象赋值给reference字段外,将目标标识字段值赋值给reference字段的效果是同样的。

另外,reference不只仅能搭配array使用,也能在map中查找:

const TodoList = types.model('TodoList', {
    todoMap: types.map(TodoItem),
    editTarget: types.reference(TodoItem)
});
复制代码

甚至,MST也容许你自定义查找器(resolver),给types.reference指定第二个参数,好比官网的这个例子:

const User = types.model({
    id: types.identifier,
    name: types.string
})

const UserByNameReference = types.maybeNull(
    types.reference(User, {
        // given an identifier, find the user
        get(identifier /* string */, parent: any /*Store*/) {
            return parent.users.find(u => u.name === identifier) || null
        },
        // given a user, produce the identifier that should be stored
        set(value /* User */) {
            return value.name
        }
    })
)

const Store = types.model({
    users: types.array(User),
    selection: UserByNameReference
})

const s = Store.create({
    users: [{ id: "1", name: "Michel" }, { id: "2", name: "Mattia" }],
    selection: "Mattia"
})
复制代码

types.identifierNumber

若对象的惟一标识字段的值为数值类型,那么可使用types.identifierNumber代替types.identifier

types.safeReference

这是一个“安全”的引用类型:

const Todo = types.model({ id: types.identifier })
const Store = types.model({
    todos: types.array(Todo),
    selectedTodo: types.safeReference(Todo)
});
复制代码

selectedTodo引用的目标从todos这个节点被移除后,selectedTodo会自动被设置为undefined

小结

MST提供的类型和类型方法很是齐全,利用好他们就能为任意数据定义恰当的类型。

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

相关文章
相关标签/搜索