在monaco-editor中给第三方库添加TS类型提示

背景

本人前段时间开发了一个 babel 插件开发练习平台,项目地址:babel-plugin-demo

其中使用到了 monaco-editor 这个库,这个库集成了 vscode 编辑器的功能,可以在网页上编写代码,但在编写插件的时候由于缺少类型提示,导致要一边查看文档有哪些 API,一边写插件,效率极低,因此想着能不能在 monaco-editor 中添加第三方库的类型提示,以提高开发效率。

具体是通过 @typescript/ata 这个库来实现的,它可以帮助我们获取 第三方库的 .d.ts文件,然后在 monaco-editor 中添加进去,这样就可以有第三方库的类型提示了。

废话不多说,下面具体演示一下。

具体实现

首先初始化一个项目,这一部分我们不过多赘述,用 vite就可以。

这里我们以 react项目为例,所以这里我们安装 @monaco-editor/react 这个库,它是基于 monaco-editor 封装的 React 组件,使用起来大同小异。

zsh 复制代码
pnpm i @monaco-editor/react

然后初始化一个 monaco-editor 编辑器, monaco-editor 的使用如果不会的可以去看看官方文档,不过官网有点不太友好,所以大家可以多去查查资料:

jsx 复制代码
import { FC } from 'react';
import Editor, { EditorProps } from '@monaco-editor/react';
const App: FC = () => {
  // 编辑器内容
  const code = `let a: number = 1;`;

  // 创建成功的回调 接受两个参数 editor 和 monaco
  const onMount: EditorProps['onMount'] = (editor, monaco) => {
    // 这里可以一写初始化编辑器的操作
  };

  return (
    <div style={{ height: '100%' }}>
      <Editor
        language="typescript" // 编辑器语言
        path="plugin.ts" // 编辑器文件名
        value={code} // 编辑器内容
        onMount={onMount} // 创建成功的回调
      ></Editor>
    </div>
  );
};

export default App;

当我们创建成功后,可以在 onMount 回调中进行一些初始化操作,这个回调会接受两个参数:

第一个参数是跟编辑器配置相关。

第二个参数是 monaco 对象,我们可以用它身上的方法 monaco.languages.typescript.typescriptDefaults.addExtraLib 方法为第三方库添加 TS 类型提示。

那么我们如何得到第三方库的 .d.ts 文件呢?

这就需要用到 @typescript/ata 这个库了

安装这个库:

zsh 复制代码
pnpm i @typescript/ata

首先创建一个 ata 函数:

ts 复制代码
import { setupTypeAcquisition } from '@typescript/ata';
import typescript from 'typescript';
const ata = setupTypeAcquisition({
  projectName: 'ata-test', // 项目名称
  typescript: typescript, // 导入 typescript 对象
  logger: console, // 日志输出
  delegate: {
    receivedFile: (code: string, path: string) => {
      // 这里可以获取到第三方库的 .d.ts 代码和路径
    },
    started: () => {
      console.log('ATA start'); // 开始执行
    },
    progress: (downloaded: number, total: number) => {
      console.log(`Got ${downloaded} out of ${total}`); // 下载进度
    },
    finished: (vfs) => {
      console.log('ATA done', vfs); // 完成执行
    },
  },
});

我们重点关注 delegate 对象,它有四个方法:

  • receivedFile(code: string, path: string):接收到文件和代码时执行的操作,我们可以在这里把 获取到的 .d.ts 文件添加给 monaco 对象。
  • started():开始执行时执行的操作
  • progress(downloaded: number, total: number):下载进度时执行的操作
  • finished(vfs):完成执行时执行的操作,这里可以获取到 vfs 对象,它是一个 Map 对象,里面存放着所有的文件路径和代码。

接下来我们就可以在 receivedFile 方法中执行 monacoaddExtraLib 方法了:

tsx 复制代码
const onMount: EditorProps['onMount'] = (editor, monaco) => {
  const ata = setupTypeAcquisition({
    projectName: 'ata-test',
    typescript: typescript,
    logger: console,
    delegate: {
      receivedFile: (code: string, path: string) => {
        // 这里添加第三方库的 .d.ts 代码和路径,注意这里的 path 要加上 `file://${path}` 前缀
        monaco.languages.typescript.typescriptDefaults.addExtraLib(
          code,
          `file://${path}`,
        );
      },
      started: () => {
        console.log('ATA start');
      },
      progress: (downloaded: number, total: number) => {
        console.log(`Got ${downloaded} out of ${total}`); // 下载进度
      },
      finished: (vfs) => {
        console.log('ATA done', vfs); /
      },
    },
  });
};

有了 ata 函数之后,如何知道该去下载哪些第三方库的 .d.ts 文件呢?

其实是通过调用 ata 函数,并给他传入编写的代码,他会去解析里面的 import 语句,然后去下载对应的 .d.ts 文件。

比如 我们要下载 axios.d.ts 文件,我们可以这样写:

tsx 复制代码
ata(`import axios from 'axios';`);

这样整个链路就走通了。

