crossoverJie
浇鸥蟹律项目背景最近我们团队自研了一个基于 React 的 H5 前端框架领导让我来负责编写框架的使用文档。我选择了 dumi 来搭建文档站点大部分内容都是手动写 Markdown 来介绍各种功能包括初始化、目录结构、生命周期、状态管理、插件系统 等等。框架里有个很重要的子包主要负责多个 App 的桥接能力深度集成了各端环境的监测和桥接逻辑。这个子包对外提供了一个 App 实例对象里面封装了很多原生能力比如 设置导航栏、录音、保存图片到相册 等这些 API 代码格式都比较统一领导希望避免在框架源码和文档里重复定义相同的接口最好能直接从源代码自动生成文档内容。需要提取的信息包括API支持的App版本、功能描述、开发状态、使用方式如果是函数的话还要有参数说明和返回值说明。我的解决方案经过一番思考我想到了一个方案核心思路在不改动源代码逻辑的前提下通过增加注释信息来补充文档需要的元数据具体实现路径定义一套规范的注释标签编写解析脚本提取信息生成 JSON 文件在文档项目中读取 JSON动态渲染成 API 文档定义注释规范我定义了一系列标准的注释标签appVersion —— 支持该API的App版本description —— API的功能描述apiType —— API类型默认是函数可选property属性和function函数usage —— 使用示例param —— 函数参数说明只有函数类型需要returns —— 函数返回值说明只有函数类型需要status —— 发布状态在实际代码中这样使用完全不会影响原来的业务逻辑const app {/*** appVersion 1.0.0* description 判断设备类型* apiType property* usage app.platform // notInApp | ios | android | HarmonyOS* status 已上线*/platform: getPlatform(),/*** appVersion 1.0.6* description 注册事件监听* param {Object} options - 配置选项* param {string} options.title - 事件名称* param {Function} options.callback - 注册事件时的处理函数逻辑* param {Function} options.onSuccess - 设置成功的回调函数可选* param {Function} options.onFail - 设置失败的回调函数可选* param {Function} options.onComplete - 无论成功失败都会执行的回调函数可选* usage app.monitor({ eventName: onOpenPage, callback: (data){ console.log(端上push消息, data ) } })* returns {String} id - 绑定事件的id* status 已上线*/monitor: ({ onSuccess, onFail, onComplete, eventName , callback () { } }) {let _id uuid();// 业务代码省略return _id;},}解析脚本接下来要写一个解析脚本把注释内容提取成键值对格式主要用正则表达式来解析注释const fs require(fs);const path require(path);/*** 解析参数或返回值标签* param {string} content - 标签内容* param {string} type - 类型 (param 或 returns)* returns {Object} 解析后的参数或返回值对象*/function parseParamOrReturn(content, type param) {const match content.match(/{([^}])}\s(\w)(?:\.(\w))?\s*-?\s*(.*)/);if (!match) return null;const paramType match[1];const parentName match[2];const childName match[3];const description match[4].trim();const isParam type param;if (childName) {// 嵌套参数或返回值 (options.title 或 data.result 格式)return {name: parentName,type: Object,description: isParam ? ${parentName} 配置对象 : ${parentName} 返回对象,required: isParam ? true : undefined,children: [{name: childName,type: paramType,description: description,required: isParam ? (!paramType.includes(?) !description.includes(可选)) : undefined}]};} else {// 普通参数或返回值return {name: parentName,type: paramType,description: description,required: isParam ? (!paramType.includes(?) !description.includes(可选)) : undefined};}}/*** 合并嵌套对象* param {Array} items - 参数或返回值数组* returns {Array} 合并后的数组*/function mergeNestedItems(items) {const merged {};items.forEach(item {if (item.children) {// 嵌套对象if (!merged[item.name]) {merged[item.name] { ...item };} else {// 合并子元素if (!merged[item.name].children) merged[item.name].children [];merged[item.name].children.push(...item.children);}} else {// 普通参数if (!merged[item.name]) {merged[item.name] item;}}});return Object.values(merged);}/*** 保存标签内容到注解对象*/function saveTagContent(annotation, tag, content) {// 确保 parameters 和 returns 数组存在if (!annotation.parameters) annotation.parameters [];if (!annotation.returns) annotation.returns [];switch (tag) {case appVersion:annotation.appVersion content;break;case sxzVersion:annotation.sxzVersion content;break;case mddVersion:annotation.mddVersion content;break;case description:annotation.description content;break;case status:annotation.status content;break;case usage:annotation.usage content.trim();break;case apiType:// 解析类型property 或 methodannotation.type content.toLowerCase();break;case param:const param parseParamOrReturn(content, param);if (param) {annotation.parameters.push(param);// 合并嵌套对象annotation.parameters mergeNestedItems(annotation.parameters);}break;case returns:const returnItem parseParamOrReturn(content, returns);if (returnItem) {annotation.returns.push(returnItem);// 合并嵌套对象annotation.returns mergeNestedItems(annotation.returns);}break;}}/*** 解析 JSDoc 注释中的注解信息 - 逐行解析*/function parseJSDocAnnotation(comment) {if (!comment) return null;const annotation {};// 按行分割注释const lines comment.split(\n);let currentTag ;let currentContent ;for (const line of lines) {// 清理行内容移除 * 和首尾空格但保留内部的换行意图const cleanLine line.replace(/^\s*\*\s*/, ).trimRight();// 跳过空行和注释开始结束标记if (!cleanLine || cleanLine / || cleanLine */) continue;// 检测标签开始const tagMatch cleanLine.match(/^(\w)\s*(.*)$/);if (tagMatch) {// 保存前一个标签的内容if (currentTag) {saveTagContent(annotation, currentTag, currentContent);}// 开始新标签currentTag tagMatch[1];currentContent tagMatch[2];} else if (currentTag) {// 继续当前标签的内容但保留换行// 对于 usage 标签我们保留原始格式if (currentTag usage) {currentContent \n cleanLine;} else {currentContent cleanLine;}}}// 保存最后一个标签的内容if (currentTag) {saveTagContent(annotation, currentTag, currentContent);}// 确保 parameters 和 returns 数组存在即使为空if (!annotation.parameters) annotation.parameters [];if (!annotation.returns) annotation.returns [];return Object.keys(annotation).length 0 ? annotation : null;}/*** 使用 apiType 标签指定类型*/function extractAnnotationsFromSource(sourceCode) {const annotations { properties: {}, methods: {} };// 使用更简单的逻辑按行分析const lines sourceCode.split(\n);for (let i 0; i lines.length; i) {const line lines[i].trim();// 检测 JSDoc 注释开始if (line.startsWith(/**)) {let jsdocContent line \n;let j i 1;// 收集完整的 JSDoc 注释while (j lines.length !lines[j].trim().startsWith(*/)) {jsdocContent lines[j] \n;j;}if (j lines.length) {jsdocContent lines[j] \n; // 包含结束的 */// 查找注释后面的代码行for (let k j 1; k lines.length; k) {const codeLine lines[k].trim();if (codeLine !codeLine.startsWith(//) !codeLine.startsWith(/*)) {// 解析注解const annotation parseJSDocAnnotation(jsdocContent);if (annotation) {// 从注解中获取类型property 或 methodlet itemType annotation.type;let name null;// 如果没有明确指定类型默认设为 methodif (!itemType) {itemType method;}// 提取名称const nameMatch codeLine.match(/^(\w)\s*[:]/);if (nameMatch) {name nameMatch[1];} else {// 如果没有匹配到名称尝试其他模式const funcMatch codeLine.match(/^(?:async\s)?(\w)\s*\(/);if (funcMatch) {name funcMatch[1];}}if (name) {if (itemType property) {annotations.properties[name] annotation;} else if (itemType method) {annotations.methods[name] annotation;} else {console.warn(未知的类型: ${itemType}名称: ${name});}} else {console.warn(无法提取名称: ${codeLine.substring(0, 50)});}}break;}}i j; // 跳过已处理的行}}}return annotations;}/*** 从文件提取注解*/function extractAnnotationsFromFile(filePath) {if (!fs.existsSync(filePath)) {console.error(文件不存在:, filePath);return { properties: {}, methods: {} };}const sourceCode fs.readFileSync(filePath, utf-8);return extractAnnotationsFromSource(sourceCode);}/*** 提取所有文件的注解*/function extractAllAnnotations(filePaths) {const allAnnotations {};filePaths.forEach(filePath {if (fs.existsSync(filePath)) {const fileName path.basename(filePath, .js);console.log(\n 处理文件: ${fileName} );const annotations extractAnnotationsFromFile(filePath);if (Object.keys(annotations.properties).length 0 ||Object.keys(annotations.methods).length 0) {allAnnotations[fileName] {fileName,...annotations};}}});return allAnnotations;}module.exports {parseJSDocAnnotation,extractAnnotationsFromSource,extractAnnotationsFromFile,extractAllAnnotations};集成到构建流程然后创建一个脚本指定要解析的源文件把生成的 JSON 文件 输出到 build 目录里const { extractAllAnnotations } require(./jsdoc-annotations);const fs require(fs);const path require(path);/*** 主函数 - 提取注解并生成JSON文件*/function main() {const filePaths [path.join(process.cwd(), ./app.js),path.join(process.cwd(), ./xxx.js),path.join(process.cwd(), ./yyy.js),].filter(fs.existsSync);if (filePaths.length 0) {console.error(未找到任何文件请检查文件路径);return;}const annotations extractAllAnnotations(filePaths);const outputPath path.join(process.cwd(), ./build/api-annotations.json);// 保存为JSON文件fs.writeFileSync(outputPath, JSON.stringify(annotations, null, 2));}main();在 package.json 里定义构建指令确保 build 的时候自动运行解析脚本{scripts: {build:annotations: node scripts/extract-annotations.js,build: (cd template/main-app npm run build) npm run build:annotations},}执行效果运行 npm run build 后会生成结构化的 JSON 文件1_json结构在文档中展示框架项目和文档项目是分开的把 JSON 文件生成到 build 文件夹上传到服务器后提供固定访问路径。有了结构化的 JSON 数据生成文档页面就很简单了。在 dumi 文档里把解析逻辑封装成组件---title: xxxorder: 2---jsx/*** inline: true*/import JsonToApi from /components/jsonToApi/index.jsx;export default () ;渲染效果如图所示2_渲染效果在将 JSON 数据解析并渲染到页面的过程中有两个关键的技术点需要特别关注要点一优雅的代码展示体验直接使用 dangerouslySetInnerHTML 来呈现代码片段会导致页面样式简陋、缺乏可读性。我们需要借助代码高亮工具来提升展示效果同时添加便捷的复制功能让开发者能够轻松复用示例代码。import React from react;import { Prism as SyntaxHighlighter } from react-syntax-highlighter;import { vscDarkPlus } from react-syntax-highlighter/dist/esm/styles/prism;const CodeBlock ({children,language javascript,showLineNumbers true,highlightLines []}) {const [copied, setCopied] React.useState(false);// 可靠的复制方法const copyToClipboard async (text) {try {// 方法1: 使用现代 Clipboard APIif (navigator.clipboard window.isSecureContext) {await navigator.clipboard.writeText(text);return true;} else {// 方法2: 使用传统的 document.execCommand兼容性更好const textArea document.createElement(textarea);textArea.value text;textArea.style.position fixed;textArea.style.left -999999px;textArea.style.top -999999px;document.body.appendChild(textArea);textArea.focus();textArea.select();const success document.execCommand(copy);document.body.removeChild(textArea);return success;}} catch (err) {console.error(复制失败:, err);// 方法3: 备用方案 - 提示用户手动复制prompt(请手动复制以下代码:, text);return false;}};const handleCopy async () {const text String(children).replace(/\n$/, );const success await copyToClipboard(text);if (success) {setCopied(true);setTimeout(() setCopied(false), 2000);}};return ({/* 语言标签 */}background: #1e1e1e,color: #fff,padding: 8px 16px,borderTopLeftRadius: 8px,borderTopRightRadius: 8px,borderBottom: 1px solid #333,fontSize: 12px,fontFamily: monospace,display: flex,justifyContent: space-between,alignItems: center}}{language}onClick{handleCopy}style{{position: absolute,top: 8px,right: 8px,background: copied ? #52c41a : #333,color: white,border: none,padding: 4px 8px,borderRadius: 4px,fontSize: 12px,cursor: pointer,zIndex: 10,transition: all 0.3s}}{copied ? ? 已复制 : ?? 复制}{/* 代码区域 */}language{language}style{vscDarkPlus}showLineNumbers{showLineNumbers}wrapLines{true}lineProps{(lineNumber) ({style: {backgroundColor: highlightLines.includes(lineNumber)? rgba(255,255,255,0.1): transparent,padding: 2px 0}})}customStyle{{margin: 0,borderTopLeftRadius: 0,borderTopRightRadius: 0,borderBottomLeftRadius: 8px,borderBottomRightRadius: 8px,padding: 16px,fontSize: 14px,lineHeight: 1.5,background: #1e1e1e,border: none,borderTop: none}}codeTagProps{{style: {fontFamily: Fira Code, Monaco, Consolas, Courier New, monospace,fontSize: 14px}}}{String(children).replace(/\n$/, )});};export default CodeBlock;要点二锚点导航方案由于我们是通过组件方式动态渲染内容无法直接使用 dumi 内置的锚点导航功能。这就需要我们自主实现一套导航系统并确保其在不同屏幕尺寸下都能保持良好的可用性避免出现布局错乱的问题。import React, { useEffect, useRef } from react;import { Anchor } from antd;export default function readJson(props){const anchorRef useRef(null);const anchorWrapperRef useRef(null);useEffect(() {// 使用更长的延迟确保 DOM 完全渲染const timer setTimeout(() {const contentElement document.querySelector(.dumi-default-content);const anchorElement anchorRef.current;if (!contentElement || !anchorElement) return;// 创建锚点容器const anchorWrapper document.createElement(div);anchorWrapper.className custom-anchor-wrapper;Object.assign(anchorWrapper.style, {position: sticky,top: 106px,width: 184px,marginInlineStart: 24px,maxHeight: 80vh,overflow: auto,overscrollBehavior: contain});// 插入到内容元素后面if (contentElement.nextSibling) {contentElement.parentNode.insertBefore(anchorWrapper, contentElement.nextSibling);} else {contentElement.parentNode.appendChild(anchorWrapper);}// 移动锚点anchorWrapper.appendChild(anchorElement);// 记录锚点容器用于清理anchorWrapperRef.current anchorWrapper;}, 500); // 500ms 延迟确保 DOM 完全渲染returntargetOffset{80}items{[{key: properties,href: #properties,title: 属性,children: Object.keys(properties).map(item ({key: item,href: #${item},title: item}))},{key: methods,href: #methods,title: 方法,children: Object.keys(methods).map(item ({key: item,href: #${item},title: item}))}]}/}

相关新闻

8-17 WPS JS宏 padStart()、padEnd()零宽断言应用-规范编号

8-17 WPS JS宏 padStart()、padEnd()零宽断言应用-规范编号

8-17 WPS JS宏 padStart()、padEnd()零宽断言应用-规范编号一、函数:padStart():用于在字符串的开始(左侧)填充指定的字符,直到达到目标长度。表达式:字符串.padStart(目标长度,填充字符串)padEnd():用于在字符串的开始…

2026/7/5 15:05:19 阅读更多 →
字节跳动突破:参考引导微调突破AI数学推理瓶颈

字节跳动突破:参考引导微调突破AI数学推理瓶颈

当你在解一道超级困难的数学题时,如果完全没有头绪,传统的强化学习就像一个盲人摸象的过程——AI模型不断尝试各种解法,但因为题目太难,几乎永远得不到"答对了"这个正向反馈。这就好比一个学生在黑暗中练习投篮&#xf…

2026/7/6 10:34:59 阅读更多 →
研究机构突破:多模态AI实现领域专家级适应性对话能力

研究机构突破:多模态AI实现领域专家级适应性对话能力

这项由北京智源人工智能研究院、北京航空航天大学、清华大学等多家研究机构联合开展的突破性研究,发表于2025年8月27日的人工智能顶级学术论文arXiv:2411.19930v4,为我们揭示了一个令人兴奋的发现:原来那些"博学"的AI聊天机器人&am…

2026/5/17 8:31:26 阅读更多 →

最新新闻

Donau集群作业管理完全手册:squeue、sacct、scancel命令详解

Donau集群作业管理完全手册:squeue、sacct、scancel命令详解

Donau集群作业管理完全手册:squeue、sacct、scancel命令详解 【免费下载链接】donau-slurm-wrappers donau-slurm-wrappers provide some scripts for Slurm Users to submit and manage jobs in Donau cluster environment 项目地址: https://gitcode.com/openeu…

2026/7/7 19:14:39 阅读更多 →
如何快速配置Minecraft视觉增强:终极光影包使用指南

如何快速配置Minecraft视觉增强:终极光影包使用指南

如何快速配置Minecraft视觉增强:终极光影包使用指南 【免费下载链接】photon A gameplay-focused shader pack for Minecraft 项目地址: https://gitcode.com/gh_mirrors/photon3/photon 你是否厌倦了Minecraft原版方块世界的单调视觉效果?想要为…

2026/7/7 19:14:39 阅读更多 →
如何使用Jailhouse-gui快速部署实时关键应用隔离环境?新手必看指南 [特殊字符]

如何使用Jailhouse-gui快速部署实时关键应用隔离环境?新手必看指南 [特殊字符]

如何使用Jailhouse-gui快速部署实时关键应用隔离环境?新手必看指南 🚀 【免费下载链接】Jailhouse-gui A graphical user interface (GUI) tool for configuring and managing Jailhouse, a Linux-based hypervisor for partitioning multicore processo…

2026/7/7 19:14:39 阅读更多 →
AD5593R与PIC32MZ的混合信号系统设计与实践

AD5593R与PIC32MZ的混合信号系统设计与实践

1. AD5593R与PIC32MZ1024EFK144的硬件协同设计AD5593R这颗芯片最吸引人的地方在于它的多功能引脚配置——8个I/O引脚可以独立配置为12位DAC输出、12位ADC输入、数字输出或数字输入。我在实际项目中经常用它来替代传统的独立ADC和DAC芯片组合,特别是在空间受限的嵌入…

2026/7/7 19:10:38 阅读更多 →
力扣1008:前序重建BST

力扣1008:前序重建BST

力扣 1008 题解析与 C 代码 一、问题解析题目描述:给定一个整数数组 preorder,表示二叉搜索树(BST)的前序遍历结果,请重建该二叉搜索树并返回其根节点。题目保证对于给定的测试用例,总能找到一棵二叉搜索树…

2026/7/7 19:06:38 阅读更多 →
Donau集群环境变量配置指南:SLURM_*变量到Donau的完美转换

Donau集群环境变量配置指南:SLURM_*变量到Donau的完美转换

Donau集群环境变量配置指南:SLURM_*变量到Donau的完美转换 【免费下载链接】donau-slurm-wrappers donau-slurm-wrappers provide some scripts for Slurm Users to submit and manage jobs in Donau cluster environment 项目地址: https://gitcode.com/openeule…

2026/7/7 19:06:37 阅读更多 →

日新闻

鸿蒙新特性:图片画廊与轮播导航——构建沉浸式图片浏览体验

鸿蒙新特性:图片画廊与轮播导航——构建沉浸式图片浏览体验

图片浏览是移动应用中最高频的场景之一。从社交应用的照片流到电商平台的商品图集,从旅游应用的景点相册到摄影作品展示——用户对图片浏览的体验要求不断提高:流畅的切换动画、直观的缩略图导航、便捷的收藏操作、自动播放模式。HarmonyOS NEXT ArkUI 虽…

2026/7/7 0:05:16 阅读更多 →
24V DC-DC降压芯片PW2312B/PW2815,SOT23-6到SOP8-EP方案对比

24V DC-DC降压芯片PW2312B/PW2815,SOT23-6到SOP8-EP方案对比

24V稳压芯片完整选型指南 PW8600 PW75XX PW2815 PW2312B LDODC/DC全方案 一、24V稳压方案概述 24V直流电源在工业自动化、门禁系统、电梯控制、汽车电子、LED驱动、监控设备等场景中应用极广,是最常见的中压直流母线电压。要将24V母线稳定降压至下游MCU、传感器…

2026/7/7 0:05:16 阅读更多 →
RAG+知识图谱混合检索与Graph RAG核心对比

RAG+知识图谱混合检索与Graph RAG核心对比

做企业RAG落地的团队,往往容易卡在一容易踩坑的选型难题: 当需求单纯靠向量RAG搞不定、单纯靠知识图谱也搞不定,必须同时依赖「文本语义理解 实体关系推理」时,到底是做「向量图谱混合检索」就够了,还是必须上「Grap…

2026/7/7 0:07:19 阅读更多 →

周新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/7 14:24:45 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里,参与了关于混合后量子密码学的讨论,应付端点攻击找茬的人,还参与留言板讨论后,发现“威胁模型”对多数人仍是陌生概念,且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/7 12:34:47 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”:我理解的渗透测试到底是什么?每次看到新闻里说某个大公司的数据被“黑”了,或者某个网站被攻击导致服务瘫痪,你是不是和我一样,心里会冒出两个念头:一是“这黑客真厉害”&#x…

2026/7/7 15:59:06 阅读更多 →

月新闻