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

Easter79
• 阅读 717

前言

进入大厂搬砖也有 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 发生切换等场景下,被卸载掉的组件发出的请求也会被中断。

深比较依赖

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

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

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

❤️ 感谢大家

1.如果本文对你有帮助,就点个在看支持下吧,你的「在看」是我创作的动力。也可以分享给你的小伙伴一起学习哈~

2.关注公众号「前端时光屋」即可加我好友,我拉你进「前端技术交流群」,大家一起共同交流和进步。

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

本文分享自微信公众号 - 前端时光屋(javascriptlab)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Karen110 Karen110
3年前
​一篇文章总结一下Python库中关于时间的常见操作
前言本次来总结一下关于Python时间的相关操作,有一个有趣的问题。如果你的业务用不到时间相关的操作,你的业务基本上会一直用不到。但是如果你的业务一旦用到了时间操作,你就会发现,淦,到处都是时间操作。。。所以思来想去,还是总结一下吧,本次会采用类型注解方式。time包importtime时间戳从1970年1月1日00:00:00标准时区诞生到现在
Stella981 Stella981
3年前
React16.4 开发简书项目 从零基础入门到实战
第1章课程导学本章主要介绍学习react的重要性,课程的知识大纲,学习前提,讲授方式及预期收获。11课程导学第2章React初探本章主要讲解React项目的开发环境搭建,工程代码结构及React中最基础的语法内容,同时对前端组件化思想进行介绍。21React简介22React开发环境准备23工程目录
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Easter79
Easter79
Lv1
今生可爱与温柔,每一样都不能少。
文章
2.8k
粉丝
5
获赞
1.2k