Monaco 探索(二)

Monaco 探索(二)

六月 17, 2021 本文共计: 1.8k 字 预计阅读时长: 10分钟

久违已久的Monaco后续,它来了,它来了~

先从整体设计出发,画图阐释架构设计:

然后再说下代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
// 引擎文件

import * as monaco from 'monaco-editor'
import { RefObject } from 'react'
import {
CloseAction,
createConnection,
ErrorAction,
MessageConnection,
MonacoLanguageClient,
MonacoServices
} from 'monaco-languageclient'
import { listen } from '@codingame/monaco-jsonrpc'

export { monaco }

/* eslint-disable @typescript-eslint/no-var-requires*/

const normalizeUrl = require('normalize-url')
const ReconnectingWebSocket = require('reconnecting-websocket').default

export type LanguageConfig = {

/* 语言id **/
id: string;

/* 语言后缀 **/
extensions: Array<string>;

/* 语言别名 **/
aliases?: Array<string>;
mimetypes?: Array<string>;
}

export type EngineProps = {
monaco?: typeof monaco;
domElement: HTMLElement| RefObject<any>;
languageConfig?: LanguageConfig;
defaultValue?: string;
modelUri?: string;
createOptions?: Record<string, any>;
rootUri?: string;
installOptions?: Record<string, any>;
wsUrl: string;
socketOptions?: Record<string, any>;
clientOptions?: Record<string, any>;
}

export type EngineServerProps = {
wsUrl: string;
socketOptions: Record<string, any>;
clientOptions: Record<string, any>;
monacoInstance?: typeof monaco;
}


export class EngineServer {

static SocketOptions: Record<string, any> = {
maxReconnectionDelay: 10000,
minReconnectionDelay: 1000,
reconnectionDelayGrowFactor: 1.3,
connectionTimeout: 10000,
maxRetries: Infinity,
debug: false
}

// eslint-disable-next-line no-underscore-dangle
private _wsurl: string;

constructor({ wsUrl, socketOptions, clientOptions }: EngineServerProps) {
// eslint-disable-next-line no-underscore-dangle
this._wsurl = wsUrl
const url = this.createUrl()
const webSocket = window.YQN_MONACO_LSP_WS = this.createWebsocket({ url, socketOptions })

listen({
webSocket,
onConnection: connection => {
// create and start the language client
const languageClient = this.createLanguageClient({ connection, clientOptions })
const disposable = languageClient.start()

connection.onClose(() => disposable.dispose())
}
})

}

protected createUrl(): string {
// eslint-disable-next-line no-underscore-dangle
if (!this._wsurl) {
throw new Error(`
EngineServer Error:
你应该为 EngineServer 传入一个用来连接语言服务器的 websocket 地址
`)
}
// eslint-disable-next-line no-underscore-dangle
return normalizeUrl(this._wsurl)
}

protected createWebsocket({ url = '', socketOptions = EngineServer.SocketOptions }): WebSocket {
// eslint-disable-next-line no-underscore-dangle
if (!this._wsurl) {
throw new Error(`
EngineServer Error:
你应该为 EngineServer 传入一个序列化后的 websocket 地址
`)
}

return new ReconnectingWebSocket(url, [], socketOptions)
}

protected createLanguageClient({ connection, clientOptions = {} }: { connection: MessageConnection; clientOptions?: Record<string, any> }): MonacoLanguageClient {
return new MonacoLanguageClient({
name: 'YQN Language Client',
clientOptions: {
// use a language id as a document selector
documentSelector: ['java'],

// disable the default error handler
errorHandler: {
error: (): ErrorAction.Continue => ErrorAction.Continue,
closed: (): CloseAction.DoNotRestart => CloseAction.DoNotRestart
},
...clientOptions
},

// create a language client connection from the JSON RPC connection on demand
connectionProvider: {
get: (errorHandler, closeHandler): Promise<any> => Promise.resolve(createConnection(connection, errorHandler, closeHandler))
}
})
}
}

