这是一篇实用性的文章, 是给react-redux已入门的读者 (推荐懂typescript)。 这篇文章讨论的问题是怎样构架一个大型react-redux项目,通过文件结构以及代码结构来进行探讨。
这篇文章总结的结构,只是作者自己的总结, 并不是完美的也有可能在一些情况下不适用。
如果你想简单粗暴直接看代码去(代码结构) 标题。
一个大型项目: 这里指3-5 个程序员同时开发的项目, 其中包括有人离岗,以及新人加入。
一个大型项目的结构, 需要考虑到两个重要的原则:可被理解性(understandability) 和 可推测性 (inferrability)。
所以一个好的 react 项目也应该遵守这两个原则。这篇文章作者会用 typescript 因为其类强型功能本身有助于提高可理解行。
任何一个react 项目有三个主要的组成部分为:
对与产品经理,或者程序员则最重要的是:
所以很自然的,一个项目的文件结构也该已一个功能为中心, 而 react 和r edux 只是实现功能的技术。
假设一个网购平台就需要两个功能:
放在 common 里面是因为,很多情况下一个组件需要的数据模型是从另一个数据模型转换过来的,多个组件都可能用到相同的数据模型, 所以没有把它们分开放到不同的文件夹里面。
这个位置也是很好理解, 因为想让开发者能更快的了解到一个功能所有的代码实现,所以分开放到不同的文件夹里。
Redux 包含了原组件有:
注解:Dispatcher 的意义为了通过定义简单的函数调用,返还一个action。Selector 是从redux store 里面读取数据的。 我把ActionType 和 Action 放到了一个文件里面。
我看过有很多项目, 把所有的action type 放到一个文件夹, 所有的 reducer 放到一个文件夹里, 把dispatcher 和 action混到一个文件放在一个文件夹里。
我自己是极力不推荐这种结构, 除非你的项目小,一旦项目大,十多个或者几十个功能, 三个文件夹里面满满的文件,读代码极大降低效率, 还会遗漏很多东西。
这种文件结构,能提高项目可理解性,因为它把功能和实现功能的代码紧密的联系起来。一个功能如果有bug,开发团队能快速的找到哪几组代码有可能产生了这个bug。 新的程序员也能很好的去理解一块代码的责任。对于可推测性, 一个功能改动,删除,或增加所碰触到的文件,都能被整个团队很好的推测,因为这个结构是不改动的。
代码结构,主要讲跟redux有直接关系的代码, 因为其他的代码太基本,也没有可复制性。
略过:
//CartProductActions.ts
export enum CartProductActionTypes {
ADD_PRODUCT = "[CART_PRODUCT] Add",
REMOVE_PRODUCT = "[CART_PRODUCT] Remove",
}
export class AddCartProductAction implements Action {
public readonly type: CartProductActionTypes = CartProductActionTypes.ADD_PRODUCT;
constructor(public product: CartProduct) {}
}
export class RemoveCartProductAction implements Action {
public readonly type: CartProductActionTypes = CartProductActionTypes.REMOVE_PRODUCT;
constructor(public product: CartProduct, public id: number) {}
}
export type CartProductAction = AddCartProductAction | RemoveCartProductAction;
Redux 包,自含typescript 的类型。
这个文件里,值得注意的就是最后一行的类型联合, 要记得,这是因为,action 相对应的 reducer 是需要知道它的 action 的类型的。
//CartProductReducer.ts
export interface CartProductState {
products: CartProduct[];
}
const initialState: CartProductState = {
products: [],
};
export function cartProductReducer(state: CartProductState = initialState, action: CartProductAction):CartProductState {
switch (action.type) {
case CartProductActionTypes.ADD_PRODUCT: {
const product = (action as AddCartProductAction).product;
const products = state.products.concat(product);
return {
...state,
products,
};
}
case CartProductActionTypes.REMOVE_PRODUCT: {
const products = [...state.products];
const id = (action as RemoveCartProductAction).id;
products.splice(id, 1);
return {
...state,
products,
};
}
default:
return state;
}
}
Reducer 是吃两个参数的, 一个是 state, 一个是 action。 typescript,需要state 里所有成员被定义, 还有 action 的类别(联合类别)。这个好处很大, 想想javascript, 程序员可以省事不把 state 写全,或者后来修改reducer 往state 里面添加成员。 当另一个程序员想要知道 state 里面有什么的时候很不方便,写selector 的时候也困难。
还要注意 switch 里面, 要把 action 强行转换,然后typescript 就会提示你这个action里面有什么。
//CartProductDispatcher.ts
export const addCartProduct = (product: Product, quantity: number) => {
const cartProduct: CartProduct = {
...product,
itemQuantity: quantity,
};
return new AddCartProductAction(cartProduct);
};
//thunk example
export const removeCartProduct = (cartProduct: CartProduct, id: number) => (dispatch: Dispatch) => {
dispatch(new RemoveCartProductAction(cartProduct, id));
};
Dispatcher 的意义在于, 程序员不需要从新码这些逻辑, DRY。Dispatcher 是为了到时候的 mapDispatchToProps.
//CartProductSelector.ts
import { createSelector } from "reselect";
import { AppState } from "../rootReducer";
import { CartProductState } from "./CartProductReducer";
const getCartState = (state: AppState) => state.cartProducState;
export const getCartProducts = createSelector(
getCartState,
(cartState: CartProductState) => cartState.products,
);
Selector, 这里用了reselect 这个包写selector 必备。Selector 代码为了到时候的 mapStateToProps。
写完上面一堆的逻辑后才能进入正式的功能开发。 这里我也只会提及怎么把 component(组件) 跟 redux 联到一起。
把一个 功能分成,view 和 container 也是自己的一个喜好, 没有强力的推荐,不过至少阅读起来这样让代码责任分的更清楚。
//CartContainer.ts
import { connect } from "react-redux";
import { CartProduct } from "../../common/models/CartProduct";
import { Product } from "../../common/models/Product";
import { addCartProduct, removeCartProduct } from "../../dux/CartProduct/CartProductDispatcher";
import { getCartProducts } from "../../dux/CartProduct/CartProductSelector";
import { AppState } from "../../dux/rootReducer";
import FloatCart from "./FloatCart";
type addCartProductDispatchProp = (product: Product, quantity: number) => void;
type removeCartProductDispatchProp = (cartProduct: CartProduct, id: number) => void;
interface DispatchProps {
addCartProduct: addCartProductDispatchProp;
removeCartProduct: removeCartProductDispatchProp;
}
interface StateProps {
cartProducts: CartProduct[];
}
export type CartProps = StateProps & DispatchProps;
const mapDispatchToProps = (dispatch): DispatchProps => ({
addCartProduct: (product: Product, quantity: number) => dispatch(addCartProduct(product, quantity)),
removeCartProduct: (product: CartProduct, quantity: number) => dispatch(removeCartProduct(product, quantity)),
});
const mapStateToProps = (state: AppState): StateProps => ({
cartProducts: getCartProducts(state),
});
export default connect(mapStateToProps, mapDispatchToProps)(FloatCart);
Container, 负责把写的react 组件和 redux 连接起来。可以看到这里有很多类别声明, 但只有一个导出的类别。 有了这个 CartProps, 程序员就可以利用 typescript 的类别提示了。 在 react 组件(component)里面程序员就可以直接查看一个dispatch 函数需要的参数, 或者有哪些参数。
//Cart.ts
import { CartProps } from "./FloatCartContainer";
...
interface State {
isOpen: boolean;
}
class FloatCart extends React.Component<CartProps, State> {
...
写react redux,很大一部分是可重复性很强的。 从数据模组,到 redux 里 action, reducer, dispatcher 和 selector, 还有连接的部分 container。 这些代码的构架都是可以被样板化的, 这样就会在团队内产生共识, 增加代码的可理解性和可推测行。 因为改动和新增都不会脱离原来的样板。
可以去看一下github,希望你看的时候我已经把代码放上去了。