实现一个简单的dart在线运行环境

240 阅读2分钟

前言

dart-pad 总是打不开,打开后运行也比较慢,于是决定自己实现一个简单的dart在线调试环境,平时不想启动服务时用来跑些不复杂的dart代码。

功能说明

在前端编辑器中输入代码,cmd+s或是在终端里输入run,代码通过websocket发送给后端,后端生成一个临时文件,运行该文件后将结果会返回给前端,前端把结果回显到终端里

前端界面如下:

image.png

线上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
          }
        ]
      };
    }
 });

最后本文源代码我放到了git上:

github.com/heartbeatsf…