浅谈在react中渲染html

259 阅读4分钟

需求描述

在react中渲染html也是一个常见的需求,比如服务端返回一段html标签字符串,以下是一段伪代码。

const list = ['<p>这是一段文本</p>','<a href="xxx">这是一个链接</a>'];

const RenderList = () => {
    // 这里要渲染list
    return;
}

渲染react html字符串,我们要如何做呢?

如果不是很熟悉React的渲染的,可能都会想到这么做:

const list = ['<p>这是一段文本</p>','<a href="xxx">这是一个链接</a>'];

const RenderList = () => {
    return (
        <>
            { list.map((item, index) => (
                <Fragment key={`${item}-${index}`}>{item}</Fragment>
            ))}
        </>
    )
}

但是这样结果并不会将字符串里的标签当作标签来渲染,而是直接渲染了包含标签的整体字符串。

方案1

不过,react为我们提供了一个dangerouslySetInnerHTML属性,它允许我们渲染html字符串,例如以上代码,我们就可以改造如下:

const list = ['<p>这是一段文本</p>','<a href="xxx">这是一个链接</a>'];


const RenderList = () => {
    return (
        <>
            { list.map((item, index) => (
                <div key={`${item}-${index}`} dangerouslySetInnerHTML={{ html:item }}></div>
            ))}
        </>
    )
}

如此一来,我们就将标签字符串渲染成了html。但是以上方案存在如下缺点:

  1. 新增了一个需要当作渲染容器的dom元素节点,以上示例为div元素。
  2. 使用dangerouslySetInnerHTML属性可能会带来XSS攻击的风险。

方案2(推荐)

幸运的是,有这样一个库html-react-parser,它的使用方式也非常简单,就是一个方法,如下所示:

import parseHTML from 'html-react-parser';

const list = ['<p>这是一段文本</p>','<a href="xxx">这是一个链接</a>'];

const RenderList = () => {
    return (
        <>
            { list.map((item, index) => (
                <Fragment key={`${item}-${index}`}>{parseHTML(item)}</Fragment>
            ))}
        </>
    )
}

可以看到,我们就是导入这个包,然后调用方法,将返回值当成ReactNode来渲染即可。

如果我们要根据一些字符串,来渲染对应的组件,我们还可以提供第二个参数的replace方法。如下所示:


import parseHTML from 'html-react-parser';

const list = ['<p>这是一段文本</p>','<a href="xxx">这是一个链接</a>'];

const RenderList = () => {
    return (
        <>
            { list.map((item, index) => (
                <Fragment key={`${item}-${index}`}>{parseHTML(item, {
                    replace(){
                        // 这里写对应的逻辑
                    }
                })}</Fragment>
            ))}
        </>
    )
}

这个方法有2个参数,第一个参数node是一个dom节点,可以是html元素节点,也可以是文本节点,它的类型如下:

export type DOMNode = Comment | Element | ProcessingInstruction | Text;

如果我们确认这里只会有元素节点和文本节点,那就只需要考虑Element和Text类型即可。每个元素节点都会有attribs和children两个属性,分别是元素的属性和子节点。例如一个p元素含有id属性,又或者是a标签的href属性。

注意: 这里的children节点是这个库包装的节点,不能直接使用,而需要使用该库提供的domToReact方法来渲染。

例如,我们将对应的a标签元素换成组件库提供的Link组件,可以写成如下代码:

import parseHTML, { domToReact, Element } from 'html-react-parser';

const list = ['<p>这是一段文本</p>','<a href="xxx">这是一个链接</a>'];

const RenderList = () => {
    return (
        <>
            { list.map((item, index) => (
                <Fragment key={`${item}-${index}`}>{parseHTML(item, {
                    // 请注意,这里我们是明确知道元素节点才会存在这2个属性
                    replace({ attribs = {},children }: Element){
                        // 获取href属性
                        const { href } = attribs;
                        if(href){
                            return <Link href={href}>{domToReact(children as DOMNode)}</Link>
                        }
                    }
                })}</Fragment>
            ))}
        </>
    )
}

如果匹配的是邮箱,我们还可以渲染出对应的用户信息或者是邮箱组件,原理和渲染链接是一个意思。我们只需要匹配一下属性值是否是一个邮箱字符串即可。

接下来我们就可以基于这个库,封装一个RenderHTMl组件,代码如下:

import React, { useState } from 'react';
import UserInfo from './user-info';
import { Link } from 'antd';
import parseHTML, { DOMNode, Element, domToReact } from 'html-react-parser';
import { marked } from 'marked';
import _ from 'lodash';

const { TextArea } = Input;

export interface RenderHTMLProps {
  html?: string;
}
const RenderHTML: React.FC<RenderHTMLProps> = ({ html }) => {
  if (!_.isString(html) || !html) {
    return null;
  }
  return (
    <>
      {parseHTML(html, {
        replace({ attribs = {}, children }: Element) {
          const { href } = attribs;
          //简单匹配qq邮箱
          const isEmail = href?.match(/(.*?)@qq.com/);
          if (isEmail) {
            return <UserInfo email={isEmail[0].replace('mailto:', '')} />;
          } else if (href) {
            return <Link href={href}>{domToReact(children as DOMNode[])}</Link>;
          }
          // 如果还存在其它情况,例如图片,列表等,则还需要单独处理,这里可能就需要定制化实现,根据实际需求来定
        },
      })}
    </>
  );
};

export default RenderHTML;

以上组件的使用很简单,如下所示:

<RenderHTML html={`<p>hello,world!</p>`} />

方案3 自己实现一个react-html-parser

根据实际需求,我们需要根据正则表达式来实现,这篇文章讲解了如何实现一个简易版本的react-html-parser,感兴趣的可以看看,这里不再赘述。