快速搭建一个代码在线编辑预览工具

简介 大家好,我是一个闲着没事热衷于重复造轮子的不知名前端,今天给大家带来的是一个代码在线编辑预览工具的实现介绍,目前这类工具使用很广泛,常见于各种文档网站及代码分享场景,相关工具也比较多,如codepen、jsrun、codesandbox、jsbin、plnkr、jsfiddle等,这些工具大体分两类,一类可以自由添加多个文件,比较像我们平常使用的编辑器,另一类固定只能单独编辑htmljscss,第二类比较常见,对于demo场景来说其实已经够用,当然,说的只是表象,底层实现方式可能还是各有千秋的。
本文主要介绍的是第二类其中的一种实现方式,完全不依赖于后端,所有逻辑都在前端完成,实现起来相当简单,使用的是vue3全家桶来开发,使用其他框架也完全可以。
ps.在本文基础上笔者开发了一个完整的线上工具,带云端保存,地址:http://lxqnsys.com/code-run/,欢迎使用。
页面结构 快速搭建一个代码在线编辑预览工具
文章图片

我挑了一个比较典型也比较好看的结构来仿照,默认布局上下分成四部分,工具栏、编辑器、预览区域及控制台,编辑器又分为三部分,分别是HTMLCSSJavaScript,其实就是三个编辑器,用来编辑代码。
各部分都可以拖动进行调节大小,比如按住js编辑器左边的灰色竖条向右拖动,那么js编辑器的宽度会减少,同时css编辑器的宽度会增加,如果向左拖动,那么css编辑器宽度会减少,js编辑器的宽度会增加,当css编辑器宽度已经不能再减少的时候css编辑器也会同时向左移,然后减少html的宽度。
在实现上,水平调节宽度和垂直调节高度原理是一样的,以调节宽度为例,三个编辑器的宽度使用一个数组来维护,用百分比来表示,那么初始就是100/3%,然后每个编辑器都有一个拖动条,位于内部的左侧,那么当按住拖动某个拖动条拖动时的逻辑如下:
1.把本次拖动瞬间的偏移量由像素转换为百分比;
2.如果是向左拖动的话,检测本次拖动编辑器的左侧是否存在还有空间可以压缩的编辑器,没有的话代表不能进行拖动;如果有的话,那么拖动时增加本次拖动编辑器的宽度,同时减少找到的第一个有空间的编辑器的宽度,直到无法再继续拖动;
3.如果是向右拖动的话,检测本次拖动编辑器及其右侧是否存在还有空间可以压缩的编辑器,没有的话也代表不能再拖动,如果有的话,找到第一个并减少该编辑器的宽度,同时增加本次拖动编辑器左侧第一个编辑器的宽度;
核心代码如下:

