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.js 的 parseComponent
方法,用於實現原始碼解析生成元件 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
包含 template
、script
、styles
、customBlocks
四個部分,將用於示例元件的動態構建。 其中 styles
是陣列,可以包含多個程式碼塊並解析; template
和script
若存在多個程式碼塊只能解析最後一個。
customBlocks
是沒在template
的HTML程式碼,處理邏輯暫未包含此內容。
0x03 元件動態樣式
檔案src\utils\style-loader\addStylesClient.js
移植 vue-style-loader
原始碼 addStylesClient 方法,用於在頁面DOM中動態建立元件樣式。
根據 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 原始碼學習系列 學習實踐的總結,希望會對您有所幫助!