从零部署一个在线 C 编辑器:Gitee、Judge0 和 EdgeOne Makers

番星

我之前的在线 C 编辑器跑在 EdgeOne Pages 上,但一直有几个问题:函数写法不兼容、中文输入法会引入非法字符、部署后前端缓存导致新旧版本冲突。

这次把它重做了一遍,改名叫番星C语言编辑器,迁移到了 EdgeOne Makers。整体思路没变:打开网页,写 C 代码,点一下运行,就能看到编译和运行结果。不需要本地装 GCC,也不需要配置 IDE。

这篇文章记录实现过程、迁移细节,以及从 Gitee 部署到 EdgeOne Makers 的完整步骤。

线上地址:

1
https://c.xingbox.me

先看成品

页面主要分成三块:

  • 左侧是 C 代码编辑器
  • 右上是运行输出
  • 右下是标准输入 stdin

默认代码是:

1
2
3
4
5
6
#include <stdio.h>

int main() {
puts("Hello, World!");
return 0;
}

点击「编译运行」之后,前端把代码提交到 /api 边缘函数,边缘函数调用 Judge0 编译运行后返回 stdout、stderr、compile_output、运行时间和内存占用。

如果代码有语法错误,就展示编译错误;如果运行成功,就展示程序输出。

页面底部有一行部署状态检测,可以直接看到当前版本和 /api 是否正常:

1
JS: v2026.07.09-edgeone | edgeApiUrl: /api | /api: 正常

如果 /api 还没生效(比如浏览器还在用旧版 JS),会显示:

1
edgeApiUrl: 未配置 | /api: 旧版JS,需强刷

这个检测在排查部署问题时很实用。


技术选型

部分方案
页面原生 HTML/CSS/JS
编辑器CodeMirror 5
编译运行Judge0 CE
API 函数EdgeOne Makers Edge Functions
代理函数EdgeOne Makers Edge Functions(备用)
部署Gitee + EdgeOne Makers

没有用 Vue、React,也没有引入复杂构建流程。原因很直接:这个工具的交互不复杂,核心问题不是组件状态管理,而是如何稳定地把代码送到远端编译服务并拿回结果。


整体原理

迁移到 EdgeOne Makers 之后,链路变成了这样:

1
2
3
4
5
6
7
8
9
10
用户浏览器
↓ 写代码、点运行
前端页面
↓ POST /api(明文代码)
EdgeOne Makers Edge Function
↓ base64 编码后转发到 Judge0
Judge0 CE
↓ 返回编译运行结果(base64)
Edge Function 解码后返回 JSON
前端页面展示 stdout / stderr / compile_output

和之前 EdgeOne Pages 的方案相比,主要区别是:

  • 之前前端自己 base64 编码,通过 /proxy?url=... 透传给 Judge0
  • 现在前端发送明文代码,/api 边缘函数负责编码、调用、解码,前端拿到的是直接可用的 JSON

这样做的好处是前端逻辑更简单,编码解码全部在边缘节点完成,减少了前端出错的可能。


为什么从 EdgeOne Pages 迁移到 EdgeOne Makers

之前用的是 EdgeOne Pages Functions,函数格式是 Cloudflare Workers 风格:

1
2
3
4
5
export default {
async fetch(request, env) {
// ...
}
};

EdgeOne Makers 要求的格式是:

1
2
3
4
export default async function onRequest(context) {
const { request } = context;
// ...
}

迁移到 EdgeOne Makers 之后,函数格式统一了,也更容易和 Cloud Functions、Agents 等其他模块配合。


前端配置

页面里放了一个全局配置:

1
2
3
4
5
6
7
8
window.__APP_CONFIG__ = {
version: 'v2026.07.09-edgeone',
judge0ApiUrl: 'https://ce.judge0.com',
edgeApiUrl: '/api',
judge0ProxyUrl: '/proxy',
useProxy: true,
backendRunUrl: ''
};

