react 基础工程(react-redux & react-router)

vue 已经很熟悉。react 虽然也用过,但是对于整体的搭建还不是很熟。这次就使用 react-redux(^8.0.5)react-router(^6.11.2)
结合上次的《从零搭建前端工程》,搭建一个基础的 react 项目。加上一点注释,后面有用到也可以代码直接拷。

代码仓库:github

一、Router

当前最新版本为 v6,据说有些大改动,旧版本请参考《升级指南》。

路由文件声明:

import { createBrowserRouter, Navigate } from 'react-router-dom';
import Home from '@/views/Home';
import NotFound from '@/views/NotFound';

const router = createBrowserRouter([
  {
    path: '/home',
    element: <Home />,
  },
  {
    path: '/game',
    // element: <CardGame />,
    async lazy() { // 路由页面懒加载,也就是真的访问到再下载,打包也是独立的 js
      const comp = await import('@/views/CardGame');
      // 这里的 default 是 webpack 的处理,会把 export default 挂在 default 属性上
      return { Component: comp.default };
    },
  },
  {
    path: '/demo/:id',
    // element: <Demo />,
    async lazy() {
      // 注意:这里要 loader 结果返回,才会渲染组件。也就是会阻塞页面。
      const comp = await import('@/views/Demo');
      return { Component: comp.default, loader: comp.loader };
    },
  },
  {
    path: '/', // v6 的重定向改为 Navigate 组件
    element: <Navigate to={'/home'}/>,
  },
  {
    path: '*', // 404页面兜底,我觉得改成和上面的一样的重定向处理应该也可以
    element: <NotFound />,
  },
]);

export default router;

然后路由声明对象的使用:

import { RouterProvider } from 'react-router-dom';
import router from '@/router';

function App() {
  return (
    <RouterProvider router={router} />
  );
}
export default App;

二、Redux

redux 使用就比较繁琐一点。先安装相关依赖:

npm i react-redux redux @reduxjs/toolkit

需要知道的是, redux 是一个公共的第三方库,和框架并没有关系。react-redux 才是为了 react 能更好地使用 redux 而推出的工具库,至于 @reduxjs/toolkit,也是为了可以更更好地使用 redux 和 react-redux。
一开始看都是 re***,我还以为 redux 就是 react 官方推的,囧。

代码都在 src/store 里:

import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './reducer';

export * from './reducer';

// 创建一个 store,把所有声明好的 reducer 传进去
const store = configureStore({
  reducer: rootReducer,
});
export default store;

reducer 代码以 src/store/reducer/number.js 为例:

import { cloneDeep } from 'lodash';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { ADD_NUMBER, MINUS_NUMBER } from '../type';

// 把每个 action 声明然后导出去
export const addNumberAction = payload => ({
  type: ADD_NUMBER,
  payload: payload,
});

export const minusNumberAction = payload => ({
  type: MINUS_NUMBER,
  payload: payload,
});

// 声明一个异步操作,这里把它叫做 thunk
// 第一个参数是 type,第二个参数是处理方法
// 处理完成后会 dispatch 一个 type 为 `${type}/fulfilled`
// 这也是为什么下面会有一个 addNumberThunk.fulfilled.toString() 的 case
export const addNumberThunk = createAsyncThunk(
  ADD_NUMBER,
  async (number) => {
    return new Promise((resolve) => setTimeout(() => resolve(number), 2000));
  },
);

// 初始的 store 值,当然它的对象归属取决于在最上层做聚合时候它的 key 值叫啥
const defaultState = {
  a: 10000,
};

export default function numberReducer(state = defaultState, action) {
  const { type, payload } = action;
  // 这里必须深拷贝,因为原数据是不允许改动的,只能传递新的数据出去
  state = cloneDeep(state);
  switch (type) {
    case ADD_NUMBER:
      state.a += payload;
      return state;
    case MINUS_NUMBER:
      state.a -= payload;
      return state;
    // addNumberThunk.fulfilled.toString() => 'ADD_NUMBER/fulfilled'
    case addNumberThunk.fulfilled.toString():
      state.a += payload;
      return state;
    default:
      return state;
  }
};

1、副作用与 createAsyncThunk

redux 讲究的是纯函数,输入输出恒定,所以每个 action 做的事情是稳定的。然后把一些需要依赖外部的环境叫做副作用,用一些中间件来处理。比如 react-thunkreact-saga,用法这里就不赘述。

其实就是在正式 store.dispatch 之前,把所有数据处理完,无论同步异步,然后再 dispatch。如果用过 vuex 的话,其实也是这样,异步的用 store.dispatch(vuex 的语法),然后再请求完数据或者别的什么都行,到真正要改变 state 数据了才用 store.commit。

也就是说,不用中间件也行。触发了事件自己怎么异步处理好数据,然后提交 dispatch。

@reduxjs/toolkit 也提供一个方法 createAsyncThunk,大同小异吧,思路都是一样的。我们只需要声明好相应的 asyncThunk.fulfilled.toString() case 处理事件就好。当然同时也有 asyncThunk.pending.toString()asyncThunk.rejected.toString(),酌情声明。

2、createSlice

这个库还提供了一个切片方法用来快速声明 reducer(个人理解):createSlice。代码在 src/store/reducer/counter.js 里。这个方法里的 reducer 是可以直接改 state 的值,据说是用了 immutable.js 来确保 state 不变。具体用法可看官网介绍

3、reducer 组合 combineReducers

@reduxjs/toolkit 提供一个方法 combineReducers,用来组合 reducer。

import { combineReducers } from '@reduxjs/toolkit';
import number from './number';
import string from './string';

const reducer = combineReducers({
  number,
  string,
});
export default reducer;

这个 reducer 挂载在顶层 state 的 data 字段下,那么它的获取方式就是 store.getState().data.number/string
具体可看 src/store/reducer/data/index.js 代码。

各类参数使用和事件的触发在 src/views/demo/index.js 里。

react 使用 store 注入:

import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import store from '@/store';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

三、其他工程化

和 vue 后来的版本一样,把 webpack 的配置藏起来,但是 react 没有暴露扩展方法,需要执行 npm run eject 把配置文件弹出来,但这一步是不可逆的,弹出来就收不回去了。也没关系,自己配置一下 webpack.config.js 就可以了。主要是不支持 alias 声明比较不方便。也不支持 less sass 好像。
需要注意的是 devServer 的配置要加上 historyApiFallback 字段的配置,不然 router 路径没法生效。简单理解就是匹配不到路径会有个默认返回,也就是无论访问什么路径,都返回默认的 index.html 内容。

devServer: {
  historyApiFallback: { disableDotRule: true, index: '/' },
},

更新!!!

2023-08-24 更新

webpack5 对于资源文件的处理有更新,详情参阅《webpack5 下 background url 图片打包失败》。

2023-09-24 更新

更新 @reduxjs/toolkit 工具的使用以及用 @craco/craco、vite 进行打包 ,详情参阅《react 基础工程 Ⅱ(@reduxjs/toolkit & @craco/craco & vite)》。