卡片消息这类东西,做聊天机器人或者社群运营工具的朋友应该都不陌生。早些年大家还在用纯文本堆信息,后来发现纯文本的触达率和可读性都撑不住,于是结构化卡片就成了刚需——一张卡片里塞标题、摘要、缩略图、跳转按钮,用户扫一眼就能抓住重点,点击率比干巴巴的文字高出一大截。而说到卡片消息的构造,绕不开两个东西:XML 和 JSON。这两个词一出来,很多人第一反应是"这不就是两种数据格式吗",但真到动手写的时候,坑就来了:什么时候用 XML、什么时候用 JSON、字段怎么对应、结构怎么嵌套、发出去为什么没渲染、按钮为什么点不动。这篇就围绕"用 XML 和 JSON 生成卡片"这件事,把我自己踩过的路、试出来的方法、以及那些文档里不会明说的细节,掰开揉碎讲一遍。不管你是刚接触卡片消息的新手,还是已经写过几版模板想优化结构的老手,下面这些内容应该都能对上你的需求。
1. 先搞清楚卡片消息到底在解决什么问题
1.1 卡片不是"好看的文字",它是结构化数据
很多人一开始会把卡片理解成"带样式的文本",这个认知偏差会直接导致后面结构设计走偏。卡片本质上是一份结构化的数据描述,它告诉客户端"这里有一个标题区、那里有一个图片区、底部有一排按钮",客户端拿到这份描述后,用自己的渲染引擎把它画出来。也就是说,你写的 XML 或 JSON 并不是最终呈现,而是一份"施工图纸"。
理解这一点非常关键,因为它决定了你写代码时的思维方式。你不是在拼字符串做排版,你是在描述一棵节点树:根节点是卡片容器,下面挂若干模块节点,模块节点下面再挂内容节点和交互节点。XML 用标签的嵌套天然表达这棵树,JSON 用对象和数组表达这棵树。两种格式描述的是同一件事,只是语法不同。
我见过不少新手上来就想着"我要把标题加粗、字号调大",然后在卡片里找字体设置字段,找了半天找不到——因为卡片协议通常只提供有限的样式能力,重点在于模块的组合而不是像素级的样式控制。想通这一层,后面的结构设计就顺了。
1.2 XML 与 JSON 两条路,各自的适用场景
既然两种格式描述的是同一件事,那为什么会有两条路?这跟使用场景和历史沿革有关。
XML 路线通常出现在协议设计较早、强调可读性和标签语义的体系里。它的优势是结构一目了然,标签名自带语义,嵌套关系用缩进一看就懂,手写调试的时候特别友好。缺点是冗长,一个简单的卡片可能要写几十行,而且解析时对标签闭合、属性引号这些细节要求严格,少一个斜杠就整份失效。
JSON 路线则是现代接口的主流选择,体积小、解析快、和大多数编程语言的字典/对象结构天然对应,用代码动态生成的时候特别顺手。缺点是嵌套深了以后括号层级容易看花眼,而且 JSON 不支持注释,调试时想标个"这里是标题"都没地方写。
实际选型的时候,我的经验是:如果卡片结构相对固定、由人工维护模板,XML 写起来更直观;如果卡片需要根据数据动态拼装、字段数量不固定,JSON 用代码生成更省事。当然最终用哪种,还得看你对接的那套体系支持什么——有些体系两种都收,有些只认一种,这个在动手前必须先确认清楚,别写完一大套才发现格式不对。
1.3 一张卡片的最小构成要素
不管用哪种格式,一张能正常渲染的卡片,通常包含这么几个层次的东西:
- 卡片容器层:定义整张卡片的整体属性,比如卡片类型、主题色、是否可转发等。
- 模块层:卡片由若干模块纵向堆叠而成,常见的有头部模块、正文模块、图片模块、按钮模块、分割线等。
- 内容层:模块内部的具体内容,比如文本内容、图片地址、按钮文字和跳转地址。
- 交互层:按钮点击后干什么,是跳链接、回传数据还是触发某个动作。
这四层在心里要有清晰的边界。我调试卡片时有个习惯:先在纸上画出模块的纵向排列顺序,标好每个模块里放什么,然后再动手写 XML 或 JSON。这个"先画后写"的习惯帮我省了大量返工——因为卡片渲染失败时,你很难从报错信息里看出是哪一层出了问题,只能靠结构清晰来缩小排查范围。
2. XML 方式生成卡片:从骨架到细节
2.1 XML 卡片的基本骨架长什么样
XML 卡片的写法,核心就是一层套一层的标签。最外层通常是一个卡片根标签,里面按顺序放各个模块标签。我拿一个"标题+正文+按钮"的经典三件套举例,结构大致是这样:
<card> <header> <title>项目周报已生成</title> <subtitle>点击查看详情</subtitle> </header> <body> <text>本周共完成 12 项任务,新增 3 个待办事项。</text> </body> <actions> <button type="link" url="https://example.com/report">查看周报</button> </actions> </card>这段结构里,card是根,header、body、actions是三个并列的模块,模块内部再放具体内容。注意这里的标签名和属性名是示意性的,实际对接时必须以目标体系的字段定义为准,不同体系对标签的命名差异很大,有的叫header,有的叫title区,有的把按钮直接叫action。我强调这一点是因为见过太多人拿着别处的示例直接抄,结果字段名对不上,卡片发出去一片空白。
写 XML 有几个硬性细节必须盯住:标签必须闭合,自闭合标签别忘了结尾的斜杠;属性值必须用引号包起来,单双引号都行但要成对;特殊字符比如&、<、>在文本内容里要转义,否则解析器会把它当成标签的一部分。这些听起来是常识,但实际调试时十次失败里有三四次就是这类低级错误。
2.2 模块嵌套的层级控制与常见陷阱
XML 最大的优势是嵌套直观,但最大的坑也在嵌套上。卡片协议通常对模块的嵌套深度和顺序有约束,比如按钮模块一般只能放在卡片末尾,图片模块不能塞进文本模块里面。这些约束如果违反,表现往往是"卡片能发出去但渲染不全"或者"某个模块直接消失",而且不一定有明确报错。
我的做法是维护一份"合法嵌套清单",把每个模块允许包含的子模块列出来,写模板时对照检查。举个常见的错误:有人想在正文模块里再嵌一个按钮模块,实现"文字后面跟个按钮"的效果,结果按钮不显示。正确做法是把按钮放到独立的动作模块里,靠模块的排列顺序来控制视觉位置,而不是靠嵌套。
还有一个容易忽略的点是模块顺序。多数卡片渲染是按模块在 XML 里出现的先后顺序纵向排列的,所以你想让图片在文字上面,就得把图片模块写在文字模块前面。这个顺序逻辑很朴素,但新手常常按"重要性"而不是"视觉顺序"来写,导致渲染出来和预期上下颠倒。
2.3 属性与文本内容的转义处理
文本内容里的特殊字符是 XML 路线的经典雷区。假设你的卡片标题是"折扣 < 5 折",这个小于号如果不转义,解析器会认为标签开始了,整份 XML 直接崩掉。标准做法是把<转成<、>转成>、&转成&、引号转成"。
手工写模板时还好,一旦是程序动态生成,就必须在拼接前做统一的转义处理。我一般会封装一个转义函数,所有要进 XML 的文本字段都过一遍,宁可多转一次也不能漏。这里有个细节:转义要在拼接之前做,而不是拼接之后对整份 XML 做替换,否则会把标签本身的尖括号也转掉,那就彻底废了。
def escape_xml(text): return (text.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace('"', """) .replace("'", "'"))顺序也有讲究,&必须第一个替换,否则后面替换出来的<里的&又会被二次转义成&lt;,显示出来就是一堆乱码。这个顺序问题我当年调了半小时才反应过来,属于典型的"知道就一秒,不知道就抓瞎"。
2.4 用模板引擎批量生成 XML 卡片
实际项目里很少手写 XML,基本都是模板加数据填充。用模板引擎的好处是把结构和数据分离,改样式不用动代码,改数据不用碰模板。Python 里jinja2是常用选择,思路是把 XML 骨架写成模板文件,占位符用双花括号,渲染时传入数据字典。
from jinja2 import Template tpl = Template(""" <card> <header><title>{{ title }}</title></header> <body><text>{{ content }}</text></body> <actions> {% for btn in buttons %} <button type="link" url="{{ btn.url }}">{{ btn.text }}</button> {% endfor %} </actions> </card> """) xml_str = tpl.render( title="任务提醒", content="你有 3 个任务今天到期", buttons=[{"text": "去处理", "url": "https://example.com/tasks"}] )用模板引擎时要注意,它默认不一定帮你做 XML 转义,jinja2对 HTML 有自动转义,但对自定义的 XML 场景需要手动开启或自己处理。我一般会在渲染前把数据字段先过一遍转义函数,双保险。另外模板里的循环生成按钮时,要控制好按钮数量上限,多数卡片协议对单张卡片的按钮数量有限制,超了会被截断或者整卡拒绝。
3. JSON 方式生成卡片:灵活但有讲究
3.1 JSON 卡片的典型结构组织
JSON 卡片的结构和 XML 描述的是同一棵树,只是换成了对象和数组的语法。同样的三件套,用 JSON 写出来大概是这样:
{ "card": { "header": { "title": "项目周报已生成", "subtitle": "点击查看详情" }, "body": { "text": "本周共完成 12 项任务,新增 3 个待办事项。" }, "actions": [ { "type": "link", "text": "查看周报", "url": "https://example.com/report" } ] } }对比一下就能看出,JSON 把 XML 的标签换成了键,把并列的模块换成了对象里的键值对,把重复的按钮换成了数组。这种结构用代码生成时特别自然——你直接构造一个字典,序列化成 JSON 字符串就完事,不用操心标签闭合和转义(JSON 序列化库会帮你处理引号和特殊字符)。
但 JSON 也有它自己的讲究。键的顺序在 JSON 里理论上不保证,虽然大多数解析器会保留插入顺序,但如果你依赖键的顺序来控制模块排列,那就是在赌解析器的实现。稳妥的做法是:需要顺序的地方用数组,比如按钮列表、多段文本列表,用数组的天然顺序来保证渲染顺序。
3.2 用代码动态拼装 JSON 卡片
JSON 路线最爽的地方就是动态拼装。假设你要根据一批数据生成一张"待办清单"卡片,每条待办是一个列表项,数量不固定,用代码写起来非常直接:
def build_todo_card(todos): items = [] for t in todos: items.append({ "type": "text", "content": f"{t['name']} - 截止 {t['deadline']}" }) return { "card": { "header": {"title": "今日待办"}, "body": {"items": items}, "actions": [ {"type": "link", "text": "全部完成", "url": "https://example.com/done"} ] } }这段代码里,待办数量完全由数据决定,卡片结构自动适配。这就是 JSON 相对 XML 在动态场景下的优势——你操作的是内存里的数据结构,序列化是最后一步,中间不需要拼字符串。
拼装时有几个经验点值得说。第一,空数组和缺失字段要区分对待,有些体系对空数组的处理是"渲染一个空模块",对缺失字段的处理是"跳过该模块",效果完全不同,得按目标体系的行为来定。第二,深层嵌套要控制层数,JSON 嵌套太深不仅可读性差,某些解析器还有深度限制,我一般控制在四层以内。第三,数值和字符串别混用,比如按钮的宽度字段,有的体系要数字有的要字符串,混用会导致渲染异常。
3.3 JSON 序列化的编码与体积优化
JSON 序列化时最容易忽略的是编码问题。中文内容如果序列化时没指定ensure_ascii=False,会被转成\uXXXX形式,虽然功能上没问题,但体积会膨胀不少,调试时也看不清内容。Python 里这样写:
import json json_str = json.dumps(card_dict, ensure_ascii=False, separators=(",", ":"))separators参数去掉多余空格,能进一步压缩体积。卡片消息往往有大小限制,字段多、文本长的时候,几百字节的差距可能就是"发得出去"和"发不出去"的区别。我做过一个粗略统计,去掉空格和关闭 ASCII 转义,一张中等复杂度的卡片体积能省下百分之十几到二十,长文本卡片省得更多。
还有一个体积优化思路是复用结构。如果一批卡片只有标题和链接不同,其他结构完全一样,那就把公共部分抽成一个基础模板,每次只覆盖变化字段,而不是从头构造整个字典。这在批量发送场景下能明显减少代码量和出错概率。
3.4 XML 与 JSON 的字段映射对照
实际项目里经常遇到"一边用 XML 一边用 JSON"的情况,比如老系统出 XML、新系统吃 JSON,中间要做转换。这时候一份清晰的字段映射表就非常必要。下面这张表是我按常见卡片结构整理的对照关系,具体字段名以实际体系为准:
| 结构层次 | XML 表达 | JSON 表达 | 注意事项 |
|---|---|---|---|
| 卡片根 | <card>标签 | "card": {}对象 | 根节点名称各体系不同 |
| 头部模块 | <header>含子标签 | "header": {} | 标题副标题为子字段 |
| 正文模块 | <body>含文本标签 | "body": {} | 多段文本用数组 |
| 按钮组 | <actions>含多个<button> | "actions": []数组 | 按钮数量有上限 |
| 按钮跳转 | url属性 | "url"键 | 地址需完整协议头 |
| 图片 | <image src="..."> | "image": {"src": "..."} | 地址需可公网访问 |
做转换的时候,我建议先转成中间的统一数据结构,再分别序列化成目标格式,而不是 XML 直接字符串替换成 JSON。字符串替换看着快,遇到嵌套和转义就崩,中间结构虽然多一步,但稳定得多。
4. 卡片渲染与交互的实操要点
4.1 图片与链接资源的可用性检查
卡片发出去不显示图片,是最高频的问题之一,而且原因往往不在卡片代码本身。图片地址必须是可公网访问的直链,不能是需要登录的页面地址,不能是本地文件路径,也不能是带防盗链的地址。我排查这类问题的顺序是:先在无痕窗口直接打开图片地址,能正常显示再谈卡片;然后检查地址是否带协议头(http或https开头);最后确认图片格式和大小在限制范围内。
链接按钮也是同理,跳转地址必须是完整 URL,相对路径在卡片里基本不认。另外有些体系对跳转域名有白名单要求,不在名单里的域名点击后会被拦截,这个在测试阶段就要验证,别等上线了才发现按钮点不动。
提示:图片和链接的可用性检查一定要在真实网络环境下做,本地开发环境能访问不代表外部能访问,这个坑我踩过不止一次。
4.2 按钮交互类型的选择逻辑
按钮的交互类型通常分几类:跳转链接、回传数据、触发动作。选哪种取决于你的业务闭环。如果点击后是要打开一个网页,用跳转链接类型;如果点击后是要让后台收到一个信号(比如"已读""确认"),用回传数据类型;如果点击后要触发某个预设流程,用触发动作类型。
选择时的关键考量是用户点击后发生什么,以及这个结果谁来处理。跳转链接最简单,不依赖后台;回传数据需要后台有对应的接收和处理逻辑;触发动作则依赖体系预定义的动作集。我一般优先用跳转链接,因为它的链路最短、最不容易出问题,只有在确实需要收集用户操作信号时才用回传类型。
回传数据的字段设计也有讲究,通常包含一个动作标识和若干业务参数。动作标识要全局唯一,业务参数要精简,别把整个业务对象塞进去,一来体积受限,二来后台解析也麻烦。我习惯只传关键 ID,后台拿到 ID 再去查详情,这样卡片体积小、扩展性也好。
4.3 多端渲染差异的兼容处理
同一份卡片,在不同客户端上渲染出来可能不一样,这是做卡片消息必须接受的现实。差异主要体现在:模块间距、字体大小、图片裁剪方式、按钮排列(横排还是竖排)、深色模式下的配色。这些差异大多不在你的控制范围内,但可以通过设计上的保守选择来降低影响。
我的兼容策略是:结构上尽量简单,少用花哨的布局;图片用固定宽高比,避免不同端裁剪差异过大;颜色用体系推荐的主题色,别自己写死十六进制值;按钮文字控制在四个字以内,防止窄屏换行。这些保守做法牺牲了一点视觉表现力,但换来的是各端表现基本一致,维护成本低很多。
如果确实需要精细控制,那就得针对不同端做适配,但这条路成本很高,除非业务对视觉一致性要求极高,否则不建议。我做过一个多端适配的卡片项目,光是调各端的间距就花了两天,最后发现用户根本注意不到那些像素级差异,投入产出比很低。
5. 调试与问题排查实录
5.1 卡片发出去没反应的排查路径
卡片发出去没反应,可能是发送环节的问题,也可能是卡片内容的问题。我的排查路径是从外到内:先确认消息本身发出去了没有(看发送接口的返回),再确认卡片内容是不是合法结构(用校验工具或最小化测试),最后确认渲染环境是否支持该卡片类型。
最小化测试是个好办法:把卡片精简到只剩一个标题,发出去看能不能渲染。能渲染说明基础链路通,问题出在复杂结构上;不能渲染说明基础链路就有问题,可能是格式不对或者类型不支持。然后逐步加回模块,加一个测一个,很快就能定位到出问题的那个模块。这个方法笨但有效,比盯着报错信息猜快得多。
5.2 常见错误速查表
下面这张表是我这些年攒下来的高频问题清单,按现象归类,方便对照排查:
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 卡片完全空白 | 格式不合法或类型不支持 | 用最小卡片测试基础链路 |
| 部分模块不显示 | 模块嵌套违规或字段名错误 | 对照字段定义逐个核对 |
| 图片显示裂图 | 地址不可公网访问或格式不支持 | 无痕窗口直接打开图片地址 |
| 按钮点击无反应 | 链接缺协议头或域名不在白名单 | 检查 URL 完整性和白名单 |
| 中文显示乱码 | 编码未指定或转义错误 | 检查序列化编码设置 |
| 卡片被截断 | 体积超限或模块数量超限 | 精简字段、减少模块数量 |
| 各端显示不一致 | 布局依赖了端特有行为 | 改用保守布局和固定比例 |
这张表建议收藏,遇到问题先对一遍,能省不少时间。表里没覆盖的情况,基本就得靠最小化测试一步步缩小范围了。
5.3 体积超限的瘦身技巧
卡片体积超限是很常见的问题,尤其是文本多、按钮多的卡片。瘦身思路按性价比排序:第一,压缩 JSON 序列化,去空格、关 ASCII 转义,这个前面说过,零成本;第二,精简文本,长文本截断加省略号,完整内容放到跳转页面里;第三,减少按钮数量,把次要操作收进跳转页面;第四,图片用外链不内嵌,内嵌图片是体积杀手;第五,去掉冗余字段,有些字段是默认值,不传也行。
我遇到过一个极端案例,一张卡片塞了二十条待办,体积直接爆掉。最后的方案是卡片只显示前五条加一句"还有 15 条待办",点击跳转到完整列表页。这个方案用户接受度很高,因为卡片本来就是用来"扫一眼"的,不是用来"读全部"的。
注意:瘦身时别动那些影响功能的关键字段,比如按钮的跳转地址、回传数据的动作标识,删了这些卡片就成摆设了。瘦身优先动展示性内容。
5.4 上线前的自检清单
卡片模板做完,上线前我一般会过一遍这个清单:结构是否合法(用校验工具跑一遍)、所有文本是否转义(特别是动态字段)、图片和链接是否可访问、按钮交互是否按预期工作、体积是否在限制内、各端渲染是否基本一致、异常数据(空值、超长文本、特殊字符)是否处理。
异常数据处理特别值得单独说。真实数据里什么都有:空字符串、超长文本、带表情符号的文本、带特殊字符的文本。这些如果不处理,轻则显示异常,重则整卡解析失败。我的做法是在数据进入卡片构造之前做一层清洗,空值给默认文案,超长文本截断,特殊字符转义,确保进卡片的都是"干净"数据。
6. 我个人的几条实操心得
写卡片这件事,技术门槛其实不高,难的是细节和稳定性。分享几条我踩坑踩出来的体会。
第一条,先确认协议再动手写。不同体系对卡片字段的定义差异很大,网上找的示例只能参考思路,不能直接抄字段。我一般会先把目标体系的字段文档过一遍,把常用的模块和字段列成一张自己的速查表,写模板时对着表来,比反复翻文档快。
第二条,模板和数据分离要彻底。结构写死在模板里,数据从外部传入,这样改样式和改数据互不影响。我见过把数据和结构混在一起拼字符串的代码,改一个字段要在十几个地方找,维护成本极高。
第三条,最小化测试是排查利器。卡片出问题别急着看报错,先发一张最简单的卡片验证链路,再逐步加复杂度。这个方法看着笨,但定位问题的速度比瞎猜快得多。
第四条,保守设计降低兼容成本。结构简单、布局保守、颜色用主题色、按钮文字短,这些选择会让卡片在各端表现稳定,省下大量适配时间。视觉上的花哨在卡片场景里收益很低,稳定和清晰才是第一位的。
第五条,异常数据一定要清洗。真实数据永远比测试数据脏,空值、超长、特殊字符都得提前处理,别指望渲染端帮你兜底。数据清洗这层做扎实了,卡片上线后的故障率会低很多。
这套方法我在几个项目里反复用过,XML 和 JSON 两条路线都跑得通,核心逻辑是一样的:把卡片当成结构化数据来描述,把结构和数据分离,把异常情况提前处理掉。剩下的就是对着目标体系的字段定义,把每个模块填对。