乐趣区

如何制作一款在线编译器

在文章开始之前先展示一下我自己做的在线编译器 JS-Encoder:

点此预览

大概三四个月之前我开始有了制作在线编译器的想法,在此之前我接触过很多的在线编译器,如 CodePen、JsBin、JsFiddle 等,这些都非常优秀且有着庞大的用户群体的编译器。

我一直对在线编译器的实现抱有浓厚兴趣,这些在线编译器支持很多种语言,代码变色,诸多的快捷键以及一些个性化设置,这使得在线编译器看上去和我们在本地下载的编译器软件也不会有太大的区别,我完全不知道这些复杂的功能要怎么实现,于是我观察 CodePenJsBin 代码发现他俩都使用了一个叫 codemirror 的工具。

codemirror

codemirror 是一个用于浏览器的 JavaScript 实现的多功能文本编辑器。它专门用于编辑代码,并带有许多语言模式和插件,可实现更高级的编辑功能。

原来这些编译器是依靠 codemirror 来实现的,codemirror 是一个非常复杂的工具,以至于我花了两天时间才熟悉它的配置项。codemirror 本身是采用直接操作 DOM 的方式,而我的项目是使用 Vue + Webpack 构建的,这违反了 Vue 数据驱动 的宗旨,于是我在 npm 上发现了 vue-codemirror 这个工具,采用 Vue 的方式构建代码编辑器

codemirror 有许多配置项,我在自己的项目中用到了如下配置,如果你想看全部配置,可以看这里

cmOptions: {
        // codemirror config
        flattenSpans: false, // 默认情况下,CodeMirror 会将使用相同 class 的两个 span 合并成一个。通过设置此项为 false 禁用此功能
        tabSize: 2, // tab 缩进空格数
        mode: '', // 模式
        theme: 'monokai', // 主题
        smartIndent: true, // 是否智能缩进
        lineNumbers: true, // 显示行号
        matchBrackets: true, // 匹配符号
        lineWiseCopyCut: true, // 如果在复制或剪切时没有选择文本,那么就会自动操作光标所在的整行
        indentWithTabs: true, // 在缩进时,是否需要把 n*tab 宽度个空格替换成 n 个 tab 字符
        electricChars: true, // 在输入可能改变当前的缩进时,是否重新缩进
        indentUnit: 2, // 缩进单位,默认 2
        autoCloseTags: true, // 自动关闭标签
        autoCloseBrackets: true, // 自动输入括弧
        foldGutter: true, // 允许在行号位置折叠
        cursorHeight: 1, // 光标高度
        keyMap: 'sublime', // 快捷键集合
        extraKeys: {
          'Ctrl-Alt': 'autocomplete',
          'Ctrl-Q': cm => {cm.foldCode(cm.getCursor())
          }
        }, // 智能提示
        gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], // 用来添加额外的 gutter
        styleActiveLine: true // 激活当前行样式
      },

这些配置只是一小部分,但足够实现我想要的功能了

mode 表示当前编辑器使用的语言

theme 表示编辑器使用的配色,官方支持很多种配色,但确没有配色预览,所以我直接使用我熟悉的 monokai 作为主题,因为我比较喜欢 vscode 的配色,所以我找到 monokai.css 文件并修改了许多样式,虽然最后还是和真正的 vscode 主题有差异,但我真的尽力了????

keymap 我设置为 sublimesublime 上大部分快捷键都是可用的

其他的配置我在注释里应该已经说明白了,这里就不解释了

codemirror 的效果还是不错的

有了 codemirror 这个神器,可以说最难的问题已经解决了,但是还有很多数不清的小问题需要解决

布局

布局方面有很多是参考 JsBin 的,因为我觉得它的界面看起来很简洁,舒服

JsBin 的布局是酱婶儿的:

分为五个窗口,鼠标放到两个窗口的边界上可以拖动改变窗口大小

鼠标的拖动会使得一个窗口宽度增加,而另一个窗口宽度减少,但是两个窗口宽度之和是不会改变的

