欢迎关注我的其他文章
webpack5从零搭建完整的react18+ts项目模板
上篇文章,我们做了项目的一些初始化工作,本篇将加入一些团队实际开发过程中必不可少的基本的配置例如
react-router、redux、axios封装等;
redux配置
安装依赖
redux也是我们实际项目开发中常用的配置项,react中的props需要安装redux、react-redux、redux-thunk、redux-persist、redux-promise、@types/redux-promise、@reduxjs/toolkit等依赖,依赖安装命令如下:
npm i redux react-redux redux-thunk redux-persist redux-promise @types/redux-promise @reduxjs/toolkit -D
or
yarn add redux react-redux redux-thunk redux-persist redux-promise @types/redux-promise @reduxjs/toolkit -D
定义redux配置文件
依赖安装完成之后,我们在src目录下创建一个redux目录用来创建redux相关配置,目录结构如下:
├─ redux
│ ├─ interface # ts 接口声明
│ │ ├─ index.ts # 声明文件
│ ├─ moudles # 模块文件目录
│ │ ├─ home.ts # redux 模块
│ ├─ index.ts # 入口文件
interface/index.ts文件主要是用来放置redux目录下文件模块使用到的ts接口声明;例如下面的内容:
// interface/index.ts
// HomeState
export interface HomeState {
userInfo: any;
}
moudles目录主要用来放置redux的模块文件,例如:
// modules/home.ts
import { HomeState } from "@/redux/interface";
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
const homeState: HomeState = {
userInfo: null,
};
const homeSlice = createSlice({
name: "home",
initialState: homeState,
reducers: {
// 设置用户信息
setUserInfo: (state: HomeState, { payload }: PayloadAction<object>) => {
state.userInfo = payload;
},
},
});
export const { setUserInfo } = homeSlice.actions;
export default homeSlice.reducer;
index.ts则是redux配置目录的入口文件,内容如下:
import { configureStore, combineReducers } from "@reduxjs/toolkit";
import { persistStore, persistReducer } from "redux-persist";
import { TypedUseSelectorHook, useDispatch as useReduxDispatch, useSelector as useReduxSelector } from "react-redux";
import storage from "redux-persist/lib/storage";
import reduxThunk from "redux-thunk";
import reduxPromise from "redux-promise";
import home from "./modules/home";
// create reducer
const reducer = combineReducers({
home
});
// redux persist
const persistConfig = {
key: "redux-state",
storage: storage
};
const persistReducerConfig = persistReducer(persistConfig, reducer);
// redux middleWares
const middleWares = [reduxThunk, reduxPromise];
// store
export const store = configureStore({
reducer: persistReducerConfig,
middleware: middleWares,
devTools: true
});
// create persist store
export const persistor = persistStore(store);
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const useSelector: TypedUseSelectorHook<RootState> = useReduxSelector;
export const useDispatch = () => useReduxDispatch<AppDispatch>();
使用redux如下:
import React from "react";
import { useSelector, useDispatch, RootState } from "@/redux/index";
import { setUserInfo } from "@/redux/modules/home";
const Home = () => {
console.log("NODE_ENV", process.env.NODE_ENV);
console.log("BASE_ENV", process.env.BASE_ENV);
const dispatch = useDispatch();
const { userInfo } = useSelector((state: RootState) => state.home);
const but = () => {
console.log("执行");
dispatch(setUserInfo({ age: 1, name: "Home", email: "2095034789@qq.com" }));
};
return (
<div>
<div>Home组件</div>
<button onClick={but}>赋值</button>
<div>name:{userInfo.name}</div>
<div>age:{userInfo.age}</div>
<div>email:{userInfo.email}</div>
</div>
);
};
export default Home;
至此redux的配置以及持久化配置就都已经基本完成了,但是这时候你在页面里面使用useSelector你会发现页面报错,这是因为我们要想在页面中使用redux还需要对src/index.ts进行改造:
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { Provider } from "react-redux";
import { store, persistor } from "@/redux";
import { PersistGate } from "redux-persist/integration/react";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<Provider store={store}>
<PersistGate persistor={persistor}>
<App />
</PersistGate>
</Provider>
);
这时候你再去使用useSelector方法,你会发现不会再报错,可以正常使用了;
路由配置
路由是前端开发中不可或缺的一个重要概念,它允许我们根据 URL 的不同来显示不同的内容和 UI。在 react 开发中,我们通常会使用第三方库来实现路由功能。最常用的就是 react-router
安装依赖
在使用 react-router 之前,我们需要先安装它。依赖安装命令如下:
npm i react-router-dom -D
or
yarn add react-router-dom -D
因为我们的项目有使用typescript所以我们还需要安装@types/react-router-dom声明文件,使typescript能识别到react-router-dom,依赖安装如下:
npm i @types/react-router-dom -D
or
yarn add @types/react-router-doom -D
定义路由配置文件
为了方便维护我们的路由信息,我们在实际项目开发中一般是定义一个路由配置文件,单独维护路由信息,在项目的src目录下创建一个router目录,在该目录下再定义一个路由配置文件index.tsx,内容如下:
// router/index.tsx
import React, { lazy, Suspense } from "react";
import { Route, Routes, Navigate } from "react-router-dom";
const Home = lazy(() => import("../pages/Home"));
const Class = lazy(() => import("../components/Class"));
const LazyDemo = lazy(() => import("../components/LazyDemo"));
type RouteObject = {
path: string;
element?: React.ReactElement | React.FC;
permissions?: Array<string>;
children?: Array<RouteObject>;
};
// 全局路径
const globalRoutes: RouteObject[] = [
{
path: "/", // 路径
element: <Home />,
permissions: ["add"], // 权限
},
];
// 主路由->后续接口中动态获取
const mainRoutes: RouteObject[] = [
{
path: "/class",
permissions: ["add", "edit"], // 权限
children: [
{
path: "",
element: <Class />
},
{
path: "lazy",
element: <LazyDemo />,
},
],
},
];
// 路由错误重定向
const NotFound = () => {
return <div>你所访问的页面不存在!</div>;
};
let routes: RouteObject[] = globalRoutes.concat(mainRoutes);
// 路由路径处理函数
const transformRoutes = (routeList: RouteObject[]) => {
return (
<>
{routeList.map((route: any) => {
return route.children && route.children.length ? (
<Route key={route.path} path={route.path} element={route.element}>
{transformRoutes(route.children)}
</Route>
) : (
<Route
key={route.path}
path={route.path}
element={route.element}
></Route>
);
})}
</>
);
};
console.log("transformRoutes", transformRoutes(routes));
const RoutersConfig = () => {
return (
<Suspense fallback={<div>loading...</div>}>
<Routes>{transformRoutes(routes)}</Routes>
</Suspense>
);
};
export default RoutersConfig;
全局注册路由
import React from "react";
import { BrowserRouter } from "react-router-dom";
import RoutersConfig from "./router/router";
function App() {
return (
<BrowserRouter>
<RoutersConfig />
</BrowserRouter>
);
}
export default App;
至此react-router-dom路由的基本配置就搭建完了
axios 封装
axios也是我们项目中常用的配置项来着,我们通常都是将axios请求统一封装,然后调用的,该节主要介绍我在项目中是如何封装axios,大家可以借鉴一下根据你们项目的实际情况去调整;
依赖安装
npm i axios -D
or
yarn add axios -D
定义axios配置文件
在src目录下创建一个api目录,目录下创建interface、config、helper、modules 等目录,在api目录下创建一个request.ts文件,内容如下:
import NProgress from "@/config/nprogress";
import axios, {
AxiosInstance,
AxiosError,
AxiosRequestConfig,
AxiosResponse,
} from "axios";
import {
showFullScreenLoading,
tryHideFullScreenLoading,
} from "@/config/serviceLoading";
import { ResultData } from "./interface/index";
import { AxiosCanceler } from "./helper/axiosCancel";
import { ResultEnum } from "@/enums/httpEnum";
import { checkStatus } from "./helper/checkStatus";
import { API_URL } from "@/config/env";
import { store } from "@/redux";
import { setToken } from "@/redux/modules/home";
import { Toast } from "antd-mobile";
console.log("API_URL", API_URL);
const axiosCanceler = new AxiosCanceler();
const config = {
// 默认地址请求地址,可在 .env 开头文件中修改
baseURL: API_URL,
// 设置超时时间(10s)
timeout: 10000,
// 跨域时候允许携带凭证
withCredentials: true,
};
class RequestHttp {
service: AxiosInstance;
public constructor(config: AxiosRequestConfig) {
// 实例化axios
this.service = axios.create(config);
/**
* @description 请求拦截器
* 客户端发送请求 -> [请求拦截器] -> 服务器
* token校验(JWT) : 接受服务器返回的token,存储到redux/本地储存当中
*/
this.service.interceptors.request.use(
(config: AxiosRequestConfig): any => {
NProgress.start();
// * 将当前请求添加到 pending 中
axiosCanceler.addPending(config);
// * 如果当前请求不需要显示 loading,在api服务中通过指定的第三个参数: { headers: { noLoading: true } }来控制不显示loading,参见loginApi
config.headers!.noLoading || showFullScreenLoading();
const token: string = store.getState().home.token;
return {
...config,
headers: { ...config.headers, "x-access-token": token },
};
},
(error: AxiosError) => {
console.log("error", error);
return Promise.reject(error);
}
);
/**
* @description 响应拦截器
* 服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息
*/
this.service.interceptors.response.use(
(response: AxiosResponse): any => {
const { data, config } = response;
NProgress.done();
// * 在请求结束后,移除本次请求(关闭loading)
axiosCanceler.removePending(config);
tryHideFullScreenLoading();
// * 登录失效
if (data.code == ResultEnum.OVERDUE) {
store.dispatch(setToken(""));
Toast.show({
content: data.msg,
maskClickable: false,
});
window.location.hash = "/login";
return Promise.reject(data);
}
// * 全局错误信息拦截(防止下载文件得时候返回数据流,没有code,直接报错)
if (data.code && data.code !== ResultEnum.SUCCESS) {
Toast.show({
content: data.msg,
maskClickable: false,
});
return Promise.reject(data);
}
// * 成功请求(在页面上除非特殊情况,否则不用处理失败逻辑)
return data;
},
async (error: AxiosError) => {
const { response } = error;
NProgress.done();
tryHideFullScreenLoading();
// 请求超时单独判断,请求超时没有 response
if (error.message.indexOf("timeout") !== -1)
Toast.show({
content: "请求超时,请稍后再试",
maskClickable: false,
})
// 根据响应的错误状态码,做不同的处理
if (response) checkStatus(String(response.status));
// 服务器结果都没有返回(可能服务器错误可能客户端断网) 断网处理:可以跳转到断网页面
if (!window.navigator.onLine) window.location.hash = "/500";
return Promise.reject(error);
}
);
}
// * 常用请求方法封装
get<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
return this.service.get(url, { params, ..._object });
}
post<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
return this.service.post(url, params, _object);
}
put<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
return this.service.put(url, params, _object);
}
delete<T>(url: string, params?: any, _object = {}): Promise<ResultData<T>> {
return this.service.delete(url, { params, ..._object });
}
}
export default new RequestHttp(config)
config/serviceLoading.ts文件内容如下:
import React from "react";
import ReactDOM from "react-dom/client";
import Loading from "@/Demo/components/Loading";
let needLoadingRequestCount = 0;
// * 显示loading
export const showFullScreenLoading = () => {
if (needLoadingRequestCount === 0) {
let dom = document.createElement("div");
dom.setAttribute("id", "loading");
document.body.appendChild(dom);
ReactDOM.createRoot(dom).render(<Loading />);
}
needLoadingRequestCount++;
};
// * 隐藏loading
export const tryHideFullScreenLoading = () => {
if (needLoadingRequestCount <= 0) return;
needLoadingRequestCount--;
if (needLoadingRequestCount === 0) {
document.body.removeChild(document.getElementById("loading") as HTMLElement);
}
};
components/Loading/index.tsx文件内容如下:
import React from "react";
import { SpinLoading } from "antd-mobile";
import "./index.less";
const Loading = ({ color = "default" }: { color?: string }) => {
return <SpinLoading color={color} className="request-loading"></SpinLoading>;
};
export default Loading;
interface/index.ts文件内容如下:
// * 请求响应参数(不包含data)
export interface Result {
code: string;
msg: string;
}
// * 请求响应参数(包含data)
export interface ResultData<T = any> extends Result {
data?: T;
}
// * 登录
export namespace Login {
export interface ReqLoginForm {
username: string;
password: string;
}
export interface ResLogin {
access_token: string;
}
export interface ResAuthButtons {
[propName: string]: any;
}
}
helper/axiosCancel.ts文件内容如下:
import axios, { AxiosRequestConfig, Canceler } from "axios";
import { isFunction } from "@/utils/is/index";
import qs from "qs";
// * 声明一个 Map 用于存储每个请求的标识 和 取消函数
let pendingMap = new Map<string, Canceler>();
// * 序列化参数
export const getPendingUrl = (config: AxiosRequestConfig) =>
[config.method, config.url, qs.stringify(config.data), qs.stringify(config.params)].join("&");
export class AxiosCanceler {
/**
* @description: 添加请求
* @param {Object} config
*/
addPending(config: AxiosRequestConfig) {
// * 在请求开始前,对之前的请求做检查取消操作
this.removePending(config);
const url = getPendingUrl(config);
config.cancelToken =
config.cancelToken ||
new axios.CancelToken(cancel => {
if (!pendingMap.has(url)) {
// 如果 pending 中不存在当前请求,则添加进去
pendingMap.set(url, cancel);
}
});
}
/**
* @description: 移除请求
* @param {Object} config
*/
removePending(config: AxiosRequestConfig) {
const url = getPendingUrl(config);
if (pendingMap.has(url)) {
// 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
const cancel = pendingMap.get(url);
cancel && cancel();
pendingMap.delete(url);
}
}
/**
* @description: 清空所有pending
*/
removeAllPending() {
pendingMap.forEach(cancel => {
cancel && isFunction(cancel) && cancel();
});
pendingMap.clear();
}
/**
* @description: 重置
*/
reset(): void {
pendingMap = new Map<string, Canceler>();
}
}
helper/checkStatus.ts文件内容如下:
import {Toast} from 'antd-mobile'
/**
* @description 校验网络请求状态码
* @param {string} status 状态码
*/
export const checkStatus = (status: string) => {
switch(status) {
case "400":
Toast.show({
content: "请求失败!请您稍后重试",
maskClickable: false,
})
break;
case "401":
Toast.show({
content: "登录失效!请您重新登录",
maskClickable: false,
})
break;
case "403":
Toast.show({
content: "当前账号无权限访问!",
maskClickable: false,
})
break;
case "404":
Toast.show({
content: "你所访问的资源不存在!",
maskClickable: false,
})
break;
case "405":
Toast.show({
content: "请求方式错误!请您稍后重试",
maskClickable: false,
})
break;
case "408":
Toast.show({
content: "请求超时!请您稍后重试",
maskClickable: false,
})
break;
case "500":
Toast.show({
content: "服务异常!",
maskClickable: false,
})
break;
case "502":
Toast.show({
content: "网关错误!",
maskClickable: false,
})
break;
case "503":
Toast.show({
content: "服务不可用!",
maskClickable: false,
})
break;
case "504":
Toast.show({
content: "网关超时!",
maskClickable: false,
})
break;
default:
Toast.show({
content: "请求失败!",
maskClickable: false,
})
}
}
modules/login.ts文件内容如下:
import { Login } from "../interface/index";
// import qs from 'qs';
import http from "../request";
/**
* @name 登录模块
*/
// * 用户登录接口
export const loginApi = (params: Login.ReqLoginForm) => {
try {
return http.get<Login.ResLogin>(`/rec`, params);
// return http.post<Login.ResLogin>(`/login`, {}, { params }); // post 请求携带 query 参数 ==> ?username=admin&password=123456
// return http.post<Login.ResLogin>(`/login`, qs.stringify(params)); // post 请求携带 表单 参数 ==> application/x-www-form-urlencoded
// return http.post<Login.ResLogin>(`/login`, params, { headers: { noLoading: true } }); // 控制当前请求不显示 loading
} catch (err) {}
};