ElementUI 組件庫 md-loader 的解析和優化

大家好,我是若川。最近組織了源碼共讀活動,感興趣的可以加我微信?ruochuan12?參與,已進行三個月了,大家一起交流學習,共同進步。


背景

相信很多同學在學習 webpack 的時候,對 loader 的概念應該有所了解,它用于模塊源碼的轉換,描述了 webpack 如何處理非 JavaScript 模塊,常見的有 css-loaderbabel-loaderurl-loadervue-loader 等。

大部分 loader 已經滿足我們的日常開發需求,不過有些時候我們仍然需要自定義 loader。為了讓你了解如何開發一個 webpack loader,我決定從 ElementUI 組件庫的 md-loader 入手,帶你去了解其中的實現原理,以及在它的基礎上,如何做進一步的優化。

文檔的設計

對于一個組件的文檔,首先我們要考慮的是如何更好地展現組件的功能,其次要考慮的是如何更方便地做文檔維護。

想要編寫好一個組件的文檔,需要做好以下幾點:

1.功能描述

對組件功能、使用場景做詳細的描述。

2.demo 演示

直觀地讓用戶感受到組件的功能,并且能展示 demo 對應的代碼。

3.接口說明

寫清楚組件支持的屬性、方法、事件等。

那么,如何方便地維護文檔呢?

ElementUI 組件庫的文檔也是一個 Vue 項目,組件的文檔頁面是單獨的路由視圖,而文檔是用 markdown 文件來描述的,在文檔內部,不僅包含了對組件的功能以及接口的描述,還可以通過編寫 vue 組件的方式直接編寫組件的 demo,這種方式對于組件文檔的維護還是比較方便的。

以 ElementUI 組件庫 Alter 組件為例:

