前端資料層的探索與實踐(二)

IrisIm發表於2019-03-16

第一部分:前端資料層的探索與實踐(一
第二部分:前端資料層的探索與實踐(二)

從實踐的角度談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預設資料,就可以先看到簡單的渲染結果,是這樣。

前端資料層的探索與實踐(二)
定義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做單獨處理,使用updateClass進行更新,可以觸發生成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的一個好處,我們可以在這次需求業務複雜的時候用它,也可以在同一個專案裡,需求不復雜的時候甩掉它。非常開心的是它讓我不用再處理那麼多的層級,希望以後在真實的業務場景中能再實踐一次!歡迎朋友們指正這次實踐的問題~

參考資料:

blog.isquaredsoftware.com/...

tommikaikkonen.github.io/...

相關文章