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