天天看點

如何寫一個代碼編輯器

示範什麼是代碼編輯器

如何寫一個代碼編輯器

示範

當我們看到這個編輯器的時候,你有沒有好奇這是這麼做出來的?如果是讓你來做,你會怎麼做?

閑扯

學無止境,我們現在的技術都是基于前一代人之上做出來的,要想成為一個進階/資深的前端,重點不是你能創造多少東西,而是你知道多少東西。有沒有解決問題的能力。遇到問題能不能找到最優解,為公司減少成本的同時提升效率。系統性的解決問題,提高代碼的維護性、穩定性、可擴充行等等。是以現代社會是一個認知的社會,隻有不斷的突破自己的認知,才能夠成為更優秀的人。

最近在搞公衆号,雖然公衆号已經寫了快兩年了,但是一直沒有推廣過。最近打算推廣,加了好多大佬的微信,我感覺我之前做的事情就是小巫見大巫,根本不值一提,前方還有好長的路要走,我隐隐感覺有點興奮,因為隻有迎難而上、才能迎刃而解。關注我公衆号,前路漫漫,一起修行!

正題

當我們想做一個事情的時候,往往最難的不是做,而是不知道從哪做起,怎麼做?我的每篇文章都會講我是如何去一點點解決問題的,希望能夠盡我的綿薄之力幫助有心之人。

  1. 看到網站之後不要急着去百度,因為百度有太多無用的資訊幹擾你,而且這些無用的資訊很可能會把你的注意力轉移,最後忘了你為什麼要百度!
  2. 以 codepen 官網為例,我是如何去查他用了什麼技術?
    • 思考,這種編輯器的功能一定是有開源庫的,因為好多網站都使用過,那麼順着思路走,找到這個開源庫的名字,我們就完成一半了。
    • 怎麼找,首先右擊打開檢查,檢視 Network 有沒有有用的資訊,比如加載了哪個js,在js源碼中找到一些線索(一般都會被打包過了,找到的幾率不大)。然後打開元素檢查,查找 class 名稱有沒有一些蹊跷
    • 為什麼找 class ,因為 class 最能直覺的找到表達者的意圖
如何寫一個代碼編輯器

class

我們找到了一個很可疑的單詞 CodeMirror

接下來去 github 上搜尋一下 CodeMirror

如何寫一個代碼編輯器

class

果然被我們找到了,點進去檢視他的用法。接下來你應該知道怎麼做了~

動工

上面講解的是我如何找工具的方法,我現在使用的不是 CodeMirror,但是我也是通過這種方法找到的。我接下來用 monacoEditor 來講解我的做法。

加載 monaco 腳本

這是一段加載 monaco 的js。主要邏輯就是 load 一段 js,将 monaco 注冊到 window 上

export default {
  // https://as.alipayobjects.com/g/cicada/monaco-editor-mirror/0.6.1/min/vs/loader.js
  load(srcPath = 'https://as.alipayobjects.com/g/cicada/monaco-editor-mirror/0.6.1/min', callback) {
    if (window.monaco) {
      callback();
      return;
    }
    const config = {
      paths: {
        vs: srcPath + '/vs'
      }
    };
    const loaderUrl = `${config.paths.vs}/loader.js`;
    const onGotAmdLoader = () => {
      if (window.LOADER_PENDING) {
        window.require.config(config);
      }
      
      window.require(['vs/editor/editor.main'], () => {
        callback();
      });

      // 當AMD加載器已被加載時調用延遲的回調
      if (window.LOADER_PENDING) {
        window.LOADER_PENDING = false;
        const loaderCallbacks = window.LOADER_CALLBACKS;
        if (loaderCallbacks && loaderCallbacks.length) {
          let currentCallback = loaderCallbacks.shift();
          while (currentCallback) {
            currentCallback.fn.call(currentCallback.window);
            currentCallback = loaderCallbacks.shift();
          }
        }
      }
    };

    if (window.LOADER_PENDING) {
      // 我們需要避免加載多個loader.js時
      // 有多個編輯器同時加載延遲調用回調除了第一個
      window.LOADER_CALLBACKS = window.LOADER_CALLBACKS || [];
      window.LOADER_CALLBACKS.push({
        window: this,
        fn: onGotAmdLoader
      });
    } else {
      if (typeof window.require === 'undefined') {
        const loaderScript = window.document.createElement('script');
        loaderScript.type = 'text/javascript';
        loaderScript.src = loaderUrl;
        loaderScript.addEventListener('load', onGotAmdLoader);
        window.document.body.appendChild(loaderScript);
        window.LOADER_PENDING = true;
      } else {
        onGotAmdLoader();
      }
    }
  }
}           

複制

封裝元件

寫一個元件将加載執行的邏輯封裝在這個元件裡,暴露出一些 api,提供給調用者使用

<template>
  <div :style="style"></div>
</template>

<script>
import monacoLoader from './MonacoLoader'
const debounce = require('lodash.debounce');

