译:Why Do React Elements Have a $$typeof Property?
December 10, 2018
为什么 React Elements 需要一个 $$typeof 属性?
译自 Dan Abramov 的博客,原文链接
编写 JSX 的时候,你或许觉得你只是在写:
<marquee bgcolor="#ffa7c4">h1</marquee>
其实,你是在调用一个方法:
React.createElement(
/* type */ 'marquee',
/* props */ { bgcolor: '#ffa7c4' },
/* children */ 'hi'
)
这个方法会返回给你一个对象。我们则称这个对象为 React 元素(React element). 它告诉 React 接下来要如何进行渲染。你的组件便会返回一个基于这个对象的树形结构。
{
type: 'marquee',
props: {
bgcolor: '#ffa7c4',
children: 'hi'
},
key: null,
ref: null,
$$typeof: Symbol.for('react.element'), // 🧐 Who dis
}
如果你使用过 React,一定对 type
, props
, key
和 ref
非常熟悉。 但什么是$$typeof
呢?为什么它是一个Symbol()
类型的值呢?
其实这个问题属于“你不一定需要知道的 React”系列,但它能提升你在 coding 过程中的幸福感。这篇文章也包含了一些你可能想知道安全性小贴士。或许有一天你会编写自己的 UI 库,这些知识能使你得心应手。这便是我诚心所念。
在客户端 UI 库普及并且拥有一些基本的保护机制之前,我们通常会先构造一个 HTML 结构,然后将它插入到 DOM 中去:
const messageEl = document.getElementById('message');
messageEl.innerHTML = '<p>' + message.text + '</p>';
这样做似乎没有问题,除非你的message.text
是类似<img src onerror="stealyearPassword()">
这样的东西。你并不希望陌生人编写的东西逐字逐行的打印在你的 HTML 中。
(有趣之处:如果你只进行客户端渲染,那么这种形式的<script>
标签中的 JavaScript 代码不会生效。但是别让这个事实麻痹了你的安全意识。)
你可以使用可靠的 API 来防御这样的注入攻击,如document.createTextNode()
或者textContent
等方式,它们只会生成纯文本节点。 你也可以对用户输入进行“escape”处理,先发制人地将<
, >
或者其他用户提供的文本替换成安全的字符。
但这看起来仍然不够,要记住在每一次将用户输入进行输出的时候进行escape
处理是一件相当麻烦的事,而某一次的疏忽就可能导致出错,这引起的开销是非常高昂的。这就是为什么现代 lib 会默认的对文本内容进行escape
,譬如 React。
<p>
{message.text}
</p>
如果message.text
是一个包含<img>
等标签的恶意字符串,那么这些标签不会被渲染成真正的 HTML 节点。React 会先将这些不可靠的内容回避掉,然后将处理后的数据插入到 DOM 中。所以你只会看到<img>
或是其它标签的文本标记,而不是对应的 HTML 节点。
要在 React element 中渲染一个任意的 HTML 串,你需要使用dangerouslySetInnerHTML={{ __html: message.text }}
。事实上这个特性相当的笨拙。其意义在于提高相关功能在 code review 或代码审查时的可见性。
这代表 React 可以完全抵御注入攻击吗?不。HTML 和 DOM 提供了大量可被攻击的接口,React 或者其他的 UI 库难以对它们一一处理。这里面大多数的攻击方式都面向属性。例如,如果你渲染一个<a href={user.website}>
这样的标签,就得谨防遇到'javascript:stealYourPassword()'
这样的用户页面。再如,像<div {...userData}>
这样展开用户输入的实践方式很少见,但同样是不可靠的。
随着技术的不断演进 React 能够提供更多的保护机制,但这些问题其实有相当一部分来自于服务端,并且应当在那里解决。
当然了,对文本内容进行 escape 处理依然是防御潜在入侵的第一“教条”。对这样的代码放心不是一件很 nice 的事吗?~
// 会自动回避不安全的字符串
<p>
{message.text}
</p>
不过,也不是总能如意。这就是$$typeof
出现的原因。
React 元素的本质上是一个纯粹的对象:
{
type: 'marquee',
props: {
bgcolor: '#ffa7c4',
children: 'hi'
},
key: null,
ref: null,
$$typeof: Symbol.for('react.element')
}
React 确切提供了通过以上的纯对象方式创建元素的方法。通常来说,你能用React.createElement()
来创建它们,不过这是可选的方式。当然了,你应该并不想像这样来编写代码——但这对优化编译器、workers(译者注:指 webworker)间的 UI 对象传递(译者注:你没办法在 webworker 中使用 JSX)或是从 React 代码包中解耦 JSX 来说是有好处的。
然而,如果你的服务器存在一个漏洞,使得用户可以存储任意的JSON对象,但客户端却需要一个 string 类型的值,这就是个麻烦了:
// Server could have a hole that lets user store JSON
let expectedTextButGotJSON = {
type: 'div',
props: {
dangerouslySetInnerHTML: {
__html: '/* put your exploit here */'
},
},
// ...
};
let message = { text: expectedTextButGotJSON };
// Dangerous in React 0.13
<p>
{message.text}
</p>
在这个 XSS 注入攻击的案例中,React 0.13 会显得非常弱势(译者注:即误将对象渲染成元素)。再次声明,这个攻击主要依赖于一个服务端漏洞。而 React 依然能为避免这个问题做一些更棒的处理。从 0.14 开始,React 做到了。
React 0.14 通过给每个 React 元素标记一个 Symbol 类型的属性来解决这个问题:
{
type: 'marquee',
props: {
bgcolor: '#ffa7c4',
children: 'hi'
},
key: null,
ref: null,
$$typeof: Symbol.for('react.element')
}
其利用 JSON 属性值不能为 Symbol 这个性质来工作。即便服务端有一个安全漏洞会返回 JSON 而不是普通文本,JSON 中的属性值也不可能为 Symbol.for(‘react.element’)。React 会检查element.$$typeof
,并且回避掉丢失了这个属性或者其属性检查不通过的元素。
Symbol.for()
好处在于Symbols是有很高的全局性的,因此在一些特殊环境如 iframe 或 webworker 上下文中也可以使用 Symbol.for()
查询到对应的规则。因此,即便在相当特殊的情景下,应用中不同部分也可以顺利的传递可靠的元素。同样的,即便同一个页面中有多份 React 实例的拷贝,它们也能在$$typeof
属性之上“达成共识”。
那么如果浏览器不支持 Symbol 类型怎么办呢?
哎,它们确实得不到这个特别的保护机制。但 React 为了保持一致性依然会在元素中定义$$typeof
属性,只不过它会被设置成一个 number 类型的值——0xeac7
.
为什么是这个特别的数字呢?因为0xeac7
看起来有点像“React”。