提升开发体验:基于 JSDoc 的 React 项目自动代码提示方案详解

需求背景

主管和其他同事基于公司的业务特点,开发了一套自研前端框架。技术选型是 React + JavaScript 的组合,上线后表现还不错。现在他们想把这个组件库推广到其他团队使用,所以让我琢磨一下:怎么能让使用者用得更顺手一点?尤其是能不能在写代码的时候有自动提示?

我调研了一下市面上常见的几种方案,大致有以下几类:

  • 把整个项目从 JavaScript 重构为 TypeScript,这样就能通过 .ts 或 .tsx 文件自动生成 .d.ts 类型声明文件;
  • 不动源码,在外面单独为每个导出的组件手动写 .d.ts 文件;
  • 使用 TypeScript 编译器解析 JavaScript 文件,直接生成 .d.ts 文件;对于那些识别不全的部分,再通过 JSDoc 注释来辅助生成更准确的类型信息。

主管的意思是,希望尽可能少投入人力,因为框架已经稳定运行了,不想为了一个“非刚需”的功能去大动干戈。所以最终我们选择了第三种方案——它的最大优点就是:对源码几乎无侵入,改动小,成本低,见效快!

不过它也不是十全十美。比如 TypeScript 自动生成的 .d.ts 文件中,很多函数参数或对象属性都会被推断成 any,即使配合 JSDoc 使用,也并不是所有组件都能有完整的类型提示。有些时候你还是得手动点进 .d.ts 文件里看定义。

但总的来说,瑕不掩瑜。毕竟现在的目标是高效产出,不是追求完美主义。

项目结构说明

我们的框架是一个典型的多包项目,主要由两个核心目录组成:packagescomponents

packages 目录下包含了四个子包:

  • cli:负责创建项目的命令行工具;
  • compatible:提供运行环境适配能力;
  • multipage:支持多页面应用架构;
  • store:实现全局状态管理。

components 目录则主要是 UI 组件和交互能力的集合。

整个项目的目录结构如下所示:

my-project/ ├── components/ │   ├── src/ │   ├── build.config.mts │   └── package.json ├── packages/ │   ├── cli/ │   ├── compatible/ │   │   ├── src/ │   │   ├── build.config.mts │   │   └── package.json │   ├── multipage/ │   └── store/ ├── scripts/   └── package.json 

除了 cli 外,其余子包都需要生成 .d.ts 文件。那我们的思路也很简单:在根目录安装 TypeScript,然后给每个子包加上 tsconfig.json,最后写个脚本批量处理这些子包,自动生成类型声明文件。

安装依赖

因为这些依赖项是多个子包共用的,所以我们统一安装在根目录下:

npm install --save-dev typescript jsdoc @types/react @types/react-dom 

tsconfig.json 配置

每个子包都是独立发布的,所以每个子包都要有自己的 tsconfig.json 文件。
下面是通用配置,利用了 TypeScript 的 emitDeclarationOnly 功能,只用来生成 .d.ts 文件:

{   "compilerOptions": {     "module": "ESNext",     "target": "ES5",     "moduleResolution": "node",     "esModuleInterop": true,     "skipLibCheck": true,     "jsx": "preserve",      "allowJs": true,     "declaration": true,     "emitDeclarationOnly": true,     "outDir": "./types",      "lib": ["es2017", "dom"]   },   "include": ["src/**/*.js", "src/**/*.jsx"],   "exclude": ["node_modules"] } 

自动化脚本编写

为了让这个流程自动化,我们还需要一个脚本,遍历 packagescomponents 文件夹,找到带有 tsconfig.json 的子包,然后执行 TypeScript 命令生成 .d.ts 文件,并放在对应层级下的 types 文件夹中。

我们在 scripts 目录下新建了一个 build-dts.js 脚本,内容如下:

const fs = require("fs"); const path = require("path"); const { execSync } = require("child_process");  const rootDir = __dirname + "/../"; const packagesDir = path.join(rootDir, "packages"); const componentsDir = path.join(rootDir, "components");  function buildDtsForPackage(pkgPath) {   const tsconfigPath = path.join(pkgPath, "tsconfig.json");   const typesOutDir = path.join(pkgPath, "types");    if (!fs.existsSync(tsconfigPath)) {     console.warn(       `⚠️ No tsconfig.json found in ${pkgPath}, skipping .d.ts generation`     );     return;   }    // ---- 清空 types 文件夹 ----   if (fs.existsSync(typesOutDir)) {     console.log(`🧹 Clearing old types folder: ${typesOutDir}`);     fs.rmSync(typesOutDir, { recursive: true, force: true });   }    try {     // 执行 tsc 命令只生成类型声明文件     execSync(`tsc`, {       cwd: pkgPath,       stdio: "inherit",     });      console.log(`✅ .d.ts generated for ${pkgPath}`);   } catch (e) {     console.error(`❌ Failed to generate .d.ts for ${pkgPath}`);   } }  function processDirectory(targetDir) {   if (!fs.existsSync(targetDir)) {     console.warn(`⚠️ Directory not found: ${targetDir}, skipping.`);     return;   }    const dirs = fs.readdirSync(targetDir);    for (const dir of dirs) {     const fullPath = path.join(targetDir, dir);     const stat = fs.statSync(fullPath);     if (stat.isDirectory()) {       buildDtsForPackage(fullPath);     }   } }  function processComponentsFlat(targetDir) {   const tsconfigPath = path.join(targetDir, "tsconfig.json");    if (!fs.existsSync(tsconfigPath)) {     console.warn(`⚠️ components/tsconfig.json not found, skipping.`);     return;   }    console.log(`✅ Building dts for flat components directory: ${targetDir}`);   buildDtsForPackage(targetDir); }  function main() {   console.log("📦 Processing packages directory...");   processDirectory(packagesDir);    console.log("🧩 Processing components directory...");   processComponentsFlat(componentsDir); // 处理扁平的 components 目录 }  main(); 

接着在根目录的 package.json 中加一条脚本指令:

{   "scripts": {      "build:dts": "node scripts/build-dts.js"   }   } 

现在只需要在终端输入:

npm run build:dts 

就能一键为所有子包生成 .d.ts 文件啦!

实际效果展示

在没有改一行源码的情况下,TypeScript 自动生成的 .d.ts 文件长这样:

提升开发体验:基于 JSDoc 的 React 项目自动代码提示方案详解

虽然类型定义可能还不够精准,但已经能帮开发者理解 API 的基本用法了。

但有些组件就比较复杂,TypeScript 推不出来详细的结构,比如下面这个组件生成的 .d.ts 就显得有点鸡肋,根本看不出组件该如何使用:

提升开发体验:基于 JSDoc 的 React 项目自动代码提示方案详解

这时候就可以借助 JSDoc 来补充说明了。

用 JSDoc 补充类型信息

Page 组件为例,我们在源码顶部加上 JSDoc 注释,定义 props 结构和生命周期参数:

/**  * @typedef {Object} PageProps  * @property {React.ReactElement} children - 子元素  * @property {(info: PageLifecycleInfo) => void} [onPageBeforeIn] - 页面进入前触发(路由切换时)  * @property {(info: PageLifecycleInfo) => void} [onPageBeforeOut] - 页面离开前触发(路由切换时)  * @property {(info: PageLifecycleInfo) => void} [onPageAfterIn] - 页面进入后触发(DOM挂载完成)  * @property {(info: PageLifecycleInfo) => void} [onPageAfterOut] - 页面离开后触发(DOM卸载前)  * @property {(info: PageLifecycleInfo) => boolean} [onPageBeforeUnmount] - 页面卸载前触发(可阻止卸载)  * @property {(info: PageLifecycleInfo) => void} [onPageAfterUnmount] - 页面卸载后触发  * @private  */  /**  * 页面生命周期信息对象,提供页面相关的上下文数据。  *  * @typedef {Object} PageLifecycleInfo  * @property {string} path - 当前路由路径  * @property {Record<string, any>} params - 路由参数对象  * @property {string} title - 页面标题  * @property {"PUSH" | "REPLACE" | "GO" | "BACK" | "FORWARD" | "LISTEN"} openMode - 路由打开方式  * @property {boolean} hideNav - 是否隐藏导航栏  * @property {boolean} micro - 是否作为微前端子页面  * @property {React.ReactElement} component - 页面组件实例  * @property {React.RefObject<HTMLElement>} pageRef - 页面根元素的 Ref 对象  * @property {PopStateEvent | HashChangeEvent} [event] - 原始路由事件对象  */  /**  * Page 是一个页面级别的容器组件,用于管理页面生命周期和渲染内容。  * @type {React.FC<PageProps>}  */ 

再执行

npm run build:dts 

这会生成的 .d.ts 文件就能清楚地告诉开发者:这个组件到底接受哪些 props。
提升开发体验:基于 JSDoc 的 React 项目自动代码提示方案详解

不过也要注意,并不是所有的 JSDoc 注释生成类型声明之后都能在编译软件上有代码提示。有时候还是会遇到一些限制。

发布到 npm

生成完 .d.ts 文件之后,还要确保它们能随着组件一起发布到 npm 上。这就需要在每个子包的 package.json 中添加如下配置:

{    "files": [     "cjs/**",     "esm/**",     "types/**"   ],   "types": "types/index.d.ts", } 

这样用户在使用组件时,就能看到清晰的类型提示和跳转定义了。

效果对比图

没有代码提示时:
提升开发体验:基于 JSDoc 的 React 项目自动代码提示方案详解

有了代码提示之后:
提升开发体验:基于 JSDoc 的 React 项目自动代码提示方案详解

是不是瞬间感觉开发起来轻松多了?通过这种“低成本+高收益”的方式,我们不仅提升了组件库的易用性,也让团队内外的开发者们写起代码来更顺手、更安心。

如果你对前端工程化有兴趣,或者想了解更多前端相关的内容,欢迎查看我的其他文章,这些内容将持续更新,希望能给你带来更多的灵感和技术分享~

发表评论

评论已关闭。

相关文章