最近给自己的工具站加了一个在线代码运行功能。
地址: https://web.fushengtool.com/dev/code_runner
最开始只是想补一个小能力:用户在页面里输入一段代码,选择语言,点击运行,然后页面返回执行结果。

我原本以为这只是一个很简单的后端接口。真正做起来以后才发现,这个功能和普通的文本处理工具不太一样。

像 JSON 格式化、时间戳转换、Base64 编解码这类工具,大部分逻辑都可以在浏览器里完成。用户输入文本,前端处理后直接展示结果即可。

但在线代码运行器不一样。它需要准备运行环境,还要处理不同语言的执行方式、运行超时、错误输出、结果截断等问题。

这篇文章记录一下我这次实现 Code Runner 的过程。

一、为什么想做这个功能

我平时写代码时,经常会遇到一些很小的验证场景。

比如:

  • 临时写几行 Python,验证一个字符串处理结果;
  • 看一段 JavaScript 示例,想快速跑一下输出;
  • 写正则或时间处理逻辑,想确认边界情况;
  • 写一个小算法,不想专门打开 IDE;
  • 看别人给的示例代码,想马上试一下。

这些需求都不大,但出现频率不低。

如果每次都打开编辑器、新建文件、配置环境,会有点麻烦。所以我就想在工具站里加一个轻量的在线代码运行功能。

第一版先支持 Python,后面再逐步扩展到 JavaScript、Go、Java、C、C++ 等语言。

二、整体设计

整体流程大概分成三层。

前端页面
  ↓
后端接口
  ↓
代码运行模块

前端页面负责:

  • 选择编程语言;
  • 编辑代码;
  • 点击运行;
  • 展示运行结果;
  • 展示错误信息和耗时。

后端接口负责:

  • 校验参数;
  • 判断语言是否支持;
  • 调用对应语言的运行逻辑;
  • 返回统一格式的结果。

代码运行模块负责:

  • 准备代码文件;
  • 执行对应命令;
  • 收集标准输出;
  • 收集错误输出;
  • 控制运行时间;
  • 处理异常情况。

拆成这几层以后,前端和后端都比较清楚。前端不用关心每种语言怎么运行,后端也可以把不同语言的差异收敛到配置里。

三、先从 Python 开始

Python 是最适合做第一版的语言。

它不需要编译,写入文件后直接执行即可。

比如用户输入:

print("hello world")

后端只需要把代码保存成临时文件,然后执行:

python main.py

再把输出结果返回给前端。

第一版做完 Python 后,整个流程基本就跑通了:

输入代码 → 提交接口 → 执行代码 → 获取输出 → 返回页面

这个阶段主要解决的是接口结构和返回格式。

我最后返回的数据大概包括:

  • 是否成功;
  • 标准输出;
  • 错误输出;
  • 退出码;
  • 是否超时;
  • 运行耗时。

这样前端展示时会比较灵活。

四、扩展到 JavaScript

JavaScript 的处理方式和 Python 类似。

用户代码保存成文件后,使用 Node.js 执行:

node main.js

这一类解释型语言的接入成本相对较低。

主要要处理的是:

  • 运行环境是否存在;
  • 错误输出如何展示;
  • 执行时间如何限制;
  • 输出太长时如何截断。

做到这里时,我发现如果后面还要继续加语言,不能每种语言都写一大段独立逻辑。否则代码会越来越散。

所以我开始把语言配置抽象出来。

五、把语言差异整理成配置

不同语言真正不同的地方,主要是这些:

  • 代码文件名;
  • 是否需要编译;
  • 编译命令;
  • 运行命令;
  • 默认超时时间;
  • 输出长度限制。

所以可以把每种语言整理成类似这样的配置:

语言:Python
文件名:main.py
运行命令:python main.py
是否需要编译:否
语言:JavaScript
文件名:main.js
运行命令:node main.js
是否需要编译:否
语言:C
文件名:main.c
编译命令:gcc main.c -o main
运行命令:./main
是否需要编译:是

