如何製做一款在線編譯器(2)

先放上文連接 如何製做一款在線編譯器(1)javascript

離上個文章發佈又過去了一個月的時間,在這一個月的時間內,我又騰出了一些時間去完善編輯器的各個功能css

如今的 JS-Encoder 長這樣:html

點擊進入編輯器vue

截圖未命名.jpg

總體和之前差很少,主要是加了一個側邊欄java

下面說一下這些日子裏所實現的一些主要功能:git

代碼格式化

代碼格式化是一個很是重要的功能,咱們平時寫代碼的時候很難去注意程序的排版結構,不少換行,符號,空格都被忽略掉,致使代碼可讀性下降。github

其實不光是本身寫代碼,如今是面向百度編程時代,上網 copy 代碼也是常有的事,代碼格式正常,看上去美觀,一複製到本身的編輯器上就不知道哪兒是哪兒了,本身修改又浪費時間。因此,一個能夠自動格式化的工具是很是必要的正則表達式

那麼如何實現自動格式化的功能呢?npm

我在 npm 上找到了一個叫作 js-beautify 的包,附上連接編程

js-beautify 能夠格式化 htmlcssjs 代碼,知足功能要求

好比我想格式化 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();
複製代碼

一樣的,格式化 cssjs 的代碼也用相同的方法

方法寫好了以後就要綁定快捷鍵,我設置的格式化快捷鍵是 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 方法將窗口中的舊代碼更新爲格式化後的代碼,大功告成:

GIF.gif

Console

其實在上一文中,就已經實現了 console 的功能,可是隻可以實現基本的輸出功能,google 自帶的 console 不光能夠查看輸出內容,還能夠在裏面寫 js 代碼調試

想要實現這個功能,首先就要在 console 窗口中添加一個可編輯的輸入框,再嘗試過 inputtextarea 以後我選擇給 div 加上 contenteditable="plaintext-only" 屬性使其變成可編輯的 div,由於這個 div 能夠根據內容自動改變高度

咱們在谷歌控制檯輸入代碼的時候,會出現返回值:

GIF1.gif

咱們知道 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 並按下回車,iframescript 標籤中會插入以下 js

try {
  console.log("1+1"); // '1+1'
  var r = eval("1+1"); // 2
  console.log(r); // 2
} catch (e) {
  console.log(e);
}
複製代碼

效果雖然沒有谷歌控制檯的好,但至少有是吧 😄

GIF2.gif

命令記憶

接下來實現控制檯的記憶功能,也就是按上下方向鍵能夠查看命令歷史,如圖:

GIF3.gif

首先給可編輯 div 綁定一個 input 事件監聽鍵盤輸入,再綁定 keydown.downkeydown.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
  }
  // 省略其餘方法
}
複製代碼

switchNextInfoswitchLastInfo 做用相同,只是 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)是同樣的

完成效果:

GIF4.gif

console 的功能暫時就是這樣了,還存在着不少缺陷,之後會慢慢完善的

顏色轉換

側邊欄的 color 選項中能夠進行顏色轉換和複製

GIF5.gif

顏色轉換的方法也很簡單:

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 來鼓勵鼓勵我哦

相關文章
相關標籤/搜索