4

在线代码调试平台CodeHouse诞生记

 2 years ago
source link: https://jelly.jd.com/article/61d69d6c9b585d01b13fb880
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
JELLY | 在线代码调试平台CodeHouse诞生记
在线代码调试平台CodeHouse诞生记
上传日期:2022.03.03
在线代码调试 CodeHouse 平台,是一个支持在线调试预览代码的平台,实现了代码开箱即用、实时预览、分享代码、一键部署、切换镜像源、安装依赖、支持主流前端框架等功能,首次接入 NutUI 组件库。支持 vue 和 react 组件的在线调试功能,目前雏形已现,未来可期~

在日常工作开发过程中,相信大家经常遇见以下场景:

  • 1、想要快速开发一个 demo 示例,然后进行手机访问,于是不得不使用脚手架新建一个项目,开发上传脚本后部署到预发服务器,再配置代理后手机才能访问;
  • 2、想要和同事分享一个代码功能,需要本地调试好后,压缩代码再发送给他人,或者上传到 git 库,供其他人下载代码后,才能继续后续操作;
  • 3、想要实时预览调试组件库中的示例、想要保存自己的代码片段,都要重复的新建项目,或者新建代码库等等操作;

以上步骤太繁琐了!能不能有个在线代码调试平台,实现开箱即用、实时预览、分享代码、一键上线等功能,来避免这些麻烦? 解决以上问题,就是我们开发在线代码调试平台的初心。

也许大家此时脑海中浮现的一个问题:市面上已经有很多个优秀的在线代码调试平台,为何要再开发一个在线代码调试平台呢?

诚然,市面上已经有很多个优秀的在线代码调试平台,诸如codesandboxcodepenstackblitz等等优秀的网站,然而这些网站尚无法完全满足我们的需求,比如:

  • 1、可用性:公司内部的依赖镜像源是jdnpm,而这些网站无法选择镜像源,都是指定的npm或者使用CDN的方式引入依赖文件;
  • 2、安全性:在线调试平台输入的代码不会造成外泄,保证了代码逻辑的安全性;
  • 3、可扩展性:可以支持部门内部的 NutUI 组件库网站 的代码示例调试、 Watchtower 前端异常监控平台 的新建项目示例、代码片段平台的核心输入功能、 Codmi 多功能脚手架 的兼容方案落地等需求;
  • 4、灵活性:网站可以自定义开发满足用户的需求,比如将调试好的代码生成二维码,供手机端扫码访问,以及安装所需版本依赖、切换依赖镜像源、支持多技栈、支持 TypeScript 等等;

综上所述,我们亟需开发一个满足自身需要,并且可以持续迭代优化的在线代码调试平台,于是 CodeHouse 在线代码调试平台 诞生了: https://codehouse.jd.com/

目前在线调试平台还是一位嗷嗷待哺的新生儿,具备了基本的功能,更多功能正待我们开发中。 首先访问网站映入眼帘的是一个技术栈的选择弹窗:

11d36b920f7b79f2.png

选择要开发的前端技术栈:VueReactHtml。进入开发页面,开发完代码后,点击下面的运行代码按钮,右侧预览区域可以实时查看当前代码执行的结果:

11d36b920f7b79f2.png

点击预览代码按钮,还可以将当前代码进行编译后上传到线上,生成预览的二维码,手机扫描即可访问。

编辑区域的右上角还有当前安装的依赖文件以及版本号,如果不满足当前需求还可以点击下面的安装依赖按钮,安装所需版本的依赖(当然,支持切换依赖源和安装相同依赖源的不同版本功能还在紧锣密鼓的开发中,敬请期待)。

此外,切换基础示例和组件示例还可以看到提供的代码示例,以及点击右上角的分享按钮可以将当前代码示例分享给好友。

在使用过程中,有任何使用的不爽的地方或者想要增加的功能,可以点击网站下面的“ 联系我们 ”反馈建议哦,我们会第一时间进行回复,共同守护该平台的茁壮成长~

基础版的在线代码调试平台完成后,已经满足组件库的代码示例在线调试功能,所以我们首先接入了部门的 NutUI 组件库 ,在元旦发版之后 NutUI 组件库已经支持 Vue React 两个技术栈版本了,在线代码调试平台的接入,不但确保了组件库给出文档的正确性,同时也方便了用户在线调试组件库示例,如虎添翼。

11d36b920f7b79f2.png

如上图所示,目前部分组件接入了在线代码调试平台,点击在线调试按钮即可一睹芳容。后续会全部接入,敬请期待~

