react框架研习

一、react项目技术架构组成

  • 基础技术react版本18.2.0
  • 路由插件react-router
  • 数据仓库react-redux
reduxjs/toolkit:redux工具类
redux-saga:异步操作中间件
redux-persist:redux持久化解决方案,解决state数据仓库数据丢失,自定义transforms定义持久化和水化操作,设置持久化过期时间,具体逻辑查看store/persistExpire.js
  • UI组件库antd
  • dotenv-cli来区分不同环境变量
涉及到的hook
1、useContext:读取上下文,用于多层组件间传值
2、useState:无状态组件可使用数据池state功能
3、useEffect:副作用,类似与监听,由于无状态组件没有钩子函数,这可以用作钩子函数的作用
4、useRef:用于绑定页面dom元素或者定义静态数据,改变它不会触发页面渲染
5、useSelector:获取redux中state数据
6、useDispatch:调用redux的action
7、useNavigate:路由跳转

二、框架搭建

脚手架创建基础项目

create-react-app my-react-app

刚创建的基础项目是没有集成路由、中央数据仓库、样式组件库。

 集成路由

安装路由依赖

yarn add react-router-dom

创建路由文件,在根目录下创建router目录,router目录下创建index.js路由文件,内容如下,具体页面按自己实际需求

import React from "react";
import { Navigate } from "react-router-dom";

const Home = React.lazy(() => import("../views/home"));
const Page1 = React.lazy(() => import("../views/page1"));
const Page2 = React.lazy(() => import("../views/page2"));
const NotFound = React.lazy(() => import("../views/notFound"));

export default [
  {
    path: "/",
    element: <Navigate to="/home" />,
  },
  {
    path: "/home",
    element: <Home />,
  },
  {
    path: "/page1",
    element: <Page1 />,
  },
  {
    path: "/page2",
    element: <Page2 />,
  },
  {
    path: "*",
    element: <NotFound />,
  },
];

在react入口文件src下index.js里用路由标签包裹根节点

import React, { Suspense } from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { HashRouter } from "react-router-dom";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
        <Suspense fallback={<Spin size="large" style={{ marginTop: 100 }} />}>
          <HashRouter>
            <App />
          </HashRouter>
        </Suspense>
);

react-router有两种路由模式,history和hash,HashRouter对应hash路由,BrowserRouter对应history路由,我这里用的是hash路由,具体按实际情况选择

在App.js如下,类似vue的路由视图,这样路由跳转的时候,页面就会在页眉和页脚之间渲染,这样路由就集成好了。

import { useRoutes } from "react-router-dom";
import routes from "./router";

function App() {
  return (
    <div className="App">
      <div className="header">页眉</div>
      <div className="content">{useRoutes(routes)}</div>
      <div className="footer">页脚</div>
    </div>
  );
}

   集成redux,一般使用redux都会使用工具类,我这里使用了reduxjs/toolkit,安装依赖

yarn add react-redux
yarn add @reduxjs/toolkit

创建中央数据仓库,在src下新增store目录,在store目录下新建index.js,这是redux主入口文件,内容如下

import { configureStore, combineReducers } from "@reduxjs/toolkit";
import storeA from "./modules/storeA";
import storeB from "./modules/storeB";

//合并reducer
const rootReducer = combineReducers({
  storeA,
  storeB,
});

//创建store对象
const store = configureStore({
  reducer: {
    storeA,
    storeB
  }
});

export { store};

redux可以包含多模块,这里包含了两个,storeA和storeB,下面是storeA的内容,storeB差不多就不写出来了,用redux工具类toolkit创建的切片对象,包含name,initialState,reducers,这里工具类会默认创建同名的action,就不需要再手动创建,只需要export导出就行,调用action时会直接调用同名reducer,但是reducer只能是同步,如果有异步的需求只能创建异步action来实现,下面写了两种方式,一种是使用reduxjs/toolkit,它默认集成了reduxThunk,可以直接使用方法createAsyncThunk来创建异步action,或者直接用两层箭头函数,也能实现异步action的效果。

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { getNewsData } from "../../api";

export const counterSlice = createSlice({
  name: "storeA",
  initialState: {
    value: 0,
    obj: {
      num: 0,
    },
  },
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
    setObjAttr: (state, action) => {
      state.obj.num += action.payload;
    },
  },
});