各字段含义:

  • edgeApiUrl:EdgeOne Makers 的 /api 地址(主路径)
  • judge0ProxyUrl:旧的 /proxy 代理地址(备用路径)
  • judge0ApiUrl:真实的 Judge0 地址
  • backendRunUrl:备用 Node 后端地址,目前留空

前端的提交逻辑是:先尝试 /api,如果失败再回退到 /proxy,最后回退到直连 Judge0。这样即使某个环节出问题,编译功能也不会完全挂掉。


/api 边缘函数

这是这次迁移的核心改动。文件在 edge-functions/api/index.js,自动映射为 /api 路由。

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
const JUDGE0_URL = 'https://ce.judge0.com';

function encodeBase64Utf8(text) {
const bytes = new TextEncoder().encode(text || '');
let binary = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
}
return btoa(binary);
}

function decodeBase64Utf8(text) {
return new TextDecoder().decode(
Uint8Array.from(atob(text), c => c.charCodeAt(0))
);
}

export default async function onRequest(context) {
const req = context instanceof Request ? context : context.request;

if (req.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: CORS_HEADERS });
}

if (req.method === 'GET') {
return new Response(JSON.stringify({ ok: true }), {
headers: CORS_HEADERS
});
}

const body = await req.json();
const sourceCode = body.source_code || '';
const stdin = body.stdin || '';
const languageId = Number(body.language_id) || 50;

const response = await fetch(
`${JUDGE0_URL}/submissions?base64_encoded=true&wait=true`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
language_id: languageId,
source_code: encodeBase64Utf8(sourceCode),
stdin: encodeBase64Utf8(stdin),
cpu_time_limit: 5,
memory_limit: 128000,
}),
}
);

const result = await response.json();

return new Response(JSON.stringify({
stdout: result.stdout ? decodeBase64Utf8(result.stdout) : '',
stderr: result.stderr ? decodeBase64Utf8(result.stderr) : '',
compile_output: result.compile_output
? decodeBase64Utf8(result.compile_output) : '',
message: result.message || '',
status: result.status || { id: 10, description: 'Internal Error' },
time: result.time ?? null,
memory: result.memory ?? null,
}), { headers: CORS_HEADERS });
}

几个注意点:

  1. 入参兼容context 可能是请求对象本身,也可能是包含 request 属性的上下文对象,所以做了 context instanceof Request 判断。
  2. GET 健康检查:访问 GET /api 会返回 {"ok":true},前端的部署状态检测就是用这个接口。
  3. base64 编码解码全在边缘节点完成:前端发的是明文代码,拿到的是明文结果。

/proxy 代理函数(备用)

edge-functions/proxy.js 仍然保留了旧的 ?url= 代理方式,作为 /api 失败时的回退路径。

它和之前逻辑一样:读 ?url= 参数,验证域名白名单,转发到 Judge0。只是格式也从 fetch(request, env) 改成了 onRequest(context)


迁移中踩的坑

1. 函数格式不兼容

EdgeOne Pages Functions 用 { fetch(request, env) } 写法,EdgeOne Makers 用 onRequest(context) 写法。直接推旧代码上去,函数不会报错但也不会执行。

2. \n 被当成真实换行符

默认模板原来用的是:

1
printf("Hello, World!\n");

但在某些环境下,JavaScript 模板字符串里的 \n(反斜杠+n,两个字符)会被处理成真实换行符,导致 C 编译器看到字符串跨行,报 missing terminating " character

最后的解决办法有两个:

  • 默认模板改用 puts("Hello, World!"),从根源避开反斜杠
  • STORAGE_VERSION 从 2 升到 3,强制清除所有旧 localStorage 缓存

3. /api 部署了但前端没调用

部署后 /api 边缘函数已经注册成功,但浏览器还在加载旧版 JS,没有 edgeApiUrl 配置。需要强刷页面(Ctrl+Shift+R)才能加载新版 JS。

这就是为什么加了部署状态检测——一眼就能看出浏览器跑的是新版还是旧版。

4. 中文输入法导致编译失败

