如何製作一款線上編譯器

lliiooiill發表於2019-07-22

在文章開始之前先展示一下我自己做的線上編譯器 JS-Encoder:

點此預覽

截圖未命名.jpg

大概三四個月之前我開始有了製作線上編譯器的想法,在此之前我接觸過很多的線上編譯器,如CodePenJsBinJsFiddle等,這些都非常優秀且有著龐大的使用者群體的編譯器。

我一直對線上編譯器的實現抱有濃厚興趣,這些線上編譯器支援很多種語言,程式碼變色,諸多的快捷鍵以及一些個性化設定,這使得線上編譯器看上去和我們在本地下載的編譯器軟體也不會有太大的區別,我完全不知道這些複雜的功能要怎麼實現,於是我觀察 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 的效果還是不錯的

截圖未命名.jpg

有了 codemirror 這個神器,可以說最難的問題已經解決了,但是還有很多數不清的小問題需要解決

佈局

佈局方面有很多是參考 JsBin 的,因為我覺得它的介面看起來很簡潔,舒服

JsBin 的佈局是醬嬸兒的:

截圖未命名.jpg

分為五個視窗,滑鼠放到兩個視窗的邊界上可以拖動改變視窗大小

GIF.gif

滑鼠的拖動會使得一個視窗寬度增加,而另一個視窗寬度減少,但是兩個視窗寬度之和是不會改變的

我的思路是:

在點選邊界的時候獲取兩個相鄰視窗的寬度,滑鼠拖動的時候計算滑鼠水平移動距離,並對兩個視窗的寬度進行相應增減

由於這五個視窗都是同級的子元件,一個視窗獲取另外一個視窗的寬度比較麻煩,於是我將這五個視窗的寬度都放在 Vuex 中儲存以便使用,每一個視窗的寬度都隨著 Vuex 中寬度資訊的改變而改變

成功實現效果:

GIF.gif

為了避免兩個視窗重合問題,我設定了 min-width: 100px; 的樣式

除了兩個視窗的問題之外,還要做到所有視窗寬度隨著瀏覽器寬度變化而改變:

GIF.gif

這個效果也很容易實現,只要在瀏覽器寬度改變的時候每個視窗的寬度加上或減去 改變寬度/視窗數量 就可以了

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上關注這個專案。隨著更多功能的實現,我會繼續更新這篇文章。

相關文章