const onDrag = (index, e) => { let client = this._dir === 'v' ? e.clientY : e.clientX // 本次移动的距离 let dx = client - this._last // 换算成百分比 let rx = (dx / this._containerSize) * 100 // 更新上一次的鼠标位置 this._last = client if (dx < 0) { // 向左/上拖动 if (!this.isCanDrag('leftUp', index)) { return } // 拖动中的编辑器增加宽度 if (this._dragItemList.value[index][this._prop] - rx < this.getMaxSize(index)) { this._dragItemList.value[index][this._prop] -= rx } else { this._dragItemList.value[index][this._prop] = this.getMaxSize(index) } // 找到左边第一个还有空间的编辑器索引 let narrowItemIndex = this.getFirstNarrowItemIndex('leftUp', index) let _minSize = this.getMinSize(narrowItemIndex) // 左边的编辑器要同比减少宽度 if (narrowItemIndex >= 0) { // 加上本次偏移还大于最小宽度 if (this._dragItemList.value[narrowItemIndex][this._prop] + rx > _minSize) { this._dragItemList.value[narrowItemIndex][this._prop] += rx } else { // 否则固定为最小宽度 this._dragItemList.value[narrowItemIndex][this._prop] = _minSize } } } else if (dx > 0) { // 向右/下拖动 if (!this.isCanDrag('rightDown', index)) { return } // 找到拖动中的编辑器及其右边的编辑器中的第一个还有空间的编辑器索引 let narrowItemIndex = this.getFirstNarrowItemIndex('rightDown', index) let _minSize = this.getMinSize(narrowItemIndex) if (narrowItemIndex <= this._dragItemList.value.length - 1) { let ax = 0 // 减去本次偏移还大于最小宽度 if (this._dragItemList.value[narrowItemIndex][this._prop] - rx > _minSize) { ax = rx } else { // 否则本次能移动的距离为到达最小宽度的距离 ax = this._dragItemList.value[narrowItemIndex][this._prop] - _minSize } // 更新拖动中的编辑器的宽度 this._dragItemList.value[narrowItemIndex][this._prop] -= ax // 左边第一个编辑器要同比增加宽度 if (index > 0) { if (this._dragItemList.value[index - 1][this._prop] + ax < this.getMaxSize(index - 1)) { this._dragItemList.value[index - 1][this._prop] += ax } else { this._dragItemList.value[index - 1][this._prop] = this.getMaxSize(index - 1) } } } } }

实现效果如下:

为了能提供多种布局的随意切换,我们有必要把上述逻辑封装一下,封装成两个组件,一个容器组件Drag.vue,一个容器的子组件DragItem.vueDragItem通过slot来显示其他内容,DragItem主要提供拖动条及绑定相关的鼠标事件,Drag组件里包含了上述提到的核心逻辑,维护对应的尺寸数组,提供相关处理方法给DragItem绑定的鼠标事件,然后只要根据所需的结构进行组合即可,下面的结构就是上述默认的布局:

这部分代码较多,有兴趣的可以查看源码。
编辑器 目前涉及到代码编辑的场景基本使用的都是codemirror,因为它功能强大,使用简单,支持语法高亮、支持多种语言和主题等,但是为了能更方便的支持语法提示,本文选择的是微软的monaco-editor,功能和VSCode一样强大,VSCode有多强就不用我多说了,缺点是整体比较复杂,代码量大,内置主题较少。
monaco-editor支持多种加载方式,esm模块加载的方式需要使用webpack,但是vite底层打包工具用的是Rollup,所以本文使用直接引入js的方式。
在官网上下载压缩包后解压到项目的public文件夹下,然后参考示例的方式在index.html文件里添加:

monaco-editor内置了10种语言,我们选择中文的,其他不用的可以直接删掉:
快速搭建一个代码在线编辑预览工具
文章图片

接下来创建编辑器就可以了:
const editor = monaco.editor.create( editorEl.value,// dom容器 { value: props.content,// 要显示的代码 language: props.language,// 代码语言,css、javascript等 minimap: { enabled: false,// 关闭小地图 }, wordWrap: 'on', // 代码超出换行 theme: 'vs-dark'// 主题 } )

就这么简单,一个带高亮、语法提示、错误提示的编辑器就可以使用了,效果如下:
快速搭建一个代码在线编辑预览工具
文章图片

其他几个常用的api如下:
// 设置文档内容 editor.setValue(props.content) // 监听编辑事件 editor.onDidChangeModelContent((e) => { console.log(editor.getValue())// 获取文档内容 }) // 监听失焦事件 editor.onDidBlurEditorText((e) => { console.log(editor.getValue()) })

预览 代码有了,接下来就可以渲染页面进行预览了,对于预览,显然是使用iframeiframe除了src属性外,HTML5还新增了一个属性srcdoc,用来渲染一段HTML代码到iframe里,这个属性IE目前不支持,不过vue3都要不支持IE了,咱也不管了,如果硬要支持也简单,使用write方法就行了:
iframeRef.value.contentWindow.document.write(htmlStr)

接下来的思路就很清晰了,把htmlcssjs代码组装起来扔给srcdoc不就完了吗:

const assembleHtml = (head, body) => { return ` ${head} ${body} ` }const run = () => { let head = ` 预览<\/title>${editData.value.code.css.content} <\/style> ` let body = ` ${editData.value.code.html.content}${editData.value.code.javascript.content} <\/script> ` let str = assembleHtml(head, body) srcdoc.value = https://www.it610.com/article/str }</code></blockquote><br /> 效果如下:<br /> <img alt="快速搭建一个代码在线编辑预览工具" onload="javascript:ImgReSize(this)" src="https://img.it610.com/image/info9/41dbb037b2dc4236be220cc4918ab1c2.jpg"/> <br /> 文章图片 <br /> <br /> 为了防止<code>js</code>代码运行出现错误阻塞页面渲染,我们把<code>js</code>代码使用<code>try catch</code>包裹起来:<br /> <blockquote><code>let body = ` ${editData.value.code.html.content}try { ${editData.value.code.javascript.content} } catch (err) { console.error('js代码运行出错') console.error(err) } <\/script> `</code></blockquote><br /> 控制台 极简方式 先介绍一种非常简单的方式,使用一个叫eruda的库,这个库是用来方便在手机上进行调试的,和<code>vConsole</code>类似,我们直接把它嵌到<code>iframe</code>里就可以支持控制台的功能了,要嵌入<code>iframe</code>里的文件我们都要放到<code>public</code>文件夹下:<br /> <blockquote><code>const run = () => { let head = ` <title>预览<\/title>${editData.value.code.css.content} <\/style> ` let body = ` ${editData.value.code.html.content} <\/script>eruda.init(); ${editData.value.code.javascript.content} <\/script> ` let str = assembleHtml(head, body) srcdoc.value = https://www.it610.com/article/str }</code></blockquote><br /> 效果如下:<br /> <img alt="快速搭建一个代码在线编辑预览工具" onload="javascript:ImgReSize(this)" src="https://img.it610.com/image/info9/98a90b66bf6e4a709f9be99d5063f2a4.jpg"/> <br /> 文章图片 <br /> <br /> 这种方式的缺点是只能嵌入到<code>iframe</code>里,不能把控制台和页面分开,导致每次代码重新运行,控制台也会重新运行,无法保留之前的日志,当然,样式也不方便控制。<br /> 自己实现 如果选择自己实现的话,那么这部分会是本项目里最复杂的,自己实现的话一般只实现一个<code>console</code>的功能,其他的比如<code>html</code>结构、请求资源之类的就不做了,毕竟实现起来费时费力,用处也不是很大。<br /> <code>console</code>大体上要支持输出两种信息,一是<code>console</code>对象打印出来的信息,二是各种报错信息,先看<code>console</code>信息。<br /> console信息<br /> 思路很简单,在<code>iframe</code>里拦截<code>console</code>对象的所有方法,当某个方法被调用时使用<code>postMessage</code>来向父页面传递信息,父页面的控制台打印出对应的信息即可。<br /> <blockquote><code>// /public/console/index.js// 重写的console对象的构造函数,直接修改console对象的方法进行拦截的方式是不行的,有兴趣可以自行尝试 function ProxyConsole() {}; // 拦截console的所有方法 [ 'debug', 'clear', 'error', 'info', 'log', 'warn', 'dir', 'props', 'group', 'groupEnd', 'dirxml', 'table', 'trace', 'assert', 'count', 'markTimeline', 'profile', 'profileEnd', 'time', 'timeEnd', 'timeStamp', 'groupCollapsed' ].forEach((method) => { let originMethod = console[method] // 设置原型方法 ProxyConsole.prototype[method] = function (...args) { // 发送信息给父窗口 window.parent.postMessage({ type: 'console', method, data: args }) // 调用原始方法 originMethod.apply(ProxyConsole, args) } }) // 覆盖原console对象 window.console = new ProxyConsole()</code></blockquote><br /> 把这个文件也嵌入到<code>iframe</code>里:<br /> <blockquote><code>const run = () => { let head = ` <title>预览<\/title>${editData.value.code.css.content} <\/style> <\/script> ` // ... }</code></blockquote><br /> 父页面监听<code>message</code>事件即可:<br /> <blockquote><code>window.addEventListener('message', (e) => { console.log(e) })</code></blockquote><br /> 如果如下:<br /> <img alt="快速搭建一个代码在线编辑预览工具" onload="javascript:ImgReSize(this)" src="https://img.it610.com/image/info9/4842fa85c135410ab22fb8aa74b3e14f.jpg"/> <br /> 文章图片 <br /> <br /> 监听获取到了信息就可以显示出来,我们一步步来看:<br /> 首先<code>console</code>的方法都可以同时接收多个参数,打印多个数据,同时打印的在同一行进行显示。<br /> 1.基本数据类型<br /> 基本数据类型只要都转成字符串显示出来就可以了,无非是使用颜色区分一下:<br /> <blockquote><code>// /public/console/index.js// ...window.parent.postMessage({ type: 'console', method, data: args.map((item) => {// 对每个要打印的数据进行处理 return handleData(item) }) })// ...// 处理数据 const handleData = https://www.it610.com/article/(content) => { let contentType = type(content) switch (contentType) { case 'boolean': // 布尔值 content = content ? 'true' : 'false' break; case 'null': // null content = 'null' break; case 'undefined': // undefined content = 'undefined' break; case 'symbol': // Symbol,Symbol不能直接通过postMessage进行传递,会报错,需要转成字符串 content = content.toString() break; default: break; } return { contentType, content, } }</code></blockquote><br /> <blockquote><code>// 日志列表 const logList = ref([])// 监听iframe信息 window.addEventListener('message', ({ data = https://www.it610.com/article/{} }) => { if (data.type === 'console') logList.value.push({ type: data.method,// console的方法名 data: data.data// 要显示的信息,一个数组,可能同时打印多条信息 }) } })</code></blockquote><br /> <blockquote><code><template v-for="(logItem, itemIndex) in log.data" :key="itemIndex"></template></code></blockquote><br /> <img alt="快速搭建一个代码在线编辑预览工具" onload="javascript:ImgReSize(this)" src="https://img.it610.com/image/info9/0b8618777f83403b8d57bdd6f1803f73.jpg"/> <br /> 文章图片 <br /> <br /> 2.函数<br /> 函数只要调用<code>toString</code>方法转成字符串即可:<br /> <blockquote><code>const handleData = https://www.it610.com/article/(content) => { let contentType = type(content) switch (contentType) { // ... case 'function': content = content.toString() break; default: break; } }</code></blockquote><br /> 3.json数据<br /> <code>json</code>数据需要格式化后进行显示,也就是带高亮、带缩进,以及支持展开收缩。<br /> 实现也很简单,高亮可以通过<code>css</code>类名控制,缩进换行可以使用<code>div</code>和<code>span</code>来包裹,具体实现就是像深拷贝一样深度优先遍历<code>json</code>树,对象或数组的话就使用一个<code>div</code>来整体包裹,这样可以很方便的实现整体缩进,具体到对象或数组的某项时也使用<code>div</code>来实现换行,需要注意的是如果是作为对象的某个属性的值的话,需要使用<code>span</code>来和属性及冒号显示在同一行,此外,也要考虑到循环引用的情况。<br /> 展开收缩时针对非空的对象和数组,所以可以在遍历下级属性之前添加一个按钮元素,按钮相对于最外层元素使用绝对定位。<br /> <blockquote><code>const handleData = https://www.it610.com/article/(content) => { let contentType = type(content) switch (contentType) { // ... case 'array': // 数组 case 'object': // 对象 content = stringify(content, false, true, []) break; default: break; } }// 序列化json数据变成html字符串 /* data:数据 hasKey:是否是作为一个key的属性值 isLast:是否在所在对象或数组中的最后一项 visited:已经遍历过的对象/数组,用来检测循环引用 */ const stringify = (data, hasKey, isLast, visited) => { let contentType = type(data) let str = '' let len = 0 let lastComma = isLast ? '' : ',' // 当数组或对象在最后一项时,不需要显示逗号 switch (contentType) { case 'object': // 对象 // 检测到循环引用就直接终止遍历 if (visited.includes(data)) { str += `检测到循环引用` } else { visited.push(data) let keys = Object.keys(data) len = keys.length // 空对象 if (len <= 0) { // 如果该对象是作为某个属性的值的话,那么左括号要和key显示在同一行 str += hasKey ? `{ }${lastComma}` : `{ }${lastComma}` } else { // 非空对象 // expandBtn是展开和收缩按钮 str += `` str += hasKey ? `{` : '{' // 这个wrap的div用来实现展开和收缩功能 str += '' // 遍历对象的所有属性 keys.forEach((key, index) => { // 是否是数组或对象 let childIsJson = ['object', 'array'].includes(type(data[key])) // 最后一项不显示逗号 str += `\"${key}\" : ${stringify(data[key], true, index >= len - 1, visited)}${index < len - 1 && !childIsJson ? ',' : ''} ` }) str += '' str += `}${lastComma}` } } break; case 'array': // 数组 if (visited.includes(data)) { str += `检测到循环引用` } else { visited.push(data) len = data.length // 空数组 if (len <= 0) { // 如果该数组是作为某个属性的值的话,那么左括号要和key显示在同一行 str += hasKey ? `[ ]${lastComma}` : `[ ]${lastComma}` } else { // 非空数组 str += `` str += hasKey ? `[` : '[' str += '' data.forEach((item, index) => { // 最后一项不显示逗号 str += `${stringify(item, true, index >= len - 1, visited)}${index < len - 1 ? ',' : ''} ` }) str += '' str += `]${lastComma}` } } break; default: // 其他类型 let res = handleData(data) let quotationMarks = res.contentType === 'string' ? '\"' : '' // 字符串添加双引号 str += `${quotationMarks}${res.content}${quotationMarks}` break; } return str }</code></blockquote><br /> 模板部分也增加一下对<code>json</code>数据的支持:<br /> <blockquote><code><template v-for="(logItem, itemIndex) in log.data" :key="itemIndex"></template></code></blockquote><br /> 最后对不同的类名写一下样式即可,效果如下:<br /> <img alt="快速搭建一个代码在线编辑预览工具" onload="javascript:ImgReSize(this)" src="https://img.it610.com/image/info9/368d76414c4d4a968578dcc9c424e9ed.jpg"/> <br /> 文章图片 <br /> <br /> 展开收缩按钮的点击事件我们使用事件代理的方式绑定到外层元素上:<br /> <blockquote><code> </code></blockquote><br /> 点击展开收缩按钮的时候根据当前的展开状态来决定是展开还是收缩,展开和收缩操作的是<code>wrap</code>元素的高度,收缩时同时插入一个省略号的元素来表示此处存在收缩,同时因为按钮使用绝对定位,脱离了正常文档流,所以也需要手动控制它的显示与隐藏,需要注意的是要能区分哪些按钮是本次可以操作的,否则可能下级是收缩状态,但是上层又把该按钮显示出来了:<br /> <blockquote><code>// 在子元素里找到有指定类名的第一个元素 const getChildByClassName = (el, className) => { let children = el.children for (let i = 0; i < children.length; i++) { if (children[i].classList.contains(className)) { return children[i] } } return null }// json数据展开收缩 let expandIndex = 0 const jsonClick = (e) => { // 点击是展开收缩按钮 if (e.target && e.target.classList.contains('expandBtn')) { let target = e.target let parent = target.parentNode // id,每个展开收缩按钮唯一的标志 let index = target.getAttribute('data-index') if (index === null) { index = expandIndex++ target.setAttribute('data-index', index) } // 获取当前状态,0表示收缩、1表示展开 let status = target.getAttribute('expand-status') || '1' // 在子节点里找到wrap元素 let wrapEl = getChildByClassName(parent, 'wrap') // 找到下层所有的按钮节点 let btnEls = wrapEl.querySelectorAll('.expandBtn') // 收缩状态 -> 展开状态 if (status === '0') { // 设置状态为展开 target.setAttribute('expand-status', '1') // 展开 wrapEl.style.height = 'auto' // 按钮箭头旋转 target.classList.remove('shrink') // 移除省略号元素 let ellipsisEl = getChildByClassName(parent, 'ellipsis') parent.removeChild(ellipsisEl) // 显示下级展开收缩按钮 for (let i = 0; i < btnEls.length; i++) { let _index = btnEls[i].getAttribute('data-for-index') // 只有被当前按钮收缩的按钮才显示 if (_index === index) { btnEls[i].removeAttribute('data-for-index') btnEls[i].style.display = 'inline-block' } } } else if (status === '1') { // 展开状态 -> 收缩状态 target.setAttribute('expand-status', '0') wrapEl.style.height = 0 target.classList.add('shrink') let ellipsisEl = document.createElement('div') ellipsisEl.textContent = '...' ellipsisEl.className = 'ellipsis' parent.insertBefore(ellipsisEl, wrapEl) for (let i = 0; i < btnEls.length; i++) { let _index = btnEls[i].getAttribute('data-for-index') // 只隐藏当前可以被隐藏的按钮 if (_index === null) { btnEls[i].setAttribute('data-for-index', index) btnEls[i].style.display = 'none' } } } } }</code></blockquote><br /> 效果如下:<br /> <br /> 4.console对象的其他方法<br /> <code>console</code>对象有些方法是有特定逻辑的,比如<code>console.assert(expression, message)</code>,只有当<code>express</code>表达式为<code>false</code>时才会打印<code>message</code>,又比如<code>console</code>的一些方法支持占位符等,这些都得进行相应的支持,先修改一下<code>console</code>拦截的逻辑:<br /> <blockquote><code> ProxyConsole.prototype[method] = function (...args) { // 发送信息给父窗口 // 针对特定方法进行参数预处理 let res = handleArgs(method, args) // 没有输出时就不发送信息 if (res.args) { window.parent.postMessage({ type: 'console', method: res.method, data: res.args.map((item) => { return handleData(item) }) }) } // 调用原始方法 originMethod.apply(ProxyConsole, args) }</code></blockquote><br /> 增加了<code>handleArgs</code>方法来对特定的方法进行参数处理,比如<code>assert</code>方法:<br /> <blockquote><code>const handleArgs = (method, contents) => { switch (method) { // 只有当第一个参数为false,才会输出第二个参数,否则不会有任何结果 case 'assert': if (contents[0]) { contents = null } else { method = 'error' contents = ['Assertion failed: ' + (contents[1] || 'console.assert')] } break; default: break; } return { method, args: contents } }</code></blockquote><br /> 再看一下占位符的处理,占位符描述如下:<br /> <img alt="快速搭建一个代码在线编辑预览工具" onload="javascript:ImgReSize(this)" src="https://img.it610.com/image/info9/fbbf17388c3d4791938a1f7e2600a47b.jpg"/> <br /> 文章图片 <br /> <br /> 可以判断第一个参数是否是字符串,以及是否包含占位符,如果包含了,那么就判断是什么占位符,然后取出后面对应位置的参数进行格式化,没有用到的参数也不能丢弃,仍然需要显示:<br /> <blockquote><code>const handleArgs = (method, contents) => { // 处理占位符 if (contents.length > 0) { if (type(contents[0]) === 'string') { // 只处理%s、%d、%i、%f、%c let match = contents[0].match(/(%[sdifc])([^%]*)/gm) // "%d年%d月%d日" -> ["%d年", "%d月", "%d日"] if (match) { // 后续参数 let sliceArgs = contents.slice(1) let strList = [] // 遍历匹配到的结果 match.forEach((item, index) => { let placeholder = item.slice(0, 2) let arg = sliceArgs[index] // 对应位置没有数据,那么就原样输出占位符 if (arg === undefined) { strList.push(item) return } let newStr = '' switch (placeholder) { // 字符串,此处为简单处理,实际和chrome控制台的输出有差异 case '%s': newStr = String(arg) + item.slice(2) break; // 整数 case '%d': case '%i': newStr = (type(arg) === 'number' ? parseInt(arg) : 'NaN') + item.slice(2) break; // 浮点数 case '%f': newStr = (type(arg) === 'number' ? arg : 'NaN') + item.slice(2) break; // 样式 case '%c': newStr = `${item.slice(2)}` break; default: break; } strList.push(newStr) }) contents = strList // 超出占位数量的剩余参数也不能丢弃,需要展示 if (sliceArgs.length > match.length) { contents = contents.concat(sliceArgs.slice(match.length)) } } } } // 处理方法 ... switch (method) {} }</code></blockquote><br /> 效果如下:<br /> <img alt="快速搭建一个代码在线编辑预览工具" onload="javascript:ImgReSize(this)" src="https://img.it610.com/image/info9/709dc72b5e37426d90b62e4684e887fa.jpg"/> <br /> 文章图片 <br /> <br /> 报错信息<br /> 报错信息上文已经涉及到了,我们对<code>js</code>代码使用<code>try catch</code>进行了包裹,并使用<code>console.error</code>进行错误输出,但是还有些错误可能是<code>try catch</code>监听不到的,比如定时器代码执行出错,或者是没有被显式捕获的<code>Promise</code>异常,我们也需要加上对应的监听及显示。<br /> <blockquote><code>// /public/console/index.js// 错误监听 window.onerror = function (message, source, lineno, colno, error) { window.parent.postMessage({ type: 'console', method: 'string', data: [message, source, lineno, colno, error].map((item) => { return handleData(item) }) }) } window.addEventListener('unhandledrejection', err => { window.parent.postMessage({ type: 'console', method: 'string', data: [handleData(err.reason.stack)] }) })// ...</code></blockquote><br /> 执行输入的js<br /> <strong>【快速搭建一个代码在线编辑预览工具】</strong><code>console</code>的最后一个功能是可以输入<code>js</code>代码然后动态执行,这个可以使用<code>eval</code>方法,<code>eval</code>能动态执行<code>js</code>代码并返回最后一个表达式的值,<code>eval</code>会带来一些安全风险,但是笔者没有找到更好的替代方案,知道的朋友请在下方留言一起探讨吧。<br /> 动态执行的代码里的输出以及最后表达式的值我们也要显示到控制台里,为了不在上层拦截<code>console</code>,我们把动态执行代码的功能交给预览的<code>iframe</code>,执行完后再把最后的表达式的值使用<code>console</code>打印一下,这样所有的输出都能显示到控制台。<br /> <blockquote><code><textarea v-model="jsInput" @keydown.enter="implementJs"></textarea></code></blockquote><br /> <blockquote><code>const jsInput = ref('') const implementJs = (e) => { // shift+enter为换行,不需要执行 if (e.shiftKey) { return } e.preventDefault() let code = jsInput.value.trim() if (code) { // 给iframe发送信息 iframeRef.value.contentWindow.postMessage({ type: 'command', data: code }) jsInput.valuehttps://www.it610.com/article/= '' } }</code></blockquote><br /> <blockquote><code>// /public/console/index.js// 接收代码执行的事件 const onMessage = ({ data = https://www.it610.com/article/{} }) => { if (data.type === 'command') { try { // 打印一下要执行的代码 console.log(data.data) // 使用eval执行代码 console.log(eval(data.data)) } catch (error) { console.error('js执行出错') console.error(error) } } } window.addEventListener('message', onMessage)</code></blockquote><br /> 效果如下:<br /> <img alt="快速搭建一个代码在线编辑预览工具" onload="javascript:ImgReSize(this)" src="https://img.it610.com/image/info9/9dbfefda32bc4e98a3db21f315799ddf.gif"/> <br /> 文章图片 <br /> <br /> 支持预处理器 除了基本的<code>html</code>、<code>js</code>和<code>css</code>,作为一个强大的工具,我们有必要支持一下常用的预处理器,比如<code>html</code>的<code>pug</code>,<code>js</code>的<code>TypeScript</code>及<code>css</code>的<code>less</code>等,实现思路相当简单,加载对应预处理器的转换器,然后转换一下即可。<br /> 动态切换编辑器语言 <code>Monaco Editor</code>想要动态修改语言的话我们需要换一种方式来设置文档,上文我们是创建编辑器的同时直接把语言通过<code>language</code>选项传递进去的,然后使用<code>setValue</code>来设置文档内容,这样后期无法再动态修改语言,我们修改为切换文档模型的方式:<br /> <blockquote><code>// 创建编辑器 editor = monaco.editor.create(editorEl.value, { minimap: { enabled: false, // 关闭小地图 }, wordWrap: 'on', // 代码超出换行 theme: 'vs-dark', // 主题 fontSize: 18, fontFamily: 'MonoLisa, monospace', }) // 更新编辑器文档模型 const updateDoc = (code, language) => { if (!editor) { return } // 获取当前的文档模型 let oldModel = editor.getModel() // 创建一个新的文档模型 let newModel = monaco.editor.createModel(code, language) // 设置成新的 editor.setModel(newModel) // 销毁旧的模型 if (oldModel) { oldModel.dispose() } }</code></blockquote><br /> 加载转换器 转换器的文件我们都放在<code>/public/parses/</code>文件夹下,然后进行动态加载,即选择了某个预处理器后再去加载对应的转换器资源,这样可以节省不必要的请求。<br /> 异步加载<code>js</code>我们使用loadjs这个小巧的库,新增一个<code>load.js</code>:<br /> <blockquote><code>// 记录加载状态 const preprocessorLoaded = { html: true, javascript: true, css: true, less: false, scss: false, sass: false, stylus: false, postcss: false, pug: false, babel: false, typescript: false }// 某个转换器需要加载多个文件 const resources = { postcss: ['postcss-cssnext', 'postcss'] }// 异步加载转换器的js资源 export const load = (preprocessorList) => { // 过滤出没有加载过的资源 let notLoaded = preprocessorList.filter((item) => { return !preprocessorLoaded[item] }) if (notLoaded.length <= 0) { return } return new Promise((resolve, reject) => { // 生成加载资源的路径 let jsList = [] notLoaded.forEach((item) => { let _resources = (resources[item] || [item]).map((r) => { return `/parses/${r}.js` }) jsList.push(..._resources) }) loadjs(jsList, { returnPromise: true }).then(() => { notLoaded.forEach((item) => { preprocessorLoaded[item] = true }) resolve() }).catch((err) => { reject(err) }) }) }</code></blockquote><br /> 然后修改一下上文预览部分的<code>run </code>方法:<br /> <blockquote><code>const run = async () => { let h = editData.value.code.HTML.language let j = editData.value.code.JS.language let c = editData.value.code.CSS.language await load([h, j, c]) // ... }</code></blockquote><br /> 转换 所有代码都使用转换器转换一下,因为有的转换器是同步方式的,有的是异步方式的,所以我们统一使用异步来处理,修改一下<code>run</code>方法:<br /> <blockquote><code>const run = async () => { // ... await load([h, j, c]) let htmlTransform = transform.html(h, editData.value.code.HTML.content) let jsTransform = transform.js(j, editData.value.code.JS.content) let cssTransform = transform.css(c, editData.value.code.CSS.content) Promise.all([htmlTransform, jsTransform, cssTransform]) .then(([htmlStr, jsStr, cssStr]) => { // ... }) .catch((error) => { // ... }) }</code></blockquote><br /> 接下来就是最后的转换操作,下面只展示部分代码,完整代码有兴趣的可查看源码:<br /> <blockquote><code>// transform.jsconst html = (preprocessor, code) => { return new Promise((resolve, reject) => { switch (preprocessor) { case 'html': // html的话原封不动的返回 resolve(code) break; case 'pug': // 调用pug的api来进行转换 resolve(window.pug.render(code)) default: resolve('') break; } }) }const js = (preprocessor, code) => { return new Promise((resolve, reject) => { let _code = '' switch (preprocessor) { case 'javascript': resolve(code) break; case 'babel': // 调用babel的api来编译,你可以根据需要设置presets _code = window.Babel.transform(code, { presets: [ 'es2015', 'es2016', 'es2017', 'react' ] }).code resolve(_code) default: resolve('') break; } }) }const css = (preprocessor, code) => { return new Promise((resolve, reject) => { switch (preprocessor) { case 'css': resolve(code) break; case 'less': window.less.render(code) .then( (output) => { resolve(output.css) }, (error) => { reject(error) } ); break; default: resolve('') break; } }) }</code></blockquote><br /> 可以看到很简单,就是调一下相关转换器的<code>api</code>来转换一下,不过想要找到这些转换器的浏览器使用版本和<code>api</code>可太难了,笔者基本都没找到,所以这里的大部分代码都是参考codepan的。<br /> 其他功能 另外还有一些实现起来简单,但是能很大提升用户体验的功能,比如添加额外的<code>css</code>或<code>js</code>资源,免去手写<code>link</code>或<code>script</code>标签的麻烦:<br /> <img alt="快速搭建一个代码在线编辑预览工具" onload="javascript:ImgReSize(this)" src="https://img.it610.com/image/info9/c437d310fb864855b8fcb7ecfb17466e.jpg"/> <br /> 文章图片 <br /> <br /> 预设一些常用模板,比如<code>vue3</code>、<code>react</code>等,方便快速开始,免去写基本结构的麻烦:<br /> <br /> 有没有更快的方法 如果你看到这里,你一定会说这是哪门子快速搭建,那有没有更快的方法呢,当然有了,就是直接克隆本项目的仓库或者codepan,改改就可以使用啦~<br /> 结尾 本文从零开始介绍了如何搭建一个代码在线编辑预览的工具,粗糙实现总有不足之处,欢迎指出。<br /> 项目仓库code-run,欢迎<code>star</code>。<br /> </p> <div class="dede_pages"><ul></ul></div> <div class="pcd_ad"> <center><div class="_ahwullr0ac"></div> <script type="text/javascript"> (window.slotbydup = window.slotbydup || []).push({ id: "u6834461", container: "_ahwullr0ac", async: true }); </script> <script type="text/javascript" src="//cpro.baidustatic.com/cpro/ui/cm.js" async="async" defer="defer" > </script></center> </div> <div class="mbd_ad"> <div style=margin-top:10px;margin-bottom:10px;> <div class="_i7aftr79jl"></div> <script type="text/javascript"> (window.slotbydup = window.slotbydup || []).push({ id: "u5950612", container: "_i7aftr79jl", async: true }); </script> <!-- ½űֻһ --> <script type="text/javascript" src="//cpro.baidustatic.com/cpro/ui/cm.js" async="async" defer="defer" > </script> </div> </div> <h3>推荐阅读</h3> <ul class="post-loop post-loop-default cols-0"> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/2819387.html" title="丑小鸭的故事告诉我们什么道理?丑小鸭的故事简短版"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="丑小鸭的故事告诉我们什么道理?丑小鸭的故事简短版" src="http://img.readke.com/230724/012T45400-0-lp.jpg"> </a> </div> <div class="item-content"> <h4 class="item-title"> <a href="/c/2819387.html"> <b>丑小鸭的故事告诉我们什么道理?丑小鸭的故事简短版 </b></a></h4> <div class="item-meta"> <div class="item-meta-right"> </div> </div> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/2746190.html" title="为什么眼药水的有效期是24个月,药店老板却说只能用一个月?"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="为什么眼药水的有效期是24个月,药店老板却说只能用一个月?" src="/images/defaultpic.gif"> </a> </div> <div class="item-content"> <h4 class="item-title"> <a href="/c/2746190.html"> <b>为什么眼药水的有效期是24个月,药店老板却说只能用一个月? </b></a></h4> <div class="item-meta"> <div class="item-meta-right"> </div> </div> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/4012567.html" title="mysql 事务sql mysql数据库中的事件"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="mysql 事务sql mysql数据库中的事件" src="/images/defaultpic.gif"> </a> </div> <div class="item-content"> <h4 class="item-title"> <a href="/c/4012567.html"> <b>mysql 事务sql mysql数据库中的事件 </b></a></h4> <div class="item-meta"> <div class="item-meta-right"> </div> </div> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/3097816.html" title="新冠疫苗加强针是指第三针吗"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="新冠疫苗加强针是指第三针吗" src="/images/defaultpic.gif"> </a> </div> <div class="item-content"> <h4 class="item-title"> <a href="/c/3097816.html"> <b>新冠疫苗加强针是指第三针吗 </b></a></h4> <div class="item-meta"> <div class="item-meta-right"> </div> </div> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/2363751.html" title="wifi5和wifi6网速差别详细介绍"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="wifi5和wifi6网速差别详细介绍" src="http://img.readke.com/230601/01195360H-0-lp.jpg"> </a> </div> <div class="item-content"> <h4 class="item-title"> <a href="/c/2363751.html"> <b>wifi5和wifi6网速差别详细介绍 </b></a></h4> <div class="item-meta"> <div class="item-meta-right"> </div> </div> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/0ZQMX52021.html" title="Go 专栏|接口 interface"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="Go 专栏|接口 interface" src="/images/defaultpic.gif"> </a> </div> <div class="item-content"> <h4 class="item-title"> <a href="/c/0ZQMX52021.html"> <b>Go 专栏|接口 interface </b></a></h4> <div class="item-meta"> <div class="item-meta-right"> </div> </div> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/1457989.html" title="英孚英语每日e课 英孚英语"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="英孚英语每日e课 英孚英语" src="/images/defaultpic.gif"> </a> </div> <div class="item-content"> <h4 class="item-title"> <a href="/c/1457989.html"> <b>英孚英语每日e课 英孚英语 </b></a></h4> <div class="item-meta"> <div class="item-meta-right"> </div> </div> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/2651029.html" title="分手后有哪些治愈你的歌?"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="分手后有哪些治愈你的歌?" src="http://img.readke.com/230707/2044501526-0-lp.jpg"> </a> </div> <div class="item-content"> <h4 class="item-title"> <a href="/c/2651029.html"> <b>分手后有哪些治愈你的歌? </b></a></h4> <div class="item-meta"> <div class="item-meta-right"> </div> </div> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/02114461032022.html" title="《都挺好》感悟"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="《都挺好》感悟" src="/images/defaultpic.gif"> </a> </div> <div class="item-content"> <h4 class="item-title"> <a href="/c/02114461032022.html"> <b>《都挺好》感悟 </b></a></h4> <div class="item-meta"> <div class="item-meta-right"> </div> </div> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/978788.html" title="这里的爱心妈咪小屋,邀您有空来坐坐! 有空来坐坐"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="这里的爱心妈咪小屋,邀您有空来坐坐! 有空来坐坐" src="/images/defaultpic.gif"> </a> </div> <div class="item-content"> <h4 class="item-title"> <a href="/c/978788.html"> <b>这里的爱心妈咪小屋,邀您有空来坐坐! 有空来坐坐 </b></a></h4> <div class="item-meta"> <div class="item-meta-right"> </div> </div> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/3378731.html" title="犯了非法低价出让国有土地使用权应当怎样处罚"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="犯了非法低价出让国有土地使用权应当怎样处罚" src="http://img.readke.com/231107/0A5434596-0-lp.jpg"> </a> </div> <div class="item-content"> <h4 class="item-title"> <a href="/c/3378731.html"> <b>犯了非法低价出让国有土地使用权应当怎样处罚 </b></a></h4> <div class="item-meta"> <div class="item-meta-right"> </div> </div> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/3203499.html" title="南昌公积金贷款额度计算公式"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="南昌公积金贷款额度计算公式" src="http://img.readke.com/231006/0H224I62-0-lp.jpg"> </a> </div> <div class="item-content"> <h4 class="item-title"> <a href="/c/3203499.html"> <b>南昌公积金贷款额度计算公式 </b></a></h4> <div class="item-meta"> <div class="item-meta-right"> </div> </div> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/3005827.html" title="连云港2023秦山岛公开水域游泳邀请赛比赛项目+年龄分组"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="连云港2023秦山岛公开水域游泳邀请赛比赛项目+年龄分组" src="http://img.readke.com/230815/1IRa122-0-lp.jpg"> </a> </div> <div class="item-content"> <h4 class="item-title"> <a href="/c/3005827.html"> <b>连云港2023秦山岛公开水域游泳邀请赛比赛项目+年龄分组 </b></a></h4> <div class="item-meta"> <div class="item-meta-right"> </div> </div> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/3045512.html" title="微信如何视频会议 微信怎样视频会议"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="微信如何视频会议 微信怎样视频会议" src="http://img.readke.com/230821/060600H21-0-lp.jpg"> </a> </div> <div class="item-content"> <h4 class="item-title"> <a href="/c/3045512.html"> <b>微信如何视频会议 微信怎样视频会议 </b></a></h4> <div class="item-meta"> <div class="item-meta-right"> </div> </div> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/0R9111KH021.html" title="索尼n3ap属什么级别(索尼n3ap级别是多少)"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="索尼n3ap属什么级别(索尼n3ap级别是多少)" src="/images/defaultpic.gif"> </a> </div> <div class="item-content"> <h4 class="item-title"> <a href="/c/0R9111KH021.html"> <b>索尼n3ap属什么级别(索尼n3ap级别是多少) </b></a></h4> <div class="item-meta"> <div class="item-meta-right"> </div> </div> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/3078950.html" title="饿了么APP账号忘记怎么办?详细解决方法"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="饿了么APP账号忘记怎么办?详细解决方法" src="/images/defaultpic.gif"> </a> </div> <div class="item-content"> <h4 class="item-title"> <a href="/c/3078950.html"> <b>饿了么APP账号忘记怎么办?详细解决方法 </b></a></h4> <div class="item-meta"> <div class="item-meta-right"> </div> </div> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/1151052.html" title="我来教你迅雷极速版怎么设置下载完成后关机 迅雷极速版设置下载完自动关机教程"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="我来教你迅雷极速版怎么设置下载完成后关机 迅雷极速版设置下载完自动关机教程" src="http://img.readke.com/230226/011P914N-0-lp.jpg"> </a> </div> <div class="item-content"> <h4 class="item-title"> <a href="/c/1151052.html"> <b>我来教你迅雷极速版怎么设置下载完成后关机 迅雷极速版设置下载完自动关机教程 </b></a></h4> <div class="item-meta"> <div class="item-meta-right"> </div> </div> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/1157826.html" title="乳腺疾病需要做哪些检查 乳腺有哪些检查"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="乳腺疾病需要做哪些检查 乳腺有哪些检查" src="/images/defaultpic.gif"> </a> </div> <div class="item-content"> <h4 class="item-title"> <a href="/c/1157826.html"> <b>乳腺疾病需要做哪些检查 乳腺有哪些检查 </b></a></h4> <div class="item-meta"> <div class="item-meta-right"> </div> </div> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/1893302.html" title="正宗烤鱼的制作方法 怎么做烤鱼"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="正宗烤鱼的制作方法 怎么做烤鱼" src="/images/defaultpic.gif"> </a> </div> <div class="item-content"> <h4 class="item-title"> <a href="/c/1893302.html"> <b>正宗烤鱼的制作方法 怎么做烤鱼 </b></a></h4> <div class="item-meta"> <div class="item-meta-right"> </div> </div> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/3762172.html" title="摄影大赛介绍语 摄影大赛介绍"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="摄影大赛介绍语 摄影大赛介绍" src="/images/defaultpic.gif"> </a> </div> <div class="item-content"> <h4 class="item-title"> <a href="/c/3762172.html"> <b>摄影大赛介绍语 摄影大赛介绍 </b></a></h4> <div class="item-meta"> <div class="item-meta-right"> </div> </div> </div> </li> </ul> <p><br /><ul class="post-loop post-loop-list cols-4"><li><a href="/c/021T5L4H022.html" title="一个人的旅行,三亚" target="_blank">一个人的旅行,三亚 </a></li> <li><a href="/c/021T5L332022.html" title="一个小故事,我的思考。" target="_blank">一个小故事,我的思考。 </a></li> <li><a href="/c/021T5L132022.html" title="一个人的碎碎念" target="_blank">一个人的碎碎念 </a></li> <li><a href="/c/021T5KA2022.html" title="七年之痒之后" target="_blank">七年之痒之后 </a></li> <li><a href="/c/021T5K622022.html" title="我从来不做坏事" target="_blank">我从来不做坏事 </a></li> <li><a href="/c/021T5K1R022.html" title="异地恋中,逐渐适应一个人到底意味着什么()" target="_blank">异地恋中,逐渐适应一个人到底意味着什么() </a></li> <li><a href="/c/021T5J402022.html" title="迷失的世界(二十七)" target="_blank">迷失的世界(二十七) </a></li> <li><a href="/c/021T5J342022.html" title="live|live to inspire 一个普通上班族的流水账0723" target="_blank">live|live to inspire 一个普通上班族的流水账0723 </a></li> <li><a href="/c/021T5J1R022.html" title="遗憾是生活的常态,但孝顺这件事,我希望每一个人都不留遗憾" target="_blank">遗憾是生活的常态,但孝顺这件事,我希望每一个人都不留遗憾 </a></li> <li><a href="/c/021T5I432022.html" title="NO.38|NO.38 我不是嫁不出去,而是不想嫁" target="_blank">NO.38|NO.38 我不是嫁不出去,而是不想嫁 </a></li> </ul></p> <div class=entry-copyright> <p></p> </div> </div> <div class="entry-footer"> <div class="prev-next sb br mb clearfix"> <p class="post-prev fl ellipsis">上一篇:<a href='/c/100HHD62021.html'>用console画条龙?</a> </p> <p class="post-next fr ellipsis">下一篇:<a href='/c/100HHDR021.html'>web文本划线的极简实现</a> </p> </div> </div> </div> </article> </main> <aside class="sidebar"> <div class="widget widget_post_thumb"> <h3 class="widget-title"><span>更多...</span></h3> <ul> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/091521414R021.html" title="高考之后"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="高考之后" src="/images/defaultpic.gif"> </a></div> <div class="item-content"> <p class="item-title"><a href="/c/091521414R021.html" title="高考之后">高考之后</a></p> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/091R2325R021.html" title="115(生活的日常)"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="115(生活的日常)" src="/images/defaultpic.gif"> </a></div> <div class="item-content"> <p class="item-title"><a href="/c/091R2325R021.html" title="115(生活的日常)">115(生活的日常)</a></p> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/09111916192021.html" title="自定义View合辑(1)-时钟"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="自定义View合辑(1)-时钟" src="/images/defaultpic.gif"> </a></div> <div class="item-content"> <p class="item-title"><a href="/c/09111916192021.html" title="自定义View合辑(1)-时钟">自定义View合辑(1)-时钟</a></p> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/12263VW52021.html" title="天气忽冷..."> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="天气忽冷..." src="/images/defaultpic.gif"> </a></div> <div class="item-content"> <p class="item-title"><a href="/c/12263VW52021.html" title="天气忽冷...">天气忽冷...</a></p> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/120134B962021.html" title="《小慧日记》第三十九篇|《小慧日记》第三十九篇 2018.4.27 星期五 天气(晴)"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="《小慧日记》第三十九篇|《小慧日记》第三十九篇 2018.4.27 星期五 天气(晴)" src="/images/defaultpic.gif"> </a></div> <div class="item-content"> <p class="item-title"><a href="/c/120134B962021.html" title="《小慧日记》第三十九篇|《小慧日记》第三十九篇 2018.4.27 星期五 天气(晴)">《小慧日记》第三十九篇|《小慧日记》第三十九篇 2018.4.27 星期五 天气(晴)</a></p> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/11013062G2021.html" title="13个最常用的CSS和HTML快速开发工具"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="13个最常用的CSS和HTML快速开发工具" src="/images/defaultpic.gif"> </a></div> <div class="item-content"> <p class="item-title"><a href="/c/11013062G2021.html" title="13个最常用的CSS和HTML快速开发工具">13个最常用的CSS和HTML快速开发工具</a></p> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/12153640B2021.html" title="Spring ioc与aop的理解"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="Spring ioc与aop的理解" src="/images/defaultpic.gif"> </a></div> <div class="item-content"> <p class="item-title"><a href="/c/12153640B2021.html" title="Spring ioc与aop的理解">Spring ioc与aop的理解</a></p> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/092R533X2021.html" title="台北吃吃喝喝一路"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="台北吃吃喝喝一路" src="/images/defaultpic.gif"> </a></div> <div class="item-content"> <p class="item-title"><a href="/c/092R533X2021.html" title="台北吃吃喝喝一路">台北吃吃喝喝一路</a></p> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/0911192O12021.html" title="记忆里的昨天【序】"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="记忆里的昨天【序】" src="/images/defaultpic.gif"> </a></div> <div class="item-content"> <p class="item-title"><a href="/c/0911192O12021.html" title="记忆里的昨天【序】">记忆里的昨天【序】</a></p> </div> </li> <li class="item"> <div class="item-img"> <a class="item-img-inner" href="/c/0Z112FK2021.html" title="爱上一座城"> <img width="480" height="300" class="attachment-default size-default wp-post-image j-lazy" alt="爱上一座城" src="/images/defaultpic.gif"> </a></div> <div class="item-content"> <p class="item-title"><a href="/c/0Z112FK2021.html" title="爱上一座城">爱上一座城</a></p> </div> </li> </ul> </div> </aside> </div> </div> <footer class="footer"> <div class="container"> <div class="clearfix"> <div class="footer-col footer-col-logo"> <!--<img src="/skin/images/logo-footer.png">--></div> <div class="footer-col footer-col-copy"> <ul class="footer-nav hidden-xs"> <li class="menu-item menu-item-706"><a href="/baike/">生活百科</a></li> <li class="menu-item menu-item-706"><a href="/it/">it技术</a></li> </ul> <div class="copyright"> <p>Copyright © 2017-2022 锐客网 <a href="http://beian.miit.gov.cn/" target="_blank" rel="nofollow">京ICP备11041112号-41</a> </p> </div> </div> <div class="footer-col footer-col-sns"> <div class="footer-sns"> </div> </div> </div> </div> </footer> <div class="action action-style-0 action-color-0 action-pos-1" style="bottom:15%;"> <div class="action-item gotop j-top"> <i class="web-icon wi action-item-icon"><svg aria-hidden="true"> <use xlink:href="#wi-arrow-up-2"></use> </svg></i></div> </div> <script> var _hmt = _hmt || []; (function() { var hm = document.createElement("script"); hm.src = "https://hm.baidu.com/hm.js?79e4e485d34c6fc717489eaa10b314e3"; var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(hm, s); })(); </script> </div> <script>var _web_js={};</script> <script src="/skin/js/index.js"></script> </body> </html>