1. 从一条消息气泡说起卡片代码生成到底在解决什么问题聊这个话题之前先还原一个很常见的场景。你在某个聊天工具里看到别人发了一张排版精致的卡片——有标题、有缩略图、有描述文字、底部还有跳转按钮点一下就能打开对应页面。很多人第一反应是这是不是要调什么官方接口才能做出来其实相当一部分卡片并不是走后台接口下发的而是发送端自己拼出来的一段结构化文本客户端解析后渲染成卡片样式。这段结构化文本常见的就是 XML 和 JSON 两种形态。所谓QQxml和json代码生成卡片的方法说白了就是搞清楚聊天场景里卡片消息的两种描述格式各自长什么样、字段怎么组织、哪些字段决定布局、哪些字段决定交互然后掌握从零手写或程序化生成这两种代码的完整路径。它解决的核心问题是——在没有可视化编辑器、没有后台管理面板的情况下仅靠一段文本代码就能让消息以卡片形态呈现并且控制它的标题、图片、摘要、按钮和跳转行为。这个内容适合谁看三类人最对口。第一类是做客户端消息模块的开发者需要理解卡片消息的解析与渲染逻辑第二类是做运营工具、消息群发工具的技术人员需要批量生成卡片代码第三类是对消息协议好奇、想自己动手拼一张卡片出来的爱好者。哪怕你之前完全没接触过这类结构化消息格式只要会一点文本编辑、能看懂键值对这篇内容都能让你从看不懂那串代码走到自己能拼出一张能用的卡片。需要先说明一点不同平台、不同版本的客户端对卡片字段的支持程度存在差异字段名和取值也可能随版本调整。下面讲的是这类卡片消息的通用组织思路和常见字段结构具体落地时以你实际对接的客户端文档为准。这是从业者必须养成的习惯——协议是活的代码要跟着文档走。2. 两种格式的底层逻辑为什么会有 XML 和 JSON 并存2.1 结构化消息的本质用文本描述一棵界面树不管是 XML 还是 JSON卡片消息的本质都是用文本描述一棵界面树。你可以把它想象成装修图纸图纸本身不是房子但它规定了哪里是墙、哪里是门、门朝哪开。卡片代码也一样它不直接画像素而是告诉客户端这里放一个标题、下面放一张图、再下面放一行描述、最底下放一个按钮按钮点了跳这个地址。客户端拿到这份图纸按自己的渲染规则把界面搭出来。理解了这一层很多困惑就迎刃而解了。比如为什么同一段代码在不同客户端上长得不完全一样因为图纸只规定了结构具体用什么字体、圆角多大、间距多少是各客户端渲染层自己决定的。再比如为什么有些字段写了没反应因为那个客户端压根没实现对这个字段的渲染。这些都是描述与渲染分离带来的正常现象不是代码写错了。XML 和 JSON 的区别主要在于表达同一棵树的方式不同。XML 用标签嵌套表达层级属性写在标签里JSON 用对象和数组表达层级键值对直接平铺。举个直观的对照msg title示例标题/title summary这是一段摘要/summary /msg{ title: 示例标题, summary: 这是一段摘要 }两者描述的是同一件事。XML 的可读性在层级深的时候更直观标签闭合一目了然JSON 在程序里处理更方便序列化和反序列化几乎是语言内置能力。这就是为什么两种格式会长期并存——历史原因加上工程习惯各有各的适用场景。2.2 选 XML 还是 JSON三个判断维度实际动手时第一个问题就是选哪种。我的经验是按三个维度判断。第一个维度是对接端的解析能力。如果目标客户端对两种格式都支持优先选你手头工具链更顺的那种。做批量生成、需要频繁拼字符串的JSON 用模板引擎处理更省心做人工调试、需要肉眼检查层级的XML 的标签闭合更容易看出结构对不对。第二个维度是字段复杂度。卡片层级浅、字段少两种都无所谓卡片层级深、嵌套多比如列表里套卡片、卡片里套按钮组XML 的嵌套表达更不容易看花眼JSON 则要注意括号配对和逗号遗漏这是新手最容易翻车的地方。第三个维度是生成方式。手工拼写选 XML 更稳因为标签必须闭合写漏了立刻能看出来程序生成选 JSON 更顺因为大多数语言序列化对象就是一行调用。下面这张表可以当作速查判断维度倾向 XML倾向 JSON解析端支持仅支持 XML 或 XML 更成熟仅支持 JSON 或 JSON 更成熟层级深度层级深、嵌套多层级浅、字段平铺生成方式手工拼写、人工调试程序序列化、批量生成可读性诉求需要肉眼核对结构需要程序快速处理注意不要因为JSON 更现代就无脑选 JSON。我见过不少人在层级很深的卡片上硬用 JSON结果括号嵌套到第五层就开始数不清调试时间翻倍。格式没有优劣只有合不合适。2.3 卡片消息的通用结构骨架不管哪种格式一张卡片消息通常由几块组成外层容器标识这是一条什么类型的消息、头部区域标题、图标、来源、主体区域图片、摘要、正文、交互区域按钮、跳转链接。用 XML 表达大致是这样一棵树msg typecard header title卡片标题/title iconhttps://example.com/icon.png/icon /header body imagehttps://example.com/cover.png/image summary这里是卡片摘要文字/summary /body actions button urlhttps://example.com/detail查看详情/button /actions /msg对应的 JSON 结构{ type: card, header: { title: 卡片标题, icon: https://example.com/icon.png }, body: { image: https://example.com/cover.png, summary: 这里是卡片摘要文字 }, actions: [ { text: 查看详情, url: https://example.com/detail } ] }这个骨架是理解一切卡片代码的起点。后面无论字段怎么变、平台怎么加特性你都可以把它往这四块里归位这是容器、这是头部、这是主体、这是交互。归位之后再查具体字段名就不会迷路。3. 核心字段逐个拆决定卡片长相的关键参数3.1 容器与类型标识第一行就决定了解析走向卡片代码的第一行往往最关键因为它决定了客户端把这段文本当什么处理。XML 里通常是根标签的类型属性JSON 里通常是顶层的 type 字段。这个值写错后面字段写得再漂亮也没用——客户端要么不认要么按普通文本显示。这里有个实操细节值得强调类型标识的取值是平台约定的枚举不是随便起的名字。你在文档里看到card、rich、template这类词要严格照抄大小写也别改。我踩过的坑就是手写时把类型值首字母大写了结果客户端静默降级成纯文本排查了半天才发现是大小写问题。这类静默失败最坑人因为不报错只是效果不对。另外有些平台会在容器层放一个版本号字段。这个字段的作用是告诉客户端按哪一版规则解析。版本号不写或写错客户端可能按默认版本处理导致新字段不生效。建议养成习惯容器层固定写上版本号哪怕当前用不到新特性也为后续扩展留好口子。3.2 标题与摘要字数、截断与转义三件事标题和摘要是卡片上最显眼的文字也是最容易出问题的地方。三个点必须盯住。第一是字数与截断。客户端对标题和摘要通常有长度上限超了会被截断并加省略号。不同平台上限不同常见的是标题十几个到几十个字符、摘要几十到上百个字符。生成代码时最好在源头就做长度控制而不是指望客户端截断——因为截断位置不可控可能把关键信息切掉。我的做法是写一个长度校验函数超长时按语义截断比如按句号切而不是硬切字符。第二是转义。XML 里、、这些字符有特殊含义出现在标题里必须转义成实体amp;、lt;、gt;否则解析直接失败。JSON 里双引号和反斜杠要转义。这是手工拼代码时最高频的错误来源。举个真实例子标题里带一个符号XML 没转义整段代码解析报错卡片发不出去。程序生成时用标准序列化库能自动处理转义手工拼就必须自己盯。第三是富文本标记。部分平台允许标题或摘要里带简单的样式标记比如加粗、换行。这类标记的语法各平台不同用之前务必确认目标客户端支持否则标记会原样显示出来很难看。3.3 图片字段地址、尺寸与加载策略图片是卡片视觉的重头。字段层面通常就是一个图片地址但背后有几个容易忽略的点。地址必须是可公网访问的完整 URL带协议头。相对路径、本地路径一律不行客户端拿不到。图片格式建议用常见格式尺寸按平台建议比例准备避免拉伸变形。有些平台支持在字段里指定宽高或裁剪模式这类字段能写就写能让渲染更可控。加载策略方面客户端一般会做懒加载和缓存。这意味着图片地址如果频繁变化比如带时间戳参数缓存命中率会下降加载变慢。我的经验是图片内容不变就别改地址需要更新图片就换一个新地址让缓存自然失效而不是在地址后面加随机参数。还有一个坑图片地址里的特殊字符同样要转义。URL 里带拼参数的情况很常见XML 里这个必须转义否则解析出错。这个细节和标题转义是同一类问题但更容易被忽略因为大家注意力都在文字上。3.4 交互按钮跳转、回调与数量限制按钮是卡片从展示走向可用的关键。字段层面通常包含按钮文字和一个行为定义行为可能是跳转链接也可能是触发一个回调动作。跳转链接类按钮核心是 URL 要合法、要能打开。回调类按钮核心是动作标识要和接收端约定一致标识写错接收端就不知道你要干什么。按钮数量一般有上限常见是几个以内超出的部分可能不渲染或被折叠生成时要控制数量。按钮文字也有长度限制太长会被截断。建议按钮文字控制在几个字以内动词开头比如查看详情立即领取这种既短又明确。多个按钮并排时注意主次分明把最重要的动作放前面。提示按钮的跳转地址如果是动态生成的务必在生成后做一次可达性校验。我遇到过地址拼错一个字符、卡片发出去点开是错误页的情况用户反馈回来才发现。批量生成场景下这种低级错误会被放大校验环节不能省。3.5 字段对照速查表把上面几类字段整理成一张对照表方便动手时查阅。注意具体字段名以目标平台文档为准这里给的是通用命名思路区域字段作用XML 表达思路JSON 表达思路常见坑容器类型标识根标签 type 属性顶层 type 键取值大小写错误导致降级容器版本号根标签 version 属性顶层 version 键不写导致新字段不生效头部标题title 标签title 键超长截断、特殊字符未转义头部图标icon 标签icon 键地址非公网可访问主体封面图image 标签image 键地址带 未转义主体摘要summary 标签summary 键超长、富文本标记不兼容交互按钮文字button 标签内容text 键过长被截断交互按钮行为button 的 url 属性url 键地址拼错、动作标识不一致4. 手写与程序化生成两条路径的完整实操4.1 手工拼写 XML 卡片的步骤与校验手工拼写适合调试和小批量场景。完整流程我一般这么走。第一步先写容器骨架把类型和版本定下来确认这段代码能被客户端识别为卡片消息。这一步不要急着填内容先验证空卡片能不能发出去、能不能显示成一个空壳。能显示说明容器层没问题再往下填。第二步填头部。标题先写短一点图标先用一个确定可访问的地址。填完发一次看头部区域是否正常。这一步重点验证转义——故意在标题里放一个看转义后是否正常显示。养成这个习惯后面就不会被转义问题绊住。第三步填主体。图片地址、摘要依次加上。图片这一步重点看加载是否正常、比例是否变形。摘要这一步重点看长度和换行。第四步加交互。先加一个按钮验证跳转是否可达。再加第二个验证并排布局是否正常。第五步整体回归。把前面每一步的验证点再走一遍确认加新字段没有破坏旧字段。这个分层验证的方法看起来慢实际上比一次性写完再排查快得多。因为一旦整体写完出问题你面对的是一个黑盒不知道是哪一层坏了分层验证时每一层都是刚验证过的出问题必然在刚加的那一层定位范围极小。4.2 用模板引擎批量生成 JSON 卡片批量场景下手工拼不现实得靠程序。核心思路是模板 数据把卡片结构写成模板把每条卡片的内容作为数据灌进去。以 Python 为例用标准库就能完成import json def build_card(title, summary, image_url, link_url): card { type: card, version: 1, header: { title: title }, body: { image: image_url, summary: summary }, actions: [ {text: 查看详情, url: link_url} ] } return json.dumps(card, ensure_asciiFalse) print(build_card(示例标题, 示例摘要, https://example.com/a.png, https://example.com/d))这段代码有几个关键点。ensure_asciiFalse保证中文不被转成 Unicode 转义序列输出可读性更好用字典构造再序列化转义由库自动处理不用手工操心函数化封装后批量生成就是循环调用。如果卡片结构更复杂、需要条件分支比如有的卡片有按钮、有的没有可以引入模板引擎把结构写成模板文件数据单独维护。这样做的好处是结构和数据分离改结构不用动数据改数据不用碰结构。模板里注意对变量做转义处理模板引擎一般有对应的过滤器别用原始输出。4.3 XML 的程序化生成与转义处理XML 程序化生成同样有标准库可用。以 Python 为例from xml.sax.saxutils import escape def build_card_xml(title, summary, image_url, link_url): t escape(title) s escape(summary) i escape(image_url) u escape(link_url) return ( fmsg typecard version1 fheadertitle{t}/title/header fbodyimage{i}/imagesummary{s}/summary/body factionsbutton url{u}查看详情/button/actions f/msg )这里的关键是escape函数它会把、、转成实体避免解析失败。注意属性值里的引号也要处理如果 URL 里可能带双引号需要额外转义。手工拼 XML 字符串时转义是最容易漏的一环用标准库的转义函数比自己写替换规则可靠得多。4.4 生成后的自动化校验清单不管手工还是程序生成发出去之前都建议过一遍校验。我整理了一份清单程序化场景可以写成校验函数自动跑容器类型标识是否在允许取值范围内大小写是否正确版本号是否填写标题、摘要长度是否在上限内所有文本字段的特殊字符是否已转义所有图片、跳转地址是否为完整可访问 URL按钮数量是否超限按钮文字是否过长整体结构能否被解析器成功解析XML 用解析器试解析JSON 用反序列化试解析最后一条尤其重要生成完先自己解析一遍解析不过就别发。这一步能拦住绝大多数低级错误成本极低收益极高。5. 常见问题排查与避坑经验实录5.1 卡片不显示或降级成纯文本这是最高频的问题。排查顺序我一般这么走先看容器类型标识对不对大小写、拼写、取值是否在允许范围内再看整体结构能否被解析XML 标签是否闭合、JSON 括号逗号是否配对最后看版本号是否缺失导致按旧规则解析。这三步能覆盖大部分降级问题。如果三步都过了还不显示就要怀疑客户端本身对该类型卡片的支持情况。有些客户端版本对某些卡片类型支持不完整这种情况下代码没问题是环境问题。判断方法是换一个已知能正常显示卡片的客户端版本对比测试。5.2 文字乱码、截断或标记原样显示乱码通常是编码问题确保生成和传输全程用统一编码一般是 UTF-8。截断是长度超限在源头做长度控制。标记原样显示是客户端不支持该标记语法去掉标记或换用支持的语法。这三类问题有个共同点都不是代码语法错误而是内容与渲染规则不匹配。所以排查时不要盯着代码结构看要盯着内容和目标客户端的渲染规则看。5.3 图片不显示或跳转失效图片不显示先查地址是否公网可访问、是否带协议头、是否被转义破坏。跳转失效先查地址是否拼写正确、是否可达、按钮行为标识是否与接收端约定一致。这两类问题的排查核心都是地址本身是否有效可以用浏览器或命令行工具单独验证地址把地址问题和卡片代码问题分开。5.4 问题速查表现象可能原因排查动作降级为纯文本类型标识错误、结构解析失败、版本缺失查类型取值、试解析、补版本号文字乱码编码不统一统一为 UTF-8文字被截断超出长度上限源头做长度控制标记原样显示客户端不支持该标记去掉或换语法图片不显示地址不可访问、未转义单独验证地址、检查转义跳转失效地址拼错、行为标识不一致验证地址、核对约定按钮不显示数量超限、文字过长减数量、缩文字5.5 几条踩坑换来的经验第一条先验证容器再填内容。很多人一上来就把整张卡片写完出问题后不知道从哪查。分层验证能把排查范围缩到最小。第二条转义交给标准库。手工写替换规则迟早漏字符标准库的转义函数经过大量验证比自己写的可靠。第三条地址单独验证。图片和跳转地址的问题用独立工具验证比在卡片里猜快得多。第四条保留一份能正常显示的样例代码。每次改结构时拿样例对比能快速发现哪里改坏了。这份样例就是你的基准线。第五条关注客户端版本差异。同一段代码在不同版本上表现可能不同测试时记录版本出问题时先确认版本环境。6. 从能用到好用卡片代码的工程化思路6.1 把卡片结构抽象成配置当卡片类型多起来之后直接在代码里拼字符串会越来越乱。更好的做法是把卡片结构抽象成配置——用一份配置描述这张卡片有哪些区域、每个区域放什么字段生成器读配置产出 XML 或 JSON。这样新增卡片类型只是加一份配置不用改生成逻辑。配置化的另一个好处是格式无关。同一份配置可以写两个渲染器一个产出 XML一个产出 JSON。哪天对接端换了格式要求换个渲染器就行配置不用动。这个思路在需要同时对接多个客户端的场景下特别省事。6.2 建立字段白名单与校验层字段名写错是另一个高频问题而且往往静默失败。解决办法是建立字段白名单把目标平台支持的字段名整理成一份清单生成时校验每个字段是否在白名单内不在就报错。这样字段名写错会在生成阶段暴露而不是等到客户端不显示才发现。校验层还可以叠加前面说的长度校验、地址校验、转义校验形成一道统一的出厂检验。所有卡片代码生成后都过这道检验能拦住绝大多数问题。6.3 版本兼容的处理策略客户端版本差异是绕不开的。策略上我建议向下兼容 特性探测基础字段用各版本都支持的保证老版本至少能显示一个可用的卡片新特性字段单独加并且接受老版本忽略它们。不要为了用新特性而牺牲老版本的可用性除非你确认目标用户都在新版本上。特性探测则是在有条件时先探测客户端支持情况再决定生成哪种结构的卡片。这需要客户端配合上报能力信息实现成本高一些但对体验提升明显。6.4 一个可复用的生成器骨架把前面的思路收拢一个可复用的生成器大致包含这几层配置层描述卡片结构、数据层每条卡片的内容、渲染层产出 XML 或 JSON、校验层白名单、长度、地址、转义、可解析性。四层分离各司其职。这个骨架不复杂但能让卡片生成从每次手忙脚乱变成按流程走。我在实际项目里用这套骨架之后卡片相关的线上问题明显减少因为大部分问题在生成阶段就被校验层拦住了。剩下的小部分问题集中在客户端版本差异上这类问题靠记录版本环境和对比测试来解决。最后分享一个小技巧给每张生成的卡片代码算一个内容指纹比如对关键字段做哈希存一份历史记录。出问题时能快速定位是哪次生成、哪版内容出的问题比翻日志高效得多。这个做法在批量生成场景下尤其值得成本很低排查时能省大量时间。