前言
dart-pad 总是打不开,打开后运行也比较慢,于是决定自己实现一个简单的dart在线调试环境,平时不想启动服务时用来跑些不复杂的dart代码。
功能说明
在前端编辑器中输入代码,cmd+s或是在终端里输入run,代码通过websocket发送给后端,后端生成一个临时文件,运行该文件后将结果会返回给前端,前端把结果回显到终端里
前端界面如下:
线上demo地址 dart-lab
前端部分
1、编辑器
使用vscode web版 monaco-editor
npm 安装:
> npm install monaco-editor
我的需求不难,所以就不使用打包工具了,直接把文件下载到本地引用
<script src="/lib/monaco-editor/min/vs/loader.js"></script>
初始化部分
var editorContent = '' // 编辑器初始内容
require.config({ paths: { 'vs': '/lib/monaco-editor/min/vs' }});
require(['vs/editor/editor.main', 'vs/basic-languages/dart/dart'], function() {
window.MonacoEnvironment = {
getWorkerUrl: function(workerId, label) {
return `data:text/javascript;charset=utf-8,importScripts("${window.location.origin}/lib/monaco-editor/min/vs/base/worker/workerMain.js");`;
}
};
monacoEditor = monaco.editor.create(document.getElementById('editor'), {
value: editorContent,
automaticLayout: true, // 窗口变化时自适应
language: 'dart', // 要支持的语言
theme: 'vs-dark' // vs code 皮肤
});
monacoEditor.onDidChangeModelContent((event) => { // 内容变化
editorContent = monacoEditor.getValue()
});
monacoEditor.addAction({ // 注册快捷键
id: 'editor',
label: 'Save',
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS], // Ctrl+M or Cmd+M
run: () => {
// 运行代码
}
});
});
重点提worker部分:
worker主要用在比较耗时的任务中,比如代码分析、代码补全、格式化代码等。
一些打包工具都提供了现成的包比如vite中的 vite-plugin-monaco-editor,webpack中的monaco-editor-webpack-plugin。
本文中用不到这些功能,所以配置下getWorkerUrl路径就可以了,不然引用worker时会出现404。
2、终端
需要个终端来显运行后的结果同时可以和后端交互
选用VS Code所使用的terminals终端 web版 xtermjs
简单调用下就可以
const term = new Terminal({
fontFamily: 'Consolas, courier-new, courier, monospace',
fontSize: 12,
rows: 14,
cursorBlink: true, // 光标是否闪动
});
term.open(document.getElementById('terminal'))
如果想让term自动适应屏幕变化,可以使用插件 addon-fit
处理终端输入输出
term.onData(text => {
term.wirte(text);
})
后端部分
文件服务器
由于是个小功能,就不使用Nginx来处理了,使用dart shelf来做静态服务器
final _staticHandler = shelf_static.createStaticHandler('public', defaultDocument: 'index.html');
final httpPort = 8084;
final cascade = Cascade().add(_staticHandler);
final server = await shelf_io.serve(
logRequests().addHandler(cascade.handler),
InternetAddress.anyIPv4, // Allows external connections
httpPort,
);
socket
使用shelf_web_socket处理socket消息
final wsPort = 8085;
var socketHandler = webSocketHandler((webSocket) async {
webSocket.stream.listen((message) {
// 消息监听
// 生成uuid
// 调用dartRun函数
// dartRun(msg, uuid)
// 返回结果给前端
});
});
shelf_io.serve(socketHandler, InternetAddress.anyIPv4, wsPort).then((server) {
print('Serving at ws://${server.address.host}:${server.port}');
});
运行代码
使用Process类来处理临时生成的dart file
Future<Response> dartRun({required String body, required String uuid}) async {
final data = jsonDecode(body);
final tempFile = File('$uuid-temp.dart');
await tempFile.writeAsString(data);
final result = await Process.run(
'dart',
[tempFile.path],
);
await tempFile.delete();
return Response.ok(
jsonEncode({
'err': result.stderr,
'exitCode': result.exitCode,
'stdout': result.stdout
}),
headers: {'Content-Type': 'application/json'},
);
}
代码自动补全功能(我没有开发这个功能)
如果要开发这个功能,有两个方式
使用LSP
如果你希望实现更复杂的代码提示,可以集成 Language Server Protocol (LSP)。通过该功能,你可以提供基于完整语言语法的智能提示、错误验证等,类似于 VSCode 的体验。
前端一般使用官网提供的LSP包monaco-languageclient, 该包提供了完整的LSP功能还例子 后端可以使用lsp_server这个包进行语法分析
自己手动实现
使用编辑器的 registerCompletionItemProvider 功能注册代码补全功能
monaco.languages.registerCompletionItemProvider('dart', {
provideCompletionItems: function(model, position) {
var word = model.getWordUntilPosition(position);
var range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn
};
// 返回补全项
return {
suggestions: [
{
label: 'print',
kind: monaco.languages.CompletionItemKind.Function,
insertText: 'print(${1:message});',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
range: range
},
{
label: 'String',
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: 'String',
range: range
},
{
label: 'int',
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: 'int',
range: range
}
]
};
}
});