序
翻译插件能够帮助我们快捷完成网页的翻译,但这种便利是有代价的,因为它会干扰许多现代网站的运作。这是因为翻译插件以破坏DOM结构的方式操作 DOM ,这会带来如 removeChild
方法的错误、让网站崩溃
本文将会探讨以下几点:
-
翻译实际带来的问题 - 最近遇到的bug
-
翻译插件的工作原理
-
可能的解决方案
遇到问题
这段时间,有个别用户反馈在使用某项目时出现网站崩溃的情况:在选择框中输入文本后页面白屏
收到反馈后第一反应肯定是想办法复现它,可是在本地怎么就是复现不出来,即使使用同样的账号、网址和操作
--> 这个项目已经上线很长时间,大部分用户在使用上也没有出现类似问题,难道只能跑去找用户、用他电脑现场排查下吗?
在准备约用户前,突然灵光一动,想到之前有遇到过翻译导致的问题,也知道翻译会改变dom结构,于是赶紧打开翻译功能试了下,果然,问题复现了!
最小复现案例和步骤:
- 可多选的选择器
复制 antd 官网中的 treeSelect 示例代码,生成一个支持多选的选择器
- 输入内容
- 删除内容
删除后出现报错:
原因探究
翻译插件运行时到底做了什么
既然已经知道问题是由翻译引起,那就得知道在翻译时,到底在页面上做了什么手脚
对于一段简单的 HTML 元素:
css
<div>hello</div>
在开启翻译后,会变为
css
<div><font>你好</font></div>
它会对页面上的文本内容进行翻译,并将翻译结果使用 font
标签进行包裹
看似只是改变了文本,并给文本加了个新标签,但实际的dom结构变化如下:
从图里可以看出,原始的TextNode
文本实际已经被卸载了! 这个对 DOM 的影响就是产生问题的主要原因。
什么时候会发生问题
对节点的操作包括了更新值、删除或者添加子项,翻译插件对 DOM 结构的影响会对节点的这三个操作都带来影响
更新节点 -> 未及时更新文本 /数字
最小复现示例如下,当点击按钮 addCount
后,页面上对应的count
没有同步更新
javascript
import { useEffect, useState } from 'react';
function App() {
const [count, setCount] = useState(1);
useEffect(() => {
console.log(count);
}, [count]);
return (
<div>
hello World{count}
<button onClick={() => setCount((count) => count + 1)}>addCount</button>
</div>
);
}
export default App;
原因在于翻译插件将hello World{count}
的内容合并到了一个<font>
标签中,导致真实的{count}
对应的节点已经不存在页面中了,故更新无效;
这问题不会导致浏览器崩溃,甚至不会有报错信息,在排查起来实际会更加困难,同时对用户也更有误导性
删除节点 -> 页面崩溃
最小复现示例如下,当点击按钮 addCount
后,页面报错
javascript
import { useState } from 'react';
function App() {
const [count, setCount] = useState(1);
return (
<div>
hello World
{count % 2 && <span>此时是{count}</span>}
<button onClick={() => setCount((count) => count + 1)}>addCount</button>
</div>
);
}
export default App;
原因在于当count
从1变为2后,React 尝试通过<span>
的父级来卸载文本内容,却无法找到对应的节点
报错信息如下:
csharp
Uncaught NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
复现要求:子节点的 TextNode
个数大于1,且数量会发生改变
复现场景1:有条件渲染的节点具有同级的兄弟节点
less
// 1.会出问题
<div>
hello World
{count % 2 && <span>此时是{count}</span>}
</div>
// 2. 会出问题
<div>
{count % 2 && <span>此时是{count}</span>}
hello World
</div>
// 3. 没有问题
<div>{count % 2 && <span>此时是{count}</span>}</div>
复现场景2:使用三元表达式渲染不同数量的文本节点
xml
<div>
{count % 2 ? (
<span>此时是{count}</span>
) : (
<>
<span>此时是</span>
{count}
</>
)}
</div>
除了上述两种场景,还有很多方式可以改变渲染的 TextNode
数量,这使得很难找到解决所有情况的有效方法
添加节点 -> 页面崩溃
最小复现示例如下,当count
从3变为4时,控制台报错
javascript
import { useState } from 'react';
function App() {
const [count, setCount] = useState(1);
return (
<div>
hello World
<div>
{count % 2 ? <span>此时是{count}</span> : count}
{count > 2 && 'Oops快出问题了'}
</div>
<button onClick={() => setCount((count) => count + 1)}>addCount</button>
</div>
);
}
export default App;
报错信息:
csharp
Uncaught NotFoundError: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.
解决方案
目前还没有终极解决方案,禁用翻译暂时是最好的解决办法,在需要国际化时,通过自己的应用程序实现本地化,使机器翻译变得没有必要。
通过设置HTML标签的lang
换为 zh
,浏览器即使开启了自动翻译功能,也不会再自动翻译我们的页面了
html
<html lang="zh">
...
</html>
不过这并不能阻止用户手动强行翻译,因此如果要从根本上禁止翻译,可以结合translate="no"
一起使用
html
<html lang="zh" translate="no">
...
</html>
不仅仅是翻译插件
翻译插件导致问题的根本原因是对DOM结构的干扰,这可能会对JavaScript操作DOM节点造成影响,这类问题在采用"虚拟DOM"技术的框架中更为明显,因为虚拟DOM通过保留对所有DOM节点的引用,实现更新时只需要更改实际发生变化的DOM部分(这一过程称为协调),因此它要求对DOM具有完全的控制和独占性,这意味着在与第三方扩展插件协作时,若这些插件修改了页面的DOM结构,就可能引发上述提到的问题
查找 BUG 经验和思路小结
-
当所有用户都出问题时,那一定是代码有问题;
-
当极小部分用户出问题时,考虑是否是翻译插件导致;
-
当只有一些高级用户(会使用浏览器插件的人)出问题时,考虑是否为第三方拓展插件导致
参考文章: