前言
最近在公司把低代码中的在线代码编辑器加了代码提示,用户写代码效率提高了很多。下面给大家分享一下方案。
演示
这里使用我做的 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
运行项目,可以看到页面
实现三方依赖提示
我们在编辑器中引入三方库的时候,会报错。
怎么解决这个问题呢,有两个解决方案。
第一个使用 @typescript/ata 库自动解析代码中的三方库,然后从jsdelivr下载三方库的 types 文件。
监听 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 文件。
文件下载完成后,代码不报错了,提示也能正常显示了。
如果想实时加载三方库,可以监听编辑器值改变事件,实时下载新的三方库。
这种方案我不建议使用,因为一般线上编辑器,不会让用户自己去选择使用哪些依赖,都是提前限定好的。并且第一种方案,从网上下载资源,网络不好的时候,下载比较慢。
第二种方案是从本地加载 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 就行了
const sourceCode = `
interface Args {
/**
* 参数
*/
params: {
/**
* 姓名
*/
name: string
/**
* 年龄
*/
age: number
}
}
`
monaco.languages.typescript.typescriptDefaults.addExtraLib(sourceCode, `interface.ts`);
然后把入参限定类型为 Args 就行了。
这里是写死的 Args,那怎么动态生成 interface 呢,我这里使用的是抽象语法树方案,把定义的参数,转换为抽象语法树,然后在用抽象语法树生成 interface 代码。
把我们写死的 interface 复制到 ast 网站查看结构,然后我们通过定义的参数,动态生成抽象语法树。
interface Args {
/**
* 参数
*/
params: {
/**
* 姓名
*/
name: string
/**
* 年龄
*/
age: number
}
}
使用 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
总结
线上编辑器增加了类型约束,用户不用去找文档,查看上下文方法怎么使用,极大的提高了用户开发效率。