MD 阅读 · 功能需求说明书
文档目的:项目经理/开发团队依据本文档,可以完整复刻出一款功能与交互一模一样的 MD 文件阅读器。 范围说明:本文档只描述功能与产品细节(行为、交互、参数、边界情况、验收标准),不限定实现框架与技术选型。 文档版本:v1.0 | 日期:2026-09-04 | 对应实现:已全部实现并通过真机验证
1. 产品概述
1.1 产品定位
一款 Markdown 文件阅读器。核心场景:用户在微信等聊天工具中收到 .md 文件,通过系统"用其他应用打开"选择本应用,即可获得排版精美的预览阅读体验;也可在应用内主动选择本地文件打开。
1.2 核心价值
- 打开即读:从微信选"用其他应用打开 → MD阅读",直接进入预览态,零操作成本。
- 渲染专业:标题、表格、代码块、流程图(Mermaid)完整渲染,尤其表格支持自适应列宽 + 横向滚动,不出现"异型表"。
- 完全离线:渲染过程不发起任何网络请求(含 Mermaid 图),无隐私风险,利于应用商店过审。
- 零权限打扰:不申请任何敏感权限,运行全程无权限弹窗。
1.3 应用基本信息
| 项 | 值 |
|---|---|
| 应用名称 | MD阅读 |
| 包名 | com.bzm.mdreader |
| 目标平台 | HarmonyOS NEXT 手机(API 12+) |
| 安装包体积 | ≈ 2.8MB(含离线 Mermaid 渲染引擎 2.45MB) |
1.4 术语约定(全文统一)
- 预览:Markdown 渲染后的排版视图(默认视图)。
- 源码:Markdown 原始文本视图。
- 全屏:隐藏系统状态栏(时间/电量)的沉浸模式,分两种——应用级全屏(整个页面)与 Mermaid 图全屏查看(单张图)。
- 主菜单:右上角
⋮按钮弹出的菜单。 - 块(Block):Markdown 中独立成段的元素(标题/段落/表格/代码块等)。
2. 功能总览
| 编号 | 功能模块 | 说明 | 优先级 |
|---|---|---|---|
| F1 | 文件打开(外部关联) | 微信"用其他应用打开"接收 .md 文件 | P0 |
| F2 | 文件打开(应用内选择) | 应用内主动选择本地 md 文件 | P0 |
| F3 | 预览渲染 | 全类型 Markdown 块渲染 | P0 |
| F4 | 表格渲染 | 列宽自适应、横向滚动、同行等高 | P0 |
| F5 | Mermaid 图渲染 | 离线渲染流程图/时序图/甘特图等 | P0 |
| F6 | Mermaid 全屏查看 | 手势缩放/拖动/双击,缩放上限按文字归一化 | P0 |
| F7 | 源码视图 | 预览/源码一键切换 | P0 |
| F8 | 应用全屏 | 隐藏状态栏,返回键先退出全屏 | P1 |
| F9 | 横竖屏 | 跟随系统重力感应自动切换 | P1 |
| F10 | 中文文件名支持 | 文件名正确解码显示 | P0 |
| F11 | 编码自动识别 | UTF-8 为主,GB18030 自动回退 | P0 |
| F12 | 大文件防崩溃 | 数百 KB、含 20+ 表格的文档不卡死不闪退 | P0 |
| F13 | 文件切换正确性 | 已开文件时打开新文件,标题与内容同步切换 | P0 |
3. 文件打开功能(F1 / F2)
3.1 外部打开(微信场景,核心链路)
用户旅程:微信收到 .md 文件 → 长按/点开 → "用其他应用打开" → 列表出现 MD阅读 → 选中 → 应用启动并直接显示该文件预览。
产品要求:
- 应用必须在系统"打开方式"列表中声明可打开 Markdown 类型的文件(
.md/text/markdown/general.markdown)。 - 只能声明"打开文件"用途(打开后展示内容),不得声明编辑/保存用途,避免审核争议。
- 打开后不得出现任何中间页:直接进入预览态,顶栏标题显示文件名。
- 若微信把
.md误标为纯文本(text/plain)传递,也应尽量兼容打开。
3.2 应用内选择文件
- 入口:右上角主菜单 → 打开文件。
- 弹出系统文件选择器,筛选 Markdown/文本类文件。
- 选中后立即渲染;打开失败(如文件被移动)时给出明确提示,不得白屏、不得崩溃。
3.3 文件名显示规则
- 顶栏标题显示文件名(含
.md后缀)。 - 中文及特殊字符文件名必须正确解码显示(如
%E7%A8%8E%E5%8A%A1.md要显示为税务.md),不得显示百分号转义串。 - 文件名过长时单行省略号截断,不得换行或挤压按钮。
3.4 文件编码识别
按以下顺序读取文件:
- 优先按 UTF-8 解码;
- 解码失败时自动回退 GB18030(兼容旧 Windows/记事本文档);
- 全程不需要用户手动选择编码。
4. 内容解析规则(F3 前置,必须逐条实现)
解析器必须把 Markdown 文本切分为块,并满足以下容错规则(真实用户文档大量不规范写法,每条都来自实测踩坑):
4.1 全局预处理(进入解析前必须完成)
| # | 规则 | 原因(不加会出的问题) |
|---|---|---|
| 1 | 剔除文件开头的 BOM 字符(U+FEFF) | Windows 记事本保存的文件首行标题会失效 |
| 2 | 统一换行符:CRLF / CR 全部转 LF | 微信转发的 Windows 文档行尾残留 \r,会同时破坏"行尾闭合符标题""独立图片行""表格分隔行"的识别 |
| 3 | 全角井号 #(U+FF03)转半角 # | 中文输入法打出的标题无法识别 |
| 4 | 全角空格(U+3000)转半角空格 | 全角空格缩进的标题/列表无法识别 |
4.2 块级元素识别规则(顶层分发)
| 块类型 | 识别规则 | 容错要求 |
|---|---|---|
| 标题 | 行首 #×1~6 | 允许行首 ≤3 个半角空格缩进;允许井号后无空格(##文字);允许行尾闭合符(## 标题 ##,闭合符需剥离);# 超过 6 个按 6 级处理 |
| 分割线 | 行内容为 --- / *** / ___(≥3 个) | 允许 ≤3 空格缩进;允许行尾空白;不得被并入上一段 |
| 引用 | 行首 > | 允许缩进与 > 后无空格;连续引用行合并为一块 |
| 无序列表 | 行首 - / * / + + 空格 | 允许缩进;连续项合并为一块 |
| 有序列表 | 行首 数字. + 空格 | 允许缩进;连续项合并为一块 |
| 独立图片行 | 整行只有  | 允许缩进与行尾空白;不得被并入段落 |
| 代码块 | 成对的 ``` 围栏 | 围栏内内容原样保留,不做任何 Markdown 解析 |
| Mermaid 块 | ```mermaid 围栏 | 单独识别为图表块(见 §6),不得当作普通代码块 |
| 表格 | 以 `\ | 开头的连续行(含分隔行 \ |
| 段落 | 其余连续非空行 | 相邻非空行合并为一段 |
兜底硬性要求:逐行扫描循环必须保证"任何一行至少被消费一次"(即使某行匹配不到任何块规则也强制前进),绝不允许死循环。
4.3 行内(Inline)元素
段落/标题/列表项/引用内需支持:**粗体**、*斜体*、` 行内代码 、链接、~~删除线~~`。行内代码内的 Markdown 符号不解析。
5. 预览渲染规范(F3 / F4)
5.1 基准排版
正文基础字号 16px,基准色 #333。
5.2 标题渲染参数(逐级定义,必须精确一致)
| 级别 | 字号 | 颜色 | 上间距 | 特殊处理 |
|---|---|---|---|---|
| H1 | 26px | #000000 | 20px | 标题下方加分割线 |
| H2 | 22px | #000000 | 16px | 标题下方加分割线 |
| H3 | 19px | #2b2b2b | 12px | — |
| H4 | 17.5px | #2b2b2b | 12px | — |
| H5 | 16.5px | #2b2b2b | 12px | — |
| H6 | 15.5px | #2b2b2b | 12px | — |
设计要点:H5/H6 必须与正文 16px 有可辨识差异(曾因 H5=16/H6=15 与正文无差别被误认为"标题没渲染")。
5.3 表格渲染(重点,实测问题最多)
- 列宽自适应:列宽按该列最长单元格的"显示宽度"计算;中日韩字符与全角字符按 2 个字符位计,其余按 1。
- 列宽公式参数(单一数据源,改一处全局生效):
- 单字符位宽 7.2px;单元格内边距 16px;防贴边余量系数 1.08;
- 列最小宽 76px;列最大宽 800px(超宽列靠横向滚动查看,不换行压缩)。
- 单元格不换行截断策略:超过 100 字符的内容在计算列宽与行高时同样按 100 截断,保证"高度估算"与"实际渲染"完全一致,表格高度不跳动。
- 同行等高:同一行所有单元格必须顶端对齐且背景等高——即使某单元格换行多行,同行其他单元格的浅灰底色也必须拉到同高,不得出现"异型表"。
- 横向滚动:整表置于横向滚动容器内;行高在此前按内容预估好,避免滚动时高度二次抖动。
- 表头加粗、隔行浅灰底、单元格间 1px 分隔线。
5.4 其他块渲染
- 代码块:深色底(#f6f8fa 类)、等宽字体、圆角卡片、保留空格缩进、可横向滚动。
- 引用:左侧 4px 竖线(灰蓝色)+ 浅灰底 + 左内边距。
- 分割线:细灰线,上下留白。
- 图片:宽度自适应内容区,保留 alt 占位;加载失败显示占位不崩。
- 列表:无序用
•,有序用1. 2. 3.,层级缩进清晰。 - 段落:行距舒适(建议 1.5 倍),段间距 12px 左右。
5.5 大文件渲染硬性要求(F12)
- 必须懒加载渲染:只构建屏幕可视区域的块,滚动时增量创建/回收。实测数据:21KB、含 22 个表格(531 单元格)的文档,若一次性构建全部组件,内存冲到 857MB 并触发 6 秒主线程卡死闪退——懒加载是红线,不是优化项。
- 同一文档只解析一次:同一份原文重复进入预览时命中缓存直接复用,严禁"渲染 → 重解析 → 再渲染"的正反馈循环(会导致反复重建直到崩溃)。
- 切换新文件时必须整体重建渲染(见 §10),不受缓存影响。
- 极端场景已知限制:单个表格内部若有数百行,暂为一次性构建(可接受);单表数千行需另行拆分。
6. Mermaid 图渲染(F5)
6.1 渲染要求
- 支持 Mermaid 语法图:流程图、时序图、类图、状态图、甘特图、饼图等。
- 必须完全离线:渲染引擎(mermaid.min.js)随安装包内置,任何情况下不得联网加载脚本或字体。
- 图渲染后宽度收敛至页面宽度(超宽图等比缩放,不出横向滚动条)。
- 渲染完成后自动探测图形实际高度并撑开容器,不得截断图形底部(探测可在渲染后约 700ms 二次确认)。
- 渲染失败(语法错误等)降级为代码块展示原文,不得白屏。
6.2 进入全屏的入口
- 图区域叠加一个透明点击层,点击整图任意位置进入全屏查看。
- 图上叠一个小角标提示(如"全屏查看"),让用户知道可点。
7. Mermaid 全屏查看器(F6,手势细节必须逐条实现)
7.1 布局
- 顶部蓝色工具条(#2a6fdb):左侧标题"渲染图",右侧"关闭"按钮。
- 图区背景深灰,图居中显示。
- 进入全屏时禁止 WebView 自身缩放(
zoomAccess=false一类的设置),缩放完全交给页面手势逻辑,避免双重缩放打架。
7.2 手势规格
| 手势 | 行为 |
|---|---|
| 双指捏合 | 以双指中心缩放 |
| 双指/单指拖动 | 平移视图 |
| 双击 | 在"适配屏幕"与"适合阅读"两档间切换 |
| 松手后 | 缩放值保持不变——严禁松手后回弹/复位(详见 7.3 防误判) |
7.3 关键规则:松手不得触发缩放复位(实测踩坑重点)
问题现象:双指缩放松手瞬间,视图被误判为"双击",自动跳到默认档——表现为"放大后又缩回""缩小后又弹回"。
根因:双指松开时两个手指先后抬起,会触发两次"手指抬起"事件;第二次抬起距第一次 <300ms 且位移很小,恰好符合双击判定条件。
必须实现的防误判规则(逐条):
- 手势状态机维护
mode:none / pinch(双指)/ pan(拖动)。 - 只要抬手指后仍有手指在屏幕上(即双指松开的第一下),直接忽略该事件,不进入任何点击判定。
- 只有"屏幕完全空手"时的抬起,才参与双击判定;双击判定窗口 300ms 内、移动距离阈值 12px。
- 双击切换目标:若当前缩放 < 阅读档则放大到阅读档,否则回到适配屏幕档。
7.4 缩放上限:按文字大小归一化(产品核心规格)
- 每张图渲染后,探测图中主体文字的实际渲染字号(取全部文字节点字号的 60% 分位数——不用中位数,避免边缘小字如连线标签拉低基准;兼容以 DOM 方式渲染的文字标签)。
- 缩放上限 = 30px ÷ 该图主体文字字号。
- 产品效果(验收标准):任何一张图,无论初始字号多大,放大到极限时主体文字都恰好约 30px,清晰可读,且所有图"放大到最大"的手感一致。
- 缩放下限:0.5 倍(可缩到比适配更小)。
- 文字为矢量渲染,任何倍率下不糊。
8. 顶部导航与菜单(F7 / F2 入口)
8.1 顶栏布局(预览/源码态)
┌──────────────────────────────────────────┐
│ 文件名.md(左对齐,加粗 18px) [⋮] │ ← 56px 高
├──────────────────────────────────────────┤
- 左侧:标题。未打开文件时显示应用名"MD阅读"。
- 右侧:主菜单按钮
⋮。 - 左上角不放置任何菜单按钮(历史上曾有"竖屏/横屏"入口,已废弃,见 §9)。
- 按钮热区 48×48px 以上(竖屏、横屏下都易点中),图标字号 30。
8.2 主菜单(右上 ⋮)菜单项与行为
| 菜单项 | 行为 |
|---|---|
| 预览 | 切到渲染视图(当前已在预览态时显示勾选态或不可点,按实现自定,两态互斥) |
| 源码 | 切到纯文本视图,等宽字体,保留原文全部字符 |
| 全屏 | 进入应用级全屏(§9.2),菜单态保持当前预览/源码不变 |
| 打开文件 | 弹出系统文件选择器(§3.2) |
8.3 视图切换要求
预览 ↔ 源码切换必须瞬时完成(同文档已解析过,直接复用),不得出现白屏或重复解析。
9. 全屏与屏幕方向(F8 / F9)
9.1 屏幕方向
- 完全跟随系统重力感应自动旋转,应用不锁定方向。
- 应用内不提供任何手动"竖屏/横屏"切换按钮(曾在左上角 ⋮ 菜单内提供,已按需求移除)。
- 横屏下顶栏、菜单、按钮布局必须正常可用(按钮 48×48 热区即为此设计)。
9.2 应用级全屏
- 进入:主菜单 → 全屏。
- 效果:隐藏系统状态栏(时间/电量/信号),内容区域扩展至屏幕顶部。
- 退出:按系统返回键退出全屏而不是退出应用(返回键必须先消耗在退出全屏上);也可从菜单再次操作退出。
- 全屏态下主菜单按钮仍然可用(悬浮于顶栏)。
9.3 Mermaid 全屏的返回键
Mermaid 全屏查看态下按返回键:退出图全屏,而非退出应用或退出应用全屏。
10. 文件切换与多文件场景(F13,实测踩坑重点)
场景:应用已打开文件 A → 用户切到微信 → 再用"其他应用打开"文件 B。
必须实现的产品行为:
- 不新开应用实例,复用已运行的应用。
- 顶栏标题立即变为 B 的文件名。
- 内容区立即且完整切换为 B 的内容——严禁出现"标题是 B、内容还是 A"的错位(这是已修复的高危 bug:视图复用机制在两文档块数相近时会错误复用旧内容,实现时必须保证换文件 = 视图整体重建,例如以"文档版本号"参与每个渲染单元的唯一标识)。
- 切换后滚动位置回到顶部。
- 若 A/B 内容恰好完全相同,也必须正常显示(不因内容相同跳过刷新而白屏)。
11. 权限与隐私合规
- 不申请任何敏感权限:无网络、无存储批量扫描、无位置、无通讯录;用户全程看不到权限弹窗。
- 文件访问仅限"用户主动选择打开的文件"与系统分享传入的文件,属免权限场景。
- 渲染全程离线,无任何埋点/上报/网络请求。
- 上述是应用商店审核友好性的核心卖点,任何功能增强不得引入联网需求。
12. 边界情况与容错清单(验收必测)
| # | 场景 | 预期 |
|---|---|---|
| E1 | 带中文文件名的 md 文件 | 标题正确显示中文名 |
| E2 | GB18030 编码的旧文档 | 正常显示,无乱码 |
| E3 | 带 BOM 的文件,首行是 # 标题 | 首行标题正常渲染 |
| E4 | CRLF 换行的 Windows 文档 | 全部块级语法正常识别 |
| E5 | 全角 #标题、全角空格缩进标题 | 正常渲染为标题 |
| E6 | ##文字(井号后无空格) | 正常渲染 |
| E7 | ## 标题 ##(闭合井号) | 渲染为 H2,闭合符不显示 |
| E8 | 行首有 1~3 空格的标题/列表/引用/分割线 | 正常识别 |
| E9 | 21KB / 22 表格 / 531 单元格大文档 | 秒开、滚动流畅、内存正常(<200MB)、不闪退 |
| E10 | 语法错误的 mermaid 块 | 降级显示源码,不崩溃 |
| E11 | 双指缩放松手 | 缩放值保持,不回弹 |
| E12 | 已开 A 文件再打开 B 文件 | 标题与内容同步切换(§10) |
| E13 | 全屏态按返回键 | 先退全屏,不退出应用 |
| E14 | 超宽表格(某列 800px 上限) | 横向滚动可看全,不换行压扁 |
| E15 | 打开被删除/无法读取的文件 | 明确提示,不崩溃 |
| E16 | 横屏使用所有功能 | 布局正常、按钮可点 |
13. 验收标准汇总
- F1~F13 全部功能按本文档参数实现,§12 的 16 条边界用例全部通过。
- 性能红线:含 20+ 表格的真实大文档(如 21KB/22 表)打开 ≤3 秒、滚动无卡顿、无内存异常增长。
- 交互红线:① 缩放松手不回弹;② 换文件内容同步刷新;③ 返回键先退全屏。
- 合规红线:全程离线、零敏感权限、零权限弹窗。
14. 附:与历史需求的差异记录(避免复刻时走弯路)
| 曾经的设计 | 现行设计 | 变更原因 |
|---|---|---|
| 左上角 ⟳ 图标切横竖屏 | 删除,改系统重力感应 | 简化交互 |
| 左上 ⋮ 菜单放竖屏/横屏 | 左上角不放任何按钮 | 同上 |
| 右上 ⋮ 含横竖屏项 | 右上 ⋮ 仅:预览/源码/全屏/打开文件 | 职责单一 |
| 缩放上限固定倍率 | 按文字 30px 归一化 | 不同图手感统一 |
| 「富文本/查看原文」 | 统一叫「预览/源码」 | 术语规范 |
| Mermaid 全屏用 WebView 原生缩放 | 页面手势接管(禁用原生缩放) | 避免双重缩放打架 |