好了,对在线代码平台有个基大概的认识之后,大家是否对在线代码调试平台产生了兴趣,如何实现在线调试代码?如何实现在线安装依赖?如何实现在线预览效果的呢?接下来让我们带着这些疑问一探究竟!

1、客户端编辑代码

客户端主要负责代码的输入和展示功能,如下图区域:

11d36b920f7b79f2.png

要论实现在线编辑输入代码的功能,codemirror当仁不让的站了出来,codemirror 可以在线编辑代码,风格包括 js, vue, react, c++等多种语言。比较强大可以自行配置语言模式。能够做到自动补全,代码折叠,可配置键盘事件,多种风格、能完成查找替换,括号匹配,分栏显示,显示行号,自行配置字体大小和风格,等强大的功能。(补充一句,后续我们也会逐渐增加这些功能,提供更好的编码体验)

因为网站使用 React 技术栈进行搭建,首先使用 npm 安装对应的依赖:npm i @uiw/react-codemirror。 然后引入其对应的各种语言包,这里仅为示例:

import "codemirror/mode/vue/vue"; //引入语言及语法模式
import "codemirror/mode/javascript/javascript"; //引入语言及语法模式
import "codemirror/mode/sass/sass"; //引入语言及语法模式
import "codemirror/theme/solarized.css"; //主题样式1
import "codemirror/theme/monokai.css"; //主题样式2
import "codemirror/addon/display/autorefresh"; //该插件可以确保如果编辑器在初始化时不可见,它将在第一次可见时刷新。
import "codemirror/addon/edit/matchbrackets"; //只要光标位于它们旁边,就会突出显示匹配的括号

调用方式如下,设置好对应的参数即可:

<CodeMirror value={sourceCode} options={{ theme: "monokai", mode: type,
styleActiveLine: true, }} onChange={(e, obj) => changeTextAre(e, obj)}
onBlur={(e) => blurTextArea(e)} />

在输入完代码之后,点击运行代码按钮,前端会把当前输入框中的代码进行压缩后附着在请求的 URL 上,形如:https://codehouse.jd.com/code/index.html?type=vue&codeMainJsValue=xxx&codeAppValue=xxx&codeScssValue=xxx,向服务端发起 GET 请求。

2、服务器编译代码

用户在客户端输入代码之后,点击运行按钮就可以通过 GET 请求,将代码通过 URL 传递到服务端,接下来服务端如何编译响应才是重中之重!

服务器在接收到前端的 GET 请求,解析 URL 上附带的代码信息,由于前端只是把 main.js、app.vue、app.scss 代码发送给服务端,并没有编写 Html 代码, 所以服务端需要将已经写好的 Html 代码返还给前端,需要做的是在 Html 文件中将 入口 main.js 的 url 上携带 JS 代码放置在 <script> 标签上返还给前端:

<!--省略非关键代码-->
<body>
  <div id="app"></div>
  <script type="module" src="./main.js"></script>
</body>
<!--省略非关键代码-->
<body>
  <div id="app"></div>
  <script
    type="module"
    src="./main.js?type=vue&codeMainJsValue=xxxx&codeAppValue=xxxxcodeScssValue=xxxx"
  ></script>
</body>

这样客户端再去渲染 Html 文件的时候,就会再次请求 main.js 文件,其 URL 上携带的代码就会再次传递给服务器,服务器此时就会继续解压拿到的参数 codeMainJsValue,比如 codeMainJsValue 经过解压后:

import { createApp } from "vue";
import App from "./app.vue";
import NutUI from "@nutui/nutui";
import "./app.scss";
import "@nutui/nutui/dist/style.css";
createApp(App).use(NutUI).mount("#app");

上面的代码在服务端进行编译解析,其中需要重点处理 import 语法,需要将其编译为浏览器支持的语法,分为以下几种情况:

1、import 引入相对路径的文件 import App from "./app.vue"; 这种情况稍微简单,把服务端接受到 main.js 文件的 URL 上携带的代码继续挂载 app.vue 文件的路径上,也就是要改成:

import App from "./app.vue?codeAppValue=xxx";

这样浏览器在后续解析的时候,会携带剩余代码继续请求服务器,服务器就可以继续处理后续的请求。

2、import 引入第三方依赖库 import NutUI from "@nutui/nutui";

如果不做处理,服务端是无法找到@nutui/nutui的,所以在服务端需要提前处理这些第三方依赖库,这里我们以@nutui/nutui为例。我们需要在服务端使用 esbuild 进行预编译,那么为什么要进行这一步操作呢?

  • 1、CommonJS 和 UMD 兼容性:因为如果不加以处理,浏览器会将所有的代码视为原生的 ES 模块,直接返回第三方依赖库,浏览器是无法识别解析的,所以需要将 CommonJS 或 UMD 发布的依赖项转换为 ESM。
  • 2、提高性能:将内部模块的 ESM 依赖关系转成单个模块,用来提高页面的加载性能。
