?系列文章目錄?
定義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 "" // 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提供的型別和型別方法非常齊全,利用好他們就能為任意資料定義恰當的型別。
喜歡本文的歡迎關注+收藏,轉載請註明出處,謝謝支援。