[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提供的類型和類型方法很是齊全,利用好他們就能爲任意數據定義恰當的類型。

喜歡本文的歡迎關注+收藏,轉載請註明出處,謝謝支持。

相關文章
相關標籤/搜索