[MobX State Tree資料元件化開發][3]:選擇正確的types.xxx

awaw00發表於2019-01-31

?系列文章目錄?

定義Model時,需要正確地定義props中各欄位的型別。本文將對MST提供的各種型別以及型別的工廠方法進行簡單的介紹,方便同學們在定義props時挑選正確的型別。

前提

定義props之前,有一個前提是,你已經明確地知道這個Model中狀態的資料型別。

如果Model用於存放由後端API返回的資料,那麼一定要和後端確認返回值在所有情況下的型別。比如,某個欄位在沒有值的時候你以為會給一個'',而後端卻給了個null;或者某個陣列你以為會給你一個空陣列[],他又甩你一個null如果你不打算自己編寫一個標準化資料的方法,那一定要和資料的提供方確定這些細節。

基礎型別

types.string

定義一個字串型別欄位。

types.number

定義一個數值型別欄位。

types.boolean

定義一個布林型別欄位。

types.integer

定義一個整數型別欄位。

注意,即使是TypeScript中也沒有“整數”這個型別,在編碼時,傳入一個帶小數的值TypeScript也無法發現其中的型別錯誤。如無必要,請使用types.number

types.Date

定義一個日期型別欄位。

這個型別儲存的值是標準的Date物件。在設定值時,可以選擇傳入數值型別的時間戳或者Date物件。

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定義完成之前就試圖使用他,這樣是不被允許的。

[MobX State Tree資料元件化開發][3]:選擇正確的types.xxx

這個時候就需要使用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提供的型別和型別方法非常齊全,利用好他們就能為任意資料定義恰當的型別。

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

相關文章