<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          我是如何閱讀 md-loader 源碼并優(yōu)化它的?

          共 16227字,需瀏覽 33分鐘

           ·

          2021-11-17 02:55

          背景

          相信很多同學(xué)在學(xué)習(xí) webpack 的時(shí)候,對(duì) loader 的概念應(yīng)該有所了解,它用于模塊源碼的轉(zhuǎn)換,描述了 webpack 如何處理非 JavaScript 模塊,常見(jiàn)的有 css-loaderbabel-loaderurl-loadervue-loader 等。

          大部分 loader 已經(jīng)滿(mǎn)足我們的日常開(kāi)發(fā)需求,不過(guò)有些時(shí)候我們?nèi)匀恍枰远x loader。為了讓你了解如何開(kāi)發(fā)一個(gè) webpack loader,我決定從 ElementUI 組件庫(kù)的 md-loader 入手,帶你去了解其中的實(shí)現(xiàn)原理,以及在它的基礎(chǔ)上,如何做進(jìn)一步的優(yōu)化。

          文檔的設(shè)計(jì)

          對(duì)于一個(gè)組件的文檔,首先我們要考慮的是如何更好地展現(xiàn)組件的功能,其次要考慮的是如何更方便地做文檔維護(hù)。

          想要編寫(xiě)好一個(gè)組件的文檔,需要做好以下幾點(diǎn):

          1.功能描述

          對(duì)組件功能、使用場(chǎng)景做詳細(xì)的描述。

          2.demo 演示

          直觀地讓用戶(hù)感受到組件的功能,并且能展示 demo 對(duì)應(yīng)的代碼。

          3.接口說(shuō)明

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

          那么,如何方便地維護(hù)文檔呢?

          ElementUI 組件庫(kù)的文檔也是一個(gè) Vue 項(xiàng)目,組件的文檔頁(yè)面是單獨(dú)的路由視圖,而文檔是用 markdown 文件來(lái)描述的,在文檔內(nèi)部,不僅包含了對(duì)組件的功能以及接口的描述,還可以通過(guò)編寫(xiě) vue 組件的方式直接編寫(xiě)組件的 demo,這種方式對(duì)于組件文檔的維護(hù)還是比較方便的。

          以 ElementUI 組件庫(kù) Alter 組件為例:

          ##?Alert?警告

          用于頁(yè)面中展示重要的提示信息。

          ###?基本用法

          頁(yè)面中的非浮層元素,不會(huì)自動(dòng)消失。

          :::demo?Alert?組件提供四種主題,由`type`屬性指定,默認(rèn)值為`info`

          ```html

          ```

          :::

          最終它在頁(yè)面上的展示效果如下:

          可以看到,組件的路由視圖對(duì)應(yīng)的是一個(gè) markdown 文件,而在我們通常的認(rèn)知中,Vue 的路由視圖應(yīng)該對(duì)應(yīng)的是一個(gè) Vue 組件。

          在 ElementUI 內(nèi)部,是通過(guò) require.ensure 的方式去加載一個(gè) .md 文件,它的返回值會(huì)作為路由視圖對(duì)應(yīng)的異步組件。

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

          因此內(nèi)部就必須要把 markdown 文件轉(zhuǎn)換一個(gè) Vue 組件,我們可以借助 webpack loader 來(lái)實(shí)現(xiàn)這一需求。

          自定義 md-loader

          首先,在 webpack 的配置規(guī)則中,需要指定 .md 文件應(yīng)用的 loader:

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

          接下來(lái),我們就來(lái)分析 md-loader 的源碼實(shí)現(xiàn):

          const?{
          ??stripScript,
          ??stripTemplate,
          ??genInlineComponentText
          }?=?require('./util');
          const?md?=?require('./config');

          module.exports?=?function(source)?{
          ??const?content?=?md.render(source);

          ??const?startTag?=?'';
          ??const?endTagLen?=?endTag.length;

          ??let?componenetsString?=?'';
          ??let?id?=?0;?//?demo?的?id
          ??let?output?=?[];?//?輸出的內(nèi)容
          ??let?start?=?0;?//?字符串開(kāi)始位置

          ??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(`<${demoComponentName}?/>`);
          ????componenetsString?+=?`${JSON.stringify(demoComponentName)}:?${demoComponentContent},`;

          ????//?重新計(jì)算下一次的位置
          ????id++;
          ????start?=?commentEnd?+?endTagLen;
          ????commentStart?=?content.indexOf(startTag,?start);
          ????commentEnd?=?content.indexOf(endTag,?commentStart?+?startTagLen);
          ??}

          ??//?僅允許在?demo?不存在時(shí),才可以在?Markdown?中寫(xiě)?script?標(biāo)簽
          ??let?pageScript?=?'';
          ??if?(componenetsString)?{
          ????pageScript?=?``;
          ??}?else?if?(content.indexOf('')?+?''.length;
          ????pageScript?=?content.slice(0,?start);
          ??}

          ??output.push(content.slice(start));

          ??return?`
          ????
          ????${pageScript}
          ??`
          ;
          };

          md-loader 要做的事情,就是把 markdown 語(yǔ)法的字符串,轉(zhuǎn)成 Vue 組件字符串。轉(zhuǎn)換的過(guò)程可以拆成三個(gè)步驟:markdown 渲染,demo 子組件的處理,構(gòu)造完整的組件。接下來(lái)我們就來(lái)依次分析這三個(gè)步驟。

          markdown 渲染

          markdown 文件內(nèi)容會(huì)渲染生成對(duì)應(yīng)的 HTML,它是通過(guò)下面這段代碼完成的:

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

          md 對(duì)象的來(lái)源如下:

          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;

          首先實(shí)例化了 config 對(duì)象,它依賴(lài)于 markdown-it-chain,通過(guò) webpack chain 的鏈?zhǔn)?API,配置了 markdown-it 的插件。而 md 對(duì)象指向的就是 markdown-it 的實(shí)例。

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

          不過(guò)我們注意到,組件文檔使用了一些非標(biāo)準(zhǔn)的 markdown 語(yǔ)法,比如:

          :::demo
          :::

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

          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?`
          ????????${description???`
          ${md.render(description)}
          `
          ?:?''}

          ????????
          ????????`
          ;
          ??????}
          ??????return?'';
          ????}
          ??});

          ??md.use(mdContainer,?'tip');
          ??md.use(mdContainer,?'warning');
          };

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

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

          //?覆蓋默認(rèn)的?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?內(nèi)
          ????const?prevToken?=?tokens[idx?-?1];
          ????const?isInDemoContainer?=?prevToken?&&?prevToken.nesting?===?1?&&?prevToken.info.trim().match(/^demo\s*(.*)$/);
          ????if?(token.info?===?'html'?&&?isInDemoContainer)?{
          ??????return?`${md.utils.escapeHtml(token.content)}
          `;
          ????}
          ????return?defaultRender(tokens,?idx,?options,?env,?self);
          ??};
          };

          對(duì)于在 demo 容器內(nèi)且?guī)в?html 標(biāo)記的 code fence,會(huì)做一層特殊處理。

          對(duì)于我們前面的示例:

          :::demo?Alert?組件提供四種主題,由`type`屬性指定,默認(rèn)值為`info`

          ```html

          ```

          :::

          經(jīng)過(guò)解析后,生成的 HTML 大致如下:

          <demo-block>
          ??<div><p>Alert?組件提供四種主題,由<code>typecode>屬性指定,默認(rèn)值為<code>infocode>。p>
          ??div>
          ??
          ??<template?slot="highlight"><pre?v-pre><code?class="html"><template>
          ??<el-alert
          ????title="成功提示的文案"
          ????type="success">
          ??</el-alert>
          ??<el-alert
          ????title="消息提示的文案"
          ????type="info">
          ??</el-alert>
          ??<el-alert
          ????title="警告提示的文案"
          ????type="warning">
          ??</el-alert>
          ??<el-alert
          ????title="錯(cuò)誤提示的文案"
          ????type="error">
          ??</el-alert>
          ??</template>
          ??code>pre>template>
          demo-block>

          demo 子組件的處理

          目前我們了解到,每一個(gè) demo 容器對(duì)應(yīng)一個(gè)示例,它會(huì)解析生成對(duì)應(yīng)的 HTML,最終會(huì)通過(guò) demo-block 組件渲染,這個(gè)組件是預(yù)先定義好的 Vue 組件:

          <template>
          ??<div
          ????class="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>
          ????<div
          ??????class="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-button
          ????????????v-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 支持了多個(gè)插槽,其中默認(rèn)插槽對(duì)應(yīng)了組件的描述部分;highlight 插槽對(duì)應(yīng)組件高亮的代碼部分;source 插槽對(duì)應(yīng) demo 實(shí)現(xiàn)的部分。

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

          module.exports?=?function(source)?{
          ??const?content?=?md.render(source);

          ??const?startTag?=?'';
          ??const?endTagLen?=?endTag.length;

          ??let?componenetsString?=?'';
          ??let?id?=?0;?//?demo?的?id
          ??let?output?=?[];?//?輸出的內(nèi)容
          ??let?start?=?0;?//?字符串開(kāi)始位置

          ??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(`<${demoComponentName}?/>`);
          ????componenetsString?+=?`${JSON.stringify(demoComponentName)}:?${demoComponentContent},`;

          ????//?重新計(jì)算下一次的位置
          ????id++;
          ????start?=?commentEnd?+?endTagLen;
          ????commentStart?=?content.indexOf(startTag,?start);
          ????commentEnd?=?content.indexOf(endTag,?commentStart?+?startTagLen);
          ??}
          ??
          ??//?處理?script
          ??//?...
          ??
          ??output.push(content.slice(start))
          };

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

          由于前面生成的 HTML 中包含了 注釋字符串,因此就可以找到注釋字符串的位置,通過(guò)字符串截取的方式來(lái)獲取注釋內(nèi)外的內(nèi)容。

          對(duì)于注釋內(nèi)的內(nèi)容,會(huì)提取其中的模板部分和 JS 部分,然后構(gòu)造出一個(gè)內(nèi)聯(lián)的組件字符串。

          前面的示例經(jīng)過(guò)處理,output 對(duì)應(yīng)的內(nèi)容如下:

          [
          ??`
          ?????

          Alert 組件提供四種主題,由type屬性指定,默認(rèn)值為info

          `
          ,
          ??``,?
          ??`<template>
          ?????<el-alert
          ???????title="成功提示的文案"
          ???????type="success">
          ?????</el-alert>
          ?????<el-alert
          ???????title="消息提示的文案"
          ???????type="info">
          ?????</el-alert>
          ?????<el-alert
          ???????title="警告提示的文案"
          ???????type="warning">
          ?????</el-alert>
          ?????<el-alert
          ???????title="錯(cuò)誤提示的文案"
          ???????type="error">
          ?????</el-alert>
          ?????</template>
          ?????

          ?????`
          ]

          處理后的 demo-block 就變成一個(gè)標(biāo)準(zhǔn)的 Vue 組件的應(yīng)用了。

          componenetsString 對(duì)應(yīng)的內(nèi)容如下:

          `"element-demo0":?(function()?{
          ??var?render?=?function()?{
          ????var?_vm?=?this
          ????var?_h?=?_vm.$createElement
          ????var?_c?=?_vm._self._c?||?_h
          ????return?_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:?"錯(cuò)誤提示的文案",?type:?"error"?}?})
          ????????]
          ??????],
          ??????2
          ????)??
          ??}??
          ??var?staticRenderFns?=?[]
          ??render._withStripped?=?true
          ??const?democomponentExport?=?{}
          ??return?{
          ????render,
          ????staticRenderFns,
          ????...democomponentExport
          ??}
          })(),`

          通過(guò)內(nèi)聯(lián)的方式定義了 element-demo0 子組件的實(shí)現(xiàn)。

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

          構(gòu)造完整的組件

          module.exports?=?function(source)?{
          ??const?content?=?md.render(source);

          ??let?componenetsString?=?'';
          ??let?output?=?[];
          ??let?start?=?0;

          ??//?循環(huán)處理?demo?子組件
          ??//?...
          ??
          ??let?pageScript?=?'';
          ??if?(componenetsString)?{
          ????pageScript?=?``;
          ??}?else?if?(content.indexOf('')?+?''.length;
          ????pageScript?=?content.slice(0,?start);
          ??}

          ??output.push(content.slice(start));

          ??return?`
          ????
          ????${pageScript}
          ??`
          ;
          };

          可以看到,output 負(fù)責(zé)組件的模板定義,pageScript 負(fù)責(zé)組件的腳本定義,最終會(huì)通過(guò)字符串拼接的方式,返回完整的組件定義。

          對(duì)于最開(kāi)始完整的示例而言,經(jīng)過(guò) md-loader 處理的結(jié)果如下:

          <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>用于頁(yè)面中展示重要的提示信息。p>
          ????<h3?id="ji-ben-yong-fa"><a?class="header-anchor"?href="#ji-ben-yong-fa"?aria-hidden="true">?a>?基本用法h3>
          ????<p>頁(yè)面中的非浮層元素,不會(huì)自動(dòng)消失。p>
          ????<demo-block>
          ??????<div><p>Alert?組件提供四種主題,由<code>typecode>屬性指定,默認(rèn)值為<code>infocode>。p>
          ??????div>
          ??????<template?slot="source">
          ????????<element-demo0/>
          ??????template>
          ??????<template?slot="highlight"><pre?v-pre><code?class="html"><template>
          ??????????<el-alert
          ????????????title="成功提示的文案"
          ????????????type="success">
          ??????????</el-alert>
          ??????????<el-alert
          ????????????title="消息提示的文案"
          ????????????type="info">
          ??????????</el-alert>
          ??????????<el-alert
          ????????????title="警告提示的文案"
          ????????????type="warning">
          ??????????</el-alert>
          ??????????<el-alert
          ????????????title="錯(cuò)誤提示的文案"
          ????????????type="error">
          ??????????</el-alert>
          ????????</template>
          ????????code>pre>
          ??????template>
          ????demo-block>
          ??section>
          template>
          <script>
          ??export?default?{
          ????name:?'component-doc',
          ????components:?{
          ??????"element-demo0":?(function()?{
          ????????var?render?=?function()?{
          ??????????var?_vm?=?this
          ??????????var?_h?=?_vm.$createElement
          ??????????var?_c?=?_vm._self._c?||?_h
          ??????????return?_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:?"錯(cuò)誤提示的文案",?type:?"error"?}?})
          ??????????????]
          ????????????],
          ????????????2
          ??????????)
          ????????}
          ????????var?staticRenderFns?=?[]
          ????????render._withStripped?=?true

          ????????const?democomponentExport?=?{}
          ????????return?{
          ??????????render,
          ??????????staticRenderFns,
          ??????????...democomponentExport
          ????????}
          ??????})(),
          ????}
          ??}
          script>

          顯然,經(jīng)過(guò) md-loader 處理后原來(lái) markdown 語(yǔ)法的字符串變成了一個(gè) Vue 組件定義的字符串,就可以交給 vue-loader 繼續(xù)處理了。

          文檔的優(yōu)化

          ElementUI 文檔的設(shè)計(jì)確實(shí)巧妙,由于我們研發(fā)的 ZoomUI 是 fork 自 ElementUI 的,很長(zhǎng)一段時(shí)間,我們也沿用了 ElementUI 文檔的編寫(xiě)方式。

          但是隨著我們自研的組件越來(lái)越多,組件使用的場(chǎng)景也越來(lái)越豐富,我們對(duì)于文檔編寫(xiě)和維護(hù)的需求也越來(lái)越多。

          我發(fā)現(xiàn)在現(xiàn)有模式下寫(xiě)文檔有幾個(gè)不爽的點(diǎn):

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

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

          3.中英文文檔需要分別寫(xiě) demo,修改一處沒(méi)法自動(dòng)同步到另一處。

          我認(rèn)為理想中編寫(xiě)一個(gè)組件的文檔的方式是這樣的:

          ##?Select?選擇器

          當(dāng)選項(xiàng)過(guò)多時(shí),使用下拉菜單展示并選擇內(nèi)容。

          ###?基礎(chǔ)用法

          適用廣泛的基礎(chǔ)單選。

          :::demo?`v-model`?的值為當(dāng)前被選中的?`zm-option`?的?`value`?屬性值。

          ```html

          ```

          :::

          ###?有禁用選項(xiàng)

          :::demo?在?`zm-option`?中,設(shè)定?`disabled`?值為?`true`,即可禁用該選項(xiàng)。
          ```html

          ```

          :::

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

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

          來(lái)看一下修改后的 md-loader 的實(shí)現(xiàn):

          const?md?=?require('./config');

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

          ??const?startTag?=?'';
          ??const?endTagLen?=?endTag.length;
          ??const?tagReg?=?/\s*<([\w-_]+)\s*\/>\s*/;

          ??let?componenetsString?=?'';
          ??let?output?=?[];?//?輸出的內(nèi)容
          ??let?start?=?0;?//?字符串開(kāi)始位置

          ??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(`<${demoComponentName}?/>`);
          ??????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?=?``;
          ??}?else?if?(content.indexOf('')?+?''.length;
          ????pageScript?=?content.slice(0,?start);
          ??}

          ??output.push(content.slice(start));
          ??return?`
          ????
          ????${pageScript}
          ??`
          ;
          };

          思路很簡(jiǎn)單,解析出每個(gè) demo 容器中的組件名稱(chēng),通過(guò)動(dòng)態(tài) import 的方式加載組件,然后在 source 插槽中直接用這個(gè)組件。

          這樣就把組件的 markdown 文檔和 demo 直接關(guān)聯(lián)起來(lái)。但這樣還不夠,我們還需要解決組件 demo 下面的代碼展示問(wèn)題,需要對(duì) code fence 渲染策略做一定的修改:

          const?path?=?require('path');
          const?fs?=?require('fs');

          const?tagReg?=?/\s*<([\w-_]+)\s*\/>\s*/;

          //?覆蓋默認(rèn)的?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?內(nèi)
          ????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?`${md.utils.escapeHtml(content)}
          `;
          ??????}
          ??????return?'';
          ????}
          ????return?defaultRender(tokens,?idx,?options,?env,?self);
          ??};
          };

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

          但是我們?nèi)绾沃缿?yīng)該從哪個(gè)路徑讀取對(duì)應(yīng)的 demo 組件呢?

          在 webpack loader 中,我們可以通過(guò) this.resourcePath 獲取到當(dāng)前處理文件的路徑,那么在執(zhí)行 markdown 渲染的過(guò)程中就可以把路徑當(dāng)做環(huán)境變量傳入:

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

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

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

          有了組件文檔的重構(gòu)方案,接下來(lái)的工作就是依次重構(gòu)組件的文檔。當(dāng)然在這個(gè)階段,新老文檔編寫(xiě)的方式都需要支持。

          因此需要對(duì) 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')
          ????}
          ??]
          }

          對(duì)于重構(gòu)的文檔,使用新的 markdown loader。當(dāng)然加載組件視圖的邏輯也需要做一定的修改,對(duì)于重構(gòu)的文檔,指向新的文檔地址。

          總結(jié)

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

          在寫(xiě)這篇文章之前,我就在粉絲群里分享了重構(gòu)文檔的方案。有同學(xué)告訴我,Element-plus 已經(jīng)用 vitepress 重寫(xiě),看了一下文檔的組織方式,和我重構(gòu)的方式非常類(lèi)似,這就是傳說(shuō)中的英雄所見(jiàn)略同嗎?

          我在之前的文章中強(qiáng)調(diào)過(guò),要善于發(fā)現(xiàn)工作中的痛點(diǎn),并通過(guò)技術(shù)的方式解決,這是優(yōu)秀的工程師重要的能力之一,希望這篇文章能夠帶給你這方面的一些思考。

          參考資料

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

          瀏覽 58
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  中文字幕欧美日韩 | 欧美成人一区免费视频 | 日本亚洲欧洲视频 | 青娱乐日韩| 超碰在线97免费 |