?系列文章目錄?
定義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.array
或types.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 "" // OKreturn `'${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); // truetodoList.editTarget = todoItem2; // todoItem2為id為'2'的TodoItem對象
console.log(getSnapshot(todoList).editTarget === '2'); // truetodoList.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 userget(identifier /* string */, parent: any /*Store*/) {return parent.users.find(u => u.name === identifier) || null},// given a user, produce the identifier that should be storedset(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提供的類型和類型方法非常齊全,利用好他們就能為任意數據定義恰當的類型。
喜歡本文的歡迎關注+收藏,轉載請注明出處,謝謝支持。