如何制作一款在线编译器

如何制作一款在线编译器_第1张图片

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

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

codemirror

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

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

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

cmOptions: {
        flattenSpans: false,
        tabSize: 2,
        mode: '',
        theme: 'monokai',
        smartIndent: true,
        lineNumbers: true,
        matchBrackets: true,
        lineWiseCopyCut: true,
        indentWithTabs: true,
        electricChars: true,
        indentUnit: 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'],
        styleActiveLine: true
      },
复制代码

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

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

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

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

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

codemirror 的效果还是不错的

如何制作一款在线编译器_第2张图片

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

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

JsBin 的布局是酱婶儿的:

如何制作一款在线编译器_第3张图片

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

如何制作一款在线编译器_第4张图片

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

我的思路是:

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

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

成功实现效果:

如何制作一款在线编译器_第5张图片

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

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

如何制作一款在线编译器_第6张图片

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

Iframe 

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

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

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

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

编译

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

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

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

async function compileSass(code) {
  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 支持MarkDownSassScssLessStylusTypeScript 和 CoffeeScript, 之后会考虑支持 LiveScript 和 JSX(React)

控制台

前面已经说了 HTMLCSSJavaScript 和 iframe 这四个窗口,就剩下 Console 窗口了

Console 窗口用于显示 iframe 控制台中的内容,如果想将这些内容显示在页面上,就要在用户触发这些方法的时候获取到里面的信息,我采取了重写 console 和 window.onerror 的方式:

注意:下面这段 js 代码是写在 iframe 内部的

let consoleInfo = []
if (console) {
  const ableMethods = ['log', 'info', 'debug', 'warn', 'error']
  for (let item of ableMethods) {
    console[item] = function (data) {
      consoleInfo.push({ data, type: item })
    }
  }
}
window.onerror = function (msg, url, row, col) {
  consoleInfo.push({ data: { msg, url, row, col }, type: 'error' })
  return false
}

console 对象远远不止这几个方法,我只是重写了一些常用方法而已

然后要在组件中获取 iframe 元素的 consoleInfo

const consoleInfo = this.$refs.iframeBox.contentWindow.consoleInfo

设置

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

  • 延迟执行时间

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

  • 将和tab等宽度的space转化为tab

  • CDN

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

  • CSS

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

总结

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

作者:lliiooiill

来源:

https://juejin.im/post/5d3516a36fb9a07ed36ee787

 热 文 推 荐 

☞ Vue单页项目SEO完全指南

☞ 长列表优化之虚拟列表

☞ 移动端浏览器前端优化策略

640?wx_fmt=png

640?wx_fmt=png

你也“在看”吗?

你可能感兴趣的:(如何制作一款在线编译器)