棋牌游戏开发中聊天文本的长度限制与安全显示

摘要:聊天框里的一条短消息,既要限制长度,也要按普通文字显示。本文区分字符串长度、字素数量与渲染方式,用 JavaScript 示例说明表情计数、输入边界和纯文本输出。

一句"大家好"显示很正常,换成带表情的句子,字数提示却突然多了几位;再粘贴一段很长的内容,聊天栏被撑开,最后几条消息也找不到了。房间聊天看起来只是一个输入框,真正需要约定的却不止"最多八十个字"。

从开发角度看,我会把三个问题分开:请求最多能带多少数据,玩家看到的文字按什么单位计数,以及收到的内容如何放到界面上。它们各有作用,不能用前端 maxlength 或删几个尖括号代替整条边界。

配图为 AI 辅助制作的完整游戏房间 UI 概念图,展示聊天面板、短消息和发送入口,不是本文程序运行截图。下面使用浏览器 DOM 的纯文本输出示例,Cocos 富文本组件或原生控件需要按各自接口另行接入。

JavaScript 字符串的 length 不是玩家眼里"几个字"的统一答案。一个表情可能占多个 UTF-16 代码单元,带组合符号的文字和家庭表情也可能由多个部分组成。直接 slice 到某个长度,可能把一个可见字符切开。即使用展开运算符按码点遍历,也不等同于按完整字素处理。

MDN 对 Intl.Segmenter 的说明中,文本可以按字素、词或句子分段。本文选择 grapheme 作为可见输入计数的近似单位,约定最多八十个字素;另外在分段前设置 2048 个代码单元的硬上限。后者是本例减少处理工作量的边界,不是网络报文大小限制,也不是通用安全阈值。

javascript 复制代码
const segmenter = new Intl.Segmenter('zh', {
  granularity: 'grapheme'
});

export function chatText(value, limit = 80) {
  if (!Number.isInteger(limit) || limit < 1 || limit > 200) {
    throw new RangeError('limit');
  }
  if (typeof value !== 'string' || value.length > 2048) return null;
  const text = value.trim();
  if (!text || /[\u0000-\u001f\u007f]/u.test(text)) return null;
  let count = 0;
  for (const _ of segmenter.segment(text)) {
    if (++count > limit) return null;
  }
  return text;
}

这里的 trim 是明确的产品选择:去掉首尾空白,但保留中间空格。内部换行和这段表达式覆盖的控制字符会被拒绝,不会被悄悄删掉后拼成另一句话。它并不覆盖全部 Unicode 控制或格式字符,也没有做内容审核。若业务需要处理双向文本、不可见字符或特定字符集合,应独立制定规则,不能把这条正则称作万能清洗。

函数返回 null 表示不符合格式,合法时返回规范化后的文字。它不截断超长消息,因为直接截断后发送容易改变原意。界面可以提示"消息过长",让玩家自己调整;展示历史消息时的省略则是另一种用途,不应复用成发送前的静默修改。

Intl.Segmenter 也需要检查目标运行环境。示例假设环境提供该 API,没有在缺失时自动退回另一种计数方式。前后端如果用不同 Unicode 数据版本或不同分段库,极端组合文字可能有差异,协议与测试样本需要一致。服务器还应限制请求体字节数,不能只看字素数量,因为一个字素可能包含很多组合成分。

通过格式检查以后,内容仍然是用户提供的普通文字。即使它包含看起来像标签的片段,也不应该因此交给 HTML 解析。对于不支持富文本的聊天栏,可以创建固定的结构,再通过 textContent 填入内容。

javascript 复制代码
function appendChat(list, message) {
  const text = chatText(message.text);
  if (text === null) return false;

  const row = document.createElement('li');
  row.className = 'chat-row';
  const name = document.createElement('strong');
  const body = document.createElement('span');
  name.textContent = message.displayName; // 另有昵称长度与格式约定
  body.textContent = text;
  row.append(name, document.createTextNode(':'), body);
  list.append(row);
  while (list.children.length > 100) {
    list.firstElementChild.remove();
  }
  return true;
}

