如何寫一個代碼編輯器

演示什麼是代碼編輯器


當咱們看到這個編輯器的時候,你有沒有好奇這是這麼作出來的?若是是讓你來作,你會怎麼作?javascript

閒扯

學無止境,咱們如今的技術都是基於前一代人之上作出來的,要想成爲一個高級/資深的前端,重點不是你能創造多少東西,而是你知道多少東西。有沒有解決問題的能力。遇到問題能不能找到最優解,爲公司減小成本的同時提高效率。系統性的解決問題,提升代碼的維護性、穩定性、可擴展行等等。因此現代社會是一個認知的社會,只有不斷的突破本身的認知,纔可以成爲更優秀的人。css

最近在搞公衆號,雖然公衆號已經寫了快兩年了,可是一直沒有推廣過。最近打算推廣,加了好多大佬的微信,我感受我以前作的事情就是小巫見大巫,根本不值一提,前方還有好長的路要走,我隱隱感受有點興奮,由於只有迎難而上、才能迎刃而解。關注我公衆號,前路漫漫,一塊兒修行!前端

正題

當咱們想作一個事情的時候,每每最難的不是作,而是不知道從哪作起,怎麼作?個人每篇文章都會講我是如何去一點點解決問題的,但願可以盡個人綿薄之力幫助有心之人。vue

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


咱們找到了一個很可疑的單詞 CodeMirrorjava

接下來去 github 上搜索一下 CodeMirrorgit


果真被咱們找到了,點進去查看他的用法。接下來你應該知道怎麼作了~github

動工

上面講解的是我如何找工具的方法,我如今使用的不是 CodeMirror,可是我也是經過這種方法找到的。 我接下來用 monacoEditor 來說解個人作法。typescript

加載 monaco 腳本

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

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,提供給調用者使用bash

<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

最後兩件小事拜託你們

  1. 有想入羣的學習前端進階的加我微信 luoxue2479 回覆加羣便可

  2. 個人公衆號 【前端技匠】,歡迎來關注

  3. 個人 github 地址,全部個人文章及源碼都在這裏,歡迎來star

https://github.com/luoxue-victor/source-code

  1. 我總結了一些 TODO,有興趣的同窗能夠一塊兒來

https://github.com/luoxue-victor/source-code/issues

相關文章
相關標籤/搜索