专栏首页前端从进阶到入院我在大厂写React,学到了什么?

我在大厂写React,学到了什么?

前言

我工作中的技术栈主要是 React + TypeScript,这篇文章我想总结一下如何在项目中运用 React 的一些技巧解决一些实际问题,本文中使用的代码都是简化后的,不代表生产环境。生产环境的代码肯定比文中的例子要复杂很多,但是简化后的思想应该是相通的。

取消请求

React 中当前正在发出请求的组件从页面上卸载了,理想情况下这个请求也应该取消掉,那么如何把请求的取消和页面的卸载关联在一起呢?

这里要考虑利用 useEffect 传入函数的返回值:

useEffect(() => {
  return () => {
    // 页面卸载时执行
  };
}, []);

假设我们的请求是利用 fetch,那么还有一个需要运用的知识点:AbortController,简单看一下它的用法:

const abortController = new AbortController();

fetch(url, {
  // 这里传入 signal 进行关联
  signal: abortController.signal,
});

// 这里调用 abort 即可取消请求
abortController.abort();

那么结合 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 }
}
复制代码

那么比如在路由发生切换,Tab 发生切换等场景下,被卸载掉的组件发出的请求也会被中断。

深比较依赖

在使用 useEffect 等需要传入依赖的 hook 时,最理想的状况是所有依赖都在真正发生变化的时候才去改变自身的引用地址,但是有些依赖不太听话,每次渲染都会重新生成一个引用,但是内部的值却没变,这可能会让 useEffect 对于依赖的「浅比较」没法正常工作。

比如说:

const getDep = () => {
  return {
    foo: 'bar',
  };
};

useEffect(() => {
  // 无限循环了
}, [getDep()]);

这是一个人为的例子,由于 getDeps 函数返回的对象每次执行都是一个全新的引用,所以会导致触发渲染->effect->渲染->effect 的无限更新。

有一个比较取巧的解决办法,把依赖转为字符串:

const getDep = () => {
  return {
    foo: 'bar',
  };
};

const dep = JSON.stringify(getDeps());

useEffect(() => {
  // ok!
}, [dep]);

这样对比的就是字符串 "{ foo: 'bar' }" 的值,而不是对象的引用,那么只有在值真正发生变化时才会触发更新。

当然最好还是用社区提供的方案:useDeepCompareEffect,它选用深比较策略,对于对象依赖来说,它逐个对比 key 和 value,在性能上会有所牺牲。

如果你的某个依赖触发了多次无意义的接口请求,那么宁愿选用 useDeepCompareEffect ,在对象比较上多花费些时间可比重复请求接口要好得多。

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 这个数字值。用useRef 保留上一次传入的依赖,每次都利用 lodash 的 isEqual 对本次依赖和旧依赖进行深比较,如果发生变化,则让 trigger 的值增加。

当然我们也可以用 fast-deep-equal 这个库,根据官方的 benchmark 对比,它比 lodash 的效率高 7 倍左右。

以 URL 为数据仓库

在公司内部的后台管理项目中,无论你做的系统面向的人群是运营还是开发,都会涉及到分享,那么保留「页面状态」就非常重要了。比如我是运营 A,在使用一个内部数据平台,我一定是想向运营 B 分享某 App 的消费数据的第二页,并且筛选为某个用户的状态的网页,并且进行讨论。那么状态和 URL 同步就尤为重要了。

在传统的状态管理思路中,我们需要在代码里用reduxrecoil等库去做一系列的数据管理,但是如果把 URL 后面的那串 query 想象成数据仓库呢?是不是也可以,尝试配合react-router封装一下。

export function useQuery() {
  const history = useHistory();
  const { search, pathname } = useLocation();
  // 保存query状态
  const queryState = useRef(qs.parse(search));
  // 设置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]);

// 分页改变 触发接口重新请求
const onPageChange = page => {
  setQuery(prevQuery => ({
    ...prevQuery,
    page,
  }));
};
复制代码

这样,所有的页面状态更改都会自动同步到 URL,非常方便。

利用 AST 做国际化

国际化中最头疼的就是手动去替换代码中的文本,转为 i18n.t(key) 这种国际化方法调用,而这一步则可以交给 Babel AST 去完成。扫描出代码中需要替换文本的位置,修改 AST 把它转为方法调用即可,比较麻烦的点在于需要考虑各种边界情况,我写过一个比较简单的例子,仅供参考:

github.com/sl1673495/b…

这样的一段源代码:

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;
复制代码

经过处理后,转变为这样:

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导入 一般第一项一定是import React 所以直接插入在后面就可以
    path.get('body.0').insertAfter(makeImportDeclaration(I18_HOOK, I18_LIB));
  },
  // 通过找到第一个jsxElement 来向上寻找Component函数并且插入i18n的hook函数
  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找到的可能是函数中调用参数的文字 也可能是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));
        }
      }
    }
  },
});
复制代码

当然,实际项目中还需要考虑文案翻译的部分,如何建立平台,如何和运营或者翻译专员协作。

以及 AST 处理各种各样的边界情况,肯定要复杂的多,以上只是简化版的思路。

总结

进入大厂搬砖也有 3 个月了,对这里的感受就是人才的密度是真的很高,可以看到社区的很多大佬在内部前端群里讨论最前沿的问题,甚至如果你和他在一个楼层,你还可以现实里跑过去和他面基,请教问题,这种感觉真的很棒。有一次我遇到了一个 TS 上的难题,就直接去对面找某个知乎上比较出名的大佬讨论解决(厚脸皮)。

在之后的工作中,对于学到的知识点我也会进行进一步的总结,发一些有价值的文章,感兴趣的话欢迎关注~

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 手把手教你用神器nextjs一键导出你的github博客文章生成静态html!

    相信有不少小伙伴和我一样用github issues记录自己的blog,但是久而久之也发现了一些小问题,比如

    ssh1995
  • 前端动画必知必会:React 和 Vue 都在用的 FLIP 思想实现小姐姐流畅移动。

    乍一看,让我们手写出这个逻辑应该是非常复杂的,先看看本文最后要实现的效果吧,和这个案例是非常类似的。

    ssh1995
  • TypeScript从零实现React自定义Hook,实现Vue中的watch功能。

    在Vue中,我们经常需要用watch去观察一个值的变化,通过新旧值的对比去做一些事情。

    ssh1995
  • Effective C++条款3 我可以不使用const?

    const是C++中用得非常频繁的一个关键字,但是如果你不使用这个关键字,对程序的运行结果影响不大,那么,我可以不使用const吗?

    ACM算法日常
  • QMap容器小知识

    Qt君
  • 学习c++中的小问题总结

    徐飞机
  • const关键字应用总结

    C++中的const关键字的用法非常灵活,而使用const将大大改善程序的健壮性,通过查阅资料,将const作用归纳如下:

    C语言与CPP编程
  • C++雾中风景3:const用法的小结

    const关键字,翻译成中文是常量,常数的意思。所以在绝大多数场合之中,const是来定义常量的,定义常量也是好的编程习惯。在C类语言之中,定义常量通常会使用宏...

    HappenLee
  • 初级程序员面试不靠谱指南(二)

    3.read-only的const。如果你突然冒出一句看似很高深的话但又不解释一般都是装逼,就像前面提到过const准确的应该理解为一个read-only的变量...

    一心一怿
  • const特性总结(不断更新)

    作者:bakari  时间:2012.6.5 1、指向const对象的指针---const int *cptr; i、在此,cptr是指向int类型的const...

    CloudDeveloper

扫码关注云+社区

领取腾讯云代金券