<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          ssh 在大廠寫React,學(xué)到了什么?

          共 5738字,需瀏覽 12分鐘

           ·

          2020-10-28 22:21

          前言

          進(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)管理思路中,我們需要在代碼里用reduxrecoil等庫去做一系列的數(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)容。



          你點(diǎn)的每個贊,我都認(rèn)真當(dāng)成了喜歡
          瀏覽 33
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  在线观看的黄色小视频 | 一级黄色视频日逼片 | 初尝人妻滑进去了 | 少妇厨房愉情理伦BD在线观看 | 色婷婷五月天亚洲 |