使用鸿蒙 Web 组件渲染富文本
在之前的 使用 TaroJS 提供的能力渲染富文本 一文中,我们介绍了在 TaroJS(小程序/H5)中渲染富文本的方式。本文将介绍鸿蒙 ArkUI 中使用 Web 组件 渲染富文本的实现方案。
其中图片 URL 提取(getAllImageUrls)和事件委托的思路与前文一致,此处不再重复。
1. WebView 方案(完整文章页)
对于包含复杂样式、表格、代码块等内容的完整文章,推荐使用 Web 组件加载 HTML。
1.1 生成 HTML 文档
将后端返回的 HTML 内容包装为完整的 HTML 文档,包含内联 CSS 样式:
generateHtmlContent(): string {
if (!this.article) return '';
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=0.9" />
<script>
var h5Port;
// 监听来自 ArkTS 侧的消息端口初始化
window.addEventListener("message", function (event) {
if (event.data === "__init_port__" && event.ports[0]) {
h5Port = event.ports[0];
}
});
// 向 ArkTS 侧发送消息
function PostMsgToEts(data) {
if (h5Port) { h5Port.postMessage(data); }
}
// 拦截所有点击事件
document.addEventListener("DOMContentLoaded", function () {
document.addEventListener("click", function (event) {
const target = event.target;
const tagName = target.tagName;
const dataset = target.dataset;
if (tagName === "A") {
PostMsgToEts(JSON.stringify({
type: "linkClick",
data: {
tagName: "A",
href: target.href || "",
dataset: dataset ? Object.assign({}, dataset) : {},
}
}));
return;
}
if (tagName === "SPAN") {
PostMsgToEts(JSON.stringify({
type: "spanClick",
data: {
tagName: "SPAN",
dataset: dataset ? Object.assign({}, dataset) : {},
}
}));
return;
}
if (tagName === "IMG" || tagName === "IMAGE") {
PostMsgToEts(JSON.stringify({
type: "imageClick",
data: {
tagName: "IMG",
src: target.src,
dataset: dataset ? Object.assign({}, dataset) : {},
}
}));
return;
}
});
});
</script>
</head>
<body>
<div id="content">\${this.article.content}</div>
</body>
</html>`;
}
1.2 加载 HTML 并建立通信
使用 Web 组件的 onControllerAttached 生命周期加载内容,在 onPageEnd 中建立 WebMessagePort 双向通信:
import { webview } from '@kit.ArkWeb';
@State images: DefaultMediaModel[] = [];
private controller: webview.WebviewController = new webview.WebviewController();
private ports: webview.WebMessagePort[] = [];
Web({ src: "", controller: this.controller })
.width('100%')
.backgroundColor("#f5f5f5")
.javaScriptAccess(true)
.zoomAccess(false)
.onControllerAttached(() => {
this.controller.loadData(
this.generateHtmlContent(), "text/html", "UTF-8", " ", " "
);
})
.onPageEnd(() => {
this.ports = this.controller.createWebMessagePorts();
if (this.ports && this.ports[0] && this.ports[1]) {
this.ports[1].onMessageEvent((result: webview.WebMessage) => {
if (typeof result === 'string') {
const messageData = JSON.parse(result) as DOMEvent;
if (messageData.type === 'linkClick' || messageData.type === 'spanClick') {
this.handleLinkClick(messageData.data as LinkData);
}
if (messageData.type === 'imageClick') {
this.handleImageClick(messageData.data as ImageData);
}
}
});
this.controller.postMessage('__init_port__', [this.ports[0]], '*');
}
})
WebMessagePort 通信流程:
createWebMessagePorts() 创建端口 0 和端口 1- 端口 1 在 ArkTS 侧注册
onMessageEvent 回调接收消息 - 通过
postMessage('__init_port__', [端口0], '*') 将端口 0 传递给 HTML 侧 - HTML 侧通过
window.addEventListener("message", ...) 接收端口 0 并保存为 h5Port - HTML 侧通过
h5Port.postMessage(data) 向 ArkTS 侧发送消息
2. 链接点击处理
与 TaroJS 方案类似,需要区分外部链接、内部链接和嵌套在 <span> 中的链接。后端需要在提交 HTML 前给标签添加 data-* 属性(data-tag-name、data-href、data-a-href、data-has-parent-a 等),具体实现参考 前文的 transformHtml 函数。
interface LinkData {
href: string;
tagName: string;
dataset: Record<string, string>;
}
handleLinkClick(linkData: LinkData) {
const { href, tagName, dataset } = linkData;
const aHref = dataset.aHref || "";
const datasetHref = dataset.href || "";
const hasParentA = dataset.hasParentA || "";
const isSpanLink = tagName === 'SPAN' && Boolean(aHref) && Boolean(hasParentA);
if (isSpanLink || tagName === 'A') {
const realLink = href || datasetHref || aHref || '';
const isExternalLink = realLink.startsWith('http://') || realLink.startsWith('https://');
if (isExternalLink) {
} else {
}
}
}
3. 图片预览
图片 URL 提取逻辑(getAllImageUrls)参考 前文实现。这里补充鸿蒙端的图片预览。
预览图片
加载文章时提取所有图片 URL 并构建预览列表,点击图片时定位到对应位置:
import { DefaultMediaModel, DefaultMediaType, MediaPreview, MediaPreviewOptions } from '@lyb/media-preview';
const list = getAllImageUrls(this.article.content)
?.filter(Boolean)
?.map(src => ({
type: DefaultMediaType.IMAGE,
sourceSrc: src,
placeholder: "预览图片",
} as DefaultMediaModel));
this.images = list;
this.mediaPreviewOptions.setMedias(list);
handleImageClick(data: ImageData) {
const src = data.src ?? "";
const idx = this.images.findIndex(item => item.sourceSrc === src);
this.mediaPreviewOptions.setInitIndex(idx === -1 ? 0 : idx);
MediaPreview.open(this.getUIContext(), this.mediaPreviewOptions);
}
4. 参考链接