第一部分:前端資料層的探索與實踐(一)
第二部分:前端資料層的探索與實踐(二)
從實踐的角度談Redux-ORM概念
Model
資料模型是Redux-ORM的核心。根據實際業務,我們會定義很多的資料模型,通過定義模型的靜態屬性欄位field對實體進行建模。一個模型代表一張表,模型的名字用靜態屬性modelName定義,模型的屬性用靜態屬性field定義,這些資料模型都繼承於Model。模型的屬性field可能是純屬性,也可能是指向另一張表的關係屬性,通常有三種關係:一對多fk,一對一oneToOne,多對多many。
我們說一個模型代表一張表,那麼一個模型例項我們可以認為這就是資料庫中的一條記錄。但模型例項並不是我們真正的底層物件,它只是一個由屬性items/itemById組成的字面量物件,要訪問真正的底層物件應使用ref屬性。
在reducer中對Model進行操作時,Redux-ORM會把action放入佇列,直到呼叫session.state才會讓佇列中的action順序執行,直到得到最終結果。
ORM
關係物件對映器。在ORM上註冊Model,使用ORM生成session。在整個應用中,ORM通常是以單例的形式存在。在註冊Model時,Redux-ORM會判斷Model是否有多對多關係,如果有,會自動生成穿越模型(through models),這就像資料庫中的關係表,裡面存放著關聯條目的id和這條對應關係本身的id。
Session
Session用於與模型類資料進行互動。也就是說在對資料進行增刪改查時,通常要使用模型來操作,此時我們想要獲取Redux-ORM中的模型,就一定要從session例項中提取對應的模型例項,而不要直接從定義Model類的模組中匯入,在操作完成後,要返回當前session例項的資料庫狀態state,以更新store。建立session例項,通常用orm.session(state)。如果在模型類中定義reducer,那麼session會以第四個引數傳入,前三個引數分別是state/payload/當前模型類的繫結版本。
實踐,真實的應用
先看一下實現效果,順便貼上程式碼庫地址:redux-orm-dva
用我自己的理解,我認為實踐應該有這四步:
- 定義模型類Model
- 初始化單例orm,並註冊Model
- 使用選擇器selector處理正規化化資料,使元件對正規化化資料不可見,更方便使用
- 定義reducer
整個demo我是在dvajs的基礎上做的,如果習慣使用redux,可以看看Redux-ORM作者的demo,已是非常詳細,但注意,這個demo還是使用的0.9以下的api,本文是基於0.9以上版本,會有一些api差異,但核心是一樣的。
程式碼分析
定義Student/Teacher/Grade/Class模型,Student/Teacher都是最基礎的結構,重點在Grade/Class,Grade和Class是一對多的關係,所以用fk
,Class和Teacher是多對多的關係(注意會自動生成穿越模型ClassTeachers),所以用many
,Class 和Student 是一對多的關係,也用fk
。
// src/models/models.js
import { attr, many, fk } from 'redux-orm';
import PropTypes from 'prop-types';
export class Class extends CommonModel {
static modelName = 'Class';
static fields = {
name: attr(),
teachers: many('Teacher'),
students: fk('Student'),
};
static propTypes = {
name: PropTypes.string.isRequired,
teachers: PropTypes.arrayOf(PropTypes.number),
students: PropTypes.arrayOf(PropTypes.number),
};
static defaultProps = {
name: '',
teachers: [],
students: [],
}
}
export class Grade extends CommonModel {
static modelName = 'Grade';
static fields = {
name: attr(),
classes: fk('Class'),
};
static propTypes = {
name: PropTypes.string.isRequired,
classes: PropTypes.arrayOf(PropTypes.number),
};
static defaultProps = {
name: '',
classes: [],
}
}
複製程式碼
所有的Model都繼承於CommonModel
,這是一個自定義的父類,提取static generate
方法。這個方法根據傳入的屬性預設值newAttributes
,生成一個新的Model例項。
// src/models/models.js
import { attr, many, fk } from 'redux-orm';
class CommonModel extends Model {
static generate(newAttributes = {}) {
this.defaultProps = this.defaultProps || {};
const combinedAttributes = {
...this.defaultProps,
...newAttributes,
};
return this.create(combinedAttributes);
}
}
複製程式碼
定義orm,這個沒啥好說的,到處都會用到orm這個單例。
// src/models/orm.js
import { ORM } from 'redux-orm';
import { Student, Teacher, Grade, Class } from './models';
const orm = new ORM();
orm.register(Student, Teacher, Grade, Class);
export default orm;
複製程式碼
定義selector。定義state之前,我們先看selector的基本用法。reselect是一個選擇庫,簡單來說,就是用它可以組合選擇,並且它可以幫你避免重複渲染。用法上記住兩個概念,一是input selector
,根據傳入的引數,做一些計算返回結果,二是following selector
,以input selector為引數,得到最終結果。
下面是最基本的用法,從Model中獲取真實資料。
// src/routes/selectors.js
import { createSelector } from 'reselect';
import orm from '../models/orm';
const selectSession = entities => orm.session(entities);
export const selectTeacher = createSelector(
selectSession,
({ Teacher }) => {
return Teacher.all().toRefArray();
},
);
複製程式碼
複雜一點的,Class
下有多個Student
,在這裡處理好資料,以便在元件中渲染出學生的名字。Grade
下有多個Class
,同理。
export const selectGrade = createSelector(
selectSession,
({ Grade, Class }) => {
return Grade.all().toRefArray().map(v => {
if (v.classes && v.classes.length !== 0) {
return {
...v,
classes: v.classes.map(stuId => {
const ModelInstance = Class.withId(stuId);
return ModelInstance ? ModelInstance.ref : '';
})
};
}
return v;
});
},
);
export const selectClass = createSelector(
selectSession,
({ Class, Student }) => {
return Class.all().toRefArray().map(v => {
if (v.students && v.students.length !== 0) {
return {
...v,
students: v.students.map(stuId => {
const studentModel = Student.withId(stuId);
return studentModel ? studentModel.ref : '';
})
};
}
return v;
});
},
);
複製程式碼
這個時候我們載入Grade
預設資料,就可以先看到簡單的渲染結果,是這樣。
editingOrm
先不管,先看orm.getEmptyState()
,會拿到註冊好的Model資料。
// src/models/example.js
import orm from './orm';
export default {
namespace: 'example',
state: {
orm: orm.getEmptyState(),
editingOrm: orm.getEmptyState(),
selectedClassId: '',
selectedGradeId: '',
},
}
複製程式碼
1、如何初始化模型資料呢,主要是使用static upsert
方法,將一條一條的資料插入資料庫即可,然後返回session.state
更新state.orm
。下面是reducer:
insertEntities(state, { payload: {data, modelType} }) {
const session = orm.session(state.orm);
const ModelClass = session[modelType];
data.forEach(v => {
ModelClass.upsert(v);
})
return {
...state,
orm: session.state,
};
},
複製程式碼
2、如何清空模型資料呢,主要是使用static delete
,可以清空整個模型,也可以這樣刪除某個模型例項ModelClass.withId(id).delete()
。
delete(state, { payload: { modelType } }) {
const session = orm.session(state.orm);
const ModelClass = session[modelType];
ModelClass.delete();
return {
...state,
orm: session.state,
};
},
複製程式碼
3、在編輯模型資料時,我們通常會有取消/儲存兩個操作,點選取消,編輯資料不應用,點選儲存,才將編輯資料應用於被編輯的條目。所以會有editingOrm
這樣的state,用於存放編輯資料。注意:Class與Teacher是多對多的關係,所以我們需要對teachers做單獨處理,使用update
對Class
進行更新,可以觸發生成editingOrm
下的穿越模型資料ClassTeachers
。
selectClass(state, { payload: { id }}) {
const session = orm.session(state.orm);
const editingSession = orm.session(state.editingOrm);
const { Class, ClassTeachers } = session;
const classData = Class.withId(id).ref;
const { Class: EditingClass } = editingSession;
const modelInstance = EditingClass.generate(classData);
const classTeachers = ClassTeachers.filter({ fromClassId: id }).all().toRefArray().map(v => v.toTeacherId);
modelInstance.update({teachers: classTeachers});
return {
...state,
selectedClassId: id,
editingOrm: editingSession.state,
}
},
複製程式碼
4、更新模型資料,使用static update
。這裡使用的editingOrm
,因為在更新class資料時,是把這一份待更新資料放入了editingOrm
,等到儲存的時候再應用於orm
。
updateSelectedClass(state, { payload }) {
const editingSession = orm.session(state.editingOrm);
const { Class } = editingSession;
const modelInstance = Class.withId(state.selectedClassId);
modelInstance.update(payload);
return {
...state,
editingOrm: editingSession.state,
}
},
複製程式碼
5、應用編輯資料到被編輯條目,這就和3類似了,只是現在是將editingOrm
的資料寫到orm
。
saveClass(state) {
const id = state.selectedClassId;
const session = orm.session(state.orm);
const editingSession = orm.session(state.editingOrm);
const { Class } = session;
const { Class: EditingClass, ClassTeachers } = editingSession;
const editingData = EditingClass.withId(id).ref;
const modelInstance = Class.withId(id);
const classTeachers = ClassTeachers.filter({ fromClassId: id }).all().toRefArray().map(v => v.toTeacherId);
modelInstance.update({
...editingData,
teachers: classTeachers,
})
return {
...state,
orm: session.state,
}
},
複製程式碼
到這兒,整個程式碼就分析完了。不知道朋友們有沒有發現非常微妙的事情,reducer彷彿總是可以複用的,只要我們傳入指定的ModelType
!不過我在這兒就沒有繼續延展了,有興趣大家可以自己再研究下,這就是你某一天寫重複程式碼終於寫煩的時候想做的事了。
結束語
其實用不用redux-orm還是取決於專案的複雜程度,而且也不需要每個元件都必須用,我覺得這是redux-orm的一個好處,我們可以在這次需求業務複雜的時候用它,也可以在同一個專案裡,需求不復雜的時候甩掉它。非常開心的是它讓我不用再處理那麼多的層級,希望以後在真實的業務場景中能再實踐一次!歡迎朋友們指正這次實踐的問題~
參考資料: