简介
大家好,我是一个闲着没事热衷于重复造轮子的不知名前端,今天给大家带来的是一个代码在线编辑预览工具的实现介绍,目前这类工具使用很广泛,常见于各种文档网站及代码分享场景,相关工具也比较多,如codepen、jsrun、codesandbox、jsbin、plnkr、jsfiddle等,这些工具大体分两类,一类可以自由添加多个文件,比较像我们平常使用的编辑器,另一类固定只能单独编辑html
、js
、css
,第二类比较常见,对于demo
场景来说其实已经够用,当然,说的只是表象,底层实现方式可能还是各有千秋的。
本文主要介绍的是第二类其中的一种实现方式,完全不依赖于后端,所有逻辑都在前端完成,实现起来相当简单,使用的是vue3
全家桶来开发,使用其他框架也完全可以。
ps.在本文基础上笔者开发了一个完整的线上工具,带云端保存,地址:http://lxqnsys.com/code-run/,欢迎使用。
页面结构
我挑了一个比较典型也比较好看的结构来仿照,默认布局上下分成四部分,工具栏、编辑器、预览区域及控制台,编辑器又分为三部分,分别是HTML
、CSS
、JavaScript
,其实就是三个编辑器,用来编辑代码。
各部分都可以拖动进行调节大小,比如按住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.vue
,DragItem
通过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())
})
预览
代码有了,接下来就可以渲染页面进行预览了,对于预览,显然是使用iframe
,iframe
除了src
属性外,HTML5
还新增了一个属性srcdoc
,用来渲染一段HTML
代码到iframe
里,这个属性IE
目前不支持,不过vue3
都要不支持IE
了,咱也不管了,如果硬要支持也简单,使用write
方法就行了:
iframeRef.value.contentWindow.document.write(htmlStr)
接下来的思路就很清晰了,把html
、css
和js
代码组装起来扔给srcdoc
不就完了吗:
const assembleHtml = (head, body) => {
return `
${head}
${body}
`
}
const run = () => {
let head = `
预览<\/title>