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参数调用,如下
1 2 | <Button onClick={() => dispatch(increment())}>+ 1 </Button> <Button onClick={() => dispatch({ type: "storeA/increment" })}>+ 1 </Button> |
对于异步操作还可以使用中间件saga,首先创建saga文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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中间件添加进去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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); |
下面就是调用方式。
1 2 3 4 5 6 7 8 9 10 | <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模块,未使用持久化方案的数据池在页面刷新后就会丢失数据,使用了持久化方案的数据池在每次页面操作都会刷新时间戳,使时间戳一直保持在最新时间,长时间未操作页面的时间超过设置的过期时间后就会失效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | 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方法,自定义了持久化和水化的逻辑,下面这个就是文件内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | 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,以及路由配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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> ); |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?