消息气泡自定义渲染器
IMKit 的 MessageBubble 内置渲染文本/图片/文件/语音/位置/流式/合并转发/系统消息;对 content.type==="custom" 的消息,允许宿主通过 renderers 提供自定义渲染器。渲染器返回 DOM 节点或字符串,并在气泡内安全展示业务卡片(订单、工单、位置共享等)。
概念
| content.type | 是否可自定义 | 说明 |
|---|---|---|
text/image/file/voice/location/stream/combine/recall/system | 否 | 由内置渲染器负责,行为与协议一致 |
custom | 是 | renderers.custom(message, helpers) 或 Map.get("custom") |
| 未知类型 | 否 | 显示 [自定义消息] 占位 |
- 渲染器签名:
(message, helpers) => Node | string;抛异常时降级为[自定义消息],不会破坏列表。 helpers提供messagePreview(message)、escapeHtml(text)、resolveMediaUrl(url)。renderers会从IMKitFacade→ChatView→MessageBubble透传,推荐在门面统一配置。- 自定义卡片仍受气泡布局约束;不要返回覆盖整页的绝对定位节点。
XSS 边界
不要直接拼接 message.content.data 到 innerHTML。使用 helpers.escapeHtml 或 textContent;图片 URL 用 resolveMediaUrl 解析,避免相对路径失效。
前置条件
- 已按 快速开始 完成
init与connect,页面能收发消息。 - 使用
@ni-paas/im-web/imkit(浏览器原生 ESM,无第三方依赖);自定义代码只调用 IMKit/IMLib 公开入口。 - 已引入默认样式:调用
ensureStyles(),或自行加载 IMKit 样式表。 - 自托管版无“UI 主题商店/付费皮肤”;所有自定义均为本地代码(不适用项)。
步骤
- 约定 custom 数据结构
例如
{type:"custom",data:{kind:"order",orderId,title,status,url}};发送端与渲染端共用同一约定。 - 实现渲染器
创建 DOM 节点,用
textContent写入用户输入;链接只允许 http(s) 并加rel="noopener"。 - 注册到门面/聊天视图
IMKit.setup({renderers})或new ChatView({renderers});Map 或普通对象都支持。 - 验证降级
故意抛错或返回空值,确认气泡显示占位而不是白屏;再验证历史消息与实时消息都能渲染。
参数表
| 参数 / 方法 | 类型 | 说明 |
|---|---|---|
renderers | Map<string,Function> | Record<string,Function> | 键为 content.type;当前仅 custom 生效 |
renderer(message, helpers) | Function | 返回 Node 或字符串 |
helpers.messagePreview | Function | 会话列表预览文案 |
helpers.escapeHtml | Function | HTML 转义 |
helpers.resolveMediaUrl | Function | 补全服务端媒体地址 |
MessageBubble.props.renderers | 同上 | 单气泡覆盖(一般不用) |
示例
订单卡片渲染器(安全 DOM 构造)
JavaScript
import { IMKit, ChatView } from '@ni-paas/im-web/imkit';
const renderers = new Map();
renderers.set('custom', (message, helpers) => {
const data = (message.content && message.content.data) || {};
// 只处理自己的业务类型,其余返回占位文本
if (data.kind !== 'order') {
return document.createTextNode(data.kind ? '[' + data.kind + ']' : '[自定义消息]');
}
const card = document.createElement('a');
card.className = 'order-card';
card.href = /^https?:\/\//.test(data.url || '') ? data.url : '#';
card.target = '_blank';
card.rel = 'noopener noreferrer';
const title = document.createElement('div');
title.className = 'order-card__title';
// 关键:用户可控文本一律 textContent / escapeHtml
title.textContent = data.title || '订单';
const meta = document.createElement('div');
meta.className = 'order-card__meta';
meta.textContent = '订单号 ' + (data.orderId || '-') + ' · ' + (data.status || '待处理');
card.appendChild(title);
card.appendChild(meta);
return card;
});
// 方式一:门面统一配置,所有 ChatView 生效
IMKit.setup({ renderers });
// 方式二:单个聊天视图
const chat = new ChatView({
conversationType: 1,
targetId: 'bob',
currentUserId: 'alice',
renderers,
});发送端构造同构 custom 消息
JavaScript
await client.sendMessage({
conversationType: 1,
targetId: 'bob',
content: {
type: 'custom',
data: {
kind: 'order',
orderId: 'SO-20261027-001',
title: '秋季外套 × 1',
status: '已发货',
url: 'https://shop.example.com/orders/SO-20261027-001',
},
},
});常见问题
能替换文本/图片气泡吗?
不能。内置类型没有 renderer 钩子;需要完全自定义时请用 custom 承载,并自行做文本/媒体渲染。
渲染器返回字符串会怎样?
字符串会作为文本节点插入气泡(不会当 HTML 解析),因此是安全的。
为什么实时消息能渲染,历史消息不行?
两者都走同一 MessageBubble;检查历史消息的 content 是否缺少 data.kind 或被服务端改写过。
renderers 能异步返回吗?
不能。渲染器必须同步返回节点/字符串;异步数据请先取回再更新消息内容或自行追加节点。