export default {
  props: {
    // 編輯器的寬,預設 100%
    width: { type: [String, Number], default: '100%' },
    // 編輯器的高,預設 100%
    height: { type: [String, Number], default: '100%' },
    // 傳進來的代碼,一段字元串
    code: { type: String, default: '// code \n' },
    // 資源路徑
    srcPath: { type: String },
    // 預設使用 js
    language: { type: String, default: 'javascript' },
    // 主題 預設 vs-dark
    theme: { type: String, default: 'vs-dark' }, // vs, hc-black
    // 一些 monaco 配置參數
    options: { type: Object, default: () => {} },
    // 截流
    changeThrottle: { type: Number, default: 0 }
  },
  // 加載資源
  created() {
    this.fetchEditor();
  },
  // 銷毀
  destroyed() {
    this.destroyMonaco();
  },
  computed: {
    style() {
      const { width, height } = this;
      const fixedWidth = width.toString().indexOf('%') !== -1 ? width : `${width}px`;
      const fixedHeight = height.toString().indexOf('%') !== -1 ? height : `${height}px`;
      return {
        width: fixedWidth,
        height: fixedHeight,
      };
    },
    editorOptions() {
      return Object.assign({}, this.defaults, this.options, {
        value: this.code,
        language: this.language,
        theme: this.theme
      });
    }
  },
  data() {
    return {
      defaults: {
        selectOnLineNumbers: true,
        roundedSelection: false,
        readOnly: false,
        cursorStyle: 'line',
        automaticLayout: false,
        glyphMargin: true
      }
    }
  },
  methods: {
    editorHasLoaded(editor, monaco) {
      this.editor = editor;
      this.monaco = monaco;
      this.editor.onDidChangeModelContent(event =>
        this.codeChangeHandler(editor, event)
      );
      this.$emit('mounted', editor);
    },
    codeChangeHandler: function(editor) {
      if (this.codeChangeEmitter) {
        this.codeChangeEmitter(editor);
      } else {
        this.codeChangeEmitter = debounce(
          function(editor) {
            this.$emit('codeChange', editor);
          },
          this.changeThrottle
        );
        this.codeChangeEmitter(editor);
      }
    },
    fetchEditor() {
      monacoLoader.load(this.srcPath, this.createMonaco);
    },
    createMonaco() {
      this.editor = window.monaco.editor.create(this.$el, this.editorOptions);
      this.editorHasLoaded(this.editor, window.monaco);
    },
    destroyMonaco() {
      if (typeof this.editor !== 'undefined') {
        this.editor.dispose();
      }
    }
  }
};
</script>           

複制

完成示範

使用元件,将元件顯示在頁面上。并将 console.log 收集起來,執行完代碼之後将其列印在螢幕上。

最後會有示範

<template>
  <div>
    <MonacoEditor
      height="300"
      language="typescript"
      :code="code"
      :editorOptions="options"
      @codeChange="onCodeChange"
    ></MonacoEditor>
    <div class="console">{{ res }}</div>
  </div>
</template>
<script>
import MonacoEditor from '../../components/vue-monaco-editor';
let logStore = [];
export default {
  components: {
    MonacoEditor
  },
  data() {
    return {
      result: 'noop',
      // 預設 code
      code:
`const noop = () => {
  console.log('noop')
}
noop()
`,
      options: {}
    };
  },
  methods: {
    clear() {
      this.result = '';
      logStore = [];
    },
    // 重寫consolelog,并儲存在logStore内
    overwriteConsoleLog() {
      console.log = function(...arg) {
        logStore.push(arg);
      };
    },
    // 抽象出一層修飾層
    modify(e) {
      if (typeof e === 'object') e = JSON.stringify(e);
      return e;
    },
    // 輸出
    printf(oriConsole) {
      const _this = this
      logStore.forEach(item => {
        function str() {
          return item.map(_this.modify);;
        }
        oriConsole(str(item));
        this.result +=
          str(item)
            .join(' ') + '\n';
      });
      console.log = oriConsole;
    },
    onCodeChange(code) {
      // 儲存 console.log 對象
      const oriConsole = console.log;
      // 清空副作用
      this.clear();
      // 重寫 console.log,為了将控制台列印值輸出在頁面上
      this.overwriteConsoleLog();
      // 擷取代碼的片段字元串
      const v = code.getValue();
      try {
        // 執行代碼
        eval(v);
        // 将控制台列印值輸出在頁面上
        this.printf(oriConsole)
      } catch (error) {
        console.error(error)
      }
    }
  }
};
</script>
<style lang="scss">
.editor {
  width: 100%;
  height: 100vh;
  margin-top: 50px;
}
.console {
  height: 500px;
  background: orange;
  font-size: 40px;
  white-space: pre-wrap;
}
</style>           

複制

效果示範

如何寫一個代碼編輯器

小結

又到了小結時刻,當我們看見一個很厲害的東西的時候,不要害怕,其實你也可以,大部分的功能其實已經被别人封裝好了,我們都是站在巨人的肩膀上。

我會經常分享一些自己工作經驗、技術體會、做的小玩意等等。更大家一起學習。

看别人十遍,不如自己動手寫一遍,我的這些源碼和文章都在這裡,可以自己下載下傳下來運作一下試試。

https://github.com/luoxue-victor/source-code/tree/master/src/views/monacoEditor