发布于 2026-01-05 2 阅读
0

最佳 Redux 架构

最佳 Redux 架构

引言

我想提醒一下各位,你们可能会对建筑设计发表意见,我并不完全尊重你们的意见,所以如果你们有什么意见,请在评论区留言,谢谢。
堆栈:React, NextJs, Typescript, Redux

这篇文章的目的不是为了编写应用程序,而是为了展示ReduxTypeScriptReact中的强大功能,当然,我们将使用Next.js来编写一些示例 API 请求。

那么,让我们开始吧。

第一步非常简单

npx create-next-app --typescript
Enter fullscreen mode Exit fullscreen mode

接下来,我们安装 npm 依赖项。

npm i redux react-redux redux-thunk reselect
Enter fullscreen mode Exit fullscreen mode

您还可以删除所有无用文件。

首先,store在根文件夹中添加一个文件夹,并在其中创建一个文件index.tsx,然后创建另一个文件夹modules,在这个文件夹中我们再创建一个文件index.ts,这里也创建一个名为 的另一个文件夹App

所以,存储文件夹应该看起来像这样。 之后,继续创建基础模块结构:
替代文字
store/modules/App
index.ts, action.ts, enums.ts, hooks.ts, reducers.ts selectors.ts, types.ts
替代文字

  1. enum.ts(对于每个新操作,您都需要在枚举类型中创建一个新属性https://www.typescriptlang.org/docs/handbook/enums.html
export enum TypeNames {
  HANDLE_CHANGE_EXAMPLE_STATUS = 'HANDLE_CHANGE_EXAMPLE_STATUS' 
}
Enter fullscreen mode Exit fullscreen mode

2.接下来,为了实现这个功能,我们需要安装开发依赖项——utility-types——
types.ts这是最重要的部分。

import { $Values } from 'utility-types';
import { TypeNames } from './enums';
Enter fullscreen mode Exit fullscreen mode

只需导入TypeNames即可$Values

export type AppInitialStateType = {
  isThisArchitecturePerfect: boolean;
};
Enter fullscreen mode Exit fullscreen mode

描述哪种类型具有 AppState

export type PayloadTypes = {
  [TypeNames.HANDLE_CHANGE_EXAMPLE_STATUS]: {
    isThisArchitecturePerfect: boolean;
  };
};
Enter fullscreen mode Exit fullscreen mode
export type ActionsValueTypes = {
  toChangeStatusOfExample: {
    type: typeof TypeNames.HANDLE_CHANGE_EXAMPLE_STATUS;
    payload: PayloadTypes[TypeNames.HANDLE_CHANGE_EXAMPLE_STATUS];
  };
};
Enter fullscreen mode Exit fullscreen mode

我们需要用这段代码来告诉 reducer 我们有哪些不同类型的 action。`specialization
*`toChangeStatusOfExample可以随意命名,但我也会给它起一个相同的名字作为(action 函数,不过现在说这个有点早)。

export type AppActionTypes = $Values<ActionsValueTypes>
Enter fullscreen mode Exit fullscreen mode

在这一步,我们需要施展 TypeScript 的魔法,我们很快就会看到我所说的魔法是什么。

所以最终我们的types.ts文件应该看起来像这样

import { $Values } from 'utility-types';
import { TypeNames } from './enums';

export type PayloadTypes = {
  [TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE]: {
    isThisArchitecturePerfect: boolean;
  };
};

export type ActionsValueTypes = {
  toChangeStatusOfExample: {
    type: typeof TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE;
    payload: PayloadTypes[TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE];
  };
};
export type AppActionTypes = $Values<ActionsValueTypes>;

export type AppInitialStateType = {
  isThisArchitecturePerfect: boolean;
};
Enter fullscreen mode Exit fullscreen mode

你可以认为它很臃肿,代码也过于复杂,但如果你珍惜时间,它将为你将来节省大量时间。

3.接下来要提交文件reducers.ts

import { TypeNames } from './enums';
import { AppActionTypes, AppInitialStateType } from './types';
Enter fullscreen mode Exit fullscreen mode

首先,我们像往常一样导入模块。

const initialState: AppInitialStateType = {};
Enter fullscreen mode Exit fullscreen mode

替代文字

令人惊讶的是,正如你所看到的,这是 TypeScript 的神奇之处,因为我们已经赋予了initialState类型AppInitialStateType描述,即 const 应该具有属性isThisArchitecturePerfectisThisArchitecturePerfect所以 当我们开始编写一些东西时,我们将再次看到 TypeScript 的神奇之处。
替代文字

替代文字

因此,当我们开始编写代码时,我们将再次见证 TypeScript 的神奇之处。

export const appReducer = (state = initialState, action: AppActionTypes): AppInitialStateType => {
  switch (action.type) {
    default:
      return state;
  }
}; 
Enter fullscreen mode Exit fullscreen mode

Pro 临时版没什么特别的,只是基本的带有 switch 结构的 redux reducer。

  1. 我们index.ts只是出口我们的建筑appReducer产品default
import { appReducer as app } from './reducers';
export default app;
Enter fullscreen mode Exit fullscreen mode

至少现在我们应该有类似的东西。

//enum.ts**

export enum TypeNames {
  HANDLE_CHANGE_STATUS_OF_EXAMPLE = 'HANDLE_CHANGE_STATUS_OF_EXAMPLE',
}

//types.ts**

import { $Values } from 'utility-types';
import { TypeNames } from './enums';

export type PayloadTypes = {
  [TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE]: {
    isThisArchitecturePerfect: boolean;
  };
};

export type ActionsValueTypes = {
  toChangeStatusOfExample: {
    type: typeof TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE;
    payload: PayloadTypes[TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE];
  };
};
export type AppActionTypes = $Values<ActionsValueTypes>;

export type AppInitialStateType = {
  isThisArchitecturePerfect: boolean;
}

//reducers.ts

import { TypeNames } from './enums';
import { AppActionTypes, AppInitialStateType } from './types';

const initialState: AppInitialStateType = {
  isThisArchitecturePerfect: true,
};
export const appReducer = (state = initialState, action: AppActionTypes): AppInitialStateType => {
  switch (action.type) {
    default:
      return state;
  }
}; 

//index.ts
import { appReducer as app } from './reducers';
export default app;

Enter fullscreen mode Exit fullscreen mode

所以如果是的话,我表示祝贺;但如果不是全部,那么在store/modules/index.ts

export { default as app } from './App';
Enter fullscreen mode Exit fullscreen mode

这是 ES6 js 的一个特性。

然后我们应该store/index.ts通过编写以下代码将其连接起来:

import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import thunkMiddleware from 'redux-thunk';
import * as reducers from './modules';

const combinedRedusers = combineReducers({ ...reducers });
const configureStore = createStore(combinecRedusers, compose(applyMiddleware(thunkMiddleware)));

export default configureStore;
Enter fullscreen mode Exit fullscreen mode

* as reducers将会导入你在上一步导入的所有 reducer,当然,我们将其应用于thunkMiddleware异步代码。并且,当然也会导出 store。

之后,我们需要将存储连接到我们的pages/_app.tsx文件,我们可以通过以下方式实现:

  1. layouts在文件夹中创建StoreLayout,这里创建index.tsx包含<Provider store={store}>{children}</Provider>,我得到类似这样的内容:
import { FC } from 'react';
import { Provider as ReduxProvider } from 'react-redux';
import store from './../../store';

const StoreLayout: FC = ({ children }) => {
  return <ReduxProvider store={store}>{children}</ReduxProvider>;
};

export default StoreLayout;
Enter fullscreen mode Exit fullscreen mode

2.layouts它的主要特点是,首先我们layouts/index.tsx用以下代码创建一个文件:

import { FC } from 'react';

export const ComposeLayouts: FC<{ layouts: any[] }> = ({ layouts, children }) => {
  if (!layouts?.length) return children;

  return layouts.reverse().reduce((acc: any, Layout: any) => <Layout>{acc}</Layout>, children);
};
Enter fullscreen mode Exit fullscreen mode

主要思路是避免嵌套,Providers因为那样至少会有很多不同的嵌套。我们可以使用`reduce()` 函数Providers简化操作 最后,我们需要将默认的 `next` 代码更改为我们自己的代码。
pages/_app.tsx

import type { AppProps } from 'next/app';
import StoreLayout from '../layouts/StoreLayout';
import { ComposeLayouts } from '../layouts/index';

const _App = ({ Component, pageProps }: AppProps) => {
  const layouts = [StoreLayout];

  return (
    <ComposeLayouts layouts={layouts}>
      <Component {...pageProps} />
    </ComposeLayouts>
  );
};
export default _App;
Enter fullscreen mode Exit fullscreen mode

当然,我们希望状态不是静态的,所以为了实现这一点,我们需要迁移到store/modules/App/action.ts并编写一个简单的动作函数,就像这样:

import { TypeNames } from './enums';
import { AppActionTypes, PayloadTypes } from './types';

export const toChangeThemePropertyies = (
  payload: PayloadTypes[TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE]
): AppActionTypes => ({
  type: TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE,
  payload
});

Enter fullscreen mode Exit fullscreen mode

重要的是要给出payload(param of function)正确的类型,由于我们有枚举类型名称,所以不会出现类型命名错误。最令人印象深刻的是,当我们编写此操作应返回AppActionTypes(其类型包含所有操作类型)时,然后在函数中编写type: TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE,有效负载将自动找到。我们很快就会看到示例。

此外,我们还有机会store/modules/App/selectors.ts使用reselect库来访问我们的状态。其主要思想是,如果 store 发生变化,而我们又使用了 store 中的某个值,组件将重新渲染reselect,这非常强大。但在我们开始创建 reducer 之前,我们需要一些准备工作。RootStoreType我喜欢创建一个新的全局文件夹models,并在这里创建文件types.ts
替代文字

import { AppInitialStateType } from '../store/modules/App/types';
export type RootStoreType = { app: AppInitialStateType };
Enter fullscreen mode Exit fullscreen mode

在这段代码中,我们应该描述RootStoreType所有内容reducers。现在回到……store/modules/App/selectors.ts

一如既往:

import { RootStoreType } from '../../../models/types';
import { createSelector } from 'reselect';
Enter fullscreen mode Exit fullscreen mode

然后,良好的实践是开始用 `get` 来命名你的选择器。

  • `someName ,like that: export const getIsThisArchitecturePerfect= createSelector() Also,createSelector` 有 2 个参数:
  • 包含函数的数组(在本例中)(state:RootStoreType) =>state.app.isThisArchitecturePerfect
  • 该函数接收一个参数(前一个数组的返回值),并返回你需要的值,结果代码如下:
import { RootStoreType } from '../../../models/types';
import { createSelector } from 'reselect';

export const getIsThisArchitecturePerfect= createSelector(
  [(state: RootStoreType) => state.app.isThisArchitecturePerfect],
  isThisArchitecturePerfect => isThisArchitecturePerfect
);
Enter fullscreen mode Exit fullscreen mode

最后,我们可以测试一下我们的逻辑是否有效,为此,请移步pages/index.tsx;并编写以下代码:


import { useSelector } from 'react-redux';
import { getIsThisArchitecturePerfect } from '../store/modules/App/selectors';

const Index = () => {
  const isThisArchitecturePerfect = useSelector(getIsThisArchitecturePerfect);
  console.log(isThisArchitecturePerfect);
  return <></>;
};

export default Index;
Enter fullscreen mode Exit fullscreen mode

我们导入useSelector来访问我们的 store,并将我们的选择器粘贴到这里,然后console.log(isThisArchitecturePerfect)我们就能看到结果了。
保存并运行。

npm run dev
Enter fullscreen mode Exit fullscreen mode

(按 F12 打开开发者工具,开玩笑啦,大家都知道)
我想你们可能会问我,为什么我们的应用这么静态,我的回答是,是的,现在,我们来添加一些动态效果。为了让应用看起来更好,我们添加一些简单的样式和 JSX 标记,还
需要使用useDispatch()来更改 store 的值,并导入 action 函数toChangeThemePropertyies。另外,我们还需要创建两个函数来更改值(第一个设置为 true,第二个设置为 false),就像这样:
替代文字

正如你所见,我特别设置了'true'`not true`,这就是 TypeScript 的神奇之处,你总能确保代码按预期运行。我不使用 CSS,因为我非常喜欢使用JSS,它拥有令人难以置信的功能,我完全不明白为什么JSS并不那么流行,但这与样式无关。

import { useDispatch, useSelector } from 'react-redux';
import { toChangeThemePropertyies } from '../store/modules/App/actions';
import { getIsThisArchitecturePerfect } from '../store/modules/App/selectors';

const Index = () => {
  const isThisArchitecturePerfect = useSelector(getIsThisArchitecturePerfect);
  const dispatch = useDispatch();

  const handleSetExampleStatusIsTrue = () => {
    dispatch(toChangeThemePropertyies({ isThisArchitecturePerfect: true }));
  };
  const handleSetExampleStatusIsFalse = () => {
    dispatch(toChangeThemePropertyies({ isThisArchitecturePerfect: false }));
  };

  const containerStyling = {
    width: 'calc(100vw + 2px)',
    margin: -10,
    height: '100vh',
    display: 'grid',
    placeItems: 'center',
    background: '#222222',
  };

  const textStyling = {
    color: 'white',
    fontFamily: 'Monospace',
  };

  const buttonContainerStyling = {
    display: 'flex',
    gap: 10,
    marginTop: 20,
    alignItems: 'center',
    justifyContent: 'center',
  };

  const buttonStyling = {
    ...textStyling,
    borderRadius: 8,
    cursor: 'pointer',
    border: '1px solid white',
    background: 'transparent',
    padding: '8px 42px',
    width: '50%',
    fontSize: 18,
    fontFamily: 'Monospace',
  };

  return (
    <>
      <div style={containerStyling}>
        <div>
          <h1 style={textStyling}>{'- Is This Architecture Perfect?'}</h1>
          <h1 style={textStyling}>{`- ${isThisArchitecturePerfect}`.toUpperCase()}</h1>
          <div style={buttonContainerStyling}>
            <button style={{ ...buttonStyling, textTransform: 'uppercase' }} onClick={handleSetExampleStatusIsTrue}>
              True
            </button>
            <button style={{ ...buttonStyling, textTransform: 'uppercase' }} onClick={handleSetExampleStatusIsFalse}>
              False
            </button>
          </div>
        </div>
      </div>
    </>
  );
};

export default Index;
Enter fullscreen mode Exit fullscreen mode

如果你足够细心,我想你应该知道为什么代码不起作用了,所以试着自己修复这个小细节。如果你不想自己动手,可以试试其他方法。
解决方法是store/modules/App/reducers.ts我们忘记写case代码了,reducer switch construction所以要解决这个问题,我们需要写这段代码。

 case TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE: {
      const { isThisArchitecturePerfect } = action.payload;
      return { ...state, isThisArchitecturePerfect };
    }
Enter fullscreen mode Exit fullscreen mode

我有一个功能可以改进这段代码。

//if your action.payload is the same as property in initial state u can write like this:
//case TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE:
//case TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE1:
//case TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE2: ({ ...state, ...action.payload });
// if not, just create a new case

case TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE: ({ ...state, ...action.payload });

Enter fullscreen mode Exit fullscreen mode

所以现在一切都能正常运行,但这还不是全部,因为正如我在引言中所说,我们将编写一些简单的 API,所以打开或创建一个pages/api文件,并在其中创建一个包含你的 API 路由的文件,在我的例子中是pages/api/example,请参考官方文档。

import type { NextApiRequest, NextApiResponse } from 'next';
import { ApiExampleResType } from '../../models/types';

export default (req: NextApiRequest, res: NextApiResponse<ApiExampleResType>) => {
  res.status(200).json({ title: '- Is This Architecture Perfect?' });
};
Enter fullscreen mode Exit fullscreen mode

是的,还有models/types.ts写入类型

 export type ApiExampleResType = { title: string }; 
Enter fullscreen mode Exit fullscreen mode

这就是我们需要用到的“TypeScript魔法”。然后,由于Next.js的getServerSideProps方法存在一些问题,所以我们在这里会简化任务,但至少在实际应用中你应该使用Next.js的getServerSideProps方法

所以你的任务是创建一个有效载荷类型的动作函数ApiExampleResType,仅用于训练,如果你懒得做,可以直接看结果:

//enum.ts**

HANDLE_CHANGE_TITLE_OF_EXAMPLE ='HANDLE_CHANGE_TITLE_OF_EXAMPLE',  

//types.ts**

import { $Values } from 'utility-types';
import { TypeNames } from './enums';
import { ApiExampleResType } from './../../../models/types';

export type PayloadTypes = {
  [TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE]: {
    isThisArchitecturePerfect: boolean;
  };
  [TypeNames.HANDLE_CHANGE_TITLE_OF_EXAMPLE]: ApiExampleResType;
};

export type ActionsValueTypes = {
  toChangeSphereCursorTitle: {
    type: typeof TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE;
    payload: PayloadTypes[TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE];
  };
  toChangeTitleOfExample: {
    type: typeof TypeNames.HANDLE_CHANGE_TITLE_OF_EXAMPLE;
    payload: PayloadTypes[TypeNames.HANDLE_CHANGE_TITLE_OF_EXAMPLE];
  };
};
export type AppActionTypes = $Values<ActionsValueTypes>;

export type AppInitialStateType = {
  isThisArchitecturePerfect: boolean;
} & ApiExampleResType;

//reducers.ts

import { TypeNames } from './enums';
import { AppActionTypes, AppInitialStateType } from './types';

const initialState: AppInitialStateType = {
  isThisArchitecturePerfect: true,
  title: 'Nothing',
};

export const appReducer = (state = initialState, action: AppActionTypes): AppInitialStateType => {
  switch (action.type) {
    case TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE:
    case TypeNames.HANDLE_CHANGE_TITLE_OF_EXAMPLE:
      return { ...state, ...action.payload };

    default:
      return state;
  }
};

//action.ts

import { TypeNames } from './enums';
import { AppActionTypes, PayloadTypes } from './types';

export const toChangeThemePropertyies = (
  payload: PayloadTypes[TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE]
): AppActionTypes => ({
  type: TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE,
  payload,
});

export const toChangeTitleOfExample = (
  payload: PayloadTypes[TypeNames.HANDLE_CHANGE_TITLE_OF_EXAMPLE]
): AppActionTypes => ({
  type: TypeNames.HANDLE_CHANGE_TITLE_OF_EXAMPLE,
  payload,
});


Enter fullscreen mode Exit fullscreen mode

您写的代码也一样(恭喜!),为了访问应用程序状态的新属性,我们需要编写一个新的选择器,下一步就是selectors.ts添加这个选择器。

export const getTitle= createSelector(
  [(state: RootStoreType) => state.app.title],
  title => title
);
Enter fullscreen mode Exit fullscreen mode

倒数第二步,opetations.ts
首先导入所有依赖项

//types 
import { Action, ActionCreator, Dispatch } from 'redux';
import { ThunkAction } from 'redux-thunk';
import { RootStoreType } from '../../../models/types';
import { AppActionTypes } from './types';
//action
import { toChangeTitleOfExample } from './actions';

Enter fullscreen mode Exit fullscreen mode

其次,我们创建了一个 thunk 函数,该函数包含一个ActionCreator<ThunkAction<Promise<Action>, RootStoreType, void, any>>async ,该闭包
(dispatch: Dispatch<AppActionTypes>): Promise<Action> =>
会向我们发送 fetch 请求/api/example并返回结果dispatch(toChangeTitleOfExample(awaited result))。这可能有点奇怪,但结果是我们有

import { Action, ActionCreator, Dispatch } from 'redux';
import { ThunkAction } from 'redux-thunk';
import { RootStoreType } from '../../../models/types';
import { toChangeTitleOfExample } from './actions';
import { AppActionTypes } from './types';

export const operatoToSetExampleTitle:
  ActionCreator<ThunkAction<Promise<Action>, RootStoreType, void, any>> =
    () =>
      async (dispatch: Dispatch<AppActionTypes>): Promise<Action> => {
      const result = await fetch('/api/example', { method: 'GET' });
      const { title } = await result.json();
      return dispatch(toChangeTitleOfExample({ title }));
    };

Enter fullscreen mode Exit fullscreen mode

最后一步是pages/index.tsx

  const title = useSelector(getTitle);

  useEffect(() => {
    dispatch(operatoToSetExampleTitle());
  }, []);

Enter fullscreen mode Exit fullscreen mode

在使用 Next.js 时,这不是最佳实践,但作为示例来说也不是最糟糕的,useEffect (()=>{...},[]) - 仅在挂载时运行,因此hooks.ts我们需要在有重复逻辑时使用operations.tsreducers.ts

结论

如果你仍然觉得它很笨重,我保证,如果你尝试使用这种结构,你会发现它非常棒,之后你将无法再使用其他架构。

感谢阅读,我非常感激♥。

源代码(GitHub)

文章来源:https://dev.to/pas8/best-redux-architecture-26j1