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


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 源碼學習系列 學習實踐的總結,希望會對您有所幫助!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM