react框架研习
一、react项目技术架构组成
- 基础技术react版本18.2.0
- 路由插件react-router
-
数据仓库react-redux
- UI组件库antd
- dotenv-cli来区分不同环境变量
二、框架搭建
脚手架创建基础项目
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> );