从实践的角度谈 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 / 当前模型类的绑定版本.
实践, 真实的应用
先看一下实现效果, 顺便贴上代码库地址: https://github.com/Irislm/redux-orm-dva
用我自己的理解, 我认为实践应该有这四步:
定义模型类 Model
初始化单例 ORM, 并注册 Model
使用选择器 selector 处理范式化数据, 使组件对范式化数据不可见, 更方便使用
定义 reducer
整个 demo 我是在 dvajs 的基础上做的, 如果习惯使用 redux, 可以看看 Redux-ORM 作者的 demo https://github.com/tommikaikkonen/redux-orm-primer , 已是非常详细, 但注意, 这个 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 的基本用法. https://github.com/reduxjs/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 默认数据, 就可以先看到简单的渲染结果, 是这样.
定义 state.state 长这样, 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 的一个好处, 我们可以在这次需求业务复杂的时候用它, 也可以在同一个项目里, 需求不复杂的时候甩掉它. 非常开心的是它让我不用再处理那么多的层级, 希望以后在真实的业务场景中能再实践一次! 欢迎朋友们指正这次实践的问题~
参考资料:
来源: https://juejin.im/post/5c88c457f265da2d8c7e0916