线上地址: rucode
github: github
PC 端
移动端
前言
1.js 外的语言学习
刷题时边学习了几门语言,学习一些语法时有在线编辑器的需求
用过的几个在线编辑器
语言支持较少,运行频率有很大限制,运行速度比较快,支持输入,无代码格式化
运行频率无限制,运行速度快,但是不支持输入,无代码格式化
做的比较专业,运行速度快,无频率限制,强制登录解锁输入,支持依赖安装,代码格式化支持部分语言
支持语言多,单语言多版本支持也多,运行速度比较快,编辑器高亮支持不全,无代码格式化
总的而言,对于在线编辑器分为几点要求
- 语言支持多
- 单语言版本支持多
- 运行速度快
- 无频率限制
- 代码高亮支持
- 代码格式化支持
- 本地保存
- 支持输入
于是,便踏上了自己做个应用的想法。。。
服务端
环境隔离
前置条件, 安装 docker
刚开始装了个 GCC 的镜像,跑了下 C++ 后感觉能行,另外 docker 能做到环境隔离
server
本来想用 Go 写服务端的, 想到服务端不会有太多时,操作 docker 运行代码即可,但是边学边写怕是年底都上不了
转头想还是 Nodejs,写了几天写完了第一个版本
在 github 找了个 dockerode 库,对在 Nodejs 中操作 docker 进行了一层封装
提前写好 Dockerfile 文件,示例 GCC
创建镜像 直接使用命令
docker build -t gcc:11
想过 使用 zx 写脚本,不过中途报错了,没管,后续整上
#!/usr/bin/env zx
const path = require('path');
const root = path.resolve('.');
const dockerPath = 'src/docker';
const nodejs = path.join(root, dockerPath, 'nodejs');
cd(nodejs);
await $`docker build -t nodejs:lts .`;
cd(root);
await $`docker build -t cpp:11 .`;
// await $`docker build -t go:latest ../src/docker/go`;
关于 docker 的使用,我看了这篇教程 Docker 从入门到实践
调试镜像,进入容器试运行,启动一个 bash 终端,进行交互
docker run -t -i gcc:11 /bin/sh
可以提前写好 C++ 测试代码文件,创建 code.cpp 文件,写入以下内容
code.cpp 文件内容
#include <iostream>
int main() {
std::cout << "hello world" << std::endl;
return 0;
}
另起终端,copy 进 docker,直接放 根目录
# mycontainer 是你正在运行的 container id, docker Desktop 可以查看当前运行的容器 id
docker cp ./code.cpp mycontainer:/
在 docker 容器终端编译运行,会输出 hello world,demo 验证结束
# 编译
g++ code.cpp -o code.out
# 运行
./code.out
API 服务
- Koa
- koa-router
做好入参校验
调起 docker 并运行代码
详情 查看 dockerode 文档
- 提前定义好每种语言运行的指令
- 将代码写入 docker
这里没有将代码写入文件 copy 进 docker ,而是直接使用 cat 命令写进 docker 中, 标准输入也写入
- 创建容器
输入运行代码的 bash 命令,禁用容器网络,容器创建成功定义 移除方法
- 获取终端输出
- 格式化输出
剔除了终端转义字符,在 浏览器端 识别不了,不过后期可以将这些字符(大部分是颜色代码)输出到浏览器端
剔除超出长度的部分
- 超出运行时间,终止并移除 container,超时机制
const timeoutSig = setTimeout(handleOutput, DockerRunConfig.timeout);
- 监听运行情况,结束后直接处理输出
container?.wait((status) => {
if (!status || status?.Status === DockerRunStatus.exited) {
clearTimeout(timeoutSig);
handleOutput();
}
});
- 标准输入的处理
- C++
g++ code.cpp -o code.out && ./code.out < input.txt
- 服务端处理结束,增加编程语言需要去 docker hub 找对应镜像,或者自己做一个,目前使用的 官方镜像比较大,1 GB 左右
客户端
编辑器
自己撸一个也可以,但是没必要,咱做的是应用,直接使用现成的成熟的编辑器
monaco editor vs codemirror
- 初始化 monaco editor,并保存示例
useEffect(() => {
if (monacoRef.current) {
const codeCache = storage.get(CodeStorageKey[type]);
editorRef.current = monaco.editor.create(monacoRef.current, {
value: codeCache || template[type],
language: languageMap[type],
theme: themeType,
formatOnType: true,
smoothScrolling: true,
formatOnPaste: true,
readOnly: false,
});
return () => editorRef.current?.dispose();
}
}, [type, themeType]);
- 代码补全以及错误提示
monaco 实现了 ts/js 语言的,采用worker的方式,语法解析需要耗费大量时间,使用worker来异步处理返回结果
import * as monaco from 'monaco-editor';
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
// @ts-ignore
self.MonacoEnvironment = {
getWorker(_: any, label: string) {
if (label === 'typescript' || label === 'javascript') {
return new tsWorker();
}
return new editorWorker();
},
};
monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true);
daisyUI
纯 CSS 组件库,风格清新简约,使用简单
在其上包装了一层,暴露统一接口出去,使用起来跟其他组件库无异
状态管理
mobx + mobx-persist-store 持久化
- 存设置、语言选择、主题等
- 管理 Toast
代码格式化
monaco 支持 Js 的格式化
其他语言的在 github 找到个 clang-format 的库,支持 C/C++/Java 等语言,作者编译成了 wasm 模块
自己编译的话需要费点时间,5 年未更新,有些仓库分支有更新,留意 PR
包装成 hook 使用
function useClangFormat({ onCodeFormatDone }: Props) {
const [clangFormat, setClangFormat] = useState<ProcessManager>();
const clangFormatRef = useRef<ProcessManager>();
useEffect(() => {
let clangFormat = new ProcessManager('clang-format', 'clang-format');
clangFormat.start();
clangFormat.workerReady = () => {
setClangFormat(clangFormat);
};
// @ts-ignore
clangFormat.workerFormatDone = (args) => {
onCodeFormatDone(args?.result);
};
clangFormatRef.current = clangFormat;
}, []);
return [clangFormat];
}
其他 语言的需要在找找
自动化构建部署
利用 github action 做构建部署,账号信息 保存到 action secret 中
- 客户端
-
main 分支 client 文件下有变更触发构建部署
-
构建代码
-
往 oss 推文件
-
往 服务器推文件
-
重启 nginx
# This is a basic workflow to help you get started with Actions
name: CI
# Controls when the action will run.
on:
# Triggers the workflow on push or pull request events but only for the master branch
push:
branches: [main]
paths:
- 'client/**'
- '!client/config/nginx.conf'
pull_request:
branches: [main]
paths:
- 'client/**'
- '!client/config/nginx.conf'
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build-client:
# The type of runner that the job will run on
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./client
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- name: Use Node.js 14.17.0
uses: actions/setup-node@v1
with:
node-version: 14.17.0
- name: Install pnpm
run: npm i pnpm -g
- name: Install Packages
run: pnpm install
- name: Build
run: pnpm build
# cdn 部署
- name: Deploy static source
run: pnpm cdn ${{ secrets.ACCESS_KEY_ID }} ${{ secrets.ACCESS_KEY_SECRET }} ${{ secrets.OSS_BUCKET }} ${{ secrets.OSS_REGION }}
# 移除 部署 cdn 的文件
- name: remove static file which is deploy cdn
run: rm -rf dist/assets
# 推到服务器
- name: Deploy to Server # 第二步,rsync推文件
uses: AEnterprise/rsync-deploy@v1.0 # 使用别人包装好的步骤镜像
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} # 引用配置,SSH私钥
ARGS: -avz --delete --exclude='*.pyc' # rsync参数,排除.pyc文件
SERVER_PORT: ${{ secrets.SSH_PORT }} # SSH端口
FOLDER: ./client/dist # 要推送的文件夹,路径相对于代码仓库的根目录
SERVER_IP: ${{ secrets.SSH_HOST }} # 引用配置,服务器的host名(IP或者域名domain.com)
USERNAME: ${{ secrets.SSH_USER }} # 引用配置,服务器登录名
SERVER_DESTINATION: /root/project/runcodeclient # 部署到目标文件夹
# nginx 重启
- name: Restart server # 第三步,重启服务
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SSH_HOST }} # 下面三个配置与上一步类似
username: ${{ secrets.SSH_USER }}
port: ${{ secrets.SSH_PORT }} # SSH端口
key: ${{ secrets.DEPLOY_KEY }}
# 重启的脚本,根据自身情况做相应改动,一般要做的是migrate数据库以及重启服务器
script: |
nginx -s reload
服务端
main 分支 server 文件夹有变动触发构建部署
-
往服务器推文件
-
进服务器 编译代码
-
kill 掉服务,重启
# This is a basic workflow to help you get started with Actions
name: CI
# Controls when the action will run.
on:
# Triggers the workflow on push or pull request events but only for the master branch
push:
branches: [main]
paths:
- 'server/**'
- '!server/config/nginx.conf'
pull_request:
branches: [main]
paths:
- 'server/**'
- '!server/config/nginx.conf'
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
build-server:
# The type of runner that the job will run on
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./server
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Deploy to Server # 第二步,rsync推文件
uses: AEnterprise/rsync-deploy@v1.0 # 使用别人包装好的步骤镜像
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} # 引用配置,SSH私钥
ARGS: -avz --delete --exclude='*.pyc' # rsync参数,排除.pyc文件
SERVER_PORT: ${{ secrets.SSH_PORT }} # SSH端口
FOLDER: ./server # 要推送的文件夹,路径相对于代码仓库的根目录
SERVER_IP: ${{ secrets.SSH_HOST }} # 引用配置,服务器的host名(IP或者域名domain.com)
USERNAME: ${{ secrets.SSH_USER }} # 引用配置,服务器登录名
SERVER_DESTINATION: /root/project/runcodeserver/ # 部署到目标文件夹
- name: Restart server # 第三步,重启服务
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SSH_HOST }} # 下面三个配置与上一步类似
username: ${{ secrets.SSH_USER }}
port: ${{ secrets.SSH_PORT }} # SSH端口
key: ${{ secrets.DEPLOY_KEY }}
# 重启的脚本,根据自身情况做相应改动,一般要做的是migrate数据库以及重启服务器
script: |
cd project/runcodeserver/server
pnpm install
pnpm build
pnpm kill
pnpm run deploy
nginx -s reload
nginx 文件变动的更新
监听两个 nginx 配置文件的变动
-
替换服务器 nginx 配置文件
-
重启 nginx
# This is a basic workflow to help you get started with Actions
name: CI
# Controls when the action will run.
on:
# Triggers the workflow on push or pull request events but only for the master branch
push:
branches: [main]
paths:
- 'client/config/nginx.conf'
pull_request:
branches: [main]
paths:
- 'client/config/nginx.conf'
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build-client:
# The type of runner that the job will run on
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./client
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- name: Deploy to Server # 第二步,rsync推文件
uses: AEnterprise/rsync-deploy@v1.0 # 使用别人包装好的步骤镜像
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} # 引用配置,SSH私钥
ARGS: -avz --delete --exclude='*.pyc' # rsync参数,排除.pyc文件
SERVER_PORT: ${{ secrets.SSH_PORT }} # SSH端口
FOLDER: ./client/config # 要推送的文件夹,路径相对于代码仓库的根目录
SERVER_IP: ${{ secrets.SSH_HOST }} # 引用配置,服务器的host名(IP或者域名domain.com)
USERNAME: ${{ secrets.SSH_USER }} # 引用配置,服务器登录名
SERVER_DESTINATION: /root/project/runcodeclient/ # 部署到目标文件夹
- name: Restart server # 第三步,重启服务
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SSH_HOST }} # 下面三个配置与上一步类似
username: ${{ secrets.SSH_USER }}
port: ${{ secrets.SSH_PORT }} # SSH端口
key: ${{ secrets.DEPLOY_KEY }}
# 重启的脚本,根据自身情况做相应改动,一般要做的是migrate数据库以及重启服务器
script: |
mv -f project/runcodeclient/config/nginx.conf /etc/nginx/conf.d/runcodec.conf
systemctl restart nginx
做完这些,main 分支推代码 稍等 3 分钟即到线上
展望
后续会持续更新功能与修复 bug
- 格式化支持度目前 50%
- 语言覆盖不够
- 标准输入目前只支持 C++