news 2026/4/23 14:28:42

前端框架文档新思路:基于源码解析的自动化方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
前端框架文档新思路:基于源码解析的自动化方案

项目背景

最近我们团队自研了一个基于 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 或 method

annotation.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 或 method)

let itemType = annotation.type;

let name = null;

// 如果没有明确指定类型,默认设为 method

if (!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: xxx

order: 2

---

```jsx

/**

* inline: true

*/

import JsonToApi from '/components/jsonToApi/index.jsx';

export default () => <JsonToApi type="app" title="xxx" desc="App原生 api 对象"/>;

```

渲染效果如图所示

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 API

if (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 (

<div className="code-container" style={{ position: 'relative', margin: '20px 0' }}>

{/* 语言标签 */}

<div style={{

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'

}}>

<span>{language}</span>

<button

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 ? '✅ 已复制' : '📋 复制'}

</button>

</div>

{/* 代码区域 */}

<SyntaxHighlighter

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$/, '')}

</SyntaxHighlighter>

</div>

);

};

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 完全渲染

return <div ref={anchorRef}>

<Anchor

targetOffset={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

}))

}

]}

/>

</div>

}

当然,在页面功能上我们还可以进一步丰富,比如增加实用的筛选功能。比如快速查看特定 App 版本支持的 API、筛选"已上线"、"开发中"或"已废弃"的接口,这些筛选能力让文档不再是静态的参考手册,而变成了一个API 探索工具,最终呈现效果如下:

3_最终效果

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/23 14:09:57

东京IT软件会社-(株)GSD|多种技术栈募集,高度人才+20分

【株式会社GSD 採用情報】 募集中のポジション Java / C# / Salesforce / AWS / GO / COBOL など多様な開発エンジニア、およびプロジェクトマネージャー&#xff08;PM&#xff09;を募集しています。 働く環境 高度人材ポイント対象&#xff08;20点&#xff09; 勤務時間&…

作者头像 李华
网站建设 2026/4/23 11:12:32

揭秘3D微结构重建:5个实战步骤带你从零掌握材料科学分析

揭秘3D微结构重建&#xff1a;5个实战步骤带你从零掌握材料科学分析 【免费下载链接】DREAM3D Data Analysis program and framework for materials science data analytics, based on the managing framework SIMPL framework. 项目地址: https://gitcode.com/gh_mirrors/dr…

作者头像 李华
网站建设 2026/4/23 12:36:13

OpenBao终极指南:构建现代应用的安全基石

OpenBao终极指南&#xff1a;构建现代应用的安全基石 【免费下载链接】openbao OpenBao exists to provide a software solution to manage, store, and distribute sensitive data including secrets, certificates, and keys. 项目地址: https://gitcode.com/gh_mirrors/op…

作者头像 李华
网站建设 2026/4/23 11:12:13

AWR1843毫米波雷达终极指南:5分钟实现实时数据可视化的完整方案

德州仪器AWR1843毫米波雷达作为业界领先的77GHz传感解决方案&#xff0c;在智能驾驶、工业检测等领域广泛应用。然而&#xff0c;从原始雷达数据到直观可视化展示的技术门槛&#xff0c;往往让初学者望而却步。今天我们将深入评测一个基于Python的开源工具&#xff0c;它能让你…

作者头像 李华
网站建设 2026/4/23 11:12:05

量价合一副图四线粘合共振变盘行情

{}DIF:EMA(CLOSE,12)-EMA(CLOSE,26); DEA:EMA(DIF,9); MACD:(DIF-DEA)*2,COLORSTICK; DIFL:EMA(VOL,12)-EMA(VOL,26); DEAL:EMA(DIFL,9); MACL:(DIFL-DEAL)*2,COLORSTICK; 基点:CONST(LLV(MACL,250)); 基:CONST(HHV(MACL,250))-基点; STICKLINE(MACL>0,0,MACL,2,-1),colorli…

作者头像 李华
网站建设 2026/4/23 11:12:34

软件定制开发哪家技术强

软件定制开发行业分析&#xff1a;广州青橙动力科技有限公司的技术优势一、行业痛点分析在软件定制开发领域&#xff0c;存在着诸多技术挑战。首先&#xff0c;不同客户的业务需求差异巨大&#xff0c;这就要求开发者能够精准地理解并转化为可行的技术方案。例如&#xff0c;在…

作者头像 李华