再见吧👋 React dangerouslySetInnerHTML

in react •  7 years ago 

背景

在 React 项目中常会遇到渲染 HTML 内容的情况。可以利用 react 的 dangerouslySetInnerHTML 属性,完成基础开发。

示例:


function createMarkup() {
  return {__html: 'First · Second'};
}

function MyComponent() {
  return <div dangerouslySetInnerHTML={createMarkup()} />;
}

不足

就作者目前查阅的资料和实践结果,上文提到的基础方案有一些不足。

以非虚拟 DOM 的方式渲染节点

React 对虚拟 DOM 设计了优化的算法(主要依赖 data-reactid),放弃走虚拟 DOM 的渲染等同于放弃这些优化。

dangerouslySetInnerHTML 的渲染方式类似于原生 JS 的 HTML 渲染,显然放弃了节点优化:


function createMarkup() {
  return {__html: '<div style="color: red">I m cool<p></p></div>'};
}

function MyComponent() {
  return <div dangerouslySetInnerHTML={createMarkup()} />;
}

实验下来,只有 dangerouslySetInnerHTML 所在的元素上带有 data-reactid ,而子元素都没有。

示例图

可以从 React 源码中证实:

// ReactDOMComponent.js 部分源码:
// 为方便阅读,只保留了 _createContentMarkup 函数的相关代码
  /**
   * Creates markup for the content between the tags.
   *
   * @private
   * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
   * @param {object} props
   * @param {object} context
   * @return {string} Content markup.
   */
  _createContentMarkup: function (transaction, props, context) {
    var ret = '';
    var innerHTML = props.dangerouslySetInnerHTML; // 拿到 dangerouslySetInnerHTML 内容
    ret = innerHTML.__html;
    return ret;
  }

// 为方便阅读,只保留了 ReactDOMComponent.Mixin.mountComponent 函数的相关代码
ReactDOMComponent.Mixin = {
  /**
   * Generates root tag markup then recurses. This method has side effects and
   * is not idempotent.
   *
   * @internal
   * @param {string} rootID The root DOM ID for this node.
   * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
   * @param {object} context
   * @return {string} The computed markup.
   */
  mountComponent: function (rootID, transaction, context) {
    this._rootNodeID = rootID;
    var props = this._currentElement.props;
    var mountImage;
    //...
    var tagOpen = this._createOpenTagMarkupAndPutListeners(transaction, props);
    // 使用 _createContentMarkup
    var tagContent = this._createContentMarkup(transaction, props, context);
    // 返回最终 dom 字符串只是把 _createContentMarkup 生成的 HTML 包裹一下
    mountImage = tagOpen + '>' + tagContent + '</' + this._currentElement.type + '>';
    return mountImage;
  }

XSS 攻击

关于 XSS 的话题有点大,作者仅以自己的实验说明:


function createMarkup() {
  return {__html: `<input type="btn" value="dont touch me" onclick="document.writeln('u idolt!')">`};
}

function MyComponent() {
  return <div dangerouslySetInnerHTML={createMarkup()} />;
}

在基础方案下,input 元素完美渲染,点击正常,如果是恶意数据源,很容易造成严重后果。因此过滤必不可少。

解决思路

1.弃用 dangerouslySetInnerHTML,把文本 HTML 内容转化为 React-DOM 对象。

React 0.x 过来的小伙伴应该还没忘记没有 JSX 的时代,手写 React DOM 对象的开发方式。就算到了如今 JSX 也是先转换成 React DOM 对象再进行后面的渲染。

把 HTML 翻译成对象数组目前已有成熟的方案,htmlparse2 是个不错的选择。
不过 htmlparse2 生成的对象跟 React 特有的 DOM 对象还有一定距离,需要做进一步的转换,开源库 react-html-parser 这里做了不错的示范。

//使用 react-html-parser 后:
import ReactHtmlParser from 'react-html-parser';

let html = `<input type="btn" value="dont touch me" onclick="document.writeln('u idolt!')">`;

function MyComponent() {
  return <div>{ ReactHtmlParser(html) }<div/>;
}

2.过滤高危元素

防止 XSS 攻击的主要手段之一就是过滤危险标签,例如 input 这种类型的元素则是重点「嫌疑人」。基于上面提到的对象转化,做到过滤并不难。安全等级和体验的平衡,取决于我们对转化后的对象的细致过滤。比如发现 tag 类型是 input 时一棍子打死,比如把有 onclick 的元素全部干掉。对于 XSS,最安全的态度是「永远不要相信用户输入的数据」。

//使用 react-html-parser 后:
import ReactHtmlParser from 'react-html-parser';

let html = `<input type="btn" value="dont touch me" onclick="document.writeln('u idolt!')">`;

function MyComponent() {
  return <div>
    { ReactHtmlParser(html, {
        transform: function transform (node) {
            // 过滤 input 标签
            if (node.type === 'input') {
                return null;
            }
        }
    }) }
    <div/>;
}

小结

已上是作者在实践过程中遇到的问题,问题恐怕不止于此,但仅两点足以让我放弃直接使用 dangerouslySetInnerHTML。这也正是 react 官方所提倡的做法,毕竟,这个属性的设计初衷就是要让开发者体会到「dangerous」。所以,再见吧,dangerouslySetInnerHTML

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

Congratulations @pobusama! You received a personal award!

1 Year on Steemit

Click here to view your Board of Honor

Support SteemitBoard's project! Vote for its witness and get one more award!

Congratulations @pobusama! You received a personal award!

Happy Birthday! - You are on the Steem blockchain for 2 years!

You can view your badges on your Steem Board and compare to others on the Steem Ranking

Vote for @Steemitboard as a witness to get one more award and increased upvotes!