使用 TaroJS 提供的能力渲染富文本
1. 准备 HTML 字符串
import { View } from "@tarojs/components";
const content = "<p>你好 <span>世界</span></p>";
export default function ArticleContent() {
return (
<View className="article-content taro_html" dangerouslySetInnerHTML={{ __html: content }} />
);
}
如果 content 是动态变化的,建议用 useMemo 缓存,可以减少不必要的重渲染。
你可能会发现未转义字符(如 <、& 等,参考:MDN「转义字符」)会直接出现在页面上(taro 不做转义),导致展示异常,你应该在后台的编辑器中做实体转义。
2. 添加样式
首先引入 Taro 提供的基础 HTML 样式
if (isWeapp) {
require("@tarojs/taro/html.css");
}
如果你需要自定义样式,可以添加自定义样式。我这里使用的是 Tailwind CSS。
.article-content.rich-text .a {
color: #3b82f6;
text-decoration-line: underline;
text-underline-offset: 4px;
}
.article-content.rich-text .span {
display: inline;
}
.article-content.rich-text .img {
max-width: 100%;
height: auto;
}
.article-content.rich-text .table {
width: 100%;
border-collapse: collapse;
}
.article-content.rich-text .th,
.article-content.rich-text .td {
border-width: 1px;
border-style: solid;
border-color: #d1d5db;
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.article-content.rich-text .th {
background-color: #e5e7eb;
font-weight: 500;
text-align: left;
}
.article-content.rich-text .tr:nth-child(even) {
background-color: #f3f4f6;
}
3. 修复微信小程序 span 不渲染
如果你在后台给文本添加了样式,你可能会发现 span 节点在小程序端不渲染,这是 taro 把它渲染成了 span(预期是 text) 相关讨论见:[Bug]: dangerouslySetInnerHTML 无法渲染 span 标签 · Issue #17747 · NervJS/taro
解决:在 transform 阶段将 span 转为小程序可识别的 text。
Taro.options.html.transformElement = (taroEle: TaroElement) => {
const nodeName = taroEle?.nodeName?.toLowerCase();
if (nodeName === "span") {
taroEle.tagName = "TEXT";
taroEle.nodeName = "text";
}
return taroEle;
};
同时,你也可以通过 Taro.options.html.transformElement 来给对应的标签添加属性。
Taro.options.html.transformElement = (taroEle: TaroElement) => {
const nodeName = taroEle?.nodeName?.toLowerCase();
if (nodeName === "img") {
taroEle.setAttribute("mode", "widthFix");
}
return taroEle;
};
4. 添加事件
给富文本添加事件有两种办法:
- 参考文档:渲染 HTML | Taro 文档
- 事件委托到容器 3. 在后台给每一个标签添加 data-[*] 属性
- data-tag-name: 标签名,区分事件触发的标签
- data-href: 跳转链接
- data-a-href: 跳转链接
- data-src: 图片 src
- data-has-parent-a: 是否有父级 a 标签,用于处理 a 标签内的 span 标签点击事件,例如
<a><span>跳转</span></a> 点击事件会触发在 span 标签上,导致无法跳转。
taro 代码:
function getAllImageUrls(content: string) {
if (!content || typeof content !== "string") return [];
const imgReg = /<img[^>]*src=["']([^"']+)["'][^>]*>/gi;
const urls: string[] = [];
let match: RegExpExecArray | null;
try {
while ((match = imgReg.exec(content)) !== null) {
const src = match[1];
if (src && src.trim()) {
urls.push(src);
}
}
} catch (error) {
console.error("解析图片URL时出错:", error);
return [];
}
return urls;
}
type DataSet = {
tagName?: string;
href?: string;
src?: string;
aHref?: string;
hasParentA?: boolean | string;
};
const handleClick: ViewProps["onClick"] = (e) => {
const { target } = e;
const { dataset } = target ?? {};
const {
tagName = "",
href = "",
src = "",
aHref = "",
hasParentA = "false",
} = (dataset as DataSet) ?? ({} as DataSet);
const lowerTagName = tagName?.toLowerCase();
if (lowerTagName === "a") {
}
if (lowerTagName === "span" && hasParentA.toString() === "true" && aHref) {
}
if (lowerTagName === "img") {
const current = images.find((item) => item === src);
if (src) {
Taro.previewImage({ urls: images, current });
}
}
};
富文本编辑器提交到后端前需要添加 dataset:
import { isString } from "lodash-es";
export const transformHtml = (html: string): string => {
if (!html || !isString(html)) return "";
try {
const tempDiv = document.createElement("div");
tempDiv.innerHTML = html;
const processElement = (element: Element) => {
if (element.nodeType === Node.COMMENT_NODE || element.nodeType === Node.TEXT_NODE) {
return;
}
const tagName = element.tagName.toLowerCase();
if (tagName === "script" || tagName === "style") {
return;
}
const isSelfClosing = ["img", "br", "hr", "input", "meta", "link"].includes(tagName);
element.setAttribute("data-tag-name", tagName);
if (element.classList.length > 0) {
if (!element.classList.contains(tagName)) {
element.classList.add(tagName);
}
} else {
element.className = tagName;
}
if (tagName === "a") {
element.setAttribute("data-href", element.getAttribute("href") ?? "");
if (element?.children?.length > 0) {
const processChildElements = (childElement: Element) => {
childElement.setAttribute("data-has-parent-a", "true");
const dataHref = element.getAttribute("data-href");
if (dataHref) {
childElement.setAttribute("data-a-href", dataHref);
}
Array.from(childElement.children).forEach(processChildElements);
};
Array.from(element.children).forEach(processChildElements);
}
}
if (tagName === "img") {
element.setAttribute("data-src", element.getAttribute("src") ?? "");
}
if (!isSelfClosing) {
Array.from(element.children).forEach(processElement);
}
};
Array.from(tempDiv.children).forEach(processElement);
let result = tempDiv.innerHTML;
result = result.replace(
/<(img|br|hr|input|meta|link)([^>]*?)(\/?)>/gi,
(match, tagName, attributes, selfClosing) => {
if (attributes.includes("class=")) {
return match.replace(/class=["']([^"']*)["']/, (classMatch, existingClass) => {
if (!existingClass.includes(tagName)) {
return `class="${existingClass} ${tagName}"`;
}
return classMatch;
});
} else {
return `<${tagName} class="${tagName}"${attributes}${selfClosing}>`;
}
},
);
return result;
} catch (error) {
console.error("Error processing HTML:", error);
return html;
}
};
参考链接