在文章開始以前先展現一下我本身作的在線編譯器 JS-Encoder
:javascript
點此預覽css
大概三四個月以前我開始有了製做在線編譯器的想法,在此以前我接觸過不少的在線編譯器,如CodePen、JsBin、JsFiddle等,這些都很是優秀且有着龐大的用戶羣體的編譯器。html
我一直對在線編譯器的實現抱有濃厚興趣,這些在線編譯器支持不少種語言,代碼變色,諸多的快捷鍵以及一些個性化設置,這使得在線編譯器看上去和咱們在本地下載的編譯器軟件也不會有太大的區別,我徹底不知道這些複雜的功能要怎麼實現,因而我觀察 CodePen
和 JsBin
代碼發現他倆都使用了一個叫 codemirror 的工具。vue
codemirror
是一個用於瀏覽器的 JavaScript
實現的多功能文本編輯器。它專門用於編輯代碼,並帶有許多語言模式和插件 ,可實現更高級的編輯功能。java
原來這些編譯器是依靠 codemirror
來實現的,codemirror
是一個很是複雜的工具,以致於我花了兩天時間才熟悉它的配置項。codemirror
自己是採用直接操做 DOM
的方式,而個人項目是使用 Vue
+ Webpack
構建的,這違反了 Vue
數據驅動 的宗旨,因而我在 npm
上發現了 vue-codemirror 這個工具,採用 Vue
的方式構建代碼編輯器git
codemirror
有許多配置項,我在本身的項目中用到了以下配置,若是你想看所有配置,能夠看這裏github
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 // 激活當前行樣式
},
複製代碼
這些配置只是一小部分,但足夠實現我想要的功能了npm
mode
表示當前編輯器使用的語言promise
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
上面加一個透明的遮罩層,這樣就不會出現拖不動的問題了
在用戶一段時間內不輸入任何字符或者用戶直接點擊運行按鈕的時候,須要將編輯器中的 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)
前面已經說了 HTML
,CSS
,JavaScript
和 iframe
這四個窗口,就剩下 Console
窗口了
Console
窗口用於顯示 iframe
控制檯中的內容,若是想將這些內容顯示在頁面上,就要在用戶觸發這些方法的時候獲取到裏面的信息,我採起了重寫 console
和 window.onerror
的方式:
注意:下面這段 js
代碼是寫在 iframe
內部的
let consoleInfo = []
// 重寫console
if (console) {
const ableMethods = ['log', 'info', 'debug', 'warn', 'error']
for (let item of ableMethods) {
console[item] = function (data) {
consoleInfo.push({ data, type: item })
}
}
}
// 重寫window.error
window.onerror = function (msg, url, row, col) {
consoleInfo.push({ data: { msg, url, row, col }, type: 'error' })
return false // return false阻止錯誤在瀏覽器控制檯中報出
}
複製代碼
console
對象遠遠不止這幾個方法,我只是重寫了一些經常使用方法而已
而後要在組件中獲取 iframe
元素的 consoleInfo
:
const consoleInfo = this.$refs.iframeBox.contentWindow.consoleInfo
複製代碼
在 JS-Encoder
中除了預處理語言的選擇以外,還有如下設置
watch
監聽值的變化, 頻繁的輸入會致使方法的頻繁觸發,因此我設置了防抖函數,在設置的延遲時間內用戶沒有輸入任何字符,纔會執行代碼CDN
,這樣會在執行 JavaScript
以前先引入 CDN
CSS
,這樣會在執行 CSS
以前先經過 link
引入快捷鍵能夠大大加快咱們的編碼速度,codemirror
也支持快捷鍵的配置,咱們在上面選擇了 sublime
做爲 keymap
的配置,也就是說,咱們在 sublime
上能用的大多數快捷鍵,均可以在線上編輯器中使用,在官網上,能夠看到全部支持的快捷鍵,可是惟獨沒有經常使用的 Tab
快捷鍵,由於 codemirror
只爲 Tab
鍵開發了縮進功能
在不少編譯器軟件上寫 html
均可以使用 Tab
實現如下功能:
這種功能來源於一個叫 emmet
的工具,這麼好用的東西固然有人爲 codemirror
實現,因此我在 npm
上找到了 codemirror-emmet 工具,下面我介紹一下它的用法:
首先導入 codemirror
和 codemirror-emmet
import CodeMirror from 'codemirror'
import codeMirrorEmmet from 'codemirror-emmet'
複製代碼
codemirror-emmet
返回一個 promise 對象:
codeMirrorEmmet.then(emmet => {
emmet(CodeMirror) // 將emmet的功能合併到codemirror中
cmOptions.extraKeys = {
...cmOptions.extraKeys,// 由於我以前已經設置了默認的extraKeys,因此這裏使用對象擴展符將以前的配置和tab合併
Tab: cm => {
if (cm.somethingSelected()) {// 當選中文本並按下tab,文本總體縮進
cm.indentSelection('add')
} else if (cm.getOption('mode').indexOf('html') > -1) {// 當前的mode爲html時,執行命令emmetExpandAbbreviation
try {
cm.execCommand('emmetExpandAbbreviation')
} catch (err) {
console.error(err)
}
} else {// 前面兩個條件都不知足,按下tab就正常縮進
const spaces = Array(cm.getOption('indentUnit') + 1).join(' ')
cm.replaceSelection(spaces, 'end', '+input')
}
},
Enter: 'emmetInsertLineBreak'
}
cmOptions.emmet = {// 配置emmet項
markupSnippets: {
'script:unpkg': 'script[src="https://unpkg.com/"]',
'script:jsd': 'script[src="https://cdn.jsdelivr.net/npm/"]'
}
}
})
複製代碼
這樣就實現了圖片上的功能
JS-Encoder
從正式開發到如今已經有兩個月,由於學業緣由,也沒有過多的時間投入到開發中。目前 JS-Encoder
仍是一個半成品,除了一些基本的以外其實還有不少功能沒有或者正在實現,若是感興趣的話能夠在github上關注這個項目。隨着更多功能的實現,我會繼續更新這篇文章。