# online-code **Repository Path**: yu-qian/online-code ## Basic Information - **Project Name**: online-code - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-05-26 - **Last Updated**: 2021-06-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 1 功能介绍 ### 1.1 功能 - 简易的在线代码编辑页面,适用于 Python/HTML/CSS 教学 - 编辑代码,并输出结果(支持 Turtle 海龟图) - 支持清晰地截图代码及结果 - 支持保存代码和下载代码 - 静态页面,启动本地服务运行 ### 1.2 界面布局  ## 2 类库介绍 ### 2.1 Monaco Editor 网页使用的代码编辑器是微软的 [Monaco Editor](https://github.com/microsoft/monaco-editor) 浏览器端代码编辑器,Monaco Editor 是为 VS Code 提供支持的代码编辑器,运行在浏览器环境中,提供代码提示,智能建议等功能。VSCode 中的代码编辑器和 Monaco Editor 使用的很多相同的核心模块,可以将 Monaco Editor 用到自己的项目中,作为云端编辑器的支持。 ### 2.2 skulpt 网页支持 Python 语言编程,由于 Python 的执行需要对应的浏览器端**解释器**进行处理,最终才能输出结果。[Skulpt](https://github.com/skulpt/skulpt) 就是一个通过 JavaScript 编写的运行在浏览器端的 Python 解释器。 ### 2.3 html2canvas 网页支持截图功能,在浏览器上截图的方式大多通过 canvas 画布来进行绘制,现在 HTML5 都支持 canvas 画布功能,在这当中,[html2canvas](https://github.com/niklasvh/html2canvas) 是非常受欢迎的库。Html2canvas 能够将网页上的元素节点绘制到 canvas 里面,并且保留 CSS 样式,通过这种方式能够实现网页的截图功能。 ### 2.4 iziToast 在代码编程过程中和执行过程中会提示一些信息给用户,网页里会使用到 [iziToast](https://izitoast.marcelodolza.com/) 。iziToast 是一个优雅、反应灵敏、灵活且轻巧的通知插件,无依赖性,因此可以单独引入文件直接使用,不受前端的框架影响。 ### 2.5 Font Awesome 在网页使用了很多字体图标,字体图标是很受欢迎的一种图标展示方式,这种方式主要是通过将矢量图形转换成文字字体,再通过代码和文字的 Unicode 来处理图形显示,具有随意缩放、更改颜色等等优点。 [Font Awesome](https://fontawesome.dashgame.com/) 是一套绝佳的图标字体库和 CSS 框架, 提供可缩放的矢量图标,可以使用 CSS 所提供的所有特性对它们进行更改,包括:大小、颜色、阴影或者其它任何支持的效果。 ## 3 技术要点介绍 ### 3.1 全局变量 在浏览器环境中,全局对象是 `window` , 如果直接在全局作用域下声明变量,会成为 `window` 对象下的属性,通过这种方式,可以将一些变量放在全局,从而让所有内部函数都可以访问得到。例如: ```javascript var a = 1 console.log(window.a) // 输出1 ``` ### 3.2 函数提升 提升(Hosting):引擎会在解释 JavaScript 代码之前首先对其进行编译,编译过程中的一部分工作就是找到所有的声明,并用合适的作用域将他们关联起来,这也正是词法作用域的核心内容。 函数提升从简单的方向来理解,就是引擎会将后面声明的一些函数放置在作用域的顶端,因此通过 `function` 声明的函数,即使放在函数调用的后面,也不会影响函数的执行。 在网页里面定义了各种业务逻辑的函数,分别负责不同的功能。下面是一个函数的定义和执行: ```javascript initialCode() function initialCode() { console.log('执行initialCode') } ``` ### 3.3 DOM 操作 DOM(Document Object Model 文档对象模型)是用来描述 HTML 页面布局的一种模型,它会被解析成 DOM 树,最终渲染到页面上,因此为了控制页面展示,需要使用 DOM 操作语法。由于页面没有引入 jQuery 之类的库,网页使用原生 JavaScript 操作 DOM。比如: - `document.getElementById` :通过标签元素的 ID 获取 DOM - `document.createElement` :创建元素 - `innerHTML` :设置元素内部的 HTML 结构 - `appendChild` :插入 DOM - `classList`:操作样式名 代码示例: HTML 定义元素的 ID ```html
``` JavaScript 获取该 ID 的元素,并且可以进行一些操作: ```javascript var el = document.getElementById("pythonEditorContainer") // 下面是简单的操作示例 el.innerHTML = '需要设置的内部HTML结构' el.appendChild('需要插入的DOM') el.classList // 能获取到样式名列表 ``` ### 3.4 职责分明 页面内部通过 `class` 属性来设置元素样式样式名,命名方式用 “-” 来分隔,同时通过元素 ID 来进行 DOM 操作,ID 命名采用驼峰形式,这样能保证两者相对独立,负责样式和负责业务逻辑的代码采取不同的选择器,方便理解和维护。如: ```html ``` 每个函数只执行特定的逻辑,主要负责输入和输出,尽量避免相互关联耦合在一起,造成混乱不清。比如将工具类的函数放置在一个全局的工具对象里面,并且将通用的函数放置在通用对象里面,方便管理和访问。 ### 3.5 本地存储 浏览器支持的存储方式有很多,`cookie` 、 `session` 、 `localStorage` 只是其中的几种,本网页使用了 `cookie` 来存储用户的访问信息,如主题设置等,使用 `localStorage` 来存储用户编写的代码,整个过程没有涉及服务端交互。 考虑到字符的不确定性,有可能在存储和传输过程中造成丢失或者执行异常,因此在存储代码时先经过 Base64 转换,Base64 转换后的字符都是在 ASCII 表内的,可以更加安全的支持网络传输和本地存储。 `localStorage` 使用示例: 设置本地存储 ```javascript // localStorage 存储的形式是key/value的形式 localStorage.setItem("这里是要存储的key值", '要存储的字符串'); ``` 获取本地存储 ```javascript localStorage.getItem("这里是要获取的key值"); ``` ## 4 代码实现 ### 4.1 提示浏览器升级 本网页使用了 [Monaco Editor](https://github.com/microsoft/monaco-editor) 来实现代码编辑,由于 Monaco Editor 本身对 IE11 以下浏览器不再支持,以及很多类库使用现代浏览器的标准,需要提示用户升级浏览器。 在 `index.html` 中提示升级浏览器的代码如下: ```html ``` 代码里面通过 `window.MSInputMethodContext` 和 `document.documentMode` 来判断浏览器环境,检测到版本太旧时,会使用 `window.location.href` 跳转到提示页面。 ### 4.2 引入 JavaScript 库 通过 HTML 的 `script` 标签引入 JavaScript,由于在加载 JavaScript 时会造成线程阻塞,为了避免不必要的卡顿,还需要将一部分需要加载的 JavaScript 库调整成异步加载。 首先是在 index.html 引入主要依赖的库: ```html ``` 上面的代码中 `html2canvas.min.js` 是下载到本地的 JavaScript 库,因此使用相对路径,而 `iziToast.min.js` 使用了 CDN 的地址进行加载,通过 CDN 加载可以更快的请求到 JavaScript,因此使用绝对路径。 在 index.html 的末尾还引入了其他需要的 JavaScript 库,同时包括需要用到的字体和 CSS 文件: ```html ``` 最后引入的 `script.js` 是网页的主要 JavaScript 逻辑,开发者代码都放在这里面,放在最后引入也是为了确保所有前置依赖的 JavaScript 能先加载好。 需要注意的是 `index.html` 还引入了 `loader.min.js` ,这是 Monaco Editor 用于加载插件的一个 JavaScript 文件,正是它可以让页面能够动态的加载一些 JavaScript 库。 ### 4.3 界面结构 在 `index.html` 的 `body` 标签中,主要设计了如下结构: - header:顶部的标题导航栏,用于展示页面名称 - editor:编辑区域,包含了工具栏 `tool-panel` 和主要编辑容器 `main-container` 在编辑容器中 `main-container` ,将页面分为左右两个容器,左容器为代码编辑界面,右容器展示运行结果。 ### 4.4 全局变量定义 在 `script.js` 中包含了网页的所有逻辑,首先是关于全局变量的定义。 存放代码变量 `codeKeeper` ,开发用户存储用户编程代码,可以用于存储临时的代码: ```javascript var codeKeeper = { python: '', html: '', css: '' } ``` 代码编辑区域容器 `codeContainer` ,在代码执行时,会通过查找元素 ID,将不同语言对应的容器赋值给 `codeContainer` 里面的属性,从而方便控制切换编辑器: ```javascript var codeContainer = { python: '', html: '', css: '' } ``` 代码编辑器实例对象 `editors` ,在创建好对应编辑器之后,会将获得的编辑器实例赋值给 `editors` 的对应属性,方便以后通过这个对象获取到编辑器,从而获取编程代码: ```javascript var editors = { python: null, html: null, css: null } ``` 支持的编程语言 `langs` ,用于控制切换编辑器类型,这里使用数组类型的数据,方便进行事件绑定和匹配: ```javascript var langs = ["python", "html", "css"] ``` 预览容器,用户预览 Python 、 HTML 和 CSS 代码的容器,在页面加载后通过选择器查询并复制给变量,方便操作页面渲染: ```javascript var viewer = { python: null, html: null, turtle: null } ``` 当前编辑器类型变量 `currentEditorType` ,用于存储当前代码编辑器类型: ```javascript var currentEditorType = 'python' ``` 工具函数容器 `util` ,存放所有的工具函数: ```javascript var util = { // ... } ``` 通用函数容器 `common ` ,用于存放所有的通用函数: ```javascript var common = { // ... } ``` ### 4.5 工具函数定义 工具函数是一个对象,提供了常用的工具方法,在使用时,只需要执行 `util.方法名()` 即可,这里是具体的定义: ```javascript var util = { getFormatTime: function () { // 格式化时间戳 var d = new Date(); var h = d.getHours(); var m = d.getMinutes(); var s = d.getSeconds(); if (h >= 0 && h <= 9) h = '0' + h; if (m >= 0 && m <= 9) m = '0' + m; if (s >= 0 && s <= 9) s = '0' + s; return h.toString() + m.toString() + s.toString(); }, setCookie: function (cookieName, cookieValue, cookieDays) { // 设置cookie if (cookieValue.length > 4050) return false; var d = new Date(); d.setTime(d.getTime() + (cookieDays * 24 * 60 * 60 * 1000)); document.cookie = cookieName + "=" + cookieValue + ";expires=" + d.toGMTString(); return true; }, getCookie: function (cookieName) { // 获取cookie var name = cookieName + "="; var cookies = document.cookie.split(';'); for (i = 0; i < cookies.length; i++) { var t = cookies[i]; while (t.charAt(0) == ' ') t = t.substring(1, t.length); if (t.indexOf(name) == 0) return t.substring(name.length, t.length); } return null; }, encode: function (str) { return btoa(encodeURIComponent(str)); }, decode: function (str) { return decodeURIComponent(atob(str)); } } ``` 上面的代码提供了多个工具函数: - `getFormatTime` :用于获取经过调整格式后的当前时间 - `setCookie` :用于操作浏览器的 cookie,实现设置 cookie 的功能,支持设置有效时间 - `getCookie` :用于获取浏览器的 cookie - `encode` :用于编码字符串,`btoa` 和 `encodeURIComponent` 是浏览器自带的方法,可以直接调用 - `decode` :用于解码字符串, `atob` 和 `decodeURIComponent` 是浏览器自带的方法,可以直接调用 ### 4.6 通用函数定义 通用函数对象包含一些和业务无关、相对独立、可以重复使用的函数,通过调用 `common.函数名()` 来进行调用: ```javascript var common = { // 提示消息 message: function (title, message, type, icon, timeout = 2000) { var c = type === "infoOK" ? "#587c0c" : type === "infoErr" ? "#b81c27" : "#000000"; iziToast.show({ title: title, message: message, theme: 'dark', color: c, icon: icon, timeout: timeout }); } } ``` 上面的函数中,`message` 用于向用户提示消息,代码的运行需要提示结果,在浏览器上提示信息的方式一般使用 Toast 提示,这种方式不会打断用户的操作体验。为了让提示消息更加方便,需要定义统一的方法来弹出提示,同时支持传递统一的参数,如下: - title:提示标题 - message:提示的消息内容 - type:提示的类型 infoOK 表示成功 infoErr 表示出错 - icon:提示消息框上的图标 - timeout:提示消息展示的时间,默认是2000毫秒(2秒),可以自己传递毫秒数 iziToast 使用示例: ```javascript iziToast.show({ title: 'Hey', // 标题 message: 'What would you like to add?' // 消息内容 }); ``` ### 4.7 业务代码 #### 4.7.1 创建编辑器 前面提到在 `index.html` 引入了编辑器的加载器 `loader.min.js` ,在代码执行之初,可以通过 `loader.min.js` 提供的 `require` 方法来加载代码和配置编辑器,如下面的代码: ```javascript // Microsoft 的 Monaco Editor // https://stackoverflow.com/questions/48934163/monaco-editor-set-font-size // 配置编辑器 require.config({ paths: { 'vs': 'https://cdnjs.loli.net/ajax/libs/monaco-editor/0.20.0/min/vs' } }); require.config({ 'vs/nls': { availableLanguages: { '*': 'zh-cn' } } }); // 加载依赖 require(['vs/editor/editor.main'], () => { // 回调函数 // Dark Mode Theme 黑色主题 monaco.editor.defineTheme('vs-darker', { base: 'vs-dark', inherit: true, rules: [{ background: '0d0d0d' }], colors: { 'editor.background': '#0d0d0d' } }); // 初始化 initDomSelector(); initEditor(); bindTabEvent(); }); ``` 当使用 `require` 加载成功之后,会调用传递进去的函数(回调函数),从而可以确保编辑器的创建是在所有依赖加载配置完成之后。在回调函数中,使用了编辑器的 `monaco.editor.defineTheme` 定义了前面加载到的黑色主题,并且后面连续调用了三个函数来实现编辑器业务功能的初始化,这是整个编辑器的**重点**逻辑,下面的**初始化工作**将详细介绍。 #### 4.7.2 初始化工作 `initDomSelector` 是一个定义好的函数,它可以将 HTML 中指定 ID 的元素设置到全局变量里面,从而方便所有函数使用: ```javascript function initDomSelector() { // 设置各编辑容器 codeContainer.python = document.getElementById("pythonEditorContainer") codeContainer.html = document.getElementById("htmlEditorContainer") codeContainer.css = document.getElementById("cssEditorContainer") // 设置各预览容器 viewer.html = document.getElementById('htmlPreview').contentWindow.document viewer.python = document.getElementById("outputContainer") viewer.turtle = document.getElementById("turtleCanvas") } ``` 上面的代码使用了浏览器提供的 `getElementById` 将 DOM 查找出来,并且赋值给提前定义好的全局变量的属性值上。 `initEditor` 是创建编辑器的核心函数,首先在创建编辑器之前,会从本地储存 `localStorage` 中获取存储的代码,如果没有保存过代码,后面会经过判断再使用默认的代码,这里会将默认代码设置为当前浏览器的时间: ```javascript function initEditor() { // 初始化 Editor var t = new Date().toLocaleString(); // 获取时间 // 获取存储的代码 var p = localStorage.getItem("code-python"); var h = localStorage.getItem("code-html"); var c = localStorage.getItem("code-css"); // 如果没有存储过代码,则使用默认的时间作为注释代码 var pythonStr = p != null ? util.decode(p) : "# " + t; var htmlStr = h != null ? util.decode(h) : ''; var cssStr = c != null ? util.decode(c) : '/* ' + t + ' */'; // 调用createEditor来创建编辑器,参数传递了各个代码容器 editors.python = createEditor(codeContainer.python, "python", pythonStr) editors.html = createEditor(codeContainer.html, "html", htmlStr) editors.css = createEditor(codeContainer.css, "css", cssStr) } ``` 上面的代码在调用了 `createEditor` 函数来创建编辑器,并且赋值给全局变量 `editors` 的对应属性,以后就可以通过该变量获取到创建好的编辑器。 `createEditor` 是一个用来创建编辑器的函数,接收三个参数,分别的容器 `container` 、创建类型 `type` 和初始代码 `str` ,然后通过调用 `monaco.editor.create` 来创建编辑器,并且返回出整个编辑器的实例。 ```javascript function createEditor(container, type, str) { // 返回创建好的编辑器 return monaco.editor.create(container, { theme: 'vs', fontSize: "16px", mouseWheelZoom: true, model: monaco.editor.createModel(str, type), wordWrap: 'on', automaticLayout: true, fontFamily: '"Fira Code", "Noto Sans SC", monospace', scrollbar: { vertical: 'auto' } }); } ``` 上面的代码中可以对编辑器的创建进行配置,支持设置字体、主题、语言支持等等。 #### 4.7.3 切换编辑器类型 创建编辑器的过程中,在最后执行了一个函数 `bindTabEvent` 来进行**事件绑定**,这个函数是用来绑定编辑器类型切换的,这里会查找到 HTML 页面中的 ID 为 `langList` 元素,这个元素里面包含了可以用来切换编辑器类型的样式的**子元素**: ```javascript function bindTabEvent() { // 绑定编辑器类型切换TAB栏事件 var container = document.getElementById("langList") container.onclick = function (e) { // 检测点击到的子元素是不是包含语言切换的样式 lang if (e.target.classList.contains('lang')) { currentEditorType = langs[e.target.dataset.index] document.querySelectorAll("#langList .lang").forEach(function (dom) { // 移除所有按钮高亮 dom.classList.remove("active") e.target.classList.add("active") }); // 重新选中按钮高亮并且设置编辑器 toggleContainerActive(currentEditorType) } } toggleContainerActive(currentEditorType) } ``` 如果发现点击的元素是切换编辑器类型的元素(包含特定的 `class` ),则先移除所有高亮状态,然后调用 `toggleContainerActive` 函数来重新设置高亮并且切换编辑器。 `toggleContainerActive` 函数被用来切换编辑器,首先切换之前会将所有编辑器设置为不可见,然后根据传递的类型参数 `type` 来决定要将哪个编辑器显示出来,代码如下: ```javascript // 切换编辑器容器显示 function toggleContainerActive(type) { codeContainer.python.style.display = "none"; codeContainer.html.style.display = "none"; codeContainer.css.style.display = "none"; if (langs[0] == type) { codeContainer.python.style.display = "block"; } if (langs[1] == type) { codeContainer.html.style.display = "block"; } if (langs[2] == type) { codeContainer.css.style.display = "block"; } } ``` #### 4.7.4 代码保存 定义 `saveCode` 函数来保存代码,在保存代码时需要知道当前的编辑器类型是什么,因此在 `saveCode` 函数中还调用了 `getCurrentEditor` 函数来获取当前类型的编辑器,代码如下: ```javascript // 保存代码到localStorage function saveCode() { // 获取当前编辑器,并且获取编辑器的内容 var code = getCurrentEditor().getValue(); code = util.encode(code); try { // 将代码保存至本地存储 localStorage.setItem("code-" + currentEditorType, code); } catch (err) { // 如果保存失败会提示错误 common.message("保存失败", "请尝试下载代码", "infoErr"); console.log(`错误信息:${err}`); return; } // 提示成功 common.message("已保存", "", "infoOK", "fa fa-check"); } ``` 不管是操作成功或失败,都会调用统一的方法对象 `common` 对象里面的 `message` 函数来提示信息。 `getCurrentEditor` 能获取到当前的编辑器类型,主要是通过全局变量 `currentEditorType` 和当前设置的支持的语言列表 `langs` 进行判断,最终返回对应的编辑器: ```javascript function getCurrentEditor() { if (langs[0] == currentEditorType) { return editors.python; } if (langs[1] == currentEditorType) { return editors.html; } if (langs[2] == currentEditorType) { return editors.css; } } ``` #### 4.7.5 下载代码 下载代码函数 `downloadCode` 也会调用上面介绍的 `getCurrentEditor` 函数来获取当前的编辑器类型,并且经过判断当前编辑类型 `currentEditorType` 来设置保存文件的后缀名,分别为 `.py` 、`.html` 、`.css` : ```javascript // 下载代码 // Reference: https://www.jianshu.com/p/40cfe9a12f9e function downloadCode() { var code = getCurrentEditor().getValue(); // 创建一个超链接用于下载 var pom = document.createElement('a'); var ext; // 判断后缀名 if (langs[0] == currentEditorType) { ext = ".py" } if (langs[1] == currentEditorType) { ext = ".html" } if (langs[2] == currentEditorType) { ext = ".css" } // 设置要下载的超链接 pom.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(code)); // 设置要下载的名字 pom.setAttribute('download', "code-" + util.getFormatTime() + ext); // 模拟点击超链接 if (document.createEvent) { var event = document.createEvent('MouseEvents'); event.initEvent('click', true, true); pom.dispatchEvent(event); } else { pom.click(); } // 提示下载消息 common.message("已开始下载", "请选择保留文件", "infoOK", "fa fa-download", 3000); } ``` #### 4.7.6 代码运行 `runCode` 函数用来运行编写的代码,里面执行了两个函数,分别用来运行 HTML+CSS 和 Python 代码: ```javascript function runCode() { htmlRender(); runPython(); } ``` `htmlRender` 函数用来运行 HTML 和 CSS 代码,主要原理就是通过获取 HTML 编辑器代码和 CSS 编辑器代码,然后将 HTML 代码插入到预览容器里面,同时动态创建一个 `style` 标签,将 CSS 代码插入进去使之生效: ```javascript function htmlRender() { // 获取html和css代码 var htmlCode = editors.html.getValue(); var cssCode = editors.css.getValue(); // 在iframe的网页中插入html viewer.html.body.innerHTML = htmlCode; // 动态创建css样式的代码 var style = document.createElement('style'); style.innerText = cssCode; // 追加样式使之生效 viewer.html.head.appendChild(style); } ``` `runPython` 函数用来解释 Python 语言,每次执行之前会清除上一次的执行结果,后面采用 skulpt 库来解释 Python 语言,相关的使用方式取自 skulpt 使用文档: ```javascript function runPython() { try { // 获取Python代码 var prog = editors.python.getValue(); } catch (err) { common.message("无代码", "请等待代码编辑器加载", "infoErr", "fa fa-bug"); console.log(`错误信息:${err}`); return; } // 清除原来的执行结果 viewer.python.innerHTML = ''; viewer.turtle.innerHTML = ''; // 使用 skulpt 提供的解释器和相关接口来解释Python代码 Sk.pre = "output"; Sk.configure({ __future__: Sk.python3, output: outf, read: builtinRead }); // 指定输出容器 (Sk.TurtleGraphics || (Sk.TurtleGraphics = {})).target = 'turtleCanvas'; var myPromise = Sk.misceval.asyncToPromise(function () { return Sk.importMainWithBody("