export default class Engine {

static defaultLanguageConfig: LanguageConfig = {
id: 'java',
extensions: ['.java'],
aliases: ['JAVA', 'java'],
mimetypes: ['text/x-java-source', 'text/x-java'],
}

static modelUri = 'file:///D:/work/composer-executer-template/composer-executer-demo/src/main/java/com/yqn/framework/executer/Application.java';

static rootUri = '/D:/work/composer-executer-template'

static defaultCodeValue = `
/**
* @desc yqn web ide
*/
// @todo: you can edit some code in here
`

private _monaco: typeof monaco;
private _ITextModel: monaco.editor.IStandaloneCodeEditor;

constructor({
domElement, modelUri, defaultValue, createOptions, rootUri, installOptions, wsUrl, socketOptions, clientOptions
}: EngineProps) {
// eslint-disable-next-line no-underscore-dangle
this._monaco = monaco
window.monaco = monaco

this.registerLanguage()
// eslint-disable-next-line no-underscore-dangle
this._ITextModel = this.createEdit({ domElement, modelUri, value: defaultValue, options: createOptions })


if (!window.YQN_MONACO_LSP_WS) {
const monacoInstance = this.install({ rootUri, options: installOptions })
// eslint-disable-next-line @typescript-eslint/no-use-before-define,no-new

// eslint-disable-next-line no-new
new EngineServer({
wsUrl, socketOptions, clientOptions, monacoInstance
})
}

}

protected registerLanguage(lc: LanguageConfig = Engine.defaultLanguageConfig): void {
// eslint-disable-next-line no-underscore-dangle
this._monaco.languages.register(lc)
}

protected createEdit({ value = Engine.defaultCodeValue, modelUri = Engine.modelUri, domElement, options = {} }): any {
if (!domElement) {
throw new Error(`
Monaco Engine Error:
你应该为 Monaco 指定一个初始化 IDE 的 DOM 元素。
`)
}

// eslint-disable-next-line no-underscore-dangle
const model = this._monaco.editor.createModel(value, 'java', this._monaco.Uri.parse(modelUri))

// eslint-disable-next-line no-underscore-dangle,@typescript-eslint/no-non-null-assertion
return this._monaco.editor.create(domElement!, {
model,
glyphMargin: true,
lightbulb: {
enabled: true
},
language: 'java',
automaticLayout: true,
...options
})

}

protected install({ rootUri = Engine.rootUri, options = {} }): any {
// eslint-disable-next-line no-underscore-dangle
return MonacoServices.install(this._monaco, { rootUri, ...options })
}

public get monaco(): typeof monaco {
// eslint-disable-next-line no-underscore-dangle
return this._monaco
}

public get ITextModal(): monaco.editor.IStandaloneCodeEditor {
// eslint-disable-next-line no-underscore-dangle
return this._ITextModel
}
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// theme 文件

import * as monaco from 'monaco-editor'

/* eslint-disable @typescript-eslint/no-var-requires*/

const Tomorrow = require('monaco-themes/themes/Tomorrow.json')
const Zenburnesque = require('monaco-themes/themes/Zenburnesque.json')
const Active4D = require('monaco-themes/themes/Active4D.json')
const Amy = require('monaco-themes/themes/Amy.json')
const Clouds = require('monaco-themes/themes/Clouds.json')

export type ThemesKeys = 'Tomorrow' | 'Zenburnesque' | 'Active4D' | 'Amy' | 'Clouds';

type ThemesProps = Record<ThemesKeys, any>;

export const Themes: ThemesProps = {
Tomorrow,
Zenburnesque,
Active4D,
Amy,
Clouds
}


export type Monaco = typeof monaco;

export const defineTheme = (monaco: Monaco): void => {
Object.keys(Themes).forEach(key => {
monaco.editor.defineTheme(key, Themes[key])
})
}

export const setTheme = (monaco: Monaco, themeName: ThemesKeys): void => {
monaco.editor.setTheme(themeName)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import React, { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import classes from 'classnames'
import { ThemesKeys } from '../lib/theme'
import { getMonacoUriAndContent, GetMonacoUriAndContentReqProps } from '../lib/api'
import { wsUrl } from '../lib/default.config'
import { setUpWorker, Engine, defineTheme, setTheme } from '../index'
import './index.less'

type UseToolMonaco = (p?: { className?: string; apiParams?: GetMonacoUriAndContentReqProps }) => {

/** Monaco edit 实例 */
monacoEdit: ReactNode;

/** 设置 ide 主题 */
setEditorTheme(theme: ThemesKeys): void;

/** 加载 loading */
loading: boolean;

/** 获取编辑器内容 */
getEditorContent(): string;
}

/** 请求参数 */
// eslint-disable-next-line no-underscore-dangle
export const _apiParams: GetMonacoUriAndContentReqProps = {
id: null,
apiId: null,
appId: null
}

const useToolMonaco: UseToolMonaco = ({
className = '',
apiParams = {}
} = {}) => {
const domRef = useRef(null)
const [loading, setLoading] = useState(false)
const monacoRef = useRef({
monaco: null,
editor: null
})

Object.assign(_apiParams, apiParams)

const monacoCls = classes('yqn-monaco-editor', className)

const monacoEdit = useMemo(() => (<div className={monacoCls} ref={domRef}/>), [monacoCls])

const setEditorTheme = useCallback((themeName: ThemesKeys) => {
setTheme(monacoRef.current.monaco, themeName)
}, [])

const getEditorContent = useCallback(() => monacoRef.current?.editor?.getValue?.(), [])

useEffect(() => {
setUpWorker()
setLoading(true)
const startEngine = ({ rootUri, modelUri, defaultValue }): void => {
const engine = new Engine({
domElement: domRef.current,
wsUrl,
rootUri,
modelUri,
defaultValue
})

defineTheme(engine.monaco)
monacoRef.current.monaco = engine.monaco
monacoRef.current.editor = engine.ITextModal
setEditorTheme('Tomorrow')
}

getMonacoUriAndContent(_apiParams)
.then(res => {
const rootUri = res?.data?.workspace
const modelUri = res?.data?.path
const defaultValue = res?.data?.content

startEngine({ rootUri, modelUri, defaultValue })
})
.catch(() => {
startEngine({ rootUri: null, modelUri: null, defaultValue: undefined })
})
.finally(() => {
setLoading(false)
})

return (): void => {
Object.assign(_apiParams, {
id: null,
apiId: null,
appId: null
})
}

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])


return {
monacoEdit,
setEditorTheme,
loading,
getEditorContent
}
}

export default useToolMonaco

但是这个时候,问题其实来了,我们履约架构提出了一点需求:

因为语言服务器的工程每次加载编译都很慢,所以需要提前建立 ws 链接加载工程

那么这个时候,原来的设计好像就不是那么实用了,不过,有解:

核心代码:

1
2
// 把 websocket 实例挂在 window 下
const webSocket = window.YQN_MONACO_LSP_WS = this.createWebsocket({ url, socketOptions })
1
2
3
4
5
6
7
8
9
10
11
// 安装语言服务器的时候检测实例,这样既不会影响原来的耦合设计,也可以新增隔离设计

if (!window.YQN_MONACO_LSP_WS) {
const monacoInstance = this.install({ rootUri, options: installOptions })
// eslint-disable-next-line @typescript-eslint/no-use-before-define,no-new

// eslint-disable-next-line no-new
new EngineServer({
wsUrl, socketOptions, clientOptions, monacoInstance
})
}

最终,再新增一个业务hooks给我们的业务同学使用(预建立ws链接,加载工程,这个 hooks 应该放在外层页面提前加载):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { useEffect } from 'react'
import { MonacoServices } from 'monaco-languageclient'
import { monaco, EngineServer } from '../lib/engine'
import { wsUrl } from '../lib/default.config'
import { getMonacoUriAndContent, GetMonacoUriAndContentReqProps } from '../lib/api'

export type UsePreConnectWsProps = (P: { apiParams: GetMonacoUriAndContentReqProps; installOptions?: Record<string, any> }) => void;

/** 请求参数 */
// eslint-disable-next-line no-underscore-dangle
export const _apiParams: GetMonacoUriAndContentReqProps = {
id: null,
apiId: null,
appId: null
}

const usePreConnectWs: UsePreConnectWsProps = ({ apiParams, installOptions = {} }) => {

Object.assign(_apiParams, apiParams)

useEffect(() => {
getMonacoUriAndContent(_apiParams)
.then(res => {
const rootUri = res?.data?.workspace

MonacoServices.install(monaco, { rootUri, ...installOptions })

// eslint-disable-next-line no-new
new EngineServer({
wsUrl,
socketOptions: undefined,
clientOptions: undefined
})
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
}

export default usePreConnectWs

那么,最终来看看我们的效果:

鼠标悬浮提示

输入联想

完美,调研+实际开发 = 5天

到此,Web IDE需求开发结束