• 如何在React项目中使用TypeScript


    本文主要记录我如何在React项目中优雅的使用TypeScript,来提高开发效率及项目的健壮性。

    项目目录及ts文件划分

    由于我在实际项目中大部分是使用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 
    
    • 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

    在项目根目录下有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): React.ReactElementconst url: stringexport default url
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    // global.d.ts
    interface Window {helloWorld: () => void;
    } 
    
    • 1
    • 2
    • 3

    接下来介绍一下src目录:

    • assets 存放静态资源如图片/视频/音频等, 参与webpack的打包过程
    • layouts 存放公共布局
    • components 存放全局公共组件
    • models dva的models文件夹
    • pages 存放页面的目录, 内部可以有页面组件components, 结构类似于全局的components
    • utils 存放js工具库, 请求库等公共js文件

    在pages和components中有存放当前组件/页面所需要的类型和接口声明的index.d.ts。另外如models中的文件由于是每个model私有类型和接口声明,所以可以直接在文件内部去声明。 具体的目录规划如上,可以根据实际项目来做更合理的划分。

    在项目中使用TypeScript具体实践

    组件声明

    1.函数组件 推荐使用React.FC来表示函数类型,当使用该类型定义组件时,props中会默认带有children属性。

    interface IProps {count: number
    }
    
    const App: React.FC = (props) => {const {count} = props;return (
    count: {count}
    ); }
    • 1
    • 2
    • 3
    • 4
    • 5

    2.类组件 类组件接受两个参数,第一个是props的定义,第二个是state的定义,如果使用React.PureComponent定义组件,则还有第三个参数,表示getSnapshotBeforeUpdate的返回值。

    interface IProps {name: string;
    }
    
    interface IState {count: number;
    }
    
    class App extends React.Component {state = {count: 0};render() {return (
    {this.state.count}{this.props.name}
    );} }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    React Hooks使用

    useState

    声明定义:

    function useState(initialState: S | (() => S)): [S, Dispatch>];
    // convenience overload when first argument is omitted
    	/**
    	 * Returns a stateful value, and a function to update it. * * @version 16.8.0 * @see https://reactjs.org/docs/hooks-reference.html#usestate */function useState(): [S | undefined, Dispatch>];/** * An alternative to `useState`. * * `useReducer` is usually preferable to `useState` when you have complex state logic that involves * multiple sub-values. It also lets you optimize performance for components that trigger deep * updates because you can pass `dispatch` down instead of callbacks. * * @version 16.8.0 * @see https://reactjs.org/docs/hooks-reference.html#usereducer */ 
    
    • 1
    • 2
    • 3
    • 4

    如果初始值能够体现出类型,那么可以不用手动声明类型,TS会自动推断出类型。如果初始值为null或者undefined则需要通过泛型显示声明类型。如下:

    const [count, setCount] = useState(1);
    
    const [user, setUser] = useState(null); 
    
    • 1
    • 2
    • 3
    useRef

    声明定义:

     function useRef(initialValue: T): MutableRefObject;
     // convenience overload for refs given as a ref prop as they typically start with a null value
     /** * `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument * (`initialValue`). The returned object will persist for the full lifetime of the component. * * Note that `useRef()` is useful for more than the `ref` attribute. It’s handy for keeping any mutable * value around similar to how you’d use instance fields in classes. * * Usage note: if you need the result of useRef to be directly mutable, include `| null` in the type * of the generic argument. * * @version 16.8.0 * @see https://reactjs.org/docs/hooks-reference.html#useref */ 
    
    • 1
    • 2
    • 3

    使用该Hook时,要根据使用场景来判断传入泛型类型,如果是获取DOM节点,则传入对应DOM类型即可;如果需要的是一个可变对象,则需要在泛型参数中包含’| null’。如下:

    // 不可变DOM节点,只读
    const inputRef = useRef(null);
    
    // 可变,可重新复制
    const idRef = useRef(null);
    idRef.current = "abc"; 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    useCallback

    声明定义:

     function useCallback any>(callback: T, deps: DependencyList): T;
     /*** `useMemo` will only recompute the memoized value when one of the `deps` has changed.** Usage note: if calling `useMemo` with a referentially stable function, also give it as the input in* the second argument.** ```ts* function expensive () { ... }** function Component () {* const expensiveResult = useMemo(expensive, [expensive])* return ...* }* ```** @version 16.8.0* @see https://reactjs.org/docs/hooks-reference.html#usememo*/ 
    
    • 1
    • 2

    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]) 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    useMemo

    声明定义:

    function useMemo(factory: () => T, deps: DependencyList | undefined): T; /*** `useDebugValue` can be used to display a label for custom hooks in React DevTools.** NOTE: We don’t recommend adding debug values to every custom hook.* It’s most valuable for custom hooks that are part of shared libraries.** @version 16.8.0* @see https://reactjs.org/docs/hooks-reference.html#usedebugvalue*/ 
    
    • 1

    useMemo和useCallback类似,只是定义类型为具体返回值的类型,而不是函数的类型。如下:

    // 会自动推导出类型: number;
    const add = useCallback((a: number, b: number) => a + b, [a, b])
    
    // 传入泛型,则指定函数类型
    const toggle = useCallback((a: number) => a * 2, [a]) 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    useContext

    声明定义:

    function useContext(context: Context/*, (not public API) observedBits?: number|boolean */): T;
    /*** Returns a stateful value, and a function to update it.** @version 16.8.0* @see https://reactjs.org/docs/hooks-reference.html#usestate*/ 
    
    • 1
    • 2

    useContext会根据传入的上下文对象自动推导出context的类型,当然也可以使用泛型来设置context的类型,如下:

    interface ITheme {
    	color: string;
    }
    const ThemeContext = React.createContext({ color: "red" });
    
    // 自动推导出类型为ITheme
    const theme = useContext(ThemeContext); // 等同于const theme = useContext(ThemeContext); 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    useReducer

    声明定义:

    function useReducer>(reducer: R,initialState: ReducerState,initializer?: undefined
    ): [ReducerState, Dispatch>];
    /*** `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument* (`initialValue`). The returned object will persist for the full lifetime of the component.** Note that `useRef()` is useful for more than the `ref` attribute. It’s handy for keeping any mutable* value around similar to how you’d use instance fields in classes.** @version 16.8.0* @see https://reactjs.org/docs/hooks-reference.html#useref*/ 
    
    • 1
    • 2
    • 3

    上面只列出了一种类型定义,我在项目中也是使用这种定义去指定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 = ( 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 (
    姓名:{state.name}
    年龄:{state.age}
    ); }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    可以看到,这样能够得到正确的类型推断,但是略微繁琐。在这篇文章中, 学到了一个泛型定义,可以稍微简化一下定义Actions的过程。案例如下:

    // 定义一个生成Action类型的泛型
    type ActionMap> = {[Key in keyof M]: M[Key] extends undefined? {type: Key}: {type: Keypayload: M[Key]}
    }
    
    type StateType = {name: string;age: number;
    }
    
    // 定义具体的Action类型
    type PayloadType = {Change_Name: string;Change_Age: number;
    }
    
    /** ActionMap会生成类型{Change_Name: {type: Types.Name;payload: string;};Change_Age: {type: Types.Age;payload: number;};}而keyof ActionMap则会生成 'Change_Name' | 'Change_Age'的类型。所以Action最终的类型便为:type Actions = {type: Types.Name;payload: string;} | {type: Types.Age;payload: number;}
    */
    type Actions = ActionMap[keyof ActionMap]
    
    const initialState = {name: '小明',age: 18
    }
    
    const reducerAction: Reducer = ( state,action, ) => {switch (action.type) {case Types.Name:return { ...state, name: action.payload };case Types.Age:return { ...state, age: action.payload };default:return state;}
    }; 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    我们定义了一个ActionMap泛型,该泛型会将传入的类型{key: value}生成为新的{key: {type: key, payload: value }类型。然后我们利用keyof关键字获取到所有的key,就可以得到我们所需要的{type: key1, payload: value1} | {type: key2, payload: value2}的类型了。只要我们定义好PayloadType类型,则可以自动推导出我们需要的Actions类型。 如果你觉得这样写还是很繁琐,那么可以去看我的这篇文章在TypeScript中使用useReducer,里面介绍了简化的useReducer使用方式。

    useImperativeHandle

    声明定义:

     function useImperativeHandle(ref: Ref|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/** * `useImperativeHandle` customizes the instance value that is exposed to parent components when using * `ref`. As always, imperative code using refs should be avoided in most cases. * * `useImperativeHandle` should be used with `React.forwardRef`. * * @version 16.8.0 * @see https://reactjs.org/docs/hooks-reference.html#useimperativehandle */ 
    
    • 1

    useImperativeHandle可以让自定义组件通过ref属性,将内部属性暴露给父组件进行访问。因为是函数式组件,所以需要结合forwardRef一起使用。案例如下:

    interface FancyProps {}
    
    interface FancyRef {focus: () => void;
    }
    
    const FancyInput = forwardRef((props, ref) => {const inputRef = useRef(null);useImperativeHandle(ref, () => ({focus: () => {inputRef.current?.focus();}}));return ();
    })
    
    const Parent = () => {// 定义子组件refconst inputRef = useRef(null);return (
    ) }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    Axios请求/响应定义封装

    axios是很流行的http库,他的ts封装已经很完美了,我们只做简单的二次封装,返回通用的数据响应格式。 首先在utils/request.ts中创建一个构造axios实例的生成器:

    import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
    
    // 拦截器定义
    export interface RequestInterceptors {// 请求拦截requestInterceptors?: (config: AxiosRequestConfig) => AxiosRequestConfigrequestInterceptorsCatch?: (err: any) => any// 响应拦截responseInterceptors?: (config: AxiosResponse) => AxiosResponseresponseInterceptorsCatch?: (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) => {// 显示loadingshowLoading();console.log('全局请求拦截器');// TODO:全局的请求头操作等等return request;},(err: any) => err,)/** * 实例请求拦截器  * 要注意 axios请求拦截器为倒序执行,所以要将实例请求拦截器注册在全局请求拦截器后面 */instance.interceptors.request.use(interceptorsObj?.requestInterceptors,interceptorsObj?.requestInterceptorsCatch,)/** * 实例响应拦截器 * axios响应拦截器为正序执行,所以要将实例响应拦截器注册在全局响应拦截器前面 */instance.interceptors.response.use(interceptorsObj?.responseInterceptors,interceptorsObj?.responseInterceptorsCatch,)// 全局响应拦截器instance.interceptors.response.use((response: AxiosResponse) => {console.log('全局响应拦截器');// 关闭loadinghideLoading();// TODO: 通用的全局响应处理,token过期重定向登录等等// 返回值为res.data,即后端接口返回的数据,减少解构的层级,以及统一响应数据格式。return response.data},(err: any) => {// 关闭loadinghideLoading();// TODO: 错误提示等return err;},)return instance;
    }
    
    export const http = RequestBuilder({baseURL: '/api'}); 
    
    • 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

    该生成器可以实现每个实例有单独的拦截器处理逻辑,并且实现全局的loading加载效果,全局拦截器的具体实现可以根据项目实际需求进行填充。生成器已经完成,但是还没法定制我们的通用响应数据,接下来我们在typings.d.ts中重新定义axios模块:

    import * as axios from 'axios';
    
    declare module 'axios' {// 定制业务相关的网络请求响应格式, T 是具体的接口返回类型数据export interface CustomSuccessData {code: number;msg?: string;message?: string;data: T;[keys: string]: any;}export interface AxiosInstance {// (config: AxiosRequestConfig): Promise>;request, D = any>(config: AxiosRequestConfig): Promise;get, D = any>(url: string, config?: AxiosRequestConfig): Promise;delete, D = any>(url: string, config?: AxiosRequestConfig): Promise;head, D = any>(url: string, config?: AxiosRequestConfig): Promise;post, D = any>(url: string,data?: D,config?: AxiosRequestConfig,): Promise;put, D = any>(url: string,data?: D,config?: AxiosRequestConfig,): Promise;patch, D = any>(url: string,data?: D,config?: AxiosRequestConfig,): Promise;}
    } 
    
    • 1
    • 2
    • 3
    • 4

    完成以上操作后,我们在业务代码中具体使用:

    import { http } from '@/utils/request';
    
    interface Req {userId: string;
    }
    
    interface Res {userName: string;userId: string;
    }
    
    // 获取用户信息接口
    const getUserInfo = async (params: Req) => {return http.get('/getUserInfo', {params})
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这个时候getUserInfo返回的就是CustomSuccessData类型的数据了。至此我们对axios简单的封装也就完成了。

    最后

    最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



    有需要的小伙伴,可以点击下方卡片领取,无偿分享

  • 相关阅读:
    Oralce集群管理-19C RAC 私有网络调整为BOND1
    半桥BUCK电路—记录篇
    DM8:单库单实例搭建本地数据守护服务
    docker--基础(一)
    《QT从基础到进阶·十八》QT中的各种鼠标事件QEvent
    MaTiJi - MT3143 - 试管装液
    Kylin 使用心得
    高防CDN为什么可以防DDOS攻击
    Java面试八股文宝典:初识数据结构-数组的应用扩展之HashMap
    66从零开始学Java之集合中的Collection体系
  • 原文地址:https://blog.csdn.net/web2022050903/article/details/127875193