这样后面新增语言时,思路会清楚很多。

不是在业务逻辑里不断加判断,而是新增一份语言配置。

六、编译型语言的处理

支持 C、C++、Java、Go 之后,流程会比 Python 和 JavaScript 多一步。

比如 C 语言:

gcc main.c -o main
./main

Java:

javac Main.java
java Main

Go:

go run main.go

这些语言需要注意几个问题。

第一,编译错误和运行错误要分开。

编译阶段失败,就不应该继续执行运行命令。比如 C 语言少了分号,编译阶段就会报错,这时候直接把编译错误返回给用户即可。

第二,不同语言的错误格式不一样。

Python 的 Traceback、Java 的 Exception、GCC 的编译错误、Node.js 的异常提示,看起来都不一样。前端展示时不能强行格式化,只要把原始错误清楚展示出来就可以。

第三,运行时间要有限制。

用户可能写出死循环,也可能代码本身执行比较慢。接口不能一直等待,所以需要设置超时时间。

第四,输出长度要有限制。

如果用户代码输出大量内容,接口和页面都会受到影响。所以需要对 stdout 和 stderr 做最大长度限制。

七、统一返回格式

为了让前端处理简单,我把所有语言的结果都统一成一套格式。

比如:

{
  "success": true,
  "stdout": "hello world",
  "stderr": "",
  "exit_code": 0,
  "timeout": false,
  "duration_ms": 120
}

如果运行失败,也使用同样的结构:

{
  "success": false,
  "stdout": "",
  "stderr": "SyntaxError: ...",
  "exit_code": 1,
  "timeout": false,
  "duration_ms": 80
}

这样前端不用针对每种语言写一套展示逻辑。

成功就展示输出,失败就展示错误。超时就给出超时提示。

八、前端体验

前端页面我主要关注几个点。

第一,打开页面要能直接试。

每种语言最好有默认示例代码。用户不用自己先想写什么,打开就能点运行。

第二,输出区域要清楚。

标准输出和错误输出最好分开展示。否则用户分不清到底是代码正常输出,还是运行错误。

第三,按钮状态要明确。

运行中要禁用按钮,避免用户连续点击提交多个请求。

第四,错误提示不要太笼统。

如果是代码报错,就展示代码错误。如果是接口异常,就展示接口异常。不要所有情况都只提示“运行失败”。

九、这次做完后的感受

做这个功能之前,我以为重点是“怎么把代码跑起来”。

做完以后,我觉得重点其实是“怎么把结果稳定地返回给用户”。

因为代码运行本身只是其中一步,前后还要处理很多细节:

  • 参数校验;
  • 临时文件管理;
  • 多语言配置;
  • 编译和运行流程;
  • 超时控制;
  • 输出截断;
  • 错误展示;
  • 前端状态管理。

这些细节都不算特别难,但少一个就会影响体验。

十、后续计划

后面我准备继续优化几个方向。

  1. 增加更多示例

给每种语言准备一些常见示例,比如字符串处理、数组遍历、时间格式化等。

  1. 优化错误提示

常见错误可以做一些简单说明,让用户更容易看懂。

  1. 支持运行历史

登录用户可以保存最近运行过的代码片段,下次继续编辑。

  1. 优化移动端体验

手机上写代码不一定方便,但查看和运行简单示例还是有需求。

  1. 逐步增加语言

先把常用语言跑稳定,再考虑更多语言,不急着一次性加太多。

十一、总结

在线代码运行器看起来是一个小功能,但实现时会遇到不少细节。

我的经验是:

  • 先从 Python 这类简单语言开始;
  • 返回格式一开始就统一;
  • 编译错误和运行错误分开处理;
  • 一定要设置超时时间;
  • 输出内容要做长度限制;
  • 前端要把 stdout 和 stderr 展示清楚;
  • 多语言最好用配置方式扩展。

如果你也想做类似功能,建议不要一上来就支持很多语言。

先把一两个语言的完整链路跑通,再逐步扩展。这样后面遇到问题时,排查会轻松很多。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