先放上文連接 如何製做一款在線編譯器(1)javascript
離上個文章發佈又過去了一個月的時間,在這一個月的時間內,我又騰出了一些時間去完善編輯器的各個功能css
如今的 JS-Encoder
長這樣:html
點擊進入編輯器vue
總體和之前差很少,主要是加了一個側邊欄java
下面說一下這些日子裏所實現的一些主要功能:git
代碼格式化是一個很是重要的功能,咱們平時寫代碼的時候很難去注意程序的排版結構,不少換行,符號,空格都被忽略掉,致使代碼可讀性下降。github
其實不光是本身寫代碼,如今是面向百度編程時代,上網 copy
代碼也是常有的事,代碼格式正常,看上去美觀,一複製到本身的編輯器上就不知道哪兒是哪兒了,本身修改又浪費時間。因此,一個能夠自動格式化的工具是很是必要的正則表達式
那麼如何實現自動格式化的功能呢?npm
我在 npm
上找到了一個叫作 js-beautify
的包,附上連接編程
js-beautify
能夠格式化 html
、css
和 js
代碼,知足功能要求
好比我想格式化 html
:
async function formatHtml(code) {
if (!loadFiles.get("beautifyHtml")) {
const beautifyHtml = await require("js-beautify").html;
loadFiles.set("beautifyHtml", beautifyHtml);
}
return loadFiles.get("beautifyHtml")(code, formatOptions);
}
複製代碼
只須要獲取當前的 html
代碼並做爲參數傳給 formatHtml
,就能夠了
在 formatHtml
中,loadFiles
的功能是用來判斷 js-beautify
是否已載入的,由於只有須要格式化代碼的時候纔有必要載入它。
loadFiles
:
class LoadFiles {
constructor() {
this.map = {};
}
set(k, v) {
this.map[k] = v;
}
get(k) {
return this.map[k];
}
}
const loadFiles = new LoadFiles();
複製代碼
一樣的,格式化 css
和 js
的代碼也用相同的方法
方法寫好了以後就要綁定快捷鍵,我設置的格式化快捷鍵是 shift
+ alt
+ f
,這也是我在 vscode
上格式化的快捷鍵
我是使用 vue-codemirror
工具來構建代碼編輯器,因而要在 extraKeys
對象中定義快捷鍵,首先要引入格式化方法:
import * as format from "../utils/prettyFormat";
複製代碼
而後:
export default function(mode = "") {
const cmOptions = {
// 其餘配置項省略
extraKeys: {
// 其餘快捷鍵配置省略
"Shift-Alt-F": async cm => {
const code = cm.getValue();
let finCode = "";
switch (mode) {
case "HTML":
await format.formatHtml(code).then(res => {
finCode = res;
});
break;
case "CSS":
await format.formatCss(code).then(res => {
finCode = res;
});
break;
case "JavaScript":
await format.formatJavaScript(code).then(res => {
finCode = res;
});
break;
}
cm.setValue(finCode);
}
}
};
// 其餘功能代碼省略
}
複製代碼
mode
是在構建代碼窗口的時候傳入的參數,表示當前窗口使用的語言。
當快捷鍵被按下時,先判斷焦點所在的代碼窗使用的語言,再將代碼傳入格式化方法進行格式化,而後調用 cm.setValue
方法將窗口中的舊代碼更新爲格式化後的代碼,大功告成:
其實在上一文中,就已經實現了 console
的功能,可是隻可以實現基本的輸出功能,google
自帶的 console
不光能夠查看輸出內容,還能夠在裏面寫 js
代碼調試
想要實現這個功能,首先就要在 console
窗口中添加一個可編輯的輸入框,再嘗試過 input
,textarea
以後我選擇給 div
加上 contenteditable="plaintext-only"
屬性使其變成可編輯的 div
,由於這個 div
能夠根據內容自動改變高度
咱們在谷歌控制檯輸入代碼的時候,會出現返回值:
咱們知道 push
方法執行後返回的是數組的長度,因此當咱們向一個含有一個元素的數組中 push
元素的時候返回的就是 2
實現這個效果其實很簡單,給 div
綁定一個監聽回車鍵按下的函數:
<div @keyup.enter="sendCommand" class="input-text" contenteditable="plaintext-only" spellcheck="false" type="text" ></div>
複製代碼
sendCommand
函數:
methods:{
// 其餘方法省略
sendCommand(e){
// 獲取可編輯div文本
let text = e.srcElement.innerText
// 使用正則去掉文本先後的換行
const code = text.replace(/^\n+|\n+$/g, '')
// 其餘代碼省略
}
}
複製代碼
sendCommand
的一些代碼被我省略掉了,由於這些省略的代碼是根據個人項目結構來的,而不是必要的,其做用是將 code
傳給一個包裝函數:
function exeCode(code) {
return (
`\ntry {\n` +
`console.log('${code}')\n` +
`var r = eval('${code}')\n` +
`console.log(r)\n` +
`} catch(e) {\n` +
`console.log(e)\n` +
`}\n`
);
}
複製代碼
使用 try...catch
來捕獲可能出現的錯誤,定義一個變量 r
,將使用 eval
方法執行 code
後的值賦給 r
,這樣 r
就是 code
執行後的返回值
注意:這裏的 console
是重寫過的,詳情請看如何製做一款在線編譯器(1)
舉個例子,當咱們在可編輯 div
中打上 1+1
並按下回車,iframe
的 script
標籤中會插入以下 js
:
try {
console.log("1+1"); // '1+1'
var r = eval("1+1"); // 2
console.log(r); // 2
} catch (e) {
console.log(e);
}
複製代碼
效果雖然沒有谷歌控制檯的好,但至少有是吧 😄
接下來實現控制檯的記憶功能,也就是按上下方向鍵能夠查看命令歷史,如圖:
首先給可編輯 div
綁定一個 input
事件監聽鍵盤輸入,再綁定 keydown.down
和 keydown.up
事件:
<div @input="change" @keydown.down="switchNextInfo" @keydown.up="switchLastInfo" @keyup.enter="sendCommand" class="input-text" contenteditable="plaintext-only" spellcheck="false" type="text" ></div>
複製代碼
在 data
中定義三個變量:
export default {
data() {
return {
commandHistory: [], // 一個用來記錄歷史命令的數組
historyIndex: 0, // 當前指令所在歷史命令數組中的下標
value: "" // 當前可編輯div內的文本
};
}
// 省略其餘配置
};
複製代碼
由於可編輯 div
內不能使用 v-model
的緣由,須要本身實現對內容的監聽:
export default {
methods: {
change(e) {
// 因爲我並無在input事件返回的event對象上找到keycode的信息,因此要經過其餘方法來判斷回車鍵是否被按下
// data屬性存儲着當前插入的字符
const data = e.data;
// inputType屬性表示的是致使文本變化的行爲
const inputType = e.inputType;
// 當data值爲null,而且inputType值爲insertLineBreak或者insertText,說明按下了回車鍵
if (data === null) {
if (inputType === "insertLineBreak" || inputType === "insertText") {
return;
}
}
// 將value設置爲div文本值
this.value = e.srcElement.innerText;
}
},
watch: {
value(newVal) {
const len = this.commandHistory.length;
// 當value改變,將命令歷史的最後一個元素值改爲value,也就是div內的文本
this.commandHistory.splice(len - 1, 1, this.value);
}
}
// 省略其餘配置
};
複製代碼
當上箭頭按下時,要先檢測光標位置是否處於 div 文本的第一行,因爲暫時沒有找到獲取行數的方法,我檢測的是光標是否在文本的開頭處:
methods: {
switchLastInfo(e) {
// 獲取光標位置
const position = this.getCursorPosition()
// 檢測光標起始位置和光標結束位置是否都爲0
if (position.start !== 0 && position.end !== 0) return
// 若是光標在文本開頭處,則找到上一個歷史命令
const history = this.getCommandHistory(-1)
// 若是上一個歷史命令存在,就將div文本設置爲上一個歷史命令
if (history) e.srcElement.innerText = history
}
// 省略其餘方法
}
複製代碼
getCursorPosition
用於獲取光標位置:
methods:{
getCursorPosition() {
// 調用window.getSelection().getRangeAt(0)獲取光標信息
const range = window.getSelection().getRangeAt(0)
// range.startOffset屬性爲光標的起始位置
const start = range.startOffset
// range.endOffset屬性爲光標的結束位置
const end = range.endOffset
return {
start,
end
}
}
// 省略其餘方法
}
複製代碼
大多數狀況下光標的起始和結束位置都是同樣的,只有在選中文本的時候纔會不一樣
getCommandHistory
方法用於獲取歷史命令:
methods: {
getCommandHistory(num) {
// 緩存歷史命令列表
const list = this.commandHistory
const newIndex = this.historyIndex + num + 1
const history = list[newIndex]
// 若是命令存在包括命令爲空字符串的狀況,改變當前指令所在歷史命令數組中的下標
if (history || history === '') this.historyIndex += num
return history
}
// 省略其餘方法
}
複製代碼
switchNextInfo
和 switchLastInfo
做用相同,只是 switchNextInfo
是在按下方向鍵的時候觸發:
methods: {
switchNextInfo(e) {
const position = this.getCursorPosition()
const len = e.srcElement.innerText.length
// 檢測光標是否在文本尾部
if (position.start !== len && position.end !== len) return
// 光標在文本尾部則獲取下一個歷史命令
const history = this.getCommandHistory(1)
// 若是命令存在包括命令爲空字符串的狀況,改變當前指令所在歷史命令數組中的下標
if (history || history === '') e.srcElement.innerText = history
}
// 省略其餘方法
}
複製代碼
當光標在文本尾部的時候,光標起始和結束位置的值和 div
內文本(字符串)的長度(length
)是同樣的
完成效果:
console
的功能暫時就是這樣了,還存在着不少缺陷,之後會慢慢完善的
側邊欄的 color
選項中能夠進行顏色轉換和複製
顏色轉換的方法也很簡單:
function switchRGB(color) {
if (!color) return;
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color);
return {
r: parseInt(result[1], 16) + "",
g: parseInt(result[2], 16) + "",
b: parseInt(result[3], 16) + ""
};
}
function switchHEX(color) {
if (!(color.r && color.g && color.b)) return;
const r = parseInt(color.r);
const g = parseInt(color.g);
const b = parseInt(color.b);
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}
export { switchRGB, switchHEX };
複製代碼
因爲 window.clipboardData
接口只在 IE
瀏覽器上實現,在其餘瀏覽器上就要經過 input
元素來實現該功能:
// 建立input元素
const input = document.createElement("input");
// 將input的value值設置爲當前色塊的HEX值
input.value = hex;
// 將input插入到body中
document.body.appendChild(input);
// 選取input內容
input.select();
// 執行copy命令
document.execCommand("Copy");
// 最後移除input元素
document.body.removeChild(input);
複製代碼
這樣就實現了將 `HEX 顏色複製到剪貼板的功能
文件上傳功能能夠將本地的特定後綴名的文件導入到編輯器中,可導入的文件類型的集合用一個數組保存起來
const limitType = [
"html",
"css",
"js",
"md",
"sass",
"scss",
"less",
"styl",
"ts",
"coffee"
];
複製代碼
其實只要使用 FileReader
類將文件內容取出,再賦給編輯窗口就行了:
async function readFile(file) {
const reader = new FileReader()
// 使用utf-8編碼讀取文件
reader.readAsText(file, 'UTF-8')
return new Promise((resolve, reject) => {
reader.onload = e => {
// 獲取文本內容
const fileString = e.target.result
// 省略其餘代碼
}
}
}
複製代碼
但若是是 html
文件,狀況可就不一樣了,由於在 html
文件中既能夠寫 css
也能夠寫 js
,因此在獲取文本內容以後還要使用正則表達式截取
截取 body
標籤中的 html
代碼:
// 獲取body標籤內的內容
const result = /<body[^>]*>([\s\S]*)<\/body>/.exec(content);
let finalCode = "";
// 若是成功會返回一個長度爲2的數組,第二個元素就是咱們要獲得的內容
if (result && result.length === 2) finalCode = result[1];
// 將body內的script標籤及其內容替換成空字符串
return finCode.replace(
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
""
);
複製代碼
獲取 style
標籤內容:
const result = /<style>(([\s\S])*?)<\/style>/g.exec(content);
let finCode = "";
if (result && result.length >= 2) finCode = result[1];
複製代碼
同時還要將 link
內的外部連接取出保存到一個數組中:
// 獲取全部link標籤
const linkList = content.match(/<link.*?(?:>|\/>)/gi);
const link = [];
if (linkList) {
for (let i = 0, content; (content = linkList[i++]); ) {
link.push(splitLink(content));
}
}
// splitLink代碼
// 獲取link標籤內的href屬性值
const result = content.match(/<link .*?href=\"(.+?)\"/);
if (result && result.length === 2) return result[1];
複製代碼
獲取 script
標籤內容:
const codeList = content.match(/<script>([\s\S]+?)<\/script>/gi);
let finCode = "";
if (codeList) {
for (let i = 0, content; (content = codeList[i++]); ) {
finCode += splitScriptContent(content) + "\n";
}
}
// splitScriptContent代碼
const result = /<script>([\s\S]+?)<\/script>/gi.exec(content);
if (result && result.length === 2) return result[1];
複製代碼
還要獲取 script
標籤引入的外部連接
const scriptList = content.match(/<script.*?(?:>|\/>)\<\/script\>/gi);
const CDN = [];
if (scriptList) {
for (let i = 0, content; (content = scriptList[i++]); ) {
CDN.push(splitScriptSrc(content));
}
}
// splitScriptSrc代碼
const result = content.match(/<script .*?src=\"(.+?)\"/);
if (result && result.length === 2) return result[1];
複製代碼
在作完這些操做以後,獲得的代碼會賦給相應的窗口做爲內容,外部連接也會保存在設置中
這就是這段時間內我在 JS-Encoder
上遇到的幾個重要問題和解決方法,固然了,不管是代碼仍是解決方法可能都是有缺陷的,若是大佬們發現了問題請及時告訴我,我會盡快修改
最後,放上項目的 github 連接,若是大家對這個項目感興趣,不妨點一個 star
來鼓勵鼓勵我哦