当前位置: 首页 > Web前端 > JavaScript

参考Codepen,做了一个基于iframe的代码预览系统

时间:2023-03-27 12:21:14 JavaScript

一直觉得Codepen的在线代码预览系统很牛逼。它可以以所见即所得的方式实时显示代码的运行效果。非常方便快捷。就在最近,我手头有一项业务需要使用类似于Codepen的功能。经过一番研究,我开发了一个demo,基本具备在线运行代码的能力。在线体验地址:https://jrainlau.github.io/on...由于业务只需要执行JS代码,所以demo也只有运行JS代码的能力。一、原理我们知道,浏览器是通过自己的引擎来处理html、css和js资源的,处理过程是在页面加载的时候开始的。如果我们想动态运行这些资源,对于html和css可以使用DOM操作,对于js可以使用eval或者newFunction()。但是这些操作复杂且不安全(eval和newFunction()容易出事故),那么有什么方法可以优雅、方便、安全地动态运行呢?让我们看看著名的Codepen是如何做到的。我在Codepen中简单写了一个按钮,绑定了样式和点击事件,可以看到白色区域已经显示了我们想要的结果。打开控制台,全神贯注观察后,我们可以发现,整个白色区域都是一个iframe,里面的html内容就是我们刚刚编辑的代码。不难想象,它的运行原理有点类似于document.write,直接将内容写入html文件,然后以iframe的形式嵌入到其他网页中,实现代码预览的逻辑。那么使用iframe有什么好处呢?iframe可以独立成为一个与宿主机隔离的沙箱环境,运行在其中的代码在大多数情况下不会对宿主机产生影响,可以有效保证安全性。通过HTML5新的sandbox属性,可以为iframe定义更细粒度的权限,比如是否允许运行脚本,是否允许弹窗等等。2.实现方法要实现类似Codepen的效果,最重要的一步就是如何将代码注入到iframe中。由于我们需要使用相关的API来操作iframe,为了安全起见,浏览器只能使用同域的iframe链接,否则会报跨域错误。首先准备一个index.html和iframe.html,使用静态资源服务器来运行它们,假设它们都运行在localhost:8080上。然后我们在index.html中插入一个iframe,它的链接是localhost:8080/iframe.html,代码如下:接下来我们就可以使用iframe了.contentDocument获取iframe的内容,然后对其进行操作:`);iframeDoc.close();//最后调用`close()`关闭“write”开关执行后,我们可以在localhost:8080/index.html看到和前面Codepen中展示的效果一样:我们只需要找到一个输入框,将写好的代码保存为变量。然后调用iframeDoc.write()将代码动态写入iframe并实时运行。3.Console输出和安全观察在Codepen页面,可以看到有一个Console面板,可以直接输出iframe中的console信息。这怎么可能?答案很简单。我们可以劫持iframe页面中的控制台和其他API。在保留原有控制台输出功能的前提下,通过postMessage向父页面输出相关信息。父页面监听消息后,将信息整理后输出到页面,实现Console面板。在iframe.html中,我们在外面写了一段js代码(因为父页面调用iframeDoc.write()覆盖了里面的所有内容):functionrewriteConsole(type){constorigin=console[type];console[type]=(...args)=>{window.parent.postMessage({from:'codeRunner',type,data:args},'*');起源.apply(控制台,参数);};}rewriteConsole('log');rewriteConsole('info');rewriteConsole('debug');rewriteConsole('warn');rewriteConsole('error');rewriteConsole('table');另外,我们会给iframe设置sandbox属性来限制它的一些权限,但是这里有个隐患,就是如果在iframe中执行window.parent.document相关的API,就可以让iframe重写父页面的内容,甚至重写沙盒属性,这肯定是不安全的,所以我们需要在iframe中屏蔽这个相关的API:Object.defineProperty(window,'disableParent',{get(){thrownewError('无法调用窗口.parent属性!');},set(){},});在调用父页面的iframeDoc.write(code)之前,我们需要将用户输入的自定义代码code替换一次,将parent.document全部替换为window.disableParent。当用户调用parent.document相关API时,实际上是在iframe中运行了window.disableParent,会直接报错无法调用window.parent属性!,有效避免套娃的安全隐患。4、使用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运行。对于控制台面板,它是另一个只读的monaco编辑器,主要有2个问题,这会有点困难。一是如何将新添加的内容一一插入;另一个是如何根据不同的控制台类型生成不同的背景颜色。问题1的解决方案非常简单。你只需要定义一个字符串类型的变量infos。每当您从iframe中收听postMessage()时,将信息添加到infos中,最后调用editor.setValue()。问题2的解决方法,我们在iframe中劫持了console的逻辑,同时在postMessage的时候告诉父页面console[type]是log还是warn还是其他什么,这样父页面就可以知道具体内容根据console[type]这里的type。接下来,我们可以调用editor.deltaDecorations方法来设置某行某列的背景色:constdeltaDecorations=[]//每当有新的consle消息被推送时,deltaDecorations中就会插入一条消息,后面会用到//这里的startLine和endLine分别代表这条新消息的起始行号和结束行号,需要自己记录//`${info.type}Decoration`对应不同的`console[type的背景??色]`className,对应具体的CSSdeltaDecorations.push({range:newmonaco.Range(startLine,1,endLine,1),options:{isWholeLine:true,className:`${info.type}Decoration`},});然后我们可以定义不同consle对应的背景色CSS[type]:.warnDecoration{background:#ffd900;宽度:100%!重要;}.errorDecoration{背景:#ff3300;width:100%!important;}具体代码可以看这里:https://github.com/jrainlau/o...5.总结本文通过分析Codepen的实现,使用iframe和monaco-editor来开发一套专门用于执行JavaScript代码的在线代码预览系统。除了代码预览和展示的作用,对于一些管理系统,往往需要手动编写一些后置脚本来处理系统中的数据。本文可以用来搭建代码预览系统,实时且安全。预览后脚本非常有用。