javascript - React 和 Redux 架构问题

标签 javascript reactjs redux

阅读之前:

这不是代码不工作的问题,而是架构的问题。此外,我目前没有使用 ReactRedux 库,因为我首先尝试了解这些部分如何在此测试应用程序中独立工作。它尽可能短,但不幸的是仍然很长,请耐心等待

简短介绍

我有一组 Bottle 模型。使用伪代码,一个瓶子是这样定义的:

class Bottle{

//members
filledLiters
filledLitersCapacity
otherMember1
otherMember2

//functions
toPostableObject(){

//removes functions by running JSON.
var cloneObj = JSON.parse(JSON.stringify(this));
//removes all members we dont want to post
delete cloneObj["otherMember1"];
}

//other functions
}

我还有一个显示所有 Bottle 项目的 React 组件。该组件还需要存储所有 Bottle 项目的先前状态(它用于动画,请忽略它)。

Redux 用法

我需要使用像这样的辅助类对一些 Bottle 项目执行复杂的操作:

var updated_bottles = BottleHandler.performOperationsOnBottles(bottleIds)
mainStore.dispatch({type:"UPDATED_BOTTLES",updated_bottles:updated_bottles})

我不想为每个操作更新商店,因为我希望商店在最后一次全部更新。因此我的 BottleReducer 看起来像这样:

var nextState = Object.assign({}, currentState);
nextState.bottles = action.updated_bottles

其中 action.updated_bottles 是执行操作后瓶子的最终状态。

问题

尽管一切正常,但我怀疑这是接近我的架构的“错误心态”。原因之一是为了避免在执行操作时保留对瓶子对象的引用并改变状态,我必须做这件丑陋的事情:

var bottlesCloneArray = mainStore.getState().
bottleReducer.bottles.map(
a => {
var l = Object.assign({}, a);
 Object.setPrototypeOf( l, Character.prototype );
return l
}
);

这是因为我需要一个克隆的对象数组,这些对象仍保留其原始功能(意味着它们是该类的实际实例克隆)

如果您能指出我逻辑中的缺陷,我将不胜感激。

P.S:我需要保留类实例的“深度克隆”的原因是,我可以在我的 React 组件中保留瓶子的先前状态,以便在更新时在两种状态之间进行动画处理渲染发生。

最佳答案

在处理 redux 架构时,将序列化和不变性放在每个决策的最前沿是非常有用的,一开始这可能很困难,尤其是当您非常习惯 OOP 时

由于商店的状态只是一个 JS 对象,因此可能很想用它来跟踪更复杂的模型类的 JS 实例,但应该更像一个数据库,您可以在其中序列化模型的表示以不可变的方式进出它。

以最原始的形式存储你的瓶子的数据表示使得诸如本地存储的持久化和商店的再水化等更高级的应用程序成为可能,然后可以允许服务器端渲染和可能的离线使用,但更重要的是它使它变得更您的应用程序中正在发生和发生的变化更加可预测和明显。

我见过的大多数 redux 应用程序(包括我的)都沿用了完全取消模型类的功能路线,并直接在数据上直接在 reducer 中执行操作 - 可能在此过程中使用助手。这样做的缺点是它会导致大型复杂的 reducer 缺乏一些上下文。

但是,如果您希望将此类助手封装到 Bottle 类中,则有一个完全合理的中间立场,但您需要根据案例类来考虑,它可以是从数据形式创建并序列化回数据形式,并且如果对其进行操作则不可变

让我们看看这对您的 Bottle 有何作用( typescript 注释以帮助显示发生了什么)

瓶箱类

interface IBottle {
  name: string,
  filledLitres: number
  capacity: number
}

class Bottle implements IBottle {

  // deserialisable
  static fromJSON(json: IBottle): Bottle {
    return new Bottle(json.name, json.filledLitres, json.capacity)
  }

  constructor(public readonly name: string, 
              public readonly filledLitres: number, 
              public readonly capacity: number) {}

