前端數據層的探索與實踐(二)

第一部分:前端數據層的探索與實踐(一
第二部分:前端數據層的探索與實踐(二)html

從實踐的角度談Redux-ORM概念

Model

數據模型是Redux-ORM的核心。根據實際業務,咱們會定義不少的數據模型,經過定義模型的靜態屬性字段field對實體進行建模。一個模型表明一張表,模型的名字用靜態屬性modelName定義,模型的屬性用靜態屬性field定義,這些數據模型都繼承於Model。模型的屬性field多是純屬性,也多是指向另外一張表的關係屬性,一般有三種關係:一對多fk,一對一oneToOne,多對多many。前端

咱們說一個模型表明一張表,那麼一個模型實例咱們能夠認爲這就是數據庫中的一條記錄。但模型實例並非咱們真正的底層對象,它只是一個由屬性items/itemById組成的字面量對象,要訪問真正的底層對象應使用ref屬性。git

在reducer中對Model進行操做時,Redux-ORM會把action放入隊列,直到調用session.state纔會讓隊列中的action順序執行,直到獲得最終結果。github

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

實踐,真實的應用

先看一下實現效果,順便貼上代碼庫地址:redux-orm-dvaapi

用我本身的理解,我認爲實踐應該有這四步:bash

  • 定義模型類Model
  • 初始化單例orm,並註冊Model
  • 使用選擇器selector處理範式化數據,使組件對範式化數據不可見,更方便使用
  • 定義reducer

整個demo我是在dvajs的基礎上作的,若是習慣使用redux,能夠看看Redux-ORM做者的demo,已經是很是詳細,但注意,這個demo仍是使用的0.9如下的api,本文是基於0.9以上版本,會有一些api差別,但核心是同樣的。session

代碼分析

定義Student/Teacher/Grade/Class模型,Student/Teacher都是最基礎的結構,重點在Grade/Class,Grade和Class是一對多的關係,因此用fk,Class和Teacher是多對多的關係(注意會自動生成穿越模型ClassTeachers),因此用many,Class 和Student 是一對多的關係,也用fkpost

// 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: '',
  },
}
複製代碼

一、如何初始化模型數據呢,主要是使用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,
      };
    },
複製代碼

二、如何清空模型數據呢,主要是使用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,
      };
    },
複製代碼

三、在編輯模型數據時,咱們一般會有取消/保存兩個操做,點擊取消,編輯數據不該用,點擊保存,纔將編輯數據應用於被編輯的條目。因此會有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,
      }
    },
複製代碼

四、更新模型數據,使用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,
      }
    },
複製代碼

五、應用編輯數據到被編輯條目,這就和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/...

相關文章
相關標籤/搜索