用户在中文输入法下打出的弯引号 ""、全角标点 ,;: 等,在 C 代码里是非法字符。前端加了 sanitizeCode() 函数,提交前自动替换:

  • 弯引号 ""'' → 移除
  • 全角双引号 → 英文引号 "
  • 全角逗号 → 英文逗号 ,
  • 全角分号 → 英文分号 ;
  • 以及其他十几种常见的中文标点

部署步骤

第一步:把项目推到 Gitee

仓库地址:

1
git@gitee.com:knight3fax/c-online-compiler.git

确认仓库里至少有这些文件:

1
2
3
4
5
6
index.html
app.js
style.css
edge-functions/api/index.js
edge-functions/proxy.js
_routes.json

第二步:在 EdgeOne Makers 创建项目

在 EdgeOne Makers 控制台新建项目,关联 Gitee 仓库:

1
knight3fax/c-online-compiler

构建配置:

1
2
3
框架预设:Other / Static
构建命令:留空
输出目录:/

项目没有打包步骤,根目录本身就是静态站点的输出目录。

第三步:确认 Edge Functions 生效

部署完成后,访问:

1
https://c.xingbox.me/api

如果返回 {"ok":true,...},说明 /api 函数已经生效。

也可以访问:

1
https://c.xingbox.me/proxy

如果返回 Missing url parameter,说明 /proxy 函数也正常。

EdgeOne Makers 控制台的 edgeFunctionsRoutes 应该能看到:

1
2
3
4
[
{ "routePath": "/api", "runtime": "Edge" },
{ "routePath": "/proxy", "runtime": "Edge" }
]

第四步:绑定域名

在 EdgeOne Makers 项目设置里绑定自定义域名 c.xingbox.me,按提示配置 DNS CNAME 记录,等证书签发和 DNS 生效后即可访问。

第五步:验证编译功能

打开页面,点「编译运行」,底部应该显示:

1
2
3
[输出]
Hello, World!
[耗时] xxxms | 后端: EdgeOne /api

如果显示的是”后端: Judge0 CE (官方)”,说明 /api 没走通,走了 proxy 回退,检查页脚的部署状态检测信息。


常见问题排查

页面能打开,但点击运行失败

先访问 https://c.xingbox.me/api 看是否返回 {"ok":true}

如果 /api 正常,打开浏览器开发者工具看 Network 面板的请求状态码。

页脚显示”旧版JS,需强刷”

浏览器缓存了旧版 JS 文件。按 Ctrl+Shift+R(或 Cmd+Shift+R)强刷页面即可。

/api 返回 500

打开浏览器开发者工具,看 /api 响应的 JSON 里有没有 errorstack 字段,通常会说明具体哪一步出了问题。

返回 429

这是 Judge0 公共服务限流。等一会儿再试,或者自己部署 Judge0 实例。


目前的限制

  • 只支持 C 语言(GCC 12.2.0)
  • 使用 Judge0 CE 公共服务,可能会被限流
  • 运行时间限制 5 秒,代码长度限制 32KB
  • 依赖 cdnjs 加载 CodeMirror,网络差时可能加载慢

后续可以考虑:

  • 增加 C++、Python、Java 等语言
  • 把 CodeMirror 资源本地化
  • 接入自建 Judge0 实例减少限流

总结

这个编辑器本质上是”静态前端 + 边缘函数 + 第三方编译 API”的组合。迁移到 EdgeOne Makers 之后,函数格式更规范,API 链路也更简洁——前端不再需要自己做 base64 编码,边缘函数全部搞定。

对我而言,这个项目最有价值的地方不是技术多高级,而是链路足够完整:从写代码到看到运行结果,一个页面闭环。教学场景下用起来很顺手。



本文为 AI 辅助编写,重写版本

  • 标题: 从零部署一个在线 C 编辑器:Gitee、Judge0 和 EdgeOne Makers
  • 作者: 番星
  • 创建于 : 2026-07-09 22:00:00
  • 更新于 : 2026-07-09 22:00:00
  • 链接: https://xingbox.me/fanxing-online-c-edgeone-judge0/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。