在React項(xiàng)目中使用TypeScript詳情
前言:
本文主要記錄我如何在React項(xiàng)目中優(yōu)雅的使用TypeScript,來(lái)提高開(kāi)發(fā)效率及項(xiàng)目的健壯性。
項(xiàng)目目錄及ts文件劃分
由于我在實(shí)際項(xiàng)目中大部分是使用umi來(lái)進(jìn)行開(kāi)發(fā)項(xiàng)目,所以使用umi生成的目錄來(lái)做案例。
. ├── 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)出聲明;后者可以放一些全局聲明的變量, 接口等, 比如說(shuō)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;
}接下來(lái)介紹一下src目錄:
- assets 存放靜態(tài)資源如圖片/視頻/音頻等, 參與webpack的打包過(guò)程
- layouts 存放公共布局
- components 存放全局公共組件
- models dva的models文件夾
- pages 存放頁(yè)面的目錄, 內(nèi)部可以有頁(yè)面組件components, 結(jié)構(gòu)類(lèi)似于全局的components
- utils 存放js工具庫(kù), 請(qǐng)求庫(kù)等公共js文件
在pages和components中有存放當(dāng)前組件/頁(yè)面所需要的類(lèi)型和接口聲明的index.d.ts。另外如models中的文件由于是每個(gè)model私有類(lèi)型和接口聲明,所以可以直接在文件內(nèi)部去聲明。 具體的目錄規(guī)劃如上,可以根據(jù)實(shí)際項(xiàng)目來(lái)做更合理的劃分。
在項(xiàng)目中使用TypeScript具體實(shí)踐
組件聲明
- 函數(shù)組件 推薦使用
React.FC<P={}>來(lái)表示函數(shù)類(lèi)型,當(dāng)使用該類(lèi)型定義組件時(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>
);
}- 類(lèi)組件 類(lèi)組件接受兩個(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)出類(lèi)型,那么可以不用手動(dòng)聲明類(lèi)型,TS會(huì)自動(dòng)推斷出類(lèi)型。如果初始值為null或者undefined則需要通過(guò)泛型顯示聲明類(lèi)型。
如下:
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)景來(lái)判斷傳入泛型類(lèi)型,如果是獲取DOM節(jié)點(diǎn),則傳入對(duì)應(yīng)DOM類(lèi)型即可;如果需要的是一個(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)推斷出類(lèi)型,如果傳入的參數(shù)不指定類(lèi)型,則會(huì)默認(rèn)為any,所以為了嚴(yán)謹(jǐn)和可維護(hù)性,一定要指定入?yún)⒌念?lèi)型。也可以手動(dòng)傳入泛型指定函數(shù)類(lèi)型。
如下:
// 會(huì)自動(dòng)推導(dǎo)出類(lèi)型: (a: number, b: number) => number; const add = useCallback((a: number, b: number) => a + b, [a, b]) // 傳入泛型,則指定函數(shù)類(lèi)型 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類(lèi)似,只是定義類(lèi)型為具體返回值的類(lèi)型,而不是函數(shù)的類(lèi)型。
如下:
// 會(huì)自動(dòng)推導(dǎo)出類(lèi)型: number; const add = useCallback((a: number, b: number) => a + b, [a, b]) // 傳入泛型,則指定函數(shù)類(lèi)型 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的類(lèi)型,當(dāng)然也可以使用泛型來(lái)設(shè)置context的類(lèi)型,
如下:
interface ITheme {
color: string;
}
const ThemeContext = React.createContext<ITheme>({ color: "red" });
// 自動(dòng)推導(dǎo)出類(lèi)型為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
*/上面只列出了一種類(lèi)型定義,我在項(xiàng)目中也是使用這種定義去指定useReducer的類(lèi)型。普通的案例如下:
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>
);
}可以看到,這樣能夠得到正確的類(lèi)型推斷,但是略微繁瑣。
案例如下:
// 定義一個(gè)生成Action類(lèi)型的泛型
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類(lèi)型
type PayloadType = {
Change_Name: string;
Change_Age: number;
}
/**
ActionMap<PayloadType>會(huì)生成類(lèi)型
{
Change_Name: {
type: Types.Name;
payload: string;
};
Change_Age: {
type: Types.Age;
payload: number;
};
}
而keyof ActionMap<PayloadType>則會(huì)生成 'Change_Name' | 'Change_Age'的類(lèi)型。
所以Action最終的類(lèi)型便為:
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ì)將傳入的類(lèi)型{key: value}生成為新的{key: {type: key, payload: value }類(lèi)型。然后我們利用keyof關(guān)鍵字獲取到所有的key,就可以得到我們所需要的{type: key1, payload: value1} | {type: key2, payload: value2}的類(lèi)型了。只要我們定義好PayloadType類(lèi)型,則可以自動(dòng)推導(dǎo)出我們需要的Actions類(lèi)型。
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可以讓自定義組件通過(guò)ref屬性,將內(nèi)部屬性暴露給父組件進(jìn)行訪問(wè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庫(kù),他的ts封裝已經(jīng)很完美了,我們只做簡(jiǎn)單的二次封裝,返回通用的數(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;
// 打開(kāi)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過(guò)期重定向登錄等等
// 返回值為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)完成,但是還沒(méi)法定制我們的通用響應(yīng)數(shù)據(jù),接下來(lái)我們?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 是具體的接口返回類(lèi)型數(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>類(lèi)型的數(shù)據(jù)了。至此我們對(duì)axios簡(jiǎn)單的封裝也就完成了。
到此這篇關(guān)于在React項(xiàng)目中使用TypeScript詳情的文章就介紹到這了,更多相關(guān)React使用TypeScript內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React?Hooks--useEffect代替常用生命周期函數(shù)方式
這篇文章主要介紹了React?Hooks--useEffect代替常用生命周期函數(shù)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09
React組件的創(chuàng)建與state同步異步詳解
這篇文章主要介紹了react組件實(shí)例屬性state,有狀態(tài)state的組件稱(chēng)作復(fù)雜組件,沒(méi)有狀態(tài)的組件稱(chēng)為簡(jiǎn)單組件,狀態(tài)里存儲(chǔ)數(shù)據(jù),數(shù)據(jù)的改變驅(qū)動(dòng)頁(yè)面的展示,本文結(jié)合實(shí)例代碼給大家詳細(xì)講解,需要的朋友可以參考下2023-03-03
useReducer?createContext代替Redux原理示例解析
這篇文章主要為大家介紹了useReducer?createContext代替Redux原理示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11
ReactNative實(shí)現(xiàn)Toast的示例
這篇文章主要介紹了ReactNative實(shí)現(xiàn)Toast的示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-12-12
使用React手寫(xiě)一個(gè)對(duì)話框或模態(tài)框的方法示例
這篇文章主要介紹了使用React手寫(xiě)一個(gè)對(duì)話框或模態(tài)框的方法示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04