MDN 对 textContent 和 innerHTML 的区分很直接:前者处理文本,后者处理 HTML。上面不会因为消息里出现<b>就主动生成一个加粗节点。昵称也不能绕开这条输出方式,因为攻击者未必只控制消息正文。示例假设 displayName 已经经过类型和长度校验;真实接入时,昵称应由可信身份信息关联,不接受客户端随便冒充另一个座位。

固定保留一百个 DOM 行,是示例的界面容量约定,不代表聊天历史只能保存一百条。这样能避免长时间开着房间不断追加节点。历史存储、分页和审计保留期限由业务另行决定;不要为了调试把完整聊天记录随意写入日志,也不要把删除界面节点误认为服务器数据已经删除。

css 复制代码
.chat-row {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  line-height: 1.5;
}

纯文本输出还需要排版配合。很长的连续字符即使不被当成代码,也可能把布局撑开;换行策略和容器宽度要一起看。本文拒绝内部换行,因此 pre-wrap 主要保留中间空格。若产品以后允许多行,应同时修改格式检查与高度限制,不要只把 CSS 改了。

如果要支持表情贴图或特殊颜色,最好传递独立的结构化类型,例如固定表情编号,再由客户端映射到内置资源。不要让用户直接传资源路径、任意标签和样式片段。纯文本和受控表情可以共存,但每个分支都要说明允许什么,而不是最后统一塞进富文本解析器碰运气。

输入法合成期间也不宜把回车立即视为发送。拼音候选词尚未确认时,按键行为与普通输入不同。实际界面应结合合成状态判断,并在真正提交前再次使用同一校验函数。这个输入体验问题与文本是否安全显示不同,不能因为输出使用了 textContent 就跳过。

服务端收到聊天请求后,应重新检查类型、请求大小、房间成员身份和发送频率,再按约定验证文字。客户端的八十字素提示只是帮助用户编辑,不是权限机制。返回给房间的发送者身份、时间和消息编号,也应由服务器决定。本文没有实现限流、广播和消息持久化,不把一个文本函数写成完整聊天系统。

本文在 Node 中检查了首尾空白、家庭表情、组合字符、八十与八十一字素边界、空消息、错误类型、控制字符和硬长度上限。测试还确认看似标签的内容会作为普通字符串保留下来,但这只证明输入函数没有改写它;没有运行真实 DOM 渲染与浏览器安全测试,不能声称整站已经通过安全审计。代码采用的 textContent 输出方式按官方接口说明选择,仍需在实际界面验证布局和交互。

聊天框的稳定性来自几个清楚的约定:数据大小有限,计数单位一致,用户文字不进入不必要的解析器,身份与房间权限交给服务器。把这些边界写明白,后面增加表情、历史消息或跨端显示时才不容易反复推倒重来。

相关推荐
普通人61 小时前
uniapp模版JS
开发语言·javascript·小程序·uni-app·1024程序员节
G018_star sky♬2 小时前
nodeJS他是如何用JavaScript封装C++的呢
开发语言·javascript·c++
wiseyao12192 小时前
uniapp scroll-view滚动到最后
前端·javascript·uni-app
怕浪猫2 小时前
Step 5 Preview 模型写 3D 象棋,两个小时上线
前端·javascript·面试
Highcharts.js2 小时前
Highcharts甘特图开发带导航功能的项目管理图示例
开发语言·前端·javascript·甘特图·highcharts·项目管理图
Hilaku3 小时前
前端不懂业务有多可怕?
前端·javascript·程序员
FRing033 小时前
JavaScript入门到精通二十八(forEach遍历、创建对象的三种方式、构造函数、new实例化执行过程、实例成员和静态成员)
开发语言·前端·javascript
进击的丸子4 小时前
代理网络环境下 Linux Pro 在线激活实操指南
javascript·后端·安全
小大宇4 小时前
vue知识点9-标签上的ref
前端·javascript·vue.js