ssh 在大廠寫React,學(xué)到了什么?
前言
進(jìn)入大廠搬磚也有 3 個月了,我工作中的技術(shù)棧主要是 React + TypeScript,這篇文章我想總結(jié)一下如何在項目中運(yùn)用 React 的一些技巧解決一些實(shí)際問題,本文中使用的代碼都是簡化后的,不代表生產(chǎn)環(huán)境。生產(chǎn)環(huán)境的代碼肯定比文中的例子要復(fù)雜很多,但是簡化后的思想應(yīng)該是相通的。
取消請求
React 中當(dāng)前正在發(fā)出請求的組件從頁面上卸載了,理想情況下這個請求也應(yīng)該取消掉,那么如何把請求的取消和頁面的卸載關(guān)聯(lián)在一起呢?
這里要考慮利用 useEffect 傳入函數(shù)的返回值:
useEffect(()?=>?{
??return?()?=>?{
????//?頁面卸載時執(zhí)行
??};
},?[]);
假設(shè)我們的請求是利用 fetch,那么還有一個需要運(yùn)用的知識點(diǎn):AbortController,簡單看一下它的用法:
const?abortController?=?new?AbortController();
fetch(url,?{
??//?這里傳入?signal?進(jìn)行關(guān)聯(lián)
??signal:?abortController.signal,
});
//?這里調(diào)用?abort?即可取消請求
abortController.abort();
那么結(jié)合 React 封裝一個 useFetch 的 hook:
export?function?useFetch?=?(config,?deps)?=>?{
??const?abortController?=?new?AbortController()
??const?[loading,?setLoading]?=?useState(false)
??const?[result,?setResult]?=?useState()
??useEffect(()?=>?{
????setLoading(true)
????fetch({
??????...config,
??????signal:?abortController.signal
????})
??????.then((res)?=>?setResult(res))
??????.finally(()?=>?setLoading(false))
??},?deps)
??useEffect(()?=>?{
????return?()?=>?abortController.abort()
??},?[])
??return?{?result,?loading?}
}
那么比如在路由發(fā)生切換,Tab 發(fā)生切換等場景下,被卸載掉的組件發(fā)出的請求也會被中斷。
深比較依賴
在使用 useEffect 等需要傳入依賴的 hook 時,最理想的狀況是所有依賴都在真正發(fā)生變化的時候才去改變自身的引用地址,但是有些依賴不太聽話,每次渲染都會重新生成一個引用,但是內(nèi)部的值卻沒變,這可能會讓 useEffect 對于依賴的「淺比較」沒法正常工作。
比如說:
const?getDep?=?()?=>?{
??return?{
????foo:?'bar',
??};
};
useEffect(()?=>?{
??//?無限循環(huán)了
},?[getDep()]);
這是一個人為的例子,由于 getDeps 函數(shù)返回的對象每次執(zhí)行都是一個全新的引用,所以會導(dǎo)致觸發(fā)渲染->effect->渲染->effect 的無限更新。
有一個比較取巧的解決辦法,把依賴轉(zhuǎn)為字符串:
const?getDep?=?()?=>?{
??return?{
????foo:?'bar',
??};
};
const?dep?=?JSON.stringify(getDeps());
useEffect(()?=>?{
??//?ok!
},?[dep]);
這樣對比的就是字符串 "{ foo: 'bar' }" 的值,而不是對象的引用,那么只有在值真正發(fā)生變化時才會觸發(fā)更新。
當(dāng)然最好還是用社區(qū)提供的方案:useDeepCompareEffect,它選用深比較策略,對于對象依賴來說,它逐個對比 key 和 value,在性能上會有所犧牲。
如果你的某個依賴觸發(fā)了多次無意義的接口請求,那么寧愿選用 useDeepCompareEffect ,在對象比較上多花費(fèi)些時間可比重復(fù)請求接口要好得多。
useDeepCompareEffect 大致原理:
import?{?isEqual?}?from?'lodash';
export?function?useDeepCompareEffect(fn,?deps)?{
??const?trigger?=?useRef(0);
??const?prevDeps?=?useRef(deps);
??if?(!isEqual(prevDeps.current,?deps))?{
????trigger.current++;
??}
??prevDeps.current?=?deps;
??return?useEffect(fn,?[trigger.current]);
}
真正傳入 useEffect 用以更新的是 trigger 這個數(shù)字值。用useRef 保留上一次傳入的依賴,每次都利用 lodash 的 isEqual 對本次依賴和舊依賴進(jìn)行深比較,如果發(fā)生變化,則讓 trigger 的值增加。
當(dāng)然我們也可以用 fast-deep-equal 這個庫,根據(jù)官方的 benchmark 對比,它比 lodash 的效率高 7 倍左右。
以 URL 為數(shù)據(jù)倉庫
在公司內(nèi)部的后臺管理項目中,無論你做的系統(tǒng)面向的人群是運(yùn)營還是開發(fā),都會涉及到分享,那么保留「頁面狀態(tài)」就非常重要了。比如我是運(yùn)營 A,在使用一個內(nèi)部數(shù)據(jù)平臺,我一定是想向運(yùn)營 B 分享某 App 的消費(fèi)數(shù)據(jù)的第二頁,并且篩選為某個用戶的狀態(tài)的網(wǎng)頁,并且進(jìn)行討論。那么狀態(tài)和 URL 同步就尤為重要了。
在傳統(tǒng)的狀態(tài)管理思路中,我們需要在代碼里用redux、recoil等庫去做一系列的數(shù)據(jù)管理,但是如果把 URL 后面的那串 query 想象成數(shù)據(jù)倉庫呢?是不是也可以,嘗試配合react-router封裝一下。
export?function?useQuery()?{
??const?history?=?useHistory();
??const?{?search,?pathname?}?=?useLocation();
??//?保存query狀態(tài)
??const?queryState?=?useRef(qs.parse(search));
??//?設(shè)置query
??const?setQuery?=?handler?=>?{
????const?nextQuery?=?handler(queryState.current);
????queryState.current?=?nextQuery;
????//?replace會使組件重新渲染
????history.replace({
??????pathname:?pathname,
??????search:?qs.stringify(nextQuery),
????});
??};
??return?[queryState.current,?setQuery];
}
在組件中,可以這樣使用:
const?[query,?setQuery]?=?useQuery();
//?接口請求依賴?page?和?size
useEffect(()?=>?{
??api.getUsers();
},?[query.page,?query,?size]);
//?分頁改變?觸發(fā)接口重新請求
const?onPageChange?=?page?=>?{
??setQuery(prevQuery?=>?({
????...prevQuery,
????page,
??}));
};
這樣,所有的頁面狀態(tài)更改都會自動同步到 URL,非常方便。
利用 AST 做國際化
國際化中最頭疼的就是手動去替換代碼中的文本,轉(zhuǎn)為 i18n.t(key) 這種國際化方法調(diào)用,而這一步則可以交給 Babel AST 去完成。掃描出代碼中需要替換文本的位置,修改 AST 把它轉(zhuǎn)為方法調(diào)用即可,比較麻煩的點(diǎn)在于需要考慮各種邊界情況,我寫過一個比較簡單的例子,僅供參考:
https://github.com/sl1673495/babel-ast-practise/blob/master/i18n.js
這樣的一段源代碼:
import?React?from?'react';
import?{?Button,?Toast,?Popover?}?from?'components';
const?Comp?=?props?=>?{
??const?tips?=?()?=>?{
????Toast.info('這是一段提示');
????Toast({
??????text:?'這是一段提示',
????});
??};
??return?(
????<div>
??????<Button?onClick={tips}>這是按鈕Button>
??????<Popover?tooltip="氣泡提示"?/>
????div>
??);
};
export?default?Comp;
經(jīng)過處理后,轉(zhuǎn)變?yōu)檫@樣:
import?React?from?'react';
import?{?useI18n?}?from?'react-intl';
import?{?Button,?Toast,?Popover?}?from?'components';
const?Comp?=?props?=>?{
??const?{?t?}?=?useI18n();
??const?tips?=?()?=>?{
????Toast.info(t('tips'));
????Toast({
??????text:?t('tips'),
????});
??};
??return?(
????<div>
??????<Button?onClick={tips}>{t('btn')}Button>
??????<Popover?tooltip={t('popover')}?/>
????div>
??);
};
export?default?Comp;
放一段腳本的 traverse 部分:
//?遍歷ast
traverse(ast,?{
??Program(path)?{
????//?i18n的import導(dǎo)入?一般第一項一定是import?React?所以直接插入在后面就可以
????path.get('body.0').insertAfter(makeImportDeclaration(I18_HOOK,?I18_LIB));
??},
??//?通過找到第一個jsxElement?來向上尋找Component函數(shù)并且插入i18n的hook函數(shù)
??JSXElement(path)?{
????const?functionParent?=?path.getFunctionParent();
????const?functionBody?=?functionParent.node.body.body;
????if?(!this.hasInsertUseI18n)?{
??????functionBody.unshift(
????????buildDestructFunction({
??????????VALUE:?t.identifier(I18_FUNC),
??????????SOURCE:?t.callExpression(t.identifier(I18_HOOK),?[]),
????????})
??????);
??????this.hasInsertUseI18n?=?true;
????}
??},
??//?jsx中的文字?直接替換成{t(key)}的形式
??JSXText(path)?{
????const?{?node?}?=?path;
????const?i18nKey?=?findI18nKey(node.value);
????if?(i18nKey)?{
??????node.value?=?`{${I18_FUNC}("${i18nKey}")}`;
????}
??},
??//?Literal找到的可能是函數(shù)中調(diào)用參數(shù)的文字?也可能是jsx屬性中的文字
??Literal(path)?{
????const?{?node?}?=?path;
????const?i18nKey?=?findI18nKey(node.value);
????if?(i18nKey)?{
??????if?(path.parent.type?===?'JSXAttribute')?{
????????path.replaceWith(
??????????t.jsxExpressionContainer(makeCallExpression(I18_FUNC,?i18nKey))
????????);
??????}?else?{
????????if?(t.isStringLiteral(node))?{
??????????path.replaceWith(makeCallExpression(I18_FUNC,?i18nKey));
????????}
??????}
????}
??},
});
當(dāng)然,實(shí)際項目中還需要考慮文案翻譯的部分,如何建立平臺,如何和運(yùn)營或者翻譯專員協(xié)作。
以及 AST 處理各種各樣的邊界情況,肯定要復(fù)雜的多,以上只是簡化版的思路。
總結(jié)
進(jìn)入大廠搬磚也有 3 個月了,對這里的感受就是人才的密度是真的很高,可以看到社區(qū)的很多大佬在內(nèi)部前端群里討論最前沿的問題,甚至如果你和他在一個樓層,你還可以現(xiàn)實(shí)里跑過去和他面基,請教問題,這種感覺真的很棒。有一次我遇到了一個 TS 上的難題,就直接去對面找某個知乎上比較出名的大佬討論解決(厚臉皮)。
在之后的工作中,對于學(xué)到的知識點(diǎn)我也會進(jìn)行進(jìn)一步的總結(jié),發(fā)一些有價值的文章,感興趣的話歡迎關(guān)注~
掃碼關(guān)注公眾號,訂閱更多精彩內(nèi)容。

