vlambda博客
学习文章列表

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


前言

进入大厂搬砖也有 3 个月了,我工作中的技术栈主要是 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 发生切换等场景下,被卸载掉的组件发出的请求也会被中断。

深比较依赖

比如说:

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 把它转为方法调用即可,比较麻烦的点在于需要考虑各种边界情况,我写过一个比较简单的例子,仅供参考:

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;

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

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 上的难题,就直接去对面找某个知乎上比较出名的大佬讨论解决(厚脸皮)。

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




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

在看点这里