但这样写起来并不好,我们可以把创建 ata 的过程抽成一个函数,通过传递一个回调的方式执行为 monaco 对象添加 .d.ts 文件的操作:

ts 复制代码
// ata.ts
import { setupTypeAcquisition } from '@typescript/ata';
import typescript from 'typescript';

type AddExtraLibs = (code: string, path: string) => void;

// 接受一个函数
export const createAta = (addExtraLibs: AddExtraLibs) => {
  const ata = setupTypeAcquisition({
    projectName: 'Babel Plugin Playground',
    typescript: typescript,
    logger: console,
    delegate: {
      receivedFile(code, path) {
        addExtraLibs(code, path);
      },
    },
  });

  // 返回 ata 函数
  return ata;
};

App 组件中,我们可以这样写:

tsx 复制代码
import { FC } from 'react';
import Editor, { EditorProps } from '@monaco-editor/react';

// 导入 ata 函数
import { createAta } from './ata';

const App: FC = () => {
  const code = `let a: number = 1;`;

  const onMount: EditorProps['onMount'] = (editor, monaco) => {
    // 传递一个回调给 ata 函数,在这里为 monaco 对象添加 .d.ts 文件
    const ata = createAta((code, path) => {
      monaco.languages.typescript.typescriptDefaults.addExtraLib(
        code,
        `file://${path}`,
      );
    });

    ata('import axios from "axios"');
  };

  return (
    <div style={{ height: '100%' }}>
      <Editor
        language="typescript" // 编辑器语言
        path="plugin.ts" // 编辑器文件名
        value={code} // 编辑器内容
        onMount={onMount} // 创建成功的回调
      ></Editor>
    </div>
  );
};

export default App;

这样就完成在monaco-editor 中为第三方库添加 TS 类型提示了。

当然,我们还可以通过监听 monaco 的代码变化来动态的下载第三方库的 .d.ts 文件,这样就可以实现在线编辑,实时获取 .d.ts 文件,就能给每个引进来的库添加 TS 类型提示了。

tsx 复制代码
...
const onMount: EditorProps['onMount'] = (editor, monaco) => {
  ...
  // 监听编辑器内容变化 触发自动补全
  editor.onDidChangeModelContent(() => {
    ata(editor.getValue());
  });

  // 首次渲染时触发自动补全
  ata(editor.getValue());
  ...
};
...

这样就可以实现在线编辑,并实时得到类型提示,开发体验非常棒。

完整代码:

tsx 复制代码
// App.tsx
import { FC } from 'react';
import Editor, { EditorProps } from '@monaco-editor/react';

// 导入 ata 函数
import { createAta } from './ata';

const App: FC = () => {
  const code = `let a: number = 1;`;

  const onMount: EditorProps['onMount'] = (editor, monaco) => {
    // 传递一个回调给 ata 函数,在这里为 monaco 对象添加 .d.ts 文件
    const ata = createAta((code, path) => {
      monaco.languages.typescript.typescriptDefaults.addExtraLib(
        code,
        `file://${path}`,
      );
    });

    // 监听编辑器内容变化 触发自动补全
    editor.onDidChangeModelContent(() => {
      ata(editor.getValue());
    });

    // 首次渲染时触发自动补全
    ata(editor.getValue());
  };

  return (
    <div style={{ height: '100%' }}>
      <Editor
        language="typescript" // 编辑器语言
        path="plugin.ts" // 编辑器文件名
        value={code} // 编辑器内容
        onMount={onMount} // 创建成功的回调
      ></Editor>
    </div>
  );
};

export default App;
ts 复制代码
// ata.ts
import { setupTypeAcquisition } from '@typescript/ata';
import typescript from 'typescript';

type AddExtraLibs = (code: string, path: string) => void;

export const createAta = (addExtraLibs: AddExtraLibs) => {
  const ata = setupTypeAcquisition({
    projectName: 'Babel Plugin Playground',
    typescript: typescript,
    logger: console,
    delegate: {
      receivedFile(code, path) {
        addExtraLibs(code, path);
      },
    },
  });

  return ata;
};
相关推荐
JavaDog程序狗1 分钟前
【实操】uniapp纯前端搞个识别植物花草小程序
前端·vue.js·uni-app
贾公子2 分钟前
element ui & plus 版本 日期时间选择器的差异
前端·javascript
贾公子7 分钟前
form组件的封装(element ui ) 简单版本
前端·javascript
贾公子8 分钟前
下拉框组件的封装(element ui )
前端·javascript
贾公子10 分钟前
ElementUI,在事件中传递自定义参数的两种方式
前端·javascript
贾公子10 分钟前
基于Vue3 + Typescript 封装 Element-Plus 组件
前端·javascript
vim怎么退出12 分钟前
43.验证二叉搜索树
前端·leetcode
记得开心一点嘛12 分钟前
使用Three.js搭建自己的3Dweb模型(从0到1无废话版本)
前端·javascript·three.js
这颗橘子不太甜QAQ12 分钟前
patch-package使用详解
前端·npm
贾公子12 分钟前
JavaScript 中的类型相等性比较 (宽松比较的小问题)
前端·javascript