在文章開始之前先展示一下我自己做的線上編譯器 JS-Encoder
:
大概三四個月之前我開始有了製作線上編譯器的想法,在此之前我接觸過很多的線上編譯器,如CodePen、JsBin、JsFiddle等,這些都非常優秀且有著龐大的使用者群體的編譯器。
我一直對線上編譯器的實現抱有濃厚興趣,這些線上編譯器支援很多種語言,程式碼變色,諸多的快捷鍵以及一些個性化設定,這使得線上編譯器看上去和我們在本地下載的編譯器軟體也不會有太大的區別,我完全不知道這些複雜的功能要怎麼實現,於是我觀察 CodePen
和 JsBin
程式碼發現他倆都使用了一個叫 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
我設定為 sublime
,sublime
上大部分快捷鍵都是可用的
其他的配置我在註釋裡應該已經說明白了,這裡就不解釋了
codemirror
的效果還是不錯的
有了 codemirror
這個神器,可以說最難的問題已經解決了,但是還有很多數不清的小問題需要解決
佈局
佈局方面有很多是參考 JsBin
的,因為我覺得它的介面看起來很簡潔,舒服
JsBin
的佈局是醬嬸兒的:
分為五個視窗,滑鼠放到兩個視窗的邊界上可以拖動改變視窗大小
滑鼠的拖動會使得一個視窗寬度增加,而另一個視窗寬度減少,但是兩個視窗寬度之和是不會改變的
我的思路是:
在點選邊界的時候獲取兩個相鄰視窗的寬度,滑鼠拖動的時候計算滑鼠水平移動距離,並對兩個視窗的寬度進行相應增減
由於這五個視窗都是同級的子元件,一個視窗獲取另外一個視窗的寬度比較麻煩,於是我將這五個視窗的寬度都放在 Vuex
中儲存以便使用,每一個視窗的寬度都隨著 Vuex
中寬度資訊的改變而改變
成功實現效果:
為了避免兩個視窗重合問題,我設定了 min-width: 100px;
的樣式
除了兩個視窗的問題之外,還要做到所有視窗寬度隨著瀏覽器寬度變化而改變:
這個效果也很容易實現,只要在瀏覽器寬度改變的時候每個視窗的寬度加上或減去 改變寬度/視窗數量 就可以了
Iframe
這是我第一次真正接觸 iframe
這個東西,可能他很簡單,但我確實在它身上花了不小的力氣
我已經解決了視窗拖動的問題,但這對 iframe
是無效的,我一直很困惑,找不出原因,最後突然想到:
iframe
是一個獨立的新頁面,在 iframe
之外觸發的事件不會影響到 iframe
本身,當我用滑鼠拖動邊界的時候,如果滑鼠進入了 iframe
中,那麼這個拖動事件就失效了,所以在拖動時候需要先給 iframe
上面加一個透明的遮罩層,這樣就不會出現拖不動的問題了
在使用者一段時間內不輸入任何字元或者使用者直接點選執行按鈕的時候,需要將編輯器中的 HTML
,CSS
和 JavaScript
程式碼放到 iframe
中,iframe
就會將最終效果展示出來,於是編輯器中的內容我也會放在 Vuex
中
編譯
codemirror
可以實現很多功能,但編譯這件事兒他是不幹的,像 JsBin
和 CodePen
這樣的編譯器不只是支援普通的 HTML
,CSS
和 JavaScript
而已,他們還支援很多這三種語言的預處理語言
比如我選擇了 TypeScript
作為預處理語言,那麼編譯器就需要先將 TypeScript
轉化為 JavaScript
再傳給 iframe
由於 JS-Encoder
是一個完全沒有後臺的編譯器,所以要引入其他預處理語言的 npm
包和檔案來編譯,比如在實現 Sass
和 Scss
的編譯上, 我引入了 Sass.js
和 Sass.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
支援MarkDown
,Sass
,Scss
,Less
,Stylus
,TypeScript
和 CoffeeScript
, 之後會考慮支援 LiveScript
和 JSX(React)
設定
在 JS-Encoder
中除了預處理語言的選擇之外,還有以下設定
- 延遲執行時間
- 每一個可編輯視窗我都設定了
watch
監聽值的變化, 頻繁的輸入會導致方法的頻繁觸發,所以我設定了防抖函式,在設定的延遲時間內使用者沒有輸入任何字元,才會執行程式碼
- 每一個可編輯視窗我都設定了
- 將和tab等寬度的space轉化為tab
- CDN
- 可以新增外部的
CDN
,這樣會在執行JavaScript
之前先引入CDN
- 可以新增外部的
- CSS
- 可以新增外部的
CSS
,這樣會在執行CSS
之前先通過link
引入
- 可以新增外部的
總結
JS-Encoder
從正式開發到現在已經有兩個月,因為學業原因,也沒有過多的時間投入到開發中。目前 JS-Encoder
還是一個半成品,除了一些基本的之外其實還有很多功能沒有或者正在實現,如果感興趣的話可以在github上關注這個專案。隨著更多功能的實現,我會繼續更新這篇文章。