需求描述
在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。但是以上方案存在如下缺点:
- 新增了一个需要当作渲染容器的dom元素节点,以上示例为div元素。
- 使用
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,感兴趣的可以看看,这里不再赘述。