在文章開始之前先展示一下我自己做的在線編譯器 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上關注這個項目。隨着更多功能的實現,我會繼續更新這篇文章。