最近因为需要实现一个在线代码的需求,所以研究了相关的内容,其中最主要的就是编辑器相关的内容
使用
对于 monaco 编辑器的选择,我使用了 @monaco-editor/react
库
typescript
import type { EditorProps, OnChange, OnMount } from '@monaco-editor/react'
import { Editor as MonacoEditor } from '@monaco-editor/react'
import { useDebounceFn } from 'ahooks'
import { Spin } from 'antd'
import { FC, useState } from 'react'
export const CodeEditor: FC<EditorProps> = props => {
const [theme, setTheme] = useState('vs')
// 处理代码修改, args需要做一层透传来完善防抖,避免触发重复构建
const { run: handleChange } = useDebounceFn<OnChange>(
(...args) => {
if (props.onChange) {
props.onChange(...args)
}
},
{
wait: 400,
},
)
return (
<MonacoEditor
loading={<Spin />}
theme={theme}
{...props}
onChange={handleChange}
/>
)
简单的对齐进行了封装使用
本地使用
在使用 @monaco-editor/react
库,会通过CDN去加载编辑器的资源
但有时候,我们所需要的环境,并不能直接去访问外网,故我们需要将这些资源改变成本地加载 首先我们需要一个公共位置去存放这些资源,如 vite 的 public 文件夹等等 我们需要把 monaco-editor
库下载下来,主要是需要 min/vs 内容,提供给编辑器
其实还有另一种方式,是安装 monaco-editor 库,并引入即可,但之前测试的时候是有问题的,包括 webpack 等等,不太稳定,所以没有选择这种方式
typescript
import { loader } from '@monaco-editor/react'
loader.config({
paths: {
// public 下存放 monaco-editor 的路径
vs: '/npm/monaco-editor/min/vs',
},
})
通过这种方式,就可以避免编辑器去加载远程资源了
类型提示
在在线代码里,我们还需要自定义一些专属的东西,如全局上下文等内容
通过 Suggestion 提供类型提示
基本的操作方法可以查看一下官网的案例 主要就是通过 monaco.languages.registerCompletionItemProvider
方法去注册,然后返回规定格式的 suggestion
即可,最后再选择一个触发方式,即 triggerCharacters
。
javascript
monaco.languages.registerCompletionItemProvider('javascript', {
provideCompletionItems: (model, position) => {
const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: model.getWordUntilPosition(position).startColumn,
endColumn: model.getWordUntilPosition(position).endColumn,
}
return {
suggestions: [
{
label: 'test', // 显示的提示内容
kind: monaco.languages.CompletionItemKind.Function, // 用来显示提示内容后的不同的图标
insertText: 'test', // 选择后粘贴到编辑器中的文字
detail: '', // 提示内容后的说明
range: range,
},
],
}
},
triggerCharacters: ['.'],
})
上面代码实现的就是按下 .
时触发提示,并提示出 test
方法
接下来,我们来根据对象结果信息,动态的去触发提示,并封装几个方法
typescript
import type { Monaco } from '@monaco-editor/react'
/**
* 将对象转化为数组形式
* @param input 待转换对象
* @param path 递归路径
* @param pathList 转换后的路径数组
*/
const convertObject2Array = (input: Record<any, any>, path: string[], pathList: string[][]) => {
const arr = Object.keys(input)
for (const k in arr) {
const res = [path, arr[k]].flat()
if (input[arr[k]].constructor !== Object) {
// Object 递归处理
// 说明找到了一条完整的路径
pathList.push(res)
if (k === arr.length - 1) return
} else {
convertObject2Array(input[arr[k]], res, pathList)
}
}
}
const getLastData = (content: string, index: number) => {
content = content.substring(0, index - 1)
const sql = content.trim().replace(/\s+/g, ' ').replace(/,/g, ', ').replace(/=/g, '= ').replace(/\(/g, '( ')
const splitRes = sql.split(' ')
const res = splitRes[splitRes.length - 1]
return res.substring(0, res.length - 1)
}
/**
* 将对象注入到monaco编辑器中进行提示
* @param monaco 编辑器实例
* @param obj 注入对象
* @param triggerCharacters 触发字符
*/
export const injectSuggestionsByObject = (monaco: Monaco, obj: Record<any, any>, triggerCharacters = ['.']) => {
const suggestionData = [] // 二维数组
convertObject2Array(obj, [], suggestionData)
monaco.languages.registerCompletionItemProvider('javascript', {
provideCompletionItems: (model, position) => {
const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: model.getWordUntilPosition(position).startColumn,
endColumn: model.getWordUntilPosition(position).endColumn,
}
const line = position.lineNumber
const content = model.getLineContent(line)
const point = content[position.column - 2]
const data = getLastData(content, position.column)
const property = data.split(triggerCharacters[0]) // [ 'basic' , 'Info' ] -----> 期望 .出 name
const suggestions: any[] = []
// 横向遍历二维数组 找到满足条件的所有行
if (property.length !== 0 && triggerCharacters.indexOf(point) !== -1) {
for (let r = 0; r < suggestionData.length; r++) {
for (let c = 0; c < property.length; c++) {
if (property[c] !== suggestionData[r][c]) {
continue
}
if (c === property.length - 1) {
// 避免重复添加相同的提示
if (suggestions.findIndex(suggestion => suggestion.label === suggestionData[r][c + 1]) === -1) {
// 转成provider需要的格式
suggestions.push({
label: suggestionData[r][c + 1], // 显示的提示内容
kind: monaco.languages.CompletionItemKind.Function, // 用来显示提示内容后的不同的图标
insertText: suggestionData[r][c + 1], // 选择后粘贴到编辑器中的文字
detail: '', // 提示内容后的说明
range: range,
})
}
}
}
}
}
return { suggestions: suggestions }
},
triggerCharacters: triggerCharacters,
})
}
使用
typescript
injectSuggestionsByObject(_monaco, {
ctx: {
state: '',
setState: '',
},
})
效果如下
但后面发现,这种方式只对对象有效,而且无法去判别方法和属性等等,而且实现起来很麻烦,所以就有了下面这种方式了
通过 Typescript 提供类型提示
这种方式的话,只需要通过 typescript
的 declare
去声明类型即可,方便很多
还是上面那个例子,现在换一种实现方式
首先先定义所需要的类型声明
typescript
/**
* 全局上下文
*/
declare const ctx: {
/**
* 全局状态
*/
state: Record<string, any>
/**
* 更新全局状态
*/
setState: (newState: Record<string, any>) => void
}
注意,使用的时候要变为字符串形式,传入给 monaco 编辑器
typescript
export const ctxType = `
/**
* 全局上下文
*/
declare const ctx: {
/**
* 全局状态
*/
state: Record<string, any>
/**
* 更新全局状态
*/
setState: (newState: Record<string, any>) => void
}
`
然后,在 monaco 编辑器中引入该类型提示即可
typescript
monaco.languages.typescript.javascriptDefaults.addExtraLib(ctxType, 'ctx.d.ts')