始终感觉 Codepen 的在线代码预览零碎很神奇,可能所见即所得地实时展现代码的运行成果,无论是代码演示,还是测试性能,都是十分方便快捷的存在。刚好最近手头有业务须要用到相似 Codepen 的能力,通过一番调研之后开发了一个具备根本的在线运行代码能力的 demo 进去。
在线体验地址:https://jrainlau.github.io/on...
因为业务只须要执行 JS 代码,因而 demo 也只具备 JS 代码的运行能力。
一、原理
咱们晓得,浏览器是通过自带的引擎来解决 html,css 和 js 资源的,处理过程在页面载入的时候就曾经开始。如果咱们想要动静地运行这些资源,对于 html 和 css 咱们能够用 DOM 操作的形式,对于 js 咱们能够用 eval
或者 new Function()
。然而这些操作都偏简单且不平安(eval
和 new Function()
很容易出事),那么有没有什么方法能够既优雅不便,又能平安地动静运行呢?咱们看看赫赫有名的 Codepen 是怎么做的。
我在 Codepen 外面简略地写了一个按钮,绑定了款式和点击事件,能够看到红色区域曾经展现出咱们想要的后果。关上控制台自吸察看后能够发现,整个红色区域是一个 iframe
,当中的 html 内容就是咱们刚刚所编辑的代码。
不难想象,它的运行原理有点相似于 document.write
,把内容间接写入到某一个 html 文件中,而后把它以 iframe 的形式内嵌到其余网页当中,实现代码预览的逻辑。那么应用 iframe 有什么益处呢?iframe 能够独立成为一个和宿主隔离的沙箱环境,在当中运行的代码在大部分状况下不会影响宿主,能无效地保障平安。配合 HTML5 新增的 sandbox
属性,能够给 iframe 定义更为精密的权限,比方是否容许它运行脚本,是否容许它弹窗等等。
二、实现形式
要实现相似 Codepen 的成果,最重要的一步就是如何把代码注入到 iframe 当中。因为咱们须要应用到操控 iframe 的相干 API,浏览器出于平安的思考,咱们只能应用同域的 iframe 链接,否则将会报跨域的谬误。
首先筹备好一个 index.html
和 iframe.html
,应用一个动态资源服务器把它们跑起来,假如均跑在 localhost:8080
。而后咱们在 index.html
外面插入一个 iframe,其链接就是 localhost:8080/iframe.html
,代码如下:
<iframe src="localhost:8080/iframe.html"></iframe>
接下来咱们能够应用 iframe.contentDocument
来获取 iframe 的内容,而后操作它:
<script> const ifrme = document.querySelector('iframe'); const iframeDoc = iframe.contentDocument; iframeDoc.open(); // 须要先调用 `open()`,关上“写”的开关 iframeDoc.write(` <body> <style>button { color: red }</style> <button>Click</button> <script> document.querySelector('button').addEventListener(() => { console.log('on-click!') }) <\/script> </body> `); iframeDoc.close(); // 最初调用 `close()`,敞开“写”的开关</script>
运行结束后,咱们能够在 localhost:8080/index.html
外面看到和前文 Codepen 所展现的一样的成果:
后续咱们只须要找个输入框,把所写的代码保留成变量,而后调用 iframeDoc.write()
就能够动静地把代码写入到 iframe 并实时运行了。
三、控制台输入及平安
察看 Codepen 的页面,能够看到有一个 Console 的面板,它能够把 iframe 当中的 console
信息间接输入。这是怎么实现的呢?答案很简略,咱们能够在 iframe 页面中劫持 console
等 API,在保留原有的控制台输入的性能的前提下,把相干的信息通过 postMessage
的形式把它们输入给父页面,父页面监听到 message 当前把信息整顿后输入到页面上,实现 Console 面板。
在 iframe.html
中,咱们在<body></body>
以外写入一段 js 代码(因为父页面调用 iframeDoc.write()
会笼罩 <body></body>
内的全部内容):
function rewriteConsole(type) { const origin = console[type]; console[type] = (...args) => { window.parent.postMessage({ from: 'codeRunner', type, data: args }, '*'); origin.apply(console, args); };}rewriteConsole('log');rewriteConsole('info');rewriteConsole('debug');rewriteConsole('warn');rewriteConsole('error');rewriteConsole('table');
此外咱们会给 iframe 设置 sandbox
属性来限度其局部权限,然而这里有一个套娃的隐患,就是如果在 iframe 外面执行 window.parent.document
相干 API 的话,能够让 iframe 去改写父页面的内容,甚至改写 sandbox
属性,这必定是不平安的,因而咱们须要在 iframe 中把这相干 API 给屏蔽掉:
Object.defineProperty(window, 'disableParent', { get() { throw new Error('无奈调用 window.parent 属性!'); }, set() {},});
在调用父页面的 iframeDoc.write(code)
之前,咱们须要先把用户输出的自定义代码 code
进行一次 replace
,把当中的所有 parent.document
改成 window.disableParent
。当用户调用 parent.document
相干 API 时,理论在 iframe 运行的是 window.disableParent
,届时将会间接报错无奈调用 window.parent 属性!
,无效防止了套娃的安全隐患。
四、应用 monaco-editor 实现编辑模块和 Console 面板模块
我所搭建的这个 online-code-runner 是基于 monaco-editor 来实现编辑模块和 Console 面板模块的,接下来会简略讲述它们别离都是怎么实现的。
对于编辑模块来说,就是一个简略的 monaco-editor,只须要简略地设置它的款式就能够了:
monaco.editor.create(document.querySelector('#editor'), { { language: 'javascript', tabSize: 2, autoIndent: true, theme: 'github', automaticLayout: true, wordWrap: 'wordWrapColumn', wordWrapColumn: 120, lineHeight: 28, fontSize: 16, minimap: { size: 'fill', }, },});
点击”执行代码“的按钮后,能够通过 editor.getValue()
把编辑模块中的内容读取进去,而后交给 iframe 去运行。
对于 Console 面板来说,它是另一个只读的 monaco-editor,次要有2个问题会有一点点吃力。其一是如何让新增加的内容挨个插入进去;其二是如何依据不同的 console
类型产生不必的背景色。
问题一的解法很简略,只须要定义一个字符串类型变量 infos
,每当监听到来自 iframe 的 postMessage()
时,就往 infos
增加当中的信息,最初调用 editor.setValue()
即可。
问题二的解法,咱们曾经在 iframe 中劫持 console
的逻辑,在 postMessage
的时候同时通知父页面 consle[type]
到底是 log
还是 warn
还是其余,因而父页面能够依据这里的 console[type]
来晓得具体的类型。
接下来咱们能够调用 editor.deltaDecorations
办法来设置某行某列的背景色:
const deltaDecorations = []// 每当有新的 consle 音讯推送过去时,都往 deltaDecorations 里插入一条信息,前面会用到// 这里的 startLine 和 endLine 代表着这条新的音讯的起始行号和完结行号,须要自行记录// `${info.type}Decoration` 为不同 `console[type]` 的背景色对应的 className,对应着具体的 CSSdeltaDecorations.push({ range: new monaco.Range(startLine, 1, endLine, 1), options: { isWholeLine: true, className: `${info.type}Decoration` },});
而后咱们能够定义不同 consle[type]
对应的背景色 CSS:
.warnDecoration { background: #ffd900; width: 100% !important;}.errorDecoration { background: #ff3300; width: 100% !important;}
具体代码能够看这里:https://github.com/jrainlau/o...
五、小结
本文通过剖析 Codepen 的实现形式,应用 iframe 的形式配合 monaco-editor 自行开发了一套专用于执行 JavaScript 代码的在线代码预览零碎。除了能够作为代码预览、展现的作用外,对于一些管理系统而言,往往须要人为编写一些后置脚本来解决零碎中的数据,正好能够利用本文的形式去搭建一套代码预览零碎,实时又平安地预览后置脚本,用途十分大。