從0到1搭建自己的元件(vue-code-view)庫(下)

Anduril發表於2021-11-03

0x00 前言

書接上文,本文將從原始碼功能方面講解下 vue-code-view 元件核心邏輯,您可以瞭解以下內容:

  • 動態元件的使用。
  • codeMirror外掛的使用。
  • 單檔案元件(SFC,single-file component) Parser。

0x01 CodeEditor元件

專案使用功能豐富的codeMirror實現線上程式碼展示編輯功能。

npm 包安裝:

npm install codemirror --save 

子元件 src\src\code-editor.vue 完整原始碼:

<template>
  <div class="code-editor">
    <textarea ref="codeContainer" />
  </div>
</template>

<script>
// 引入核心
import CodeMirror from "codemirror";
import "codemirror/lib/codemirror.css"; 

// 主題 theme style
import "codemirror/theme/base16-light.css";
import "codemirror/theme/base16-dark.css"; 
// 語言 mode
import "codemirror/mode/vue/vue";  
// 括號/標籤 匹配
import "codemirror/addon/edit/matchbrackets";
import "codemirror/addon/edit/matchtags";
// 括號/標籤 自動關閉
import "codemirror/addon/edit/closebrackets";
import "codemirror/addon/edit/closetag"; 
// 程式碼摺疊
import "codemirror/addon/fold/foldgutter.css";
import "codemirror/addon/fold/brace-fold";
import "codemirror/addon/fold/foldcode";
import "codemirror/addon/fold/foldgutter";
import "codemirror/addon/fold/comment-fold";
// 縮排檔案
import "codemirror/addon/fold/indent-fold";
// 游標行背景高亮
import "codemirror/addon/selection/active-line"; 

export default {
  name: "CodeEditor",
  props: {
    value: { type: String },
    readOnly: { type: Boolean },
    theme: { type: String },
    matchBrackets: { type: Boolean },
    lineNumbers: { type: Boolean },
    lineWrapping: { type: Boolean },
    tabSize: { type: Number },
    codeHandler: { type: Function },
  },
  data() {
    return {
      // 編輯器例項
      codeEditor: null,
      // 預設配置
      defaultOptions: {
        mode: "text/x-vue", //語法高亮   MIME-TYPE    
        gutters: [
          "CodeMirror-linenumbers",
          "CodeMirror-foldgutter", 
        ], 
        lineNumbers: this.lineNumbers, //顯示行號
        lineWrapping: this.lineWrapping || "wrap", // 長行時文字是換行  換行(wrap)/滾動(scroll)
        styleActiveLine: true, // 高亮選中行
        tabSize: this.tabSize || 2, // tab 字元的寬度
        theme: this.theme || "base16-dark", //設定主題 
        autoCloseBrackets: true, // 括號自動關閉
        autoCloseTags: true, // 標籤自動關閉
        matchTags: true, // 標籤匹配
        matchBrackets: this.matchBrackets || true, // 括號匹配
        foldGutter: true, // 程式碼摺疊
        readOnly: this.readOnly ? "nocursor" : false, //  boolean|string  “nocursor” 設定只讀外,編輯區域還不能獲得焦點。
      },
    };
  },
  watch: {
    value(value) {
      const editorValue = this.codeEditor.getValue();
      if (value !== editorValue) {
        this.codeEditor.setValue(this.value);
      }
    },
    immediate: true,
    deep: true,
  },
  mounted() {
    // 初始化
    this._initialize();
  },
  methods: {
    // 初始化
    _initialize() {
      // 初始化編輯器例項,傳入需要被例項化的文字域物件和預設配置
      this.codeEditor = CodeMirror.fromTextArea(
        this.$refs.codeContainer,
        this.defaultOptions
      ); 
      this.codeEditor.setValue(this.value); 
      // 使用 prop function 替換 onChange 事件
      this.codeEditor.on("change", (item) => {
        this.codeHandler(item.getValue());
      });
    },
  },
};
</script>

外掛啟用功能的配置選項,同時需要引入相關的js,css 檔案。

引數 說明 型別
mode 支援語言語法高亮 MIME-TYPE string
lineNumbers 是否在編輯器左側顯示行號。 boolean
lineWrapping 在長行時文字是換行(wrap)還是滾動(scroll),預設為滾動(scroll)。 boolean
styleActiveLine 高亮選中行 boolean
tabSize tab 字元的寬度 number
theme 設定主題 tring
autoCloseBrackets 括號自動關閉 boolean
autoCloseTags 標籤自動關閉 boolean
matchTags 標籤匹配 boolean
matchBrackets 括號匹配 boolean
foldGutter 程式碼摺疊 boolean
readOnly 是否只讀。 “nocursor” 設定只讀外,編輯區域還不能獲得焦點。 boolean|string

元件初始化時,會自動初始化編輯器示例,同時將原始碼賦值給編輯器,並註冊監聽change事件。當編輯器的值發生改變時,會觸發 onchange 事件,呼叫元件prop 屬性 codeHandler將最新值傳給父元件。

// 初始化編輯器例項,傳入需要被例項化的文字域物件和預設配置 
this.codeEditor = CodeMirror.fromTextArea( this.$refs.codeContainer, this.defaultOptions );   
this.codeEditor.setValue(this.value);  
// 註冊監聽`change`事件
this.codeEditor.on("change", (item) => { this.codeHandler(item.getValue()); });

0x02 SFC Parser

元件的功能場景是用於簡單示例程式碼執行展示,將原始碼視為 單檔案元件(SFC,single-file component)的簡單例項。

檔案src\utils\sfcParser\parser.js 移植 vue 原始碼 sfc/parser.jsparseComponent 方法,用於實現原始碼解析生成元件 SFCDescriptor

暫不支援元件和樣式的動態引入,此處功能程式碼已經移除。