我的思路是:

在点击边界的时候获取两个相邻窗口的宽度,鼠标拖动的时候计算鼠标水平移动距离,并对两个窗口的宽度进行相应增减

由于这五个窗口都是同级的子组件,一个窗口获取另外一个窗口的宽度比较麻烦,于是我将这五个窗口的宽度都放在 Vuex 中储存以便使用,每一个窗口的宽度都随着 Vuex 中宽度信息的改变而改变

成功实现效果:

为了避免两个窗口重合问题,我设置了 min-width: 100px; 的样式

除了两个窗口的问题之外,还要做到所有窗口宽度随着浏览器宽度变化而改变:

这个效果也很容易实现,只要在浏览器宽度改变的时候每个窗口的宽度加上或减去 改变宽度 / 窗口数量 就可以了

Iframe

这是我第一次真正接触 iframe 这个东西,可能他很简单,但我确实在它身上花了不小的力气

我已经解决了窗口拖动的问题,但这对 iframe 是无效的,我一直很困惑,找不出原因,最后突然想到:

iframe 是一个独立的新页面,在 iframe 之外触发的事件不会影响到 iframe 本身,当我用鼠标拖动边界的时候,如果鼠标进入了 iframe 中,那么这个拖动事件就失效了,所以在拖动时候需要先给 iframe 上面加一个透明的遮罩层,这样就不会出现拖不动的问题了

在用户一段时间内不输入任何字符或者用户直接点击运行按钮的时候,需要将编辑器中的 HTMLCSSJavaScript 代码放到 iframe 中,iframe 就会将最终效果展示出来,于是编辑器中的内容我也会放在 Vuex

编译

codemirror 可以实现很多功能,但编译这件事儿他是不干的,像 JsBinCodePen 这样的编译器不只是支持普通的 HTMLCSSJavaScript 而已,他们还支持很多这三种语言的预处理语言

比如我选择了 TypeScript 作为预处理语言,那么编译器就需要先将 TypeScript 转化为 JavaScript 再传给 iframe

由于 JS-Encoder 是一个完全没有后台的编译器,所以要引入其他预处理语言的 npm 包和文件来编译,比如在实现 SassScss 的编译上,我引入了 Sass.jsSass.worker.js 来编译:

async function compileSass(code) {
  // scss&sass
  if (!loadFiles.get('sass')) {const Sass = await require('./sass')
    Sass.setWorkerUrl('static/js/sass.worker.js')
    loadFiles.set('sass', Sass)
  }

  const defSass = loadFiles.get('sass')
  const sass = new defSass()
  
  return new Promise((resolve, reject) => {
    sass.compile(code, result => {if (result.status === 0) resolve(result.text)
      else reject(new Error('fail to get result'))
    })
  })
}

这里 loadFiles 只是用于判断是否已经引入过这些文件而已,我是在官方文档上看到这个编译方法的

目前 JS-Encoder 支持 MarkDownSassScssLessStylusTypeScriptCoffeeScript,之后会考虑支持 LiveScriptJSX(React)

设置

JS-Encoder 中除了预处理语言的选择之外,还有以下设置

  • 延迟执行时间

    • 每一个可编辑窗口我都设置了 watch 监听值的变化,频繁的输入会导致方法的频繁触发,所以我设置了防抖函数,在设置的延迟时间内用户没有输入任何字符,才会执行代码
  • 将和 tab 等宽度的 space 转化为 tab
  • CDN

    • 可以添加外部的 CDN,这样会在执行 JavaScript 之前先引入 CDN
  • CSS

    • 可以添加外部的 CSS,这样会在执行 CSS 之前先通过 link 引入

总结

JS-Encoder 从正式开发到现在已经有两个月,因为学业原因,也没有过多的时间投入到开发中。目前 JS-Encoder 还是一个半成品,除了一些基本的之外其实还有很多功能没有或者正在实现,如果感兴趣的话可以在 github 上关注这个项目。随着更多功能的实现,我会继续更新这篇文章。

退出移动版