export const { increment, decrement, incrementByAmount, setObjAttr } =
  counterSlice.actions;
//action creator
export const axiosFun = createAsyncThunk(
  "storeA/axiosFun",
  async (arg, { dispatch }) => {
    const result = await getNewsData({
      currentPage: 1,
      pageSize: 20,
      category: "30571001",
    });
    if (result.type === "00") {
      dispatch(setObjAttr(result.total));
    }
  }
);

export const axiosFun2 = (arg) => async (dispatch) => {
  const result = await getNewsData({
    currentPage: 1,
    pageSize: 20,
    category: "30571001",
  });
  if (result.type === "00") {
    dispatch(setObjAttr(result.total));
  }
};

export default counterSlice.reducer;

   页面调用方式有两种,一种是直接import引入,使用dispatch调用,还有一种是直接使用dispatch使用type参数调用,如下

<Button onClick={() => dispatch(increment())}>+1</Button>
<Button onClick={() => dispatch({ type: "storeA/increment" })}>+1</Button>

  对于异步操作还可以使用中间件saga,首先创建saga文件

import { takeLatest, put, all } from "redux-saga/effects";

const effects = {
  testSaga1: function* (e) {
    console.log("通过saga1中间件调用了effect");
    yield put({ type: "storeA/increment" });
  },
  testSaga2: function* (e) {
    console.log("通过saga2中间件调用了effect");
    yield put({ type: "storeA/increment" });
  },
};

function* watcher() {
  yield all([
    yield takeLatest("storeA/testSaga1", effects.testSaga1),
    yield takeLatest("storeA/testSaga2", effects.testSaga2),
  ]);
}

export default watcher;

  takeLates:表示监听最新的一次调用,第一个参数表示监听的类型,比如多次dispatch({type:"storeA/testSaga1"}),saga只会触发一次,如果使用的是takeEvery,则有几次调用就会触发几次,上诉例子表明监听两个调用类型,当监听到调用后回去执行effects里面的方法,effects里面的方法使用的是put,类型dispatch。

  上面只是创建了saga的中间件文件,需要把saga中间件注册到redux中,如下,修改原本创建store方法,把sagaMiddleware中间件添加进去。

import createSagaMiddleware from "redux-saga";
import saga from "./saga"; //这个就是上面刚刚创建的saga文件

//创建saga中间件
const sagaMiddleware = createSagaMiddleware();

//创建store对象
const store = configureStore({
  reducer: {
    storeA,
    storeB
  },
     middleware: (getDefaultMiddleware) => {
    return getDefaultMiddleware({
      serializableCheck: false,
    }).concat(sagaMiddleware);
  },
});

sagaMiddleware.run(saga);

  下面就是调用方式。

<Button
        onClick={() => {
          dispatch({
            type: "storeA/testSaga1",
            payload: { name: "zhouyun", sex: "man" },
          });
        }}
      >
        调用saga1
      </Button>

  到这里基本框架搭建完毕,但是缺少redux的持久化支持,因为redux没做持久化支持刷新页面会出现保存在redux的数据会丢失,基本的解决思路是使用localStorage来存储redux的数据,但localStorage是没有过期时间的,所以需要有个数据过期功能。所以这里我使用了第三方的解决方案redux-persist,本质也是使用的localStorage来存储数据,这里有两个概念,分为持久化和水化,持久化就是redux-persist将redux数据保存到localStorage,并且会打上时间戳,水化就是将localStorage的数据读取出来返回给redux的过程,数据过期的逻辑就在这两个过程里,持久化过程打时间戳,水化过程判断时间戳,如果已经过期则重置数据使存储的数据失效。下面是具体集成步骤:

  修改原有的store文件,下面这个是最全的文件,包含了saga和redux-persist持久化,持久化配置里面的whitelist白名单就是表示需要使用持久化方案的store模块,blacklist黑名单表示不使用持久化方案的store模块,未使用持久化方案的数据池在页面刷新后就会丢失数据,使用了持久化方案的数据池在每次页面操作都会刷新时间戳,使时间戳一直保持在最新时间,长时间未操作页面的时间超过设置的过期时间后就会失效。