// SFCDescriptor 介面宣告
export interface SFCDescriptor {
  template: SFCBlock | undefined; //
  script: SFCBlock | undefined;
  styles: SFCBlock[];
  customBlocks: SFCBlock[];
}

export interface SFCBlock {
  type: string;
  content: string;
  attrs: Record<string, string>;
  start?: number;
  end?: number;
  lang?: string;
  src?: string;
  scoped?: boolean;
  module?: string | boolean;
}

SFCDescriptor 包含 templatescriptstylescustomBlocks 四個部分,將用於示例元件的動態構建。 其中 styles是陣列,可以包含多個程式碼塊並解析; templatescript 若存在多個程式碼塊只能解析最後一個。
customBlocks是沒在template的HTML程式碼,處理邏輯暫未包含此內容。

0x03 元件動態樣式

檔案src\utils\style-loader\addStylesClient.js 移植 vue-style-loader 原始碼 addStylesClient 方法,用於在頁面DOM中動態建立元件樣式。

image.png

根據 SFCDescriptor 中的 styles和元件編號,在DOM中新增對應樣式內容,若新增刪除 <style>,頁面DOM中對應建立或移除該樣式內容。若更新 <style>內容,DOM節點只更新對應塊的內容,優化頁面效能。

0x04 CodeViewer 元件

使用 JSX 語法實現元件核心程式碼。

<script> 
export default {
  name: "CodeViewer", 
  props: {
    theme: { type: String, default: "dark" }, //light 
    source: { type: String }, 
  },
  data() {
    return {
      code: ``, 
      dynamicComponent: {
        component: {
          template: "<div>Hello Vue.js!</div>",
        },
      }, 
    };
  },
  created() {
    this.viewId = `vcv-${generateId()}`; 
    // 元件樣式動態更新
    this.stylesUpdateHandler = addStylesClient(this.viewId, {});
  },
  mounted() {
    this._initialize();
  },
  methods: {
    // 初始化
    _initialize() {
      ...
    },
    // 生成元件
    genComponent() {
      ...
    },
    // 更新 code 內容
    handleCodeChange(val) {
      ...
    },
    // 動態元件render
    renderPreview() { 
      ...
    }, 
  },
  computed: {
    // 原始碼解析為sfcDescriptor
    sfcDescriptor: function () {
      return parseComponent(this.code);
    }, 
  },
  watch: { 
    // 監聽原始碼內容
    code(newSource, oldSource) {
       this.genComponent();
    },
  },
  // JSX 渲染函式
  render() { 
    ...
  },
};
</script> 

元件初始化生成元件編號,註冊方法 stylesUpdateHandler 用於樣式的動態新增。

元件初始化呼叫 handleCodeChange 方法將傳入prop source值賦值給code

methods: {
    _initialize() { 
      this.handleCodeChange(this.source);
    },
    handleCodeChange(val) {
      this.code = val;
    },
}

計算屬性sfcDescriptor 呼叫parseComponent方法解析code內容生成元件的 sfcDescriptor

computed: {
    // 原始碼解析為sfcDescriptor
    sfcDescriptor: function () {
      return parseComponent(this.code);
    }, 
  },

元件監聽code值是否發生變化,呼叫genComponent方法更新元件。

 methods: { 
    // 生成元件
    genComponent() {
      ...
    }, 
  }, 
  watch: { 
    // 監聽原始碼內容
    code(newSource, oldSource) {
       this.genComponent();
    },
  },

方法 genComponent將程式碼的sfcDescriptor 動態生成元件,更新至 dynamicComponent 用於示例呈現。同時呼叫 stylesUpdateHandler方法使用addStylesClient在DOM中新增例項中樣式,用於示例樣式渲染。

  genComponent() {
      const { template, script, styles, customBlocks, errors } = this.sfcDescriptor; 
      
      const templateCode = template ? template.content.trim() : ``;
      let scriptCode = script ? script.content.trim() : ``;
      const styleCodes = genStyleInjectionCode(styles, this.viewId);

      // 構建元件
      const demoComponent = {};

      // 元件 script
      if (!isEmpty(scriptCode)) {
        const componentScript = {};
        scriptCode = scriptCode.replace(
          /export\s+default/,
          "componentScript ="
        );
        eval(scriptCode);
        extend(demoComponent, componentScript);
      }

      // 元件 template 
      demoComponent.template = `<section id="${this.viewId}" class="result-box" >
        ${templateCode}
      </section>`;

      // 元件 style 
      this.stylesUpdateHandler(styleCodes);

      // 元件內容更新
      extend(this.dynamicComponent, {
        name: this.viewId,
        component: demoComponent,
      });
    },

JSX 渲染函式展示基於code內容動態生成的元件內容。呼叫 CodeEditor 元件傳入原始碼value和主題theme,提供了 codeHandler 處理方法handleCodeChange用於獲取編輯器內最新的程式碼。

  methods: { 
    renderPreview() { 
      const renderComponent = this.dynamicComponent.component;

      return (
        <div class="code-view zoom-1">
          <renderComponent></renderComponent>
        </div>
      );
    },
  },
  // JSX 渲染函式
  render() { 
    return (
      <div ref="codeViewer">
        <div class="code-view-wrapper"> 
          {this.renderPreview()}  
          ...
          <CodeEditor 
              codeHandler={this.handleCodeChange}
              theme={`base16-${this.theme}`}
              value={this.code}
            />
        </div>
      </div>
    );
  },

handleCodeChange 被呼叫後,觸發 watch =>genComponent=>render ,頁面內容重新整理,從而達到程式碼線上編輯,實時預覽效果的功能。


完結

此元件編寫是個人對於 ?Element 2 原始碼學習系列 學習實踐的總結,希望會對您有所幫助!

相關文章