const devDependencies = require(packagePath).devDependencies; //获取到package.json文件中的依赖list
const deps = Object.keys(devDependencies); //获取对应的key值
const result = await esbuild.build({
  entryPoints: deps,
  bundle: true,
  format: "esm",
  logLevel: "error",
  splitting: true,
  sourcemap: false,
  outdir: cacheDir, //设置输出的文件目录
  treeShaking: "ignore-annotations",
  metafile: true,
  define: { "process.env.NODE_ENV": '"development"' },
});

于是就可以生成 esbuild 编译后的文件,同时生成了 devDependenciesPath 文件,用来保存文件和对应的路径:

{
  "@nutui/nutui": "/dist/node_modules/@nutui/nutui.js",
  "vue": "/dist/node_modules/vue.js"
}

客户端请求到来之后,服务器判断接受到的是第三方依赖:@nutui/nutui,就去该文件 devDependenciesPath 中查找其对应编译后的代码:

也就是 import NutUI from "@nutui/nutui"; 改为了 import NutUI from './dist/node_modules/@nutui/nutui.js';

这里再提及一点,引入 json 格式的文件,默认是会有缓存的,也就是说即使动态的改变了 json 文件,我们拿到的依然是之前的数据,所以需要做清除 json 文件缓存的处理:

//获取现有依赖
queryDependencies() {
    const packagePath = '../../../dependencies/package.json';
    delete require.cache[require.resolve(packagePath)]; //清除缓存
    const config = require(packagePath); //重新引入依赖
    return config.devDependencies;
}

3、import 按需引入和默认引入的情况 例如按需引入 import { createApp } from "vue";,或者混合引入 import React,{ useEffect, useState } from "react";

按需引入和默认引入文件的情况略有复杂,需要区别出按需引入的文件以及默认引入的文件,进行不同的处理

const replaceCode = code.replace(
  /import\s*(\w*)\s*,?\s*{?\s*([^}\n]*)}?\s*from\s*["|']([^as][\w\/@.-]*)["|'];*\n/g,
  function ($1, $2, $3, $4) {
    const defaultImport = $2 ? $2.trim() : null;
    const mulImport = $3 ? $3.trim() : null;
    const source = $4;
    const objImport = {
      defaultImport,
      mulImport: mulImport ? mulImport.split(",") : null,
      source,
    };
    arrImport.push(objImport);
    return "";
  }
);

这里根据正则表达式匹配出 import 的各种情况,然后对于按需引入的做单独的处理,例如:

import React from "./dist/node_modules/react.js";
const useEffect = React.useEffect;
const useState = React.useState;

对于没有默认导出的情况,比如 import { reactive, toRefs } from "vue";,需要做如下处理:

const defaults = item.source.replace(/[^\w*]/g, "");
strImport += `import * as ${defaults} from '${item.source}';\n`;
item.mulImport.forEach((value) => {
  strImport += `const ${value} = ${defaults}.${value.trim()};\n`;
});

最终改为:

import * as vue from "./dist/node_modules/vue.js";
const reactive = vue.reactive;
const toRefs = vue.toRefs;

这样才能正确的使用 esbuild 编译后的文件代码。

对于 Typescript 开发的代码,引入了 Ts 的编译

esbuild.transformSync(code, {
  loader: "ts",
}).code;

对于 vue 的编译使用了 @vue 提供的编译库:

const compilerSFC = require("@vue/compiler-sfc");
const compilerDOM = require("@vue/compiler-dom");

对于 React 的编译使用了 esbuild

handleCode = esbuild.transformSync(code, {
  loader: "jsx",
}).code;

对于 SCSS 样式文件,通过引入 node-sass,进行编译返还给浏览器编译好的 css 文件:

const compileFile = require("node-sass-online/sass.node.js");
compileFile.Sass.compile(scssCode, function (result) {
  resolve(result.text.toString());
});

通过上述步骤之后,代码

import { createApp } from "vue";
import App from "./app.vue";
import "./app.scss";
createApp(App).mount("#app");

会被编译为

import * as vue from "./dist/node_modules/vue.js";
const createApp = vue.createApp;
import App from "./app.vue?codeAppValue=xxx";
import "./app.scss?codeScssValue=xxx";
createApp(App).mount("#app");

这样服务端将代码进行编译处理后,返还给浏览器进行渲染,浏览器会继续对于其中的引入文件代码,再次请求服务器。

3、服务器安装依赖