import { configureStore, combineReducers } from "@reduxjs/toolkit";
import storeA from "./modules/storeA";
import storeB from "./modules/storeB";
import createSagaMiddleware from "redux-saga";
import saga from "./saga";
import { persistStore, persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage";
import expireReduxState from "./persistExpire";

//合并reducer
const rootReducer = combineReducers({
  storeA,
  storeB,
});

//持久化配置
const persistConfig = {
  key: "root",
  storage,
  whitelist: ["storeA"],
  blacklist: ["storeB"],
  transforms: [
    //自定义transform,定义持久化和水化逻辑
    expireReduxState("storeA", {
      expireAfter: 5, //持久化过期时间,单位秒
      expiredState: {
        //持久化过期后的重置初始化对象
        value: 0,
        obj: {
          num: 20,
        },
      },
    }),
  ],
};

//持久化reducer
const persistReducers = persistReducer(persistConfig, rootReducer);
//创建saga中间件
const sagaMiddleware = createSagaMiddleware();
//创建store对象
const store = configureStore({
  reducer: persistReducers,
  middleware: (getDefaultMiddleware) => {
    return getDefaultMiddleware({
      serializableCheck: false,
    }).concat(sagaMiddleware);
  },
});

sagaMiddleware.run(saga);

const persistor = persistStore(store);
export { store, persistor };

  persistExpire.js是自定义的transform方法,自定义了持久化和水化的逻辑,下面这个就是文件内容:

import { createTransform } from "redux-persist";

//时间戳过期时间属性key
const EXPIRE_DEFAULT_KEY = "persistedTimestamp";

/**
 * Returns the numeric value of the specified date as the number of seconds since January 1, 1970, 00:00:00 UTC
 * @param {Date} date
 */
const getUnixTime = (date) => {
  return Number((date.getTime() / 1000).toFixed(0));
};

/**
 * Creates transform object with defined expiry config
 * @param {string} key - reducerKey
 * @param {object} config - expiry config
 * @returns {Transform<{}, any>}
 */
const expireReduxState = (key, config) => {
  const default_config = {
    expiredState: {},
    expireKey: EXPIRE_DEFAULT_KEY,
    expireAfter: null, // expiration time in seconds
  };
  config = Object.assign({}, default_config, config);
  return createTransform(
    //持久化逻辑,添加过期时间
    (inbound) => {
      inbound = inbound || {};
      if (config.expireAfter) {
        inbound = Object.assign({}, inbound, {
          [config.expireKey]: new Date(),
        });
      }
      return inbound;
    },
    //水化逻辑,判断时间戳是否过期
    (outbound) => {
      outbound = outbound || {};
      if (config.expireAfter && outbound.hasOwnProperty(config.expireKey)) {
        let stateAge =
          getUnixTime(new Date(outbound[config.expireKey])) +
          config.expireAfter;
        let current = getUnixTime(new Date());
        // if persisted state expired, set it to default state.
        if (stateAge < current) {
          outbound = config.expiredState;
        }
      }
      return outbound;
    },
    {
      whitelist: [key],
    }
  );
};

export default expireReduxState;

  到这一个基本的react技术架构就搭建完成了,至于react hooks的使用可以自己查看react官网的使用例子。

    在react入口文件index.js添加redux的配置,下面是部分代码片段,包含了UI组件库antd,redux,持久化persistor,以及路由配置。

import { HashRouter, BrowserRouter } from "react-router-dom";
import { Spin, ConfigProvider } from "antd";
import { store, persistor } from "./store";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import zhCN from "antd/locale/zh_CN";

root = ReactDOM.createRoot(mountNode);
  root.render(
    <ConfigProvider locale={zhCN}>
      <Provider store={store}>
        <PersistGate loading={null} persistor={persistor}>
          <Suspense fallback={<Spin size="large" style={{ marginTop: 100 }} />}>
            <BrowserRouter>
              <App />
            </BrowserRouter>
          </Suspense>
        </PersistGate>
      </Provider>
    </ConfigProvider>
  );

  

posted @ 2024-06-14 15:43  颜色不一样的烟火  阅读(1)  评论(0编辑  收藏  举报