使用monaco editor 实现 typescript 代码提示

1,432 阅读2分钟

前言

最近在公司把低代码中的在线代码编辑器加了代码提示,用户写代码效率提高了很多。下面给大家分享一下方案。

演示

Kapture 2025-05-06 at 20.09.21.gif

这里使用我做的 demo 项目演示的,比较简单,公司里的功能不能拿来演示。

实现

初始化项目

首先使用 vite 创建一个 react 项目。

npm create vite@latest

然后进入项目,安装@monaco-editor/react依赖。这里我使用的是封装好的 react 版本 monaco,使用起来更加简单。

pnpm i @monaco-editor/react

改造 App.tsx 文件里的代码

import MonacoEditor from '@monaco-editor/react';

function App() {
  const code = `console.log("hello world");`;

  return <MonacoEditor
    height={'100vh'}
    language={"typescript"}
    theme='vs-dark'
    options={{
      fontSize: 18,
      fontFamily: 'monospace',
    }}
    value={code}
    path='main.ts'
  />
}

export default App

运行项目,可以看到页面

image.png

实现三方依赖提示

我们在编辑器中引入三方库的时候,会报错。

image.png

怎么解决这个问题呢,有两个解决方案。

第一个使用 @typescript/ata 库自动解析代码中的三方库,然后从jsdelivr下载三方库的 types 文件。

image.png

监听 onMount 事件

import MonacoEditor from '@monaco-editor/react';
import { setupTypeAcquisition } from '@typescript/ata';
import ts from 'typescript';

function App() {
  const code = `import lodash from "lodash";`;

  return <MonacoEditor
    height={'100vh'}
    language={"typescript"}
    theme='vs-dark'
    options={{
      fontSize: 18,
      fontFamily: 'monospace',
    }}
    value={code}
    path='main.ts'
    onMount={(editor, monaco) => {

      monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
        esModuleInterop: true,
      });

      const ata = setupTypeAcquisition({
        projectName: "My ATA Project",
        typescript: ts,
        logger: console,
        delegate: {
          receivedFile: (code: string, path: string) => {
            // 把三方库的提示添加到编辑器中
            monaco.languages.typescript.typescriptDefaults.addExtraLib(code, `file://${path}`)
          },
        },
      });

      ata(editor.getValue())
    }}
  />
}

export default App

从浏览器中的 network 中可以看到,下载了很多 lodash types 文件。

image.png

文件下载完成后,代码不报错了,提示也能正常显示了。

image.png

如果想实时加载三方库,可以监听编辑器值改变事件,实时下载新的三方库。

image.png

这种方案我不建议使用,因为一般线上编辑器,不会让用户自己去选择使用哪些依赖,都是提前限定好的。并且第一种方案,从网上下载资源,网络不好的时候,下载比较慢。

第二种方案是从本地加载 types 文件。使用import.meta.glob匹配对应的 d.ts文件,返回代码。

import MonacoEditor from '@monaco-editor/react';

function App() {
  const code = `import lodash from "lodash";
import dayjs from "dayjs";`;

  return <MonacoEditor
    height={'100vh'}
    language={"typescript"}
    theme='vs-dark'
    options={{
      fontSize: 18,
      fontFamily: 'monospace',
    }}
    value={code}
    path='main.ts'
    onMount={async (_, monaco) => {
      monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
        esModuleInterop: true,
      });

      // 动态匹配 lodash 类型文件,返回路径和代码
      const lodashTypeFiles = import.meta.glob('/node_modules/@types/lodash/**/*.d.ts', {
        query: '?raw',
        eager: true,
        import: 'default',
      });

      Object.keys(lodashTypeFiles).forEach(key => {
        const code = lodashTypeFiles[key] as string;
        monaco.languages.typescript.typescriptDefaults.addExtraLib(code, `file://${key}`)
      });

      // 动态匹配 dayjs 的类型文件,返回路径和代码
      const dayjsTypeFiles = import.meta.glob('/node_modules/dayjs/**/*.d.ts', {
        query: '?raw',
        eager: true,
        import: 'default',
      });

      Object.keys(dayjsTypeFiles).forEach(key => {
        const code = dayjsTypeFiles[key] as string;
        monaco.languages.typescript.typescriptDefaults.addExtraLib(code, `file://${key}`)
      });
    }}
  />
}

export default App

这种方案比第一种方案加载速度快很多。

动态类型

低代码平台里的代码编辑器会接受一些参数,这些参数是提前定义好的,那怎么根据定义的参数动态生成ts类型 呢,下面和大家也分享一下。

比如我现在有个定义好的入参数,数据格式为:

 [
  {
    name: 'name',
    description: "名称",
    type: 'string',
  },
  {
    name: 'age',
    description: "年龄",
    type: 'number',
  },
]

这里只需要定义一个 interface 就行了

image.png

const sourceCode = `
    interface Args {
      /**
       * 参数
       */
      params: {
        /**
         * 姓名
         */
        name: string
        /**
         * 年龄
         */
        age: number
      }
    }
      `
monaco.languages.typescript.typescriptDefaults.addExtraLib(sourceCode, `interface.ts`);

然后把入参限定类型为 Args 就行了。

image.png

这里是写死的 Args,那怎么动态生成 interface 呢,我这里使用的是抽象语法树方案,把定义的参数,转换为抽象语法树,然后在用抽象语法树生成 interface 代码。

把我们写死的 interface 复制到 ast 网站查看结构,然后我们通过定义的参数,动态生成抽象语法树。

interface Args {
      /**
       * 参数
       */
      params: {
        /**
         * 姓名
         */
        name: string
        /**
         * 年龄
         */
        age: number
      }
    }

image.png

使用 babel 创建语法树

import * as Babel from '@babel/standalone';

const t = Babel.packages.types;

interface Param {
  name: string;
  description: string;
  type: string;
  params?: Param[];
}
function createObjectAst(param: Param) {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  let p: any = null;
  if (param.type === 'object') {
    p = p = t.objectTypeProperty(
      t.identifier(param.name),
      t.objectTypeAnnotation(
        (param.params || []).map(p => createObjectAst(p))
      )
    )
  } else {
    p = t.objectTypeProperty(
      t.identifier(param.name),
      t.genericTypeAnnotation(
        t.identifier(param.type),
        null
      ),
    )
  }

  t.addComment(p, 'leading', `*\n * ${param.description}\n `);
  return p;
}
export function createParamsInterface() {
  const params: Param[] = [
    {
      name: 'name',
      description: "名称",
      type: 'string',
    },
    {
      name: 'age',
      description: "年龄",
      type: 'number',
    },
    {
      name: 'department',
      description: "部门",
      type: 'object',
      params: [
        {
          name: 'name',
          description: "名称",
          type: 'string',
        },
        {
          name: 'code',
          description: "编码",
          type: 'string',
        },
      ]
    }
  ];

  const inputAst = t.program([
    t.interfaceDeclaration(
      t.identifier('Args'),
      null,
      null,
      t.objectTypeAnnotation([
        t.objectTypeProperty(
          t.identifier('params'),
          t.objectTypeAnnotation(
            params.map((param) => {
              return createObjectAst(param)
            })
          )
        ),
      ])
    ),
  ]);

  return Babel.packages.generator.default(inputAst, { comments: true }).code;
}

生成的 interface

image.png

image.png

总结

线上编辑器增加了类型约束,用户不用去找文档,查看上下文方法怎么使用,极大的提高了用户开发效率。