为什么React元素有$$typeof属性

原文信息: 查看原文查看原文

Why Do React Elements Have a $$typeof Property?

- Dan Abramov

你也许觉得你在写JSX

<marquee bgcolor="#ffa7c4">hi</marquee>

但你实际是在调用一个函数:

React.createElement(
  /* type */ 'marquee',
  /* props */ { bgcolor: '#ffa7c4' },
  /* children */ 'hi'
)

而且,这个函数返回给你一个对象,我们称这个对象是一个React元素,它告诉React接下来要渲染什么。你的组件返回一个这种对象的树。

{
  type: 'marquee',
  props: {
    bgcolor: '#ffa7c4',
    children: 'hi',
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element'), // 🧐 这是什么
}

如果你用过React,你应该对typepropskeyref字段比较了解。但是什么是$$typeof?而且,为什么它用一个Symbol()作为值

这是那些你使用React不需要知道的东西中的一个,但是如果你知道将会让你感觉更好。这个文章里也有一些关于安全的提示,你可能也想知道。也许以后你会写自己的UI库,而所有这些迟早有用。我也很希望是这样。


在客户端UI库普及且添加基本保护前,应用代码对构建HTML且添加到DOM中,是很普遍的方式:

const messageEl = document.getElementById('message');
messageEl.innerHTML = '<p>' + message.text + '</p>';

这种能正常工作,除非你的message.text是像'<img src onerror="stealYourPassword()">'这样的内容。你不想应用渲染的HTML代码中出现被陌生人重写的内容按照按照字面显示。

(有趣的是:如果你只做了客户端渲染,这里的一个<script>标签不会允许JavaScript,但是别让这些带你进入一种虚假的安全感。)

为了阻止这种攻击,你可以用像document.createTextNode()或者textContent只处理文本这样的安全API,你可以可以先发制人,将输入框内的内容或其他用户提交的文本进行转义,替换掉一些潜在的危险内容,比如<>等。

然而,错误的成本是很高的,每次插入用户输入的字符串,想要在输出的时候记住这一点也是很难的。这就是为什么像React这样的现代化库要默认转义文本内容的字符串:

<p>
  {message.text}
</p>

如果message.text是一个包含<img>或其他标签的恶意字符串,它将不会转化成真正的<img>标签,React将转义这个内容再插入到DOM中。所以你看到的不是<img>标签,而只是看到它的标记内容。

要在React元素内随意渲染HTML内容,你需要写dangerouslySetInnerHTML={{ __html: message.text }}。事实上,这样难写的是一个特性,意味着需要对代码评审和代码库有高度透明的了解。


这意味着React对注入攻击是完全安全的吗?不是的。HTML和DOM提供了大量的攻击接口,想要React或其他UI库缓和攻击很难活很慢。大部分现有攻击媒介是通过属性的。比如,如果你要渲染<a href={user.website}>,注意用户的website可能是'javascript: stealYourPassword()'。对用户输入的内容使用拓展运算符很少见,比如<div {...userData}>,但也是危险的操作。

React可以随着时间提供更多保护,但在很多场景下,还有很多服务器issue终要被修复。

然而,转义文本内容是合理提防的第一行,它捕获了很多潜在的攻击。知道像下面这样的代码是安全的,不是很好吗?

// 自动转义
<p>
  {message.text}
</p>

嗯,那样也不总是对的。这就是$$typeof怎么来的。


React元素被设计成纯对象:

{
  type: 'marquee',
  props: {
    bgcolor: '#ffa7c4',
    children: 'hi',
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element'),
}

通常时候,你使用React.createElement()来创建它们,这不是必需的。像上面我刚写的让React支持纯元素对象是非常有效的。当然,你可能不会想像它们这样写,但是这对优化编译器、在workers间传递UI元素 或 从React包中解耦JSX 很有用。

然而,如果当客户端带啊需要一个字符串,你的服务器有一个漏洞,让用户存一个任意的JSON对象,这会是一个问题:

// 服务器有漏洞,让用户存储JSON
let expectedTextButGotJSON = {
  type: 'div',
  props: {
    dangerouslySetInnerHTML: {
      __html: '/* 把你的代码放这 */'
    },
  },
  // ...
};
let message = { text: expectedTextButGotJSON };

// 在React 0.13中是很危险的
<p>
  {message.text}
</p>

在上面这个场景下,React 0.13很容易受XXS攻击。再清楚地说,这种攻击依赖于一个已经存在的服务器漏洞。React还是可以在保护人们不受这个攻击上做更好的工作。从React 0.14开始,就开始支持这种了。

React 0.14中的修复是使用一个Symbol为每个React元素做一个标记

{
  type: 'marquee',
  props: {
    bgcolor: '#ffa7c4',
    children: 'hi',
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element'),
}

这种可以工作,因为你不能将Symbols放进JSON。所以,即使服务器有一个安全漏洞且返回JSON而不是文本,JSON不能包含Symbol.for('react.element')。React将检查element.$$typeof,如果缺少它或它是无效的,就会终端进程。

使用Symbol.for()很特有的一个好的事情,Symboliframesworkers这种环境间是全局的。所以这个修复不能阻止那些即使在奇怪的场景下在应用的不同部分间传递信任的元素。类似地,即使页面中有React的多个拷贝,它们仍然“赞同”有效的$$typeof值。


那些不支持Symbols的浏览器呢?

哎,它们不能得到额外的保护。React仍然持续地在元素上包含$$typeof,但它被设置为一个数字 - 0xeac7

为什么这个数字是特有的呢?0xeac7看起来有一点像“React”。

分享于 2019-02-19

访问量 1429

预览图片