##?Alert?警告用于頁面中展示重要的提示信息。###?基本用法頁面中的非浮層元素,不會自動消失。:::demo?Alert?組件提供四種主題,由`type`屬性指定,默認值為`info`。```html
<template><el-alerttitle="成功提示的文案"type="success"></el-alert><el-alerttitle="消息提示的文案"type="info"></el-alert><el-alerttitle="警告提示的文案"type="warning"></el-alert><el-alerttitle="錯誤提示的文案"type="error"></el-alert>
</template>
```
:::

最終它在頁面上的展示效果如下:

b82bb032f2af12e4bc1a695046a3475d.png

可以看到,組件的路由視圖對應的是一個 markdown 文件,而在我們通常的認知中,Vue 的路由視圖應該對應的是一個 Vue 組件。

在 ElementUI 內部,是通過 require.ensure 的方式去加載一個 .md 文件,它的返回值會作為路由視圖對應的異步組件。

const?LOAD_DOCS_MAP?=?{'zh-CN':?path?=>?{return?r?=>?require.ensure([],?()?=>r(require(`./docs/zh-CN${path}.md`)),'zh-CN');},//?...
}

因此內部就必須要把 markdown 文件轉換一個 Vue 組件,我們可以借助 webpack loader 來實現這一需求。

自定義 md-loader

首先,在 webpack 的配置規則中,需要指定 .md 文件應用的 loader:

{test:?/\.md$/,use:?[{loader:?'vue-loader',options:?{compilerOptions:?{preserveWhitespace:?false}}},{loader:?path.resolve(__dirname,?'./md-loader/index.js')}]
}

接下來,我們就來分析 md-loader 的源碼實現:

const?{stripScript,stripTemplate,genInlineComponentText
}?=?require('./util');
const?md?=?require('./config');module.exports?=?function(source)?{const?content?=?md.render(source);const?startTag?=?'<!--element-demo:';const?startTagLen?=?startTag.length;const?endTag?=?':element-demo-->';const?endTagLen?=?endTag.length;let?componenetsString?=?'';let?id?=?0;?//?demo?的?idlet?output?=?[];?//?輸出的內容let?start?=?0;?//?字符串開始位置let?commentStart?=?content.indexOf(startTag);let?commentEnd?=?content.indexOf(endTag,?commentStart?+?startTagLen);while?(commentStart?!==?-1?&&?commentEnd?!==?-1)?{output.push(content.slice(start,?commentStart));const?commentContent?=?content.slice(commentStart?+?startTagLen,?commentEnd);const?html?=?stripTemplate(commentContent);const?script?=?stripScript(commentContent);let?demoComponentContent?=?genInlineComponentText(html,?script);const?demoComponentName?=?`element-demo${id}`;output.push(`<template?slot="source"><${demoComponentName}?/></template>`);componenetsString?+=?`${JSON.stringify(demoComponentName)}:?${demoComponentContent},`;//?重新計算下一次的位置id++;start?=?commentEnd?+?endTagLen;commentStart?=?content.indexOf(startTag,?start);commentEnd?=?content.indexOf(endTag,?commentStart?+?startTagLen);}//?僅允許在?demo?不存在時,才可以在?Markdown?中寫?script?標簽let?pageScript?=?'';if?(componenetsString)?{pageScript?=?`<script>export?default?{name:?'component-doc',components:?{${componenetsString}}}</script>`;}?else?if?(content.indexOf('<script>')?===?0)?{start?=?content.indexOf('</script>')?+?'</script>'.length;pageScript?=?content.slice(0,?start);}output.push(content.slice(start));return?`<template><section?class="content?element-doc">${output.join('')}</section></template>${pageScript}`;
};

md-loader 要做的事情,就是把 markdown 語法的字符串,轉成 Vue 組件字符串。轉換的過程可以拆成三個步驟:markdown 渲染,demo 子組件的處理,構造完整的組件。接下來我們就來依次分析這三個步驟。

markdown 渲染

markdown 文件內容會渲染生成對應的 HTML,它是通過下面這段代碼完成的:

const?md?=?require('./config');
module.exports?=?function(source)?{const?content?=?md.render(source);
}

md 對象的來源如下:

const?Config?=?require('markdown-it-chain');
const?anchorPlugin?=?require('markdown-it-anchor');
const?slugify?=?require('transliteration').slugify;
const?containers?=?require('./containers');
const?overWriteFenceRule?=?require('./fence');const?config?=?new?Config();config.options.html(true).end().plugin('anchor').use(anchorPlugin,?[{level:?2,slugify:?slugify,permalink:?true,permalinkBefore:?true}]).end().plugin('containers').use(containers).end();const?md?=?config.toMd();
overWriteFenceRule(md);module.exports?=?md;

首先實例化了 config 對象,它依賴于 markdown-it-chain,通過 webpack chain 的鏈式 API,配置了 markdown-it 的插件。而 md 對象指向的就是 markdown-it 的實例。

markdown-it 的實例提供了很多 API,具體可以參考它的官網文檔。其中 md.render 就是把 markdown 字符串渲染生成 HTML。

不過我們注意到,組件文檔使用了一些非標準的 markdown 語法,比如:

:::demo
:::

它實際上是一個 markdown 的自定義容器,借助于 markdown-it-container 插件,就可以解析這個自定義容器:

const?mdContainer?=?require('markdown-it-container');module.exports?=?md?=>?{md.use(mdContainer,?'demo',?{validate(params)?{return?params.trim().match(/^demo\s*(.*)$/);},render(tokens,?idx)?{const?m?=?tokens[idx].info.trim().match(/^demo\s*(.*)$/);if?(tokens[idx].nesting?===?1)?{const?description?=?m?&&?m.length?>?1???m[1]?:?'';const?content?=?tokens[idx?+?1].type?===?'fence'???tokens[idx?+?1].content?:?'';return?`<demo-block>${description???`<div>${md.render(description)}</div>`?:?''}<!--element-demo:?${content}:element-demo-->`;}return?'</demo-block>';}});md.use(mdContainer,?'tip');md.use(mdContainer,?'warning');
};

可以看到,對于 demo 這個自定義容器,它會解析 demo 后面緊接著的描述字符串以及 code fence,并生成新的 HTML 字符串。

此外,code fence 也定義了新的渲染策略:

//?覆蓋默認的?fence?渲染策略
module.exports?=?md?=>?{const?defaultRender?=?md.renderer.rules.fence;md.renderer.rules.fence?=?(tokens,?idx,?options,?env,?self)?=>?{const?token?=?tokens[idx];//?判斷該?fence?是否在?:::demo?內const?prevToken?=?tokens[idx?-?1];const?isInDemoContainer?=?prevToken?&&?prevToken.nesting?===?1?&&?prevToken.info.trim().match(/^demo\s*(.*)$/);if?(token.info?===?'html'?&&?isInDemoContainer)?{return?`<template?slot="highlight"><pre?v-pre><code?class="html">${md.utils.escapeHtml(token.content)}</code></pre></template>`;}return?defaultRender(tokens,?idx,?options,?env,?self);};
};

對于在 demo 容器內且帶有 html 標記的 code fence,會做一層特殊處理。

對于我們前面的示例:

:::demo?Alert?組件提供四種主題,由`type`屬性指定,默認值為`info`。```html
<template><el-alerttitle="成功提示的文案"type="success"></el-alert><el-alerttitle="消息提示的文案"type="info"></el-alert><el-alerttitle="警告提示的文案"type="warning"></el-alert><el-alerttitle="錯誤提示的文案"type="error"></el-alert>
</template>
```
:::

經過解析后,生成的 HTML 大致如下:

<demo-block><div><p>Alert?組件提供四種主題,由<code>type</code>屬性指定,默認值為<code>info</code>。</p></div><!--element-demo:?<template><el-alerttitle="成功提示的文案"type="success"></el-alert><el-alerttitle="消息提示的文案"type="info"></el-alert><el-alerttitle="警告提示的文案"type="warning"></el-alert><el-alerttitle="錯誤提示的文案"type="error"></el-alert></template>:element-demo--><template?slot="highlight"><pre?v-pre><code?class="html">&lt;template&gt;&lt;el-alerttitle=&quot;成功提示的文案&quot;type=&quot;success&quot;&gt;&lt;/el-alert&gt;&lt;el-alerttitle=&quot;消息提示的文案&quot;type=&quot;info&quot;&gt;&lt;/el-alert&gt;&lt;el-alerttitle=&quot;警告提示的文案&quot;type=&quot;warning&quot;&gt;&lt;/el-alert&gt;&lt;el-alerttitle=&quot;錯誤提示的文案&quot;type=&quot;error&quot;&gt;&lt;/el-alert&gt;&lt;/template&gt;</code></pre></template>
</demo-block>

demo 子組件的處理

目前我們了解到,每一個 demo 容器對應一個示例,它會解析生成對應的 HTML,最終會通過 demo-block 組件渲染,這個組件是預先定義好的 Vue 組件:

<template><divclass="demo-block":class="[blockClass,?{?'hover':?hovering?}]"@mouseenter="hovering?=?true"@mouseleave="hovering?=?false"><div?class="source"><slot?name="source"></slot></div><div?class="meta"?ref="meta"><div?class="description"?v-if="$slots.default"><slot></slot></div><div?class="highlight"><slot?name="highlight"></slot></div></div><divclass="demo-block-control"ref="control":class="{?'is-fixed':?fixedControl?}"@click="isExpanded?=?!isExpanded"><transition?name="arrow-slide"><i?:class="[iconClass,?{?'hovering':?hovering?}]"></i></transition><transition?name="text-slide"><span?v-show="hovering">{{?controlText?}}</span></transition><el-tooltip?effect="dark"?:content="langConfig['tooltip-text']"?placement="right"><transition?name="text-slide"><el-buttonv-show="hovering?||?isExpanded"size="small"type="text"class="control-button"@click.stop="goCodepen">{{?langConfig['button-text']?}}</el-button></transition></el-tooltip></div></div>
</template>

demo-block 支持了多個插槽,其中默認插槽對應了組件的描述部分;highlight 插槽對應組件高亮的代碼部分;source 插槽對應 demo 實現的部分。

因此,目前我們生成的 HTML 字符串還不能夠直接被 demo-block 組件使用,需要進一步的處理:

module.exports?=?function(source)?{const?content?=?md.render(source);const?startTag?=?'<!--element-demo:';const?startTagLen?=?startTag.length;const?endTag?=?':element-demo-->';const?endTagLen?=?endTag.length;let?componenetsString?=?'';let?id?=?0;?//?demo?的?idlet?output?=?[];?//?輸出的內容let?start?=?0;?//?字符串開始位置let?commentStart?=?content.indexOf(startTag);let?commentEnd?=?content.indexOf(endTag,?commentStart?+?startTagLen);while?(commentStart?!==?-1?&&?commentEnd?!==?-1)?{output.push(content.slice(start,?commentStart));const?commentContent?=?content.slice(commentStart?+?startTagLen,?commentEnd);const?html?=?stripTemplate(commentContent);const?script?=?stripScript(commentContent);let?demoComponentContent?=?genInlineComponentText(html,?script);const?demoComponentName?=?`element-demo${id}`;output.push(`<template?slot="source"><${demoComponentName}?/></template>`);componenetsString?+=?`${JSON.stringify(demoComponentName)}:?${demoComponentContent},`;//?重新計算下一次的位置id++;start?=?commentEnd?+?endTagLen;commentStart?=?content.indexOf(startTag,?start);commentEnd?=?content.indexOf(endTag,?commentStart?+?startTagLen);}//?處理?script//?...output.push(content.slice(start))
};

其中 output 表示要輸出的模板內容,componenetsString 表示要輸出的腳本內容。這段代碼要做的事情就是填充 demo-block 組件內部的 source 插槽,并且插槽的內容是一個 demo 子組件。

由于前面生成的 HTML 中包含了 <!--element-demo: ?和 :element-demo--> 注釋字符串,因此就可以找到注釋字符串的位置,通過字符串截取的方式來獲取注釋內外的內容。

對于注釋內的內容,會提取其中的模板部分和 JS 部分,然后構造出一個內聯的組件字符串。

前面的示例經過處理,output 對應的內容如下:

[`<demo-block><div><p>Alert 組件提供四種主題,由<code>type</code>屬性指定,默認值為<code>info</code>。</p></div>`,`<template?slot="source"><element-demo0?/></template>`,?`<template?slot="highlight"><pre?v-pre><code?class="html">&lt;template&gt;&lt;el-alerttitle=&quot;成功提示的文案&quot;type=&quot;success&quot;&gt;&lt;/el-alert&gt;&lt;el-alerttitle=&quot;消息提示的文案&quot;type=&quot;info&quot;&gt;&lt;/el-alert&gt;&lt;el-alerttitle=&quot;警告提示的文案&quot;type=&quot;warning&quot;&gt;&lt;/el-alert&gt;&lt;el-alerttitle=&quot;錯誤提示的文案&quot;type=&quot;error&quot;&gt;&lt;/el-alert&gt;&lt;/template&gt;</code></pre></template><demo-block>`
]

處理后的 demo-block 就變成一個標準的 Vue 組件的應用了。

componenetsString 對應的內容如下:

`"element-demo0":?(function()?{var?render?=?function()?{var?_vm?=?thisvar?_h?=?_vm.$createElementvar?_c?=?_vm._self._c?||?_hreturn?_c("div",[[_c("el-alert",?{?attrs:?{?title:?"成功提示的文案",?type:?"success"?}?}),_vm._v("?"),_c("el-alert",?{?attrs:?{?title:?"消息提示的文案",?type:?"info"?}?}),_vm._v("?"),_c("el-alert",?{?attrs:?{?title:?"警告提示的文案",?type:?"warning"?}?}),_vm._v("?"),_c("el-alert",?{?attrs:?{?title:?"錯誤提示的文案",?type:?"error"?}?})]],2)??}??var?staticRenderFns?=?[]render._withStripped?=?trueconst?democomponentExport?=?{}return?{render,staticRenderFns,...democomponentExport}
})(),`

通過內聯的方式定義了 element-demo0 子組件的實現。

示例只是處理了單個 demo 子組件,如果有多個 demo 容器,就可以通過循環查找注釋字符串 element-demo:,處理所有的 demo-block

構造完整的組件

module.exports?=?function(source)?{const?content?=?md.render(source);let?componenetsString?=?'';let?output?=?[];let?start?=?0;//?循環處理?demo?子組件//?...let?pageScript?=?'';if?(componenetsString)?{pageScript?=?`<script>export?default?{name:?'component-doc',components:?{${componenetsString}}}</script>`;}?else?if?(content.indexOf('<script>')?===?0)?{start?=?content.indexOf('</script>')?+?'</script>'.length;pageScript?=?content.slice(0,?start);}output.push(content.slice(start));return?`<template><section?class="content?element-doc">${output.join('')}</section></template>${pageScript}`;
};

可以看到,output 負責組件的模板定義,pageScript 負責組件的腳本定義,最終會通過字符串拼接的方式,返回完整的組件定義。

對于最開始完整的示例而言,經過 md-loader 處理的結果如下:

<template><section?class="content?element-doc"><h2?id="alert-jing-gao"><a?class="header-anchor"?href="#alert-jing-gao"?aria-hidden="true">?</a>?Alert?警告</h2><p>用于頁面中展示重要的提示信息。</p><h3?id="ji-ben-yong-fa"><a?class="header-anchor"?href="#ji-ben-yong-fa"?aria-hidden="true">?</a>?基本用法</h3><p>頁面中的非浮層元素,不會自動消失。</p><demo-block><div><p>Alert?組件提供四種主題,由<code>type</code>屬性指定,默認值為<code>info</code>。</p></div><template?slot="source"><element-demo0/></template><template?slot="highlight"><pre?v-pre><code?class="html">&lt;template&gt;&lt;el-alerttitle=&quot;成功提示的文案&quot;type=&quot;success&quot;&gt;&lt;/el-alert&gt;&lt;el-alerttitle=&quot;消息提示的文案&quot;type=&quot;info&quot;&gt;&lt;/el-alert&gt;&lt;el-alerttitle=&quot;警告提示的文案&quot;type=&quot;warning&quot;&gt;&lt;/el-alert&gt;&lt;el-alerttitle=&quot;錯誤提示的文案&quot;type=&quot;error&quot;&gt;&lt;/el-alert&gt;&lt;/template&gt;</code></pre></template></demo-block></section>
</template>
<script>export?default?{name:?'component-doc',components:?{"element-demo0":?(function()?{var?render?=?function()?{var?_vm?=?thisvar?_h?=?_vm.$createElementvar?_c?=?_vm._self._c?||?_hreturn?_c("div",[[_c("el-alert",?{?attrs:?{?title:?"成功提示的文案",?type:?"success"?}?}),_vm._v("?"),_c("el-alert",?{?attrs:?{?title:?"消息提示的文案",?type:?"info"?}?}),_vm._v("?"),_c("el-alert",?{?attrs:?{?title:?"警告提示的文案",?type:?"warning"?}?}),_vm._v("?"),_c("el-alert",?{?attrs:?{?title:?"錯誤提示的文案",?type:?"error"?}?})]],2)}var?staticRenderFns?=?[]render._withStripped?=?trueconst?democomponentExport?=?{}return?{render,staticRenderFns,...democomponentExport}})(),}}
</script>

顯然,經過 md-loader 處理后原來 markdown 語法的字符串變成了一個 Vue 組件定義的字符串,就可以交給 vue-loader 繼續處理了。

文檔的優化

ElementUI 文檔的設計確實巧妙,由于我們研發的 ZoomUI 是 fork 自 ElementUI 的,很長一段時間,我們也沿用了 ElementUI 文檔的編寫方式。

但是隨著我們自研的組件越來越多,組件使用的場景也越來越豐富,我們對于文檔編寫和維護的需求也越來越多。

我發現在現有模式下寫文檔有幾個不爽的點:

1.在 .md 中寫 Vue 組件不方便,沒法格式化代碼,IDE 的智能提示不夠友好。

2.在 demo 中寫 style 是無效的,需要在外部的 css 文件另外定義樣式。

3.中英文文檔需要分別寫 demo,修改一處沒法自動同步到另一處。

我認為理想中編寫一個組件的文檔的方式是這樣的:

13f69687bf833db7a8295019a5e0512e.png

##?Select?選擇器當選項過多時,使用下拉菜單展示并選擇內容。###?基礎用法適用廣泛的基礎單選。:::demo?`v-model`?的值為當前被選中的?`zm-option`?的?`value`?屬性值。```html
<basic/>
```
:::###?有禁用選項:::demo?在?`zm-option`?中,設定?`disabled`?值為?`true`,即可禁用該選項。
```html
<disabled/>
```
:::

所有組件的 demo 拆成一個個 Vue 組件,然后在 markdown 文檔中引入這些同名的組件。通過這種方式,前面提到的三個痛點就解決了。

那么,想達到這種效果,我們需要對 md-loader 做哪些修改呢?

來看一下修改后的 md-loader 的實現:

const?md?=?require('./config');module.exports?=?function(source)?{const?content?=?md.render(source,?{resourcePath:?this.resourcePath});const?startTag?=?'<!--element-demo:';const?startTagLen?=?startTag.length;const?endTag?=?':element-demo-->';const?endTagLen?=?endTag.length;const?tagReg?=?/\s*<([\w-_]+)\s*\/>\s*/;let?componenetsString?=?'';let?output?=?[];?//?輸出的內容let?start?=?0;?//?字符串開始位置let?commentStart?=?content.indexOf(startTag);let?commentEnd?=?content.indexOf(endTag,?commentStart?+?startTagLen);while?(commentStart?!==?-1?&&?commentEnd?!==?-1)?{output.push(content.slice(start,?commentStart));const?commentContent?=?content.slice(commentStart?+?startTagLen,?commentEnd);const?matches?=?commentContent.match(tagReg);if?(matches)?{const?demoComponentName?=?matches[1];output.push(`<template?slot="source"><${demoComponentName}?/></template>`);const?imports?=?`()=>import('../demos/${demoComponentName}.vue')`;componenetsString?+=?`${JSON.stringify(demoComponentName)}:?${imports},`;}start?=?commentEnd?+?endTagLen;commentStart?=?content.indexOf(startTag,?start);commentEnd?=?content.indexOf(endTag,?commentStart?+?startTagLen);}let?pageScript?=?'';if?(componenetsString)?{pageScript?=?`<script>export?default?{name:?'component-doc',components:?{${componenetsString}}}</script>`;}?else?if?(content.indexOf('<script>')?===?0)?{start?=?content.indexOf('</script>')?+?'</script>'.length;pageScript?=?content.slice(0,?start);}output.push(content.slice(start));return?`<template><section?class="content?element-doc">${output.join('')}</section></template>${pageScript}`;
};

思路很簡單,解析出每個 demo 容器中的組件名稱,通過動態 import 的方式加載組件,然后在 source 插槽中直接用這個組件。

這樣就把組件的 markdown 文檔和 demo 直接關聯起來。但這樣還不夠,我們還需要解決組件 demo 下面的代碼展示問題,需要對 code fence 渲染策略做一定的修改:

const?path?=?require('path');
const?fs?=?require('fs');const?tagReg?=?/\s*<([\w-_]+)\s*\/>\s*/;//?覆蓋默認的?fence?渲染策略
module.exports?=?md?=>?{const?defaultRender?=?md.renderer.rules.fence;md.renderer.rules.fence?=?(tokens,?idx,?options,?env,?self)?=>?{const?token?=?tokens[idx];//?判斷該?fence?是否在?:::demo?內const?prevToken?=?tokens[idx?-?1];const?isInDemoContainer?=?prevToken?&&?prevToken.nesting?===?1?&&?prevToken.info.trim().match(/^demo\s*(.*)$/);if?(token.info?===?'html'?&&?isInDemoContainer)?{const?matches?=?token.content.match(tagReg);if?(matches)?{const?componentName?=?matches[1];const?componentPath?=?path.resolve(env.resourcePath,?`../../demos/${componentName}.vue`);const?content?=?fs.readFileSync(componentPath,?'utf-8');return?`<template?slot="highlight"><pre?v-pre><code?class="html">${md.utils.escapeHtml(content)}</code></pre></template>`;}return?'';}return?defaultRender(tokens,?idx,?options,?env,?self);};
};

由于組件 demo 的代碼已經不在 markdown 文檔中維護了,因此只能從組件文件中讀取了。

但是我們如何知道應該從哪個路徑讀取對應的 demo 組件呢?

在 webpack loader 中,我們可以通過 this.resourcePath 獲取到當前處理文件的路徑,那么在執行 markdown 渲染的過程中就可以把路徑當做環境變量傳入:

const?content?=?md.render(source,?{resourcePath:?this.resourcePath
})

這樣在 markdown 處理器的內部我們就可以通過 env.resourcePath 拿到處理的 markdown 文件路徑,從而通過相對路徑計算出要讀取組件的路徑,然后讀取它們的內容:

const?componentPath?=?path.resolve(env.resourcePath,?`../../demos/${componentName}.vue`);
const?content?=?fs.readFileSync(componentPath,?'utf-8');

有了組件文檔的重構方案,接下來的工作就是依次重構組件的文檔。當然在這個階段,新老文檔編寫的方式都需要支持。

因此需要對 webpack 的配置做一些修改:

{test:?/examples(\/|\\)docs(\/|\\).*\.md$/,use:?[{loader:?'vue-loader',options:?{compilerOptions:?{preserveWhitespace:?false}}},{loader:?path.resolve(__dirname,?'./md-loader/index.js')}]
},?{test:?/(examples(\/|\\)docs-next(\/|\\).*|changelog\.[\w-_]+)\.md$/i,use:?[{loader:?'vue-loader',options:?{compilerOptions:?{preserveWhitespace:?false}}},{loader:?path.resolve(__dirname,?'./md-loader-next/index.js')}]
}

對于重構的文檔,使用新的 markdown loader。當然加載組件視圖的邏輯也需要做一定的修改,對于重構的文檔,指向新的文檔地址。

總結

ElementUI 通過 markdown 編寫組件文檔的思路還是非常棒的,主要利用了自定義 md-loader 對 markdown 文件內容做了一層處理,解析成 Vue 組件字符串,再交給 vue-loader 處理。

在寫這篇文章之前,我就在粉絲群里分享了重構文檔的方案。有同學告訴我,Element-plus 已經用 vitepress 重寫,看了一下文檔的組織方式,和我重構的方式非常類似,這就是傳說中的英雄所見略同嗎?

我在之前的文章中強調過,要善于發現工作中的痛點,并通過技術的方式解決,這是優秀的工程師重要的能力之一,希望這篇文章能夠帶給你這方面的一些思考。

參考資料

[1] markdown-it-chain:??https://github.com/ulivz/markdown-it-chain
[2] markdown-it:?https://markdown-it.github.io/markdown-it/


最近組建了一個江西人的前端交流群,如果你是江西人可以加我微信?ruochuan12?私信 江西?拉你進群。

推薦閱讀

1個月,200+人,一起讀了4周源碼
我歷時3年才寫了10余篇源碼文章,但收獲了100w+閱讀

老姚淺談:怎么學JavaScript?

我在阿里招前端,該怎么幫你(可進面試群)

847b746eeac85832018b1081207120a2.gif

·················?若川簡介?·················

你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》10余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助1000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。

f611b3283a52b7c1992811e256223bd8.png

識別方二維碼加我微信、拉你進源碼共讀

今日話題

略。歡迎分享、收藏、點贊、在看我的公眾號文章~

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/275243.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/275243.shtml
英文地址,請注明出處:http://en.pswp.cn/news/275243.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

一個html5流星雨源碼

流星會隨著鼠標的方向劃過&#xff0c;按緊鼠標左鍵可以增長流星的尾巴。 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html lang"zh-CN"> <head> <title>流星雨<…

csdn 用戶 螞蟻翹大象_用戶界面設計師房間里的大象

csdn 用戶 螞蟻翹大象Once upon a time, an educated eye detected a new trend in UI designs, particularly, in Dribbble. It was a conceptual proposition, not an actual design for a customer or an app. Trying to explain the characteristics of this new trend, a …

面試官問發布訂閱模式是在問什么?

大家好&#xff0c;我是若川。最近組織了源碼共讀活動&#xff0c;感興趣的可以加我微信 ruochuan12 參與&#xff0c;已進行了三個多月&#xff0c;大家一起交流學習&#xff0c;共同進步。本文來自 simonezhou 小姐姐投稿的第八期筆記。面試官常問發布訂閱、觀察者模式&#…

linux服務器內存、根目錄使用率、某進程的監控告警腳本

腳本內容如下 #!/bin/bash#磁盤超過百分之80發送郵件告警DISK_USEDdf -T |sed -n "2p" |awk {print ($4/$3)*100}DISK_percentage80if [ expr "$DISK_USED > $DISK_percentage" ]thenecho "$HOSTNAME服務器當前硬盤使用率為$DISK_USED%" | ma…

figma下載_不用擔心Figma中的間距

figma下載重點 (Top highlight)I spend way too much time caring about spacing when designing interfaces and building design systems. You are probably no stranger to the constant 1 px and 8 px nudging, continuous checking of the bottom or in-between space for…

【建議收藏】面試官賊喜歡問的 32+ vue 修飾符,你掌握幾種啦?

大家好&#xff0c;我是若川。最近組織了源碼共讀活動&#xff0c;感興趣的可以加我微信 ruochuan12 參與&#xff0c;已進行了三個多月&#xff0c;大家一起交流學習&#xff0c;共同進步。前言vue簡潔好用體現在很多個地方&#xff0c;比如其內置了32修飾符&#xff0c;可以很…

知識管理系統Data Solution研發日記之一 場景設計與需求列出

在平時開發的過程中&#xff0c;經常會查找一些資料&#xff0c;從網上下載一些網頁&#xff0c;壓縮格式文件到自己的電腦中&#xff0c;然后閱讀。程序有別于其他行業的一個特征是&#xff0c;所有的資料&#xff0c;數據&#xff0c;壓縮文件&#xff0c;只用于產生可以工作…

系列TCP/IP協議-動態IP選路協議(008)

一、引言 前一章已經說過了IP數據包是如何分發的。為啥這一章還要說這個問題&#xff1f;在網絡很小、只有單個連接點、沒有多余的路由的時候&#xff0c;使用靜態選路是可以的。但是一旦網絡變大一點就會出現各種問題。在大網絡中的網絡選路將在該節說明。 ??動態選路協議用…

shields 徽標_我們如何準確地記住著名徽標的特征和顏色?

shields 徽標The logos of global corporations like Apple, Starbucks, Adidas, and IKEA are designed to create instant brand associations in the minds of billions who see them every day. But how accurately can we remember the features and colors of these famo…

面了三次字節,他的一些感悟

大家好&#xff0c;我是若川。最近組織了源碼共讀活動&#xff0c;感興趣的可以加我微信 ruochuan12 參與&#xff0c;已進行了三個多月&#xff0c;大家一起交流學習&#xff0c;共同進步。今天分享一篇小K投稿的字節面試記錄&#xff0c;這是他第三次面字節了&#xff0c;之前…

JavaScript數組內置排序函數

javascript內置的sort函數是多種排序算法的集合 JavaScript實現多維數組、對象數組排序&#xff0c;其實用的就是原生的sort()方法&#xff0c;用于對數組的元素進行排序。 sort() 方法用于對數組的元素進行排序。語法如下&#xff1a; ArrayObject.sort(order); 測試A&#xf…

解決Wireshark安裝Npcap組件失敗

2019獨角獸企業重金招聘Python工程師標準>>> 解決Wireshark安裝Npcap組件失敗 從Wireshark 3.0開始&#xff0c;Npcap取代Winpcap組件&#xff0c;成為Wireshark默認的網卡核心驅動。由于該組件屬于驅動程序&#xff0c;所以安裝時候容易被殺毒/防火墻軟件攔截&…

adobe清理工具_Adobe終于通過其新的漸變工具實現了這一點-UX評論

adobe清理工具的Photoshop (Photoshop) UX:用戶體驗&#xff1a; At first glance, the UX looks okay; it’s pretty clear. The user gets to know how to use this tool right away. The color palette is located above, and the gradient down below. The diamond betwee…

GMF學習系列(二) 一些知識點(續2)

8.插件的國際化&#xff0c;可以參考nwpu.cdcsp.sbpel.diagram.part中messages.java的做法。 9.Text自動提示功能 import org.eclipse.jface.bindings.keys.KeyStroke; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.fieldassist.AutoCompleteField; im…

新手向:前端程序員必學基本技能——調試JS代碼

1前言大家好&#xff0c;我是若川。最近組織了源碼共讀活動&#xff0c;感興趣的可以加我微信 ruochuan12 參與&#xff0c;已進行三個月了&#xff0c;大家一起交流學習&#xff0c;共同進步。想學源碼&#xff0c;極力推薦之前我寫的《學習源碼整體架構系列》 包含jQuery、un…

iOS開發ApplePay的介紹與實現

1、Apple Pay的介紹 Apple Pay官方1.1 Apple Pay概念 Apple Pay&#xff0c;簡單來說, 就是一種移動支付方式。通過Touch ID/ Passcode&#xff0c;用戶可使用存儲在iPhone 6, 6p等之后的新設備上的信用卡和借記卡支付證書來授權支付&#xff1b; 它是蘋果公司在2014蘋果秋季新…

mes建設指南_給予和接受建設性批評的設計師指南

mes建設指南Constructive criticism, or more plainly, feedback, plays a crucial role in a designer’s job. Design is an iterative process, so we are often either asking for feedback on our own work or dishing it out to a fellow designer.建設性的批評&#xff…

面試官:請實現一個通用函數把 callback 轉成 promise

1. 前言大家好&#xff0c;我是若川。最近組織了源碼共讀活動&#xff0c;感興趣的可以加我微信 ruochuan12 參與&#xff0c;或者在公眾號&#xff1a;若川視野&#xff0c;回復"源碼"參與&#xff0c;每周大家一起學習200行左右的源碼&#xff0c;共同進步。已進行…

java中filter的用法

filter過濾器主要使用于前臺向后臺傳遞數據是的過濾操作。程度很簡單就不說明了&#xff0c;直接給幾個已經寫好的代碼&#xff1a; 一、使瀏覽器不緩存頁面的過濾器 Java代碼 import javax.servlet.*;import javax.servlet.http.HttpServletResponse;import java.io.IOExcept…

我很喜歡玩游戲,那么我就適合做游戲程序員嗎?

作者&#xff1a;黃小斜文章來源&#xff1a;【程序員江湖】游戲在今天的普及度已經不是端游時代可以比肩的了。如今人手一臺手機、平板就可以吃雞、打農藥&#xff0c;不僅是男生&#xff0c;也有很多女生加入了游戲圈。相信現在在看文章的你也玩游戲&#xff0c;雖然愛玩的程…