在线代码平台还支持了在线安装第三方依赖的功能,由于目前功能还在完善,避免用户安装不同版本的同名依赖,会导致相互覆盖的问题,暂时权限没有对外,待开发成熟后再对外开放权限,不过基本的在线安装依赖功能已经完成,我们来了解一下如何实现在线安装依赖。

11d36b920f7b79f2.png

也许有人会想到,在线安装依赖还不简单,用户输入要安装的依赖名字和版本号,服务端收到请求后执行 npm install <依赖名称><依赖的版本号>,不就行了吗? 如果这样做的话,就会导致在安装依赖的过程中服务端的项目是不可使用的,也就是说一个用户安装了依赖,就会影响同时在线的其他用户。所以我们需要在服务端项目代码并列的文件夹中去独立的操作安装依赖的功能。

在接受到客户端在线安装依赖的请求之后,服务端就会开启子进程,切换到和后端项目并列的文件夹中,我们暂且称之为 dependencies,针对提供的依赖名称、依赖版本号和依赖的镜像源,进行安装依赖文件。

const childProcess = require("child_process");
const spawnSync = childProcess.spawnSync;
const runShellSync = (shell) => {
  spawnSync(shell, { stdio: ["inherit", "inherit", "inherit"], shell: true });
};
runShellSync(
  `cd ${filePath.dependencies} && npm install ${dependent}@${dependentVersion} -D --registry=${dependentSource}`
);

安装完毕后,服务端监听对应的依赖文件夹发生了变化,

const chokidar = require("chokidar");
const watcher = chokidar.watch(watchPath, {
  ignored: /[\/\\]\./,
  persistent: true,
});

则重新启动 esbuild 的编译,生成新的编译后的文件,待前端用户后续的使用。

4、服务器在线预览

在线代码调试平台还提供了实时预览功能,点击预览按钮,就可以把当前的代码部署到线上,生成对应的二维码供用户手机访问。 这里我们既可以自己开发编译代码功能,当前也可以借用现有的框架进行编译,比如大名鼎鼎的 vite,服务端在接收到用户在线预览的请求之后,需要把接收到的代码,分别对应的生成不同的文件,放在独立的文件夹中。然后同样启动子进程进行编译:

runShellSync(
  `cd ${filePath.dependencies} && npx vite build --mode '${type}${currPage}'`
);

由于编译完毕后,会对应的生成编译后的文件夹,服务端在监听到文件发生变化之后,就可以获取到编译后的代码了。接下来需要将其部署到公司的 OSS 平台,这里推荐一款非常优秀的 jdoss 快速上传、下载插件

const UploadOssPlugin = require("@jd/upload-oss-tools");
const optionUploadOssPlugin = Object.assign({
      localFullPath: // 被上传的本地绝对路径,自行配置
      access: //生成的 access key
      secret: //生成的 secret key
      site:  // 远程 oss 路径
      useHttps:  // 是否启用https通信 默认true
      cover:  // 是否覆盖远程空间文件 默认true
      printCdnFile: // 是否手动刷新cdn文件 默认false
      bucket:// 空间名字 仅能由小写字母、数字、点号(.)、中划线(-)组成
      folder: // 空间文件名称 非必填
      ignoreRegexp: // 排除的文件规则 正则字符串
      uploadEnd: function (res) {
        // 文件上传完毕回调函数,返回上传文件的总数,成功数量,失败数量,未上传数量
      },
    });

发布完成之后,再删除该文件夹,减少服务端的代码量。然后将生成的 html 链接在返还给前端,前端使用 qrcode 生成对应的二维码,供用户扫码看效果。

import QRCode from "qrcode";
QRCode.toCanvas("dom元素", url, {
  margin: 0,
  width: 100,
});

本篇文章将 在线代码调试平台 的核心功能做了简要的概述,实际完成开发起来,里面的细节还是费了不少的力气,前后也查阅了许多的资料,熟悉 vite 原理的同学相信看到这里也发现,其核心原理借鉴了尤大大的 vite,站在巨人的肩膀上完成了该平台,在此致以万分的感谢~

最后,在线代码平台的雏形已现,接下来可扩展性还很强大,即可以作为一个独立的平台,又可插拔式的接入各个内部系统,互相支撑,未来可期!

[1] codehouse 在线代码调试平台: https://codehouse.jd.com/

[2] NutUI 组件库网站: https://nutui.jd.com/#/

[3] Watchtower 前端异常监控平台: http://watchtower.jd.com/

[4] Codmi 多功能脚手架: http://npm.m.jd.com/package/@jd/codmi-cli

[5] codemirror 官方网站: https://codemirror.net/6/

[6] vite 官网: https://vitejs.cn/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK