改进基础要素,解放医疗AI生产力
官临促犯项目背景最近我们团队自研了一个基于 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}))}]}/}

相关新闻

VirtualLab:衍射角计算器

VirtualLab:衍射角计算器

摘要衍射光栅的定义特征是其结构的周期性,正如傅立叶理论所预测的那样,这会导致入射光在透射和反射方面被分割成一组离散的级次。这些传播级次的数量,以及每个传播级次的偏转角,取决于辐射的波长、光栅前后介质的折射率、结构的周…

2026/5/17 8:16:10 阅读更多 →
TongWeb8.0 hibernate事务接口

TongWeb8.0 hibernate事务接口

问题&#xff1a;TongWeb8 的hibernate事务接口配置是什么&#xff1f; weblogic下hibernate应用配置如下&#xff1a;<property name"hibernate.transaction.manager_lookup_class" value"org.hibernate.transaction.WeblogicTransactionManagerLookup"…

2026/7/5 15:17:25 阅读更多 →
VMware Aria Suite Lifecycle 8.18 Patch 7 - 应用生命周期管理

VMware Aria Suite Lifecycle 8.18 Patch 7 - 应用生命周期管理

VMware Aria Suite Lifecycle 8.18 Patch 7 - 应用生命周期管理 请访问原文链接&#xff1a;https://sysin.org/blog/vmware-aria-suite-lifecycle/ 查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页&#xff1a;sysin.org 应用生命周期管理 VMware Aria Suite L…

2026/7/5 12:08:18 阅读更多 →

最新新闻

ncmdump终极指南:5分钟掌握网易云音乐NCM转MP3完整免费解决方案

ncmdump终极指南:5分钟掌握网易云音乐NCM转MP3完整免费解决方案

ncmdump终极指南&#xff1a;5分钟掌握网易云音乐NCM转MP3完整免费解决方案 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 你是否曾被网易云音乐下载的NCM格式文件困扰&#xff1f;想要在车载音响、手机播放器或任何设备上自由播放…

2026/7/6 7:33:11 阅读更多 →
Java密钥派生函数KDF详解:从PBKDF2到HKDF的实战指南

Java密钥派生函数KDF详解:从PBKDF2到HKDF的实战指南

1. 项目概述&#xff1a;为什么我们需要KDF&#xff1f;如果你在Java世界里摸爬滚打了一段时间&#xff0c;尤其是在处理密码、加密密钥或者任何需要从“种子”生成更多密钥的场景时&#xff0c;大概率会碰到一个词&#xff1a;KDF&#xff0c;也就是密钥派生函数。这玩意儿听起…

2026/7/6 7:33:11 阅读更多 →
STM32F429ZI与PCF8591的ADC/DAC信号转换实战

STM32F429ZI与PCF8591的ADC/DAC信号转换实战

1. PCF8591与STM32F429ZI的信号转换方案概述在嵌入式系统开发中&#xff0c;模拟信号与数字信号的相互转换是常见需求。PCF8591作为一款集成了ADC和DAC功能的芯片&#xff0c;通过I2C接口与主控芯片通信&#xff0c;能够实现4通道模拟输入和1通道模拟输出。而STM32F429ZI作为ST…

2026/7/6 7:31:11 阅读更多 →
STM32与EEPROM数据存储方案及优化实践

STM32与EEPROM数据存储方案及优化实践

1. 项目背景与核心需求在嵌入式系统开发中&#xff0c;数据持久化存储是一个基础但至关重要的功能。STM32L4A6RG作为一款低功耗微控制器&#xff0c;其内部Flash虽然可以用于数据存储&#xff0c;但存在擦写次数有限&#xff08;约1万次&#xff09;和操作复杂的缺点。而M24C04…

2026/7/6 7:31:11 阅读更多 →
STM32与AD74413R实现高精度同步数据采集与输出方案

STM32与AD74413R实现高精度同步数据采集与输出方案

1. 项目背景与核心需求在工业自动化、测试测量和音频处理等领域&#xff0c;经常需要同时实现高精度模拟信号采集&#xff08;ADC&#xff09;和输出&#xff08;DAC&#xff09;的功能。传统方案通常需要分别使用独立的ADC和DAC芯片&#xff0c;这不仅增加了系统复杂度&#x…

2026/7/6 7:29:11 阅读更多 →
PCF8591与PIC18LF45K42信号转换系统设计

PCF8591与PIC18LF45K42信号转换系统设计

1. 项目背景与核心器件选型在工业控制和嵌入式系统设计中&#xff0c;信号转换是连接模拟世界与数字系统的关键桥梁。PCF8591作为一款集成了ADC和DAC功能的混合信号转换芯片&#xff0c;配合PIC18LF45K42这款高性能8位MCU&#xff0c;能够构建出高性价比的多通道信号处理系统。…

2026/7/6 7:29:10 阅读更多 →

日新闻

H2 与 MySQL 单元测试兼容性:5 个关键 SQL 语句差异与规避方案

H2 与 MySQL 单元测试兼容性:5 个关键 SQL 语句差异与规避方案

H2与MySQL单元测试兼容性&#xff1a;5个关键SQL语句差异与规避方案1. 单元测试中的数据库兼容性挑战在Java开发领域&#xff0c;单元测试是保证代码质量的重要环节。当应用涉及数据库操作时&#xff0c;测试环境的搭建往往成为开发者的痛点。H2数据库因其轻量级、内存模式和快…

2026/7/6 0:01:17 阅读更多 →
Windows任务栏终极清理指南:用RBTray一键隐藏窗口到系统托盘

Windows任务栏终极清理指南:用RBTray一键隐藏窗口到系统托盘

Windows任务栏终极清理指南&#xff1a;用RBTray一键隐藏窗口到系统托盘 【免费下载链接】rbtray A fork of RBTray from http://sourceforge.net/p/rbtray/code/. 项目地址: https://gitcode.com/gh_mirrors/rb/rbtray 你是否厌倦了Windows任务栏上密密麻麻的图标&…

2026/7/6 0:01:17 阅读更多 →
Visual C++ 运行时库一键安装终极指南:告别DLL缺失烦恼

Visual C++ 运行时库一键安装终极指南:告别DLL缺失烦恼

Visual C 运行时库一键安装终极指南&#xff1a;告别DLL缺失烦恼 【免费下载链接】vcredist AIO Repack for latest Microsoft Visual C Redistributable Runtimes 项目地址: https://gitcode.com/gh_mirrors/vc/vcredist 你是否曾经遇到过这样的情况&#xff1a;下载了…

2026/7/6 0:05:19 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/7/6 6:52:56 阅读更多 →

月新闻