上次有一个玄乎的问题,那时候玄学解决。但是过了几天,再次出现,这一次,已经查出原因,彻底把它解决了。点击这里回顾
问 题 分 析先捋一下整个过程:
1. react源码错误,必然是有react之外的原生dom操作
2. 确认过代码,没有任何其他原生dom操作
3. 对方在控制台做了dom操作?不可能,无技术背景
4. 那只能是浏览器插件、中间人注入(基本不可能优先级调最低)、翻译
5. 忘了上次打断点的事情吧,不能投机取巧
上次的经验告诉我,直接远程控制是最好的方法。于是马上连上了远程控制。检查了一下浏览器插件,没有什么插件有影响——浏览器插件pass。确认一下是否翻译,问了对方说有没有开了翻译,对方说没有(远程桌面看不见弹出菜单的,所以需要人家告诉我)
我:"have you opened the extensions?"
对方:"yeah, these"
我:"did you turnedthe translation on?"
对方: "i didnt translate on that website"
ok,人家说没有翻译,那我就假设这是实话。既然问题发生的根本原因就是有react之外的原生dom操作,那就是dom节点数很有可能不一样。于是我在控制台输入了一下$$('*')
,发现对方电脑上是2400个节点。在我电脑上输一下,只有2000个节点。让同事帮忙看看,一样也是2000个节点。于是我决定对比一下第一个不一样的节点是怎样的,在对方的电脑控制台上输了一段简单的脚本:
$$('*').reduce((acc,{tagName})=>`${acc}${tagName},`,'')
我:"could you please copy the txt and send me"
于是我拿到了用户整个页面所有的标签字符串集合,在我打开的页面的控制台下,和我的对比一下:
var arr = otherHtml.split(',')$$('*').findIndex(({tagName},i)=>tagName!==arr[i])
发现index为103,找到第103个节点,发现是一个link标签,引入了下的一个css,而且html这个标签多了一个叫做
translated-ltr
的class。顾名思义,翻译实锤了
于是,再继续展开主内容,发现对方的页面上多了很多font
标签!!
果然,还是开了翻译,只是人家“觉得没有开”。其实,很有可能是之前设置了一律翻译,所以后面就一直不用管,所有的网站都会自动翻译。接着让用户按照我的要求,将翻译关掉。最后,多次重复的操作,问题也没有出现了
我:"look at this wiki, and close the translate completely"
对方:"as you wish"
(一会后)
对方:"okie done"
其实,估计之前大家都是脚手架一把刷,并没有注意到html的lang的值,而且我们这个系统都是英文的。
<html lang="zh-cn">
于是出现了一个所有的内容都是英文的“中文”页面,到了海外Chrome翻译的逻辑就是,这是“中文”页面,需要自动翻译,然后就“英文翻译成英文”,视觉上无变化,实际上dom节点已经多了很多font
了
复 盘上次
于是我还是想看看为什么上次打断点就没事了,打开维基百科(/wiki/Wiki )试一下,在开启了翻译的条件下打断点会发生什么。打开source面板,勾选了load事件
自动翻译也开启
刷新页面,发现一进来的时候,一切安好,html标签是这样
class="client-js" lang="en" dir="ltr">
点了两下下一步的时候,html标签发生了变化,核心特征:有translated-ltr类
class="client-js translated-ltr ve-not-available" lang="zh-CN" dir="ltr">
再看看element面板,很多font包裹
实际上这就是一个页面load成功后,Chrome的翻译功能去拉css和js回来、修改页面内容的过程。复盘一下上次能解决问题的断点操作:
我在报错的发生前最后一个接口的返回打了断点,勾选了error事件的断点
页面进来,有一个cors报错,error卡住。此时已经有请求出去了,断点卡一下争取到了时间(你看起来是pending,实际上response已经到你家门口了)
再点下一步,前面的数据秒出,一瞬间又卡了,因为最后一个接口也回来了
此时还没到拉翻译资源的时候,但页面已经展示完整。我点一下按钮,成功越过翻译导致的页面元素错乱。这是一个创建按钮,创建成功了后面就是用户自己操作了
因为创建是频率稍微低一些的行为,所以几天内再无收到反馈
出现问题通常是setstate后删掉某个元素,那个元素追溯不到报错了。这里点了按钮的确是会删掉按钮并切换页面内容
尝 试 错 误
继续来作死,一起看看怎么样才能把react玩坏
const { useState, useLayoutEffect } = React;export default function App() {useLayoutEffect(() => {const font = document.createElement("font"); const app = document.querySelector(".App"); // 制造font包裹的效果,模拟翻译的效果,破坏原有结构 while (app.firstChild) {font.appendChild(app.firstChild); } app.appendChild(font); setTimeout(() => {// set个state看看setShow(false); }, 1000); }, []); const [show, setShow] = useState(true); return ( <div className="App"><h1>Hello CodeSandboxh1>{show && ( <>123123<h2>Start editing to see some magic happen!h2> >)} div> );}
预期效果出现了:
其实也不需要手动改,你只需要右键开启翻译为中文就可以复现了。问题根源在于react提前把parentNode存起来了,所以操作的时候找不到子节点
解 决 方 法
错误边界组件
利用react的两个生命周期来感知翻译错误,然后展示兜底ui,提示用户关掉翻译。并给出操作文档链接(/chrome/answer/173424?co=GENIE.Platform%3DDesktop&hl=en )。使用的时候只需要用TranslateErrorBoundary包一下组件即可
class TranslateErrorBoundary extends ponent {constructor(props) {super(props); this.state = { translateError: false }; } static getDerivedStateFromError() {if (document.documentElement.classList.contains("translated-ltr")) {return { translateError: true }; } } componentDidCatch(e, info) {// 上报翻译错误 report(e, info); } render() {if (this.state.translateError) {return ( <><strong> translate error! you' d better to turn your google-translate off and reload. seestrong><a target="_blank" rel="noopener noreferrer" href="文档链接"> 文档a> >); } return this.props.children; }}// usage<TranslateErrorBoundary> <Cpn />TranslateErrorBoundary>
不要让一块可删改的react元素最外层存在文本节点
话不多说,看?
<div className="App"> <h1>Hello CodeSandboxh1> {show && ( <>123123<h2>Start editing to see some magic happen!h2> > )}div>
这一块,有最外层的123123文本节点,所以翻译了会报错:
{show && ( <> 123123 <h2>Start editing to see some magic happen!h2> >)}
为什么呢?先看看翻译后结果,发现原本想删的节点是"123123",而他父节点却再也找不到它了
{show && ( <> <font><font>123123font>font> <h2><font><font>Start editing to see some magic happen!font>font>h2> >)}
改正措施: 加上span标签,不要让123123裸露
{show && ( <> <span>123123span> <h2>Start editing to see some magic happen!h2> > )}// 翻译后{show && ( <> <span><font><font>123123font>font>span> <h2><font><font>Start editing to see some magic happen!font>font>h2> > )}
因为最外层的是span,所以即使加了font,也是在span内部加了,删除元素的时候找的是span,都不会出问题
再看一个?
{label !== undefined ? (
<div>
{label}
div>
) : null}
{children}
</div>
这里的话,label就是纯文本。经过上面的例子,相信大家都知道
{label}
那里要套一个span了。但是这还是有风险:如果这个组件对外部使用,外部靠children传进来,意味着children的内容是多变的,比如传一个字符串进来,setstate后是一个其他节点,那么问题再次出现
错误条件再次重复一遍:一块可删改的react元素最外层存在文本节点。此时children是一块元素,而且是可变的,最外层就是children这个对象的最外层所有节点,其中存在一个文本节点是字符串,因此满足出错条件
例如children是文本节点
textNode1
,那么正常情况下setstate后如果children发生变化,删掉textNode1
的方式就是textNode1ParentNode.removeChild(textNode1)
。如果翻译了,文本节点包了两层font,那么textNode1
再也不是textNode1ParentNode
的子节点了。此外,即使把外层div换成span、section、article同理,都会出错
推论:不要在任何元素下直接裸露可变文本节点
代码都是自己写的,像
props.children
这种那么灵活的,尤其是要注意一下,如果是可能有文本节点的最好包一个span,确认没有的就可以不用包,防止外国用户翻译后源码出错。其实可以写一个工具,扫一下ast,发现有裸露文本节点的自动包一层span
要不,提个issue问问react那边可不可以不把parent节点先存起来,删元素的时候直接
node.parentNode.removeChild
?
总结
1. 使用数据驱动视图的框架如react、vue,如果遇到源码错误,考虑一下是不是有原生dom操作打乱了
2. 如果确认不是原生dom操作导致,考虑一下浏览器插件、翻译
3. 确实需要在react、vue中使用原生操作,需要考虑到这个隐患
4. 国际化的业务,如果出现这种问题,建议首先从浏览器翻译开始排查
5. 不要让一块可删改的react元素最外层存在文本节点,确认会有可变文本节点,需要套一层span
b站全灰了,但我一下把它弄回来了——css 滤镜
1年多职业生涯中最玄乎的线上问题
前端与前端联调的姿势
[js算法]手把手带你从leetcode原题——【两数相加】到大数相加
内功修炼之lodash——Object系列
那个前端写的页面好酷——大量的粒子(元素)的动效实现)
内功修炼之lodash—— clone&cloneDeep(一定有你遗漏的js基础知识
如果觉得《input重置为空后点击出现上次的值_上次玄乎的问题后续来了》对你有帮助,请点赞、收藏,并留下你的观点哦!