目录项目目录及ts文件划分在项目中使用typescript具体实践组件声明React Hooks使用useStateuseRefuseCallbackuseMemouseContex
前言:
本文主要记录我如何在React项目中优雅的使用TypeScript,来提高开发效率及项目的健壮性。
由于我在实际项目中大部分是使用umi
来进行开发项目,所以使用umi
生成的目录来做案例。
.
├── README.md
├── global.d.ts
├── mock
├── package.JSON
├── src
│ ├── assets
│ ├── components
│ │ └── PublicComA
│ │ ├── index.d.ts
│ │ ├── index.less
│ │ └── index.tsx
│ ├── layouts
│ ├── models
│ ├── pages
│ │ ├── PageA
│ │ │ ├── index.d.ts
│ │ │ ├── index.less
│ │ │ └── index.tsx
│ │ ├── index.less
│ │ └── index.tsx
│ └── utils
├── tsconfig.json
├── typings.d.ts
└── yarn.lock
在项目根目录下有typings.d.ts和global.d.ts这两个文件, 前者我们可以放置一些全局的导出模块,比如CSS,less, 图片的导出声明;后者可以放一些全局声明的变量, 接口等, 比如说window下全局变量的声明等。
如下:
// typings.d.ts
declare module '*.css';
declare module '*.less';
declare module "*.png";
declare module "*.jpeg";
declare module '*.svg' {
export function ReactComponent(props: React.SVGProps<SVGSVGElement>): React.ReactElement
const url: string
export default url
}
// global.d.ts
interface Window {
helloWorld: () => void;
}
接下来介绍一下src目录:
在pages和components中有存放当前组件/页面所需要的类型和接口声明的index.d.ts。另外如models中的文件由于是每个model私有类型和接口声明,所以可以直接在文件内部去声明。 具体的目录规划如上,可以根据实际项目来做更合理的划分。
React.FC<P={}>
来表示函数类型,当使用该类型定义组件时,props中会默认带有children属性。interface IProps {
count: number
}
const App: React.FC<IProps> = (props) => {
const {count} = props;
return (
<div className="App">
<span>count: {count}</span>
</div>
);
}
React.PureComponent<P, S={} SS={}>
定义组件,则还有第三个参数,表示getSnapshotBeforeUpdate
的返回值。interface IProps {
name: string;
}
interface IState {
count: number;
}
class App extends React.Component<IProps, IState> {
state = {
count: 0
};
render() {
return (
<div>
{this.state.count}
{this.props.name}
</div>
);
}
}
声明定义:
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
// convenience overload when first argument is omitted
function useState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>];
如果初始值能够体现出类型,那么可以不用手动声明类型,TS会自动推断出类型。如果初始值为null或者undefined则需要通过泛型显示声明类型。
如下:
const [count, setCount] = useState(1);
const [user, setUser] = useState<IUser | null>(null);
声明定义:
function useRef<T>(initialValue: T): MutableRefObject<T>;
// convenience overload for refs given as a ref prop as they typically start with a null value
使用该Hook时,要根据使用场景来判断传入泛型类型,如果是获取DOM节点,则传入对应DOM类型即可;如果需要的是一个可变对象,则需要在泛型参数中包含'| null'。
如下:
// 不可变DOM节点,只读
const inputRef = useRef<htmlInputElement>(null);
// 可变,可重新复制
const idRef = useRef<string | null>(null);
idRef.current = "abc";
声明定义:
function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;
useCallback会根据返回值自动推断出类型,如果传入的参数不指定类型,则会默认为any
,所以为了严谨和可维护性,一定要指定入参的类型。也可以手动传入泛型指定函数类型。
如下:
// 会自动推导出类型: (a: number, b: number) => number;
const add = useCallback((a: number, b: number) => a + b, [a, b])
// 传入泛型,则指定函数类型
const toggle = useCallback<(a: number) => number>((a: number) => a * 2, [a])
声明定义:
function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T;
useMemo和useCallback类似,只是定义类型为具体返回值的类型,而不是函数的类型。
如下:
// 会自动推导出类型: number;
const add = useCallback((a: number, b: number) => a + b, [a, b])
// 传入泛型,则指定函数类型
const toggle = useCallback<number>((a: number) => a * 2, [a])
声明定义:
function useContext<T>(context: Context<T>): T;
useContext会根据传入的上下文对象自动推导出context的类型,当然也可以使用泛型来设置context的类型,
如下:
interface ITheme {
color: string;
}
const ThemeContext = React.createContext<ITheme>({ color: "red" });
// 自动推导出类型为ITheme
const theme = useContext(ThemeContext); // 等同于const theme = useContext<ITheme>(ThemeContext);
声明定义:
function useReducer<R extends Reducer<any, any>>(
reducer: R,
initialState: ReducerState<R>,
initializer?: undefined
): [ReducerState<R>, Dispatch<ReducerAction<R>>];
上面只列出了一种类型定义,我在项目中也是使用这种定义去指定useReducer
的类型。普通的案例如下:
type StateType = {
name: string;
age: number;
}
type Actions = {
type: 'Change_Name';
payload: string;
} | {
type: 'Change_Age';
payload: number;
}
const initialState = {
name: '小明',
age: 18
}
const reducerAction: Reducer<StateType, Actions> = (
state,
action,
) => {
switch (action.type) {
case 'Change_Name':
return { ...state, name: action.payload };
case 'Change_Age':
return { ...state, age: action.payload };
default:
return state;
}
};
function Index() {
const [state, dispatch] = useReducer(reducerAction, initialState);
return (
<div>
<div>姓名:{state.name}</div>
<div>年龄:{state.age}</div>
</div>
);
}
可以看到,这样能够得到正确的类型推断,但是略微繁琐。
案例如下:
// 定义一个生成Action类型的泛型
type ActionMap<M extends Record<string, any>> = {
[Key in keyof M]: M[Key] extends undefined
? {
type: Key
}
: {
type: Key
payload: M[Key]
}
}
type StateType = {
name: string;
age: number;
}
// 定义具体的Action类型
type PayloadType = {
Change_Name: string;
Change_Age: number;
}
type Actions = ActionMap<PayloadType>[keyof ActionMap<PayloadType>]
const initialState = {
name: '小明',
age: 18
}
const reducerAction: Reducer<StateType, Actions> = (
state,
action,
) => {
switch (action.type) {
case Types.Name:
return { ...state, name: action.payload };
case Types.Age:
return { ...state, age: action.payload };
default:
return state;
}
};
我们定义了一个ActionMap
泛型,该泛型会将传入的类型{key: value}
生成为新的{key: {type: key, payload: value }
类型。然后我们利用keyof
关键字获取到所有的key,就可以得到我们所需要的{type: key1, payload: value1} | {type: key2, payload: value2}
的类型了。只要我们定义好PayloadType
类型,则可以自动推导出我们需要的Actions
类型。
声明定义:
function useImperativeHandle<T, R extends T>(ref: Ref<T>|undefined, init: () => R, deps?: DependencyList): void;
// NOTE: this does not accept strings, but this will have to be fixed by removing strings from type Ref<T>
useImperativeHandle
可以让自定义组件通过ref
属性,将内部属性暴露给父组件进行访问。因为是函数式组件,所以需要结合forwardRef
一起使用。
案例如下:
interface FancyProps {}
interface FancyRef {
focus: () => void;
}
const FancyInput = forwardRef<FancyRef, FancyProps>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current?.focus();
}
}));
return (
<input ref={inputRef} {...props} />
);
})
const Parent = () => {
// 定义子组件ref
const inputRef = useRef<FancyRef>(null);
return (
<div>
<FancyInput
ref={inputRef}
/>
<button
onClick={() => {
// 调用子组件方法
inputRef.current?.focus();
}}
>聚焦</button>
</div>
)
}
axios
是很流行的Http库,他的ts封装已经很完美了,我们只做简单的二次封装,返回通用的数据响应格式。 首先在utils/request.ts
中创建一个构造axios实例的生成器:
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
// 拦截器定义
export interface RequestInterceptors {
// 请求拦截
requestInterceptors?: (config: AxiosRequestConfig) => AxiosRequestConfig
requestInterceptorsCatch?: (err: any) => any
// 响应拦截
responseInterceptors?: (config: AxiosResponse) => AxiosResponse
responseInterceptorsCatch?: (err: any) => any
}
// 生成axios实例的参数,实例可以单独传入拦截器
export interface RequestConfig extends AxiosRequestConfig {
interceptorsObj?: RequestInterceptors
}
// loading请求数量
let loadinGCount: number = 0;
// 打开loading
const showLoading = () => {
loadingCount ++;
if(loadingCount > 0) {
// 显示loading
// Loading.show()
}
}
// 关闭loading
const hideLoading = () => {
loadingCount --;
if(loadingCount <= 0) {
// 隐藏loading
// Loading.hide();
}
}
function RequestBuilder(config: RequestConfig) {
const { interceptorsObj, ...res } = config;
const instance: AxiosInstance = axios.create(res);
// 全局请求拦截器
instance.interceptors.request.use(
(request: AxiosRequestConfig) => {
// 显示loading
showLoading();
console.log('全局请求拦截器');
// TODO:全局的请求头操作等等
return request;
},
(err: any) => err,
)
instance.interceptors.request.use(
interceptorsObj?.requestInterceptors,
interceptorsObj?.requestInterceptorsCatch,
)
instance.interceptors.response.use(
interceptorsObj?.responseInterceptors,
interceptorsObj?.responseInterceptorsCatch,
)
// 全局响应拦截器
instance.interceptors.response.use(
(response: AxiosResponse) => {
console.log('全局响应拦截器');
// 关闭loading
hideLoading();
// TODO: 通用的全局响应处理,token过期重定向登录等等
// 返回值为res.data,即后端接口返回的数据,减少解构的层级,以及统一响应数据格式。
return response.data
},
(err: any) => {
// 关闭loading
hideLoading();
// TODO: 错误提示等
return err;
},
)
return instance;
}
export const http = RequestBuilder({baseURL: '/api'});
该生成器可以实现每个实例有单独的拦截器处理逻辑,并且实现全局的loading加载效果,全局拦截器的具体实现可以根据项目实际需求进行填充。生成器已经完成,但是还没法定制我们的通用响应数据,接下来我们在typings.d.ts
中重新定义axios模块:
import * as axios from 'axios';
declare module 'axios' {
// 定制业务相关的网络请求响应格式, T 是具体的接口返回类型数据
export interface CustomSuccessData<T> {
code: number;
msg?: string;
message?: string;
data: T;
[keys: string]: any;
}
export interface AxiosInstance {
// <T = any>(config: AxiosRequestConfig): Promise<CustomSuccessData<T>>;
request<T = any, R = CustomSuccessData<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
get<T = any, R = CustomSuccessData<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
delete<T = any, R = CustomSuccessData<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
head<T = any, R = CustomSuccessData<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
post<T = any, R = CustomSuccessData<T>, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>,
): Promise<R>;
put<T = any, R = CustomSuccessData<T>, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>,
): Promise<R>;
patch<T = any, R = CustomSuccessData<T>, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>,
): Promise<R>;
}
}
完成以上操作后,我们在业务代码中具体使用:
import { http } from '@/utils/request';
interface Req {
userId: string;
}
interface Res {
userName: string;
userId: string;
}
// 获取用户信息接口
const getUserInfo = async (params: Req) => {
return http.get<Res>('/getUserInfo', {params})
}
这个时候getUserInfo
返回的就是CustomSuccessData<Res>
类型的数据了。至此我们对axios
简单的封装也就完成了。
到此这篇关于在React项目中使用TypeScript详情的文章就介绍到这了,更多相关React使用TypeScript内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!
--结束END--
本文标题: 在React项目中使用TypeScript详情
本文链接: https://lsjlt.com/news/167943.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-01-12
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0