  // can still encapuslate computed properties so that is not needed to be done done manually in the views
  get nameAndSize() {
    return `${this.name}: ${this.capacity} Litres` 
  }

  // note that operations are immutable, they return a new instance with the new state
  fill(litres: number): Bottle {
    return new Bottle(this.name, Math.min(this.filledLitres + litres, this.capacity), this.capacity)
  }

  drink(litres: number): Bottle {
    return new Bottle(this.name, Math.max(this.filledLitres - litres, 0), this.capacity)
  }

  // serialisable
  toJSON(): IBottle {
    return {
      name: this.name,
      filledLitres: this.filledLitres,
      capacity: this.capacity
    }
  }

  // instances can be considered equal if properties are the same, as all are immutable
  equals(bottle: Bottle): boolean {
    return bottle.name === this.name &&
           bottle.filledLitres === this.filledLitres && 
           bottle.capacity === this.capacity
  }

  // cloning is easy as it is immutable
  copy(): Bottle {
    return new Bottle(this.name, this.filledLitres, this.capacity)
  }

}

存储状态 注意它包含一个数据表示而不是类实例的数组

interface IBottleStore {
  bottles: Array<IBottle>
}

瓶子选择器 在这里,我们使用选择器从商店中提取数据,并将其转换为类实例,您可以将这些实例作为 Prop 传递给 React 组件。 如果使用像 reselect 这样的库,这个结果将被内存,所以你的实例引用将保持不变,直到它们在商店中的基础数据发生变化。 这对于使用 PureComponent 优化 React 很重要,它仅通过引用比较 props。

const bottlesSelector = (state: IBottleStore): Array<Bottle> => state.bottles.map(v => Bottle.fromJSON(v))

瓶子 reducer 在你的 reducer 中,你可以使用 Bottle 类作为执行操作的助手,而不是直接在 reducer 中对数据本身做所有事情

interface IDrinkAction {
  type: 'drink'
  name: string
  litres: number
}

const bottlesReducer = (state: Array<IBottle>, action: IDrinkAction): Array<IBottle> => {
  switch(action.type) {
    case 'drink': 
      // immutably create an array of class instances from current state
      return state.map(v => Bottle.fromJSON(v)) 
              // find the correct bottle and drink from it (drink returns a new instance of Bottle so is immutable)
              .map((b: Bottle): Bottle => b.name === action.name ? b.drink(action.litres) : b)
                // serialise back to date form to put back in the store
                .map((b: Bottle): IBottle => b.toJSON())
    default:
      return state
  }
}

虽然这个 drink/fill 示例相当简单,并且可以直接在 reducer 中的数据上直接在尽可能多的行中轻松完成,但它说明了使用用案例类来表示更真实世界的数据仍然可以做到,并且可以使它更容易理解并使代码更有条理,而不是拥有一个巨大的 reducer 和手动计算 View 中的属性,作为奖励,Bottle 类也很容易可测试。

通过在整个过程中保持不变,如果设计正确,您的 React 类的先前状态将继续保留对您之前的瓶子的引用(处于它们自己的先前状态),因此无需以某种方式跟踪您自己进行动画等

关于javascript - React 和 Redux 架构问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47469506/

相关文章:

javascript - ('click' ) 与 ('tapone' ) 之间的区别

javascript - 在 javascript 中显示多个计时器

javascript - 在 React 中停止路由更改的音频?

javascript - 如果连接到未更改的商店,React componentDidUpdate 方法不会在继承的 Prop 更改时触发

javascript - JS Map返回问题

javascript - 从 SCEditor 获取值

javascript - 在react.js中填充必要的数据

javascript - 如何使 react 处理程序为多个值工作?

javascript - 使用 redux saga 和 redux 表单在 React 应用程序中显示表单错误

javascript - React - 如何编写像 netflix 这样的纯键盘应用程序?