在React項(xiàng)目中使用TypeScript詳情
前言:
本文主要記錄我如何在React項(xiàng)目中優(yōu)雅的使用TypeScript,來提高開發(fā)效率及項(xiàng)目的健壯性。
項(xiàng)目目錄及ts文件劃分
由于我在實(shí)際項(xiàng)目中大部分是使用umi
來進(jìn)行開發(fā)項(xiàng)目,所以使用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
在項(xiàng)目根目錄下有typings.d.ts和global.d.ts這兩個(gè)文件, 前者我們可以放置一些全局的導(dǎo)出模塊,比如css,less, 圖片的導(dǎo)出聲明;后者可以放一些全局聲明的變量, 接口等, 比如說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目錄:
- assets 存放靜態(tài)資源如圖片/視頻/音頻等, 參與webpack的打包過程
- layouts 存放公共布局
- components 存放全局公共組件
- models dva的models文件夾
- pages 存放頁面的目錄, 內(nèi)部可以有頁面組件components, 結(jié)構(gòu)類似于全局的components
- utils 存放js工具庫, 請(qǐng)求庫等公共js文件
在pages和components中有存放當(dāng)前組件/頁面所需要的類型和接口聲明的index.d.ts。另外如models中的文件由于是每個(gè)model私有類型和接口聲明,所以可以直接在文件內(nèi)部去聲明。 具體的目錄規(guī)劃如上,可以根據(jù)實(shí)際項(xiàng)目來做更合理的劃分。
在項(xiàng)目中使用TypeScript具體實(shí)踐
組件聲明
- 函數(shù)組件 推薦使用
React.FC<P={}>
來表示函數(shù)類型,當(dāng)使用該類型定義組件時(shí),props中會(huì)默認(rèn)帶有children屬性。
interface IProps { count: number } const App: React.FC<IProps> = (props) => { const {count} = props; return ( <div className="App"> <span>count: {count}</span> </div> ); }
- 類組件 類組件接受兩個(gè)參數(shù),第一個(gè)是props的定義,第二個(gè)是state的定義,如果使用
React.PureComponent<P, S={} SS={}>
定義組件,則還有第三個(gè)參數(shù),表示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> ); } }
React Hooks使用
useState
聲明定義:
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>]; // 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>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>]; /** * 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 */
如果初始值能夠體現(xiàn)出類型,那么可以不用手動(dòng)聲明類型,TS會(huì)自動(dòng)推斷出類型。如果初始值為null或者undefined則需要通過泛型顯示聲明類型。
如下:
const [count, setCount] = useState(1); const [user, setUser] = useState<IUser | null>(null);
useRef
聲明定義:
function useRef<T>(initialValue: T): MutableRefObject<T>; // 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 */
使用該Hook時(shí),要根據(jù)使用場(chǎng)景來判斷傳入泛型類型,如果是獲取DOM節(jié)點(diǎn),則傳入對(duì)應(yīng)DOM類型即可;如果需要的是一個(gè)可變對(duì)象,則需要在泛型參數(shù)中包含'| null'。
如下:
// 不可變DOM節(jié)點(diǎn),只讀 const inputRef = useRef<HTMLInputElement>(null); // 可變,可重新復(fù)制 const idRef = useRef<string | null>(null); idRef.current = "abc";
useCallback
聲明定義:
function useCallback<T extends (...args: any[]) => 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 */
useCallback會(huì)根據(jù)返回值自動(dòng)推斷出類型,如果傳入的參數(shù)不指定類型,則會(huì)默認(rèn)為any
,所以為了嚴(yán)謹(jǐn)和可維護(hù)性,一定要指定入?yún)⒌念愋?。也可以手?dòng)傳入泛型指定函數(shù)類型。
如下:
// 會(huì)自動(dòng)推導(dǎo)出類型: (a: number, b: number) => number; const add = useCallback((a: number, b: number) => a + b, [a, b]) // 傳入泛型,則指定函數(shù)類型 const toggle = useCallback<(a: number) => number>((a: number) => a * 2, [a])
useMemo
聲明定義:
function useMemo<T>(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 */
useMemo和useCallback類似,只是定義類型為具體返回值的類型,而不是函數(shù)的類型。
如下:
// 會(huì)自動(dòng)推導(dǎo)出類型: number; const add = useCallback((a: number, b: number) => a + b, [a, b]) // 傳入泛型,則指定函數(shù)類型 const toggle = useCallback<number>((a: number) => a * 2, [a])
useContext
聲明定義:
function useContext<T>(context: Context<T>/*, (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 */
useContext會(huì)根據(jù)傳入的上下文對(duì)象自動(dòng)推導(dǎo)出context的類型,當(dāng)然也可以使用泛型來設(shè)置context的類型,
如下:
interface ITheme { color: string; } const ThemeContext = React.createContext<ITheme>({ color: "red" }); // 自動(dòng)推導(dǎo)出類型為ITheme const theme = useContext(ThemeContext); // 等同于const theme = useContext<ITheme>(ThemeContext);
useReducer
聲明定義:
function useReducer<R extends Reducer<any, any>>( reducer: R, initialState: ReducerState<R>, initializer?: undefined ): [ReducerState<R>, Dispatch<ReducerAction<R>>]; /** * `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 */
上面只列出了一種類型定義,我在項(xiàng)目中也是使用這種定義去指定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> ); }
可以看到,這樣能夠得到正確的類型推斷,但是略微繁瑣。
案例如下:
// 定義一個(gè)生成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; } /** ActionMap<PayloadType>會(huì)生成類型 { Change_Name: { type: Types.Name; payload: string; }; Change_Age: { type: Types.Age; payload: number; }; } 而keyof ActionMap<PayloadType>則會(huì)生成 'Change_Name' | 'Change_Age'的類型。 所以Action最終的類型便為: type Actions = { type: Types.Name; payload: string; } | { type: Types.Age; payload: 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; } };
我們定義了一個(gè)ActionMap
泛型,該泛型會(huì)將傳入的類型{key: value}
生成為新的{key: {type: key, payload: value }
類型。然后我們利用keyof
關(guān)鍵字獲取到所有的key,就可以得到我們所需要的{type: key1, payload: value1} | {type: key2, payload: value2}
的類型了。只要我們定義好PayloadType
類型,則可以自動(dòng)推導(dǎo)出我們需要的Actions
類型。
useImperativeHandle
聲明定義:
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` 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 */
useImperativeHandle
可以讓自定義組件通過ref
屬性,將內(nèi)部屬性暴露給父組件進(jìn)行訪問。因?yàn)槭呛瘮?shù)式組件,所以需要結(jié)合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={() => { // 調(diào)用子組件方法 inputRef.current?.focus(); }} >聚焦</button> </div> ) }
Axios請(qǐng)求/響應(yīng)定義封裝
axios
是很流行的http庫,他的ts封裝已經(jīng)很完美了,我們只做簡單的二次封裝,返回通用的數(shù)據(jù)響應(yīng)格式。 首先在utils/request.ts
中創(chuàng)建一個(gè)構(gòu)造axios實(shí)例的生成器:
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; // 攔截器定義 export interface RequestInterceptors { // 請(qǐng)求攔截 requestInterceptors?: (config: AxiosRequestConfig) => AxiosRequestConfig requestInterceptorsCatch?: (err: any) => any // 響應(yīng)攔截 responseInterceptors?: (config: AxiosResponse) => AxiosResponse responseInterceptorsCatch?: (err: any) => any } // 生成axios實(shí)例的參數(shù),實(shí)例可以單獨(dú)傳入攔截器 export interface RequestConfig extends AxiosRequestConfig { interceptorsObj?: RequestInterceptors } // loading請(qǐng)求數(shù)量 let loadingCount: number = 0; // 打開loading const showLoading = () => { loadingCount ++; if(loadingCount > 0) { // 顯示loading // Loading.show() } } // 關(guān)閉loading const hideLoading = () => { loadingCount --; if(loadingCount <= 0) { // 隱藏loading // Loading.hide(); } } function RequestBuilder(config: RequestConfig) { const { interceptorsObj, ...res } = config; const instance: AxiosInstance = axios.create(res); // 全局請(qǐng)求攔截器 instance.interceptors.request.use( (request: AxiosRequestConfig) => { // 顯示loading showLoading(); console.log('全局請(qǐng)求攔截器'); // TODO:全局的請(qǐng)求頭操作等等 return request; }, (err: any) => err, ) /** * 實(shí)例請(qǐng)求攔截器 * 要注意 axios請(qǐng)求攔截器為倒序執(zhí)行,所以要將實(shí)例請(qǐng)求攔截器注冊(cè)在全局請(qǐng)求攔截器后面 */ instance.interceptors.request.use( interceptorsObj?.requestInterceptors, interceptorsObj?.requestInterceptorsCatch, ) /** * 實(shí)例響應(yīng)攔截器 * axios響應(yīng)攔截器為正序執(zhí)行,所以要將實(shí)例響應(yīng)攔截器注冊(cè)在全局響應(yīng)攔截器前面 */ instance.interceptors.response.use( interceptorsObj?.responseInterceptors, interceptorsObj?.responseInterceptorsCatch, ) // 全局響應(yīng)攔截器 instance.interceptors.response.use( (response: AxiosResponse) => { console.log('全局響應(yīng)攔截器'); // 關(guān)閉loading hideLoading(); // TODO: 通用的全局響應(yīng)處理,token過期重定向登錄等等 // 返回值為res.data,即后端接口返回的數(shù)據(jù),減少解構(gòu)的層級(jí),以及統(tǒng)一響應(yīng)數(shù)據(jù)格式。 return response.data }, (err: any) => { // 關(guān)閉loading hideLoading(); // TODO: 錯(cuò)誤提示等 return err; }, ) return instance; } export const http = RequestBuilder({baseURL: '/api'});
該生成器可以實(shí)現(xiàn)每個(gè)實(shí)例有單獨(dú)的攔截器處理邏輯,并且實(shí)現(xiàn)全局的loading加載效果,全局?jǐn)r截器的具體實(shí)現(xiàn)可以根據(jù)項(xiàng)目實(shí)際需求進(jìn)行填充。生成器已經(jīng)完成,但是還沒法定制我們的通用響應(yīng)數(shù)據(jù),接下來我們?cè)?code>typings.d.ts中重新定義axios模塊:
import * as axios from 'axios'; declare module 'axios' { // 定制業(yè)務(wù)相關(guān)的網(wǎng)絡(luò)請(qǐng)求響應(yīng)格式, T 是具體的接口返回類型數(shù)據(jù) 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>; } }
完成以上操作后,我們?cè)跇I(yè)務(wù)代碼中具體使用:
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}) }
這個(gè)時(shí)候getUserInfo
返回的就是CustomSuccessData<Res>
類型的數(shù)據(jù)了。至此我們對(duì)axios
簡單的封裝也就完成了。
到此這篇關(guān)于在React項(xiàng)目中使用TypeScript詳情的文章就介紹到這了,更多相關(guān)React使用TypeScript內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React實(shí)現(xiàn)復(fù)雜搜索表單的展開收起功能
本節(jié)對(duì)于需要展開收起效果的查詢表單進(jìn)行概述,主要涉及前端樣式知識(shí)。對(duì)React實(shí)現(xiàn)復(fù)雜搜索表單的展開-收起功能感興趣的朋友一起看看吧2021-09-09React函數(shù)式組件Hook中的useState函數(shù)的詳細(xì)解析
Hook 就是 JavaScript 函數(shù),這個(gè)函數(shù)可以幫助你鉤入(hook into) React State以及生命周期等特性,這篇文章主要介紹了React Hook useState函數(shù)的詳細(xì)解析的相關(guān)資料,需要的朋友可以參考下2022-10-10react-router-domV6版本的路由和嵌套路由寫法詳解
本文主要介紹了react-router-domV6版本的路由和嵌套路由寫法詳解,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03詳解Jotai Immer如何實(shí)現(xiàn)undo redo功能示例詳解
這篇文章主要為大家介紹了詳解Jotai Immer如何實(shí)現(xiàn)undo redo功能示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04