3

NutUI 是一款非常优秀的移动端组件库, GitHub 上已获得 1.8k 的 star,NPM 下载量超过 13k。公司内部已赋能支持 40+ 个项目,外部接入使用项目达到 20+ 个。使用者将会获得如下收益:

  1. 组件库生态系统覆盖面广,布局类组件、操作反类馈类组件、基础类组件、导航类组件超过 50+,每一类充分考虑了它们的使用场景。
  2. 活跃的讨论群体,如果你用的不爽可以在 GitHub 上提一些问题,如果你还觉得慢也可以在我们的微信群里直接 @ 到开发者本人。
  3. API 解释详细,Demo 使用场景列举丰富。可以说哪怕你是一个后端开发人员,在有了一定 Vue 使用基础之后就可以快速使用 NutUI 去开发你的网站了。
  4. 官网功能强大,提供了组件搜索、NutUI 版本切换、Demo 展示等功能。
  5. 支持按需加载,从而减少我们开发项目的体积。
  6. 新功能的增加不会对旧版本的代码有影响,可以说是向前兼容,在不改变代码的情况下,可以安心的更新。

 

NutUI 的历史已经有 2 年了,2017 年的版本是 v1.0 ,那是一个造轮子和摸索的过程。最开始的组件大都是来源于业务,项目中经过抽离封装做成的组件。

那还是一个人人都可以提交组件的年代,当时最朴素的一个想法就是复用,先把业务中组件数量积累下来。比如一个地址配送组件一个同学开发花了2-3人日,在另一个购物场景项目中也会有,如果每人都去开发一个必然浪费。最早组件库仅仅是在公司内部使用,那时候的 NutUi是下面这个样子:

早期的首页:

早期的文档页:

通过上面两张图我们可以看到,当初的网站有很多不足乃至成为痛点,具体表现以下几点:

  1. 官网首页风格色调昏暗、沉重,展示内容过多,没有做很好的信息分类。
  2. 右侧展示区域缺少了 Demo 实时的展示,只有代码展示,不能直观的体现插件的 UI。
  3. 左侧导航部分组件没有分类,不利于使用者查找想要的组件。
  4. 开发人员在编写组件库文档时候也要花费大量的精力在文档的细节上,例如样式、功能。
  5. Demo 展示不全面,各个组件风格不统一。
  6. 组件经过没有自动化测试,仅仅是开发者自测,很难考虑全面。

针对这些痛点 2.0 作出如下改变:

  1. 专业设计师提供网站及其组件内部标准设计稿。
  2. 引入自动化测试工具以及定期代码评审,为组件稳定保驾护航。
  3. 开发一键构建官网工具,开发者仅需记住简单 MD 标签即可轻松完成组件说明文档。
  4. 同时 2.0 也顺应潮流支持国际化一键换肤等新特性。

对比 1.0 我们的组件库有了全面的提升,代码规范简约,可维护性强,组件使用场景考虑充分,功能更加完善。那么这么多的组件,它的官方网站是怎么构建的呢?本文将为你揭秘。

通过这篇文章后你会了解 NutUI 组件库官方网站的开发流程。同时文章里面我们详细分析了为什么选用 Webpack 插件的这种形式去开发,以及 .md.vue 这种方式的好处。

在开发这个插件功能中,我们还用到了一些 Node 操作,和 NPM 依赖包,它和我们平时的前端开发有不少区别,不同于页面的交互逻辑,我感觉这更有意思,那么咱们就一探究竟吧。

使用

在文章开始之前,我们先介绍下这个插件的使用方法,这有助于你理解我们实现这个功能的思路。

总的来说,它是一个 Webpack 插件,所以在使用上只需在 Webpack 中配置,具体如下:

{
    [
        ...new mdtohtml({
            entry: "./docs",
            output: "./sites/doc/page/",
            template: "./doc-site/template.html",
            nav: "left",
            needCode: false,
            isProduction: isDev
        })
    ];
}

属性说明

参数 说明
entry string 需要处理文件的文件夹路径
output string 处理完成后输出的文件路径
template string 转换成 .vue 之后需要配置的 HTML 模版路径
nav string 生成网站导航的位置,目前只支持左边
needCode boolean 是否需要代码显示工具
isProduction boolean 是开发环境还是编译环境

设计思考

需求解析

我们 NutUI 的官方网站的需求是什么呢?

  1. 需要统一的展示风格。
  2. 减少开发人员的编码工作,只需要关心所写的文档。
  3. 可以通过输入组件名称进行内部检索快速找到组件。
  4. 为每个组件说明文档建立导航书签。
  5. 区分 HTML 和 JS 代码并高亮
  6. 每个组件在右侧需要展示 Demo。

实现思路

了解了具体需求,下面就可以开发功能了,其中最重要的就是选择一条对的路。

这里我选择的是通过 .md 转换成 .vue ,为什么呢?

使用 MD 编辑的优点

  1. 语法简单,即使是非开发者也能快速上手。所有 MD 标记都是基于这四个符号(* - +. >)或组合,而 HTML 的标签浩如烟海,不好记还会写一些没有样式的标签。
  2. 组件库文档会有很多的代码展示和样式展示,使用 HTML 标签不好控制而使用 MD 就会方便很多,可以轻松的控制代码展示格式。我们想展示一段 CSS 代码或者 JS 代码只要使用 ```js``` 或者 ```css``` 就可以做代码的展示。
  3. 容易使用固定的模版,让编写文档的人按照模版去编写文档,更容易让文档统一。
  4. MD 文档不像 HTML 标签一样需要严格的闭合,这也是选择 MD 开发的原因之一。
  5. 基于 MD 转换成 HTML 的 NPM 包市面上有很多,在处理这部分上面我们可以节省很多工作。
  6. 首先 NutUI 组件是基于 Vue 的,在同一个构建工具下,换一种框架成本太高。
  7. 采用 .vue 模版开发正好可以把 .md 转换过来的 HTML 直接嵌套到 template 中。
  8. 模块式的开发有利于对每个组件进行统一管理。

风格管理

我们的 Style 很少使用 Class ,而是基于标签选择去做样式处理:

h1,h2,p
{
    color: #333333;
}

h1
{
    font-size: 30px;
    font-weight: 700;
    margin: 10px 0 20px;
}

这样的好处就是我们在编写文档时不用去关心这些,只需要记住简单的几个 MD 语法就可以写出一篇相对完整的文档。

基本书写格式如下:

  1. # 一级标题-组件名
  2. ## 二级标题-书签
  3. ```css``` 用来展示 CSS 代码
  4. ```js ``` 用来展示 JS 代码
  5. |表头|表头

语言转换

MD 转换 HTML 原理

首先感谢 AST 让我们可以实现这个功能,下面我们先看下它是如何进行转换的,我们不仅仅会开车,还要学会修车。同时它也是目前市面上各种代码转换工具的基础。话不多说,先开车了。

首先我们举个简单的例子,下面是一段 MD 格式的片段:

##  marked 转换
### marked 转换
\`\`\`js
var a = 0;
var b = 2;
\`\`\`

通过 AST 处理结果,如下:

它处理的结果是一个大的对象,每个节点都有特定的 Type ,我们根据这些内容就可以进行处理,重新生成一份我们想要的格式。

通过上面的图片我们可以看到: ## 的 Type 为 heading , depth:2

通过这个可以理解为这是一个 h2 标签,而 ``` 的 Type 为 Code , 我们写在 ```里面的内容都放在 Code 这个对象里面。

它的结构就像一颗大树,有不同的枝干,我想这也是为什么 AST 被称为抽象语法树了。通过处理生成的 AST 对象大体结构大家可以参考下图:

接下来我们看下详细的转换,例如我们这个项目里面是需要把它转换为 HTML。

我们就是通过递归的方式去处理这个对象的结构,把它们转换成想要的文本。 在 NPM 包里面也有很多工具包可以帮我去做处理这个对象,例如:
通过 estraverse 这个插件库去遍历 AST 对象的所有节点:

const res = estraverse.traverse(ast, {
    enter: function (node, parent) {
        if (node.type == "heading") return "";
    },
    leave: function (node, parent) {
        if (node.type == "code") console.log(node.id.name);
    }
});

说明: 通过 estraverse.traverse 这个方法去遍历整个 AST 对象。 在遍历的过程中它接受一个 option,其中有两个属性分别是 enterleave 。它们分别代表监听遍历的进入阶段和离开阶段。通常我们只需要定义 enter 里面的方法就好,例如上面的例子,当条件满足的时候我们去执行某些我们想要的处理方式。

上面仅仅是对代码转换过程的一个简单的模拟,而实际开发过程中我们可以借助封装好的工具去完成上面的事情。看到这大家是不是也跃跃欲试尝试着自己去转换一番代码。其实随着大家对 AST 这个方向去研究,就会发现 Vue React Babel ESlint Webpack 中的 Loader、代码对比工具中都有 AST 的影子。AST 这种对文件分析的方式其实就在我们身边。

转换实现方案

在写这个插件之初,我在 NPM 库中找了很多的成熟的包,这里我列举两种实现方案,仅供大家参考。

方案一
const parser = require("@babel/parser");
const remark = require("remark");
const guide = require("remark-preset-lint-md-style-guide");
const html = require("remark-html");
getAst = (path) => {
    // 读取入口文件
    const content = fs.readFileSync(path, "utf-8");
    remark()
        .use(guide)
        .use(html)
        .process(content, function (err, file) {
            console.log(String(file));
        });
};
getAst("./src/test.md");

转换结果如下:

<h2>
    mdtoVue 代码转换测试
</h2>
<pre>
    <code class="language-js">
        var a = 0; var b = 2;
    </code>
</pre>
方案二

使用插件 marked

下载

npm i marked -D

使用

const fs = require("fs");
const marked = require("marked");
test = (path) => {
    const content = fs.readFileSync(path, "utf-8");
    const html = marked(content);
    console.log(html);
};
test("./src/test.md");

输出结果:

<h2 id="mdtovue-代码转换测试">mdtoVue 代码转换测试</h2>
<pre><code class="language-js">var a = 0;
var b = 2;</code></pre>

最终我选择的是方案二 ,因为只需要 marked(content) 就完成了,至于内部是怎么处理的,我们不用去理会。

等等还没结束,我们的插件莫非就这么简单?当然不是了,大家喝口水慢慢看下去哈~

选定了转换工具我们还需要去定制化其中的一些内容,例如我们需要在里面加个二维码,加个书签目录,一般的网站都会有这类的需求,那么具体如何做到的呢?各位观众请往下看~

控制 marked 转换输出结果

这里我们拿网站中二维码展示这个功能举例:marked 暴露出了一个叫 rendererMd 的属性,我们通过这个属性就可以处理 marked 转换之后代码的结果。

_that.rendererMd.heading = function (text, level) {
    const headcode = `<i class="qrcode"><a :href="demourl">
                             <span>请使用手机扫码体验</span>
                             <img :src="codeurl" alt=""></a>
                          </i>`;
    const codeHead = `<h1>` + text + headcode + `</h1>`;

    if (_that.options.hasMarkList && _that.options.needCode) {
        if (level == 1) {
            return codeHead;
        } else if (level == 2) {
            return maskIdHead;
        } else {
            return normal;
        }
    }
};

从上面的代码中我们可以了解 rendererMd 是一个对象,其中就是 AST中的 Type ,例如:headingcode 等等。可以通过一个 fn ,它接受两个参数 text 内容和 level 就是 depth 大家可以看看文章前面的 AST 处理结果。通过改变 marked 转换的内容,我们可以把每个组件文档开头二维码的 HTML 结构插入到转换结果中去 ,在把上面的转换的结果在拼接成一个 .vue 文件 就像下面这样:

write(param){
    const _that = this;
    return new Promise((resolve, reject) => {
        const outPath = path.join(param.outsrc, param.name);
        const contexts =
            `<template>
                            <div  @click="dsCode">
                            <div v-if="content" class="layer">
                            <pre><span class="close-box" @click="closelayer"></span><div v-html="content"></div></pre>
                            </div>` +
            param.html +
            (_that.options.hasMarkList
                ? '<ul class="markList">' + _that.Articlehead + "</ul>"
                : "") +
            `<nut-backtop :right="50" :bottom="50"></nut-backtop>
                            </div>
                        </template><script>import root from '../root.js';
        export default {
            mixins:[root]
        }</script>`;
        _that.Articlehead = "";
        _that.Articleheadcount = 0;
        fs.writeFile(outPath, contexts, "utf8", (err, res) => {});
    });
}

上面的整个过程我们做了 3 件事:

  1. 通过 mark.md 文件 转换成了 HTML 语言,并插入了我们想要定制化的代码结构。
  2. 把 HTML 文本插入到了一个通用的 vue 模版里面。
  3. 通过 fs.writeFile 生成一个新的 .vue 文件 。

这就完成了我们 .md 转 .vue 转换的第一个功能,把 MD 语言转换成 Vue语言。

转换流程优化

下面的内容比较枯燥无味,不过它却是这个插件中不可或缺的部分,没有它整个转换过程将会变得奇慢无比。

有了上面的基础,接下来我们就需要借助 Node 去进行文件的读写了,其实作为一个前端开发人员,我对于这块的掌握开始是 0 ,不过凭借着看过代码无数,心中自然有数的看片定律,通过 Node 官方文档的学习,我把 get 到的知识分享给大家,接下来献丑了。

老样子,开车之前先找路,理清思路,事半功倍!

  1. 借助 Node 去寻找 .md 的文件。
  2. 把所有的文件路径保存并增加历史记录这里我们是通过 hash 来记录历史的。

要求就是不管这个 .md 文件放在什么地方,我们都需要它找出并解析出来。而且这个速度要快,毕竟时间就是生命。我当时首先考虑的就是一次性抓取路径并存储,再次执行的时候通过 hash 对比添加。具体思路我们看下面的流程:

这样的好处就是只有当文件有变动的时候才会再次执行转换,如果文件没有变动我们就没有必要去一遍遍的执行了。代码如下:

const { hashElement } = require("folder-hash");
hashElement(_that.options.entry, {
    folders: { exclude: [".*", "node_modules", "test_coverage"] },
    files: { include: ["*.md"] },
    matchBasename: true
}).then((res) => {});

它的返回 res 结构如下 :

{
    name: ".",
    hash: "YZOrKDx9LCLd8X39PoFTflXGpRU=",
    children: [
        {
            name: "examples",
            hash: "aG8wg8np5SGddTnw1ex74PC9EnM=",
            children: [
                {
                    name: "readme-example1.js",
                    hash: "Xlw8S2iomJWbxOJmmDBnKcauyQ8="
                },
                {
                    name: "readme-with-callbacks.js",
                    hash: "ybvTHLCQBvWHeKZtGYZK7+6VPUw="
                },
                {
                    name: "readme-with-promises.js",
                    hash: "43i9tE0kSFyJYd9J2O0nkKC+tmI="
                },
                { name: "sample.js", hash: "PRTD9nsZw3l73O/w5B2FH2qniFk=" }
            ]
        },
        { name: "index.js", hash: "kQQWXdgKuGfBf7ND3rxjThTLVNA=" },
        { name: "package.json", hash: "w7F0S11l6VefDknvmIy8jmKx+Ng=" },
        {
            name: "test",
            hash: "H5x0JDoV7dEGxI65e8IsencDZ1A=,",
            children: [
                { name: "parameters.js", hash: "3gCEobqzHGzQiHmCDe5yX8weq7M=" },
                { name: "test.js", hash: "kg7p8lbaVf1CPtWLAIvkHkdu1oo=" }
            ]
        }
    ]
};

我们只需要一个递归把整个结构处理成一个文件路径映射就完成了 hash 的提取工作

const fileHash = {};
const disfile = (res, outpath) => {
    if (res.children) {
        disfile(res.children, res.name);
    }
    fileHash[res.name + outpath] = res.hash;
};
disfile(obj, "");

而最终我们得到的是一个有完整的路径和对应 hash 的对象:

{
    "./src/test.md": "3gCEobqzHGzQiHmCDe5yX8weq7M",
    "./src/tes2.md": "3gCEobqzHGzQiHmCDe5yX8weq7M",
    "./src/test/tes2.md": "3gCEobqzHGzQiHmCDe5yX8weq7M"
}

我们把这个对象通过 Node 的 fs 去保存到一个 cache 文件中。可以使用 fs.writeFile 把文件写进去。
这里的路径主要是方便使用 fs.readFile 去获取文件的内容进行转换。

从流程图中我们看到有了这一步之后,再次执行的时候,我们只要对比下文件的 hash 和历史 hash 有没有变化就行了,如果没有变化就可以跳过剩下的过程,这就节约了很多时间,提高了转换效率。
对比 hash 的代码我们把它放到了一个新的 js 文件。

实时编译的实现

我们在写文档的过程中往往习惯边看边写,这里就需要有实时编译的功能。这个功能看起来很难实际上实现起来却不难:

filelisten() {
  const _that = this;
  const watcher = Chokidar.watch(path, {
    persistent: true,
    usePolling: true
  });
  const log = console.dir.bind(console);
  const watchAction = function ({ event, eventPath }) {
    // 这里进行文件更改后的操作
    if (/\.md$/.test(eventPath)) {
      _that.vueDesWrite(eventPath);
    }
  };
  watcher
    .on("change", (path) =>
        watchAction({ event: "change", eventPath: path })
       )
    .on("unlink", (path) =>
        watchAction({ event: "remove", eventPath: path })
       );
}

核心方法就是 Chokidar.watch 当我们检测到有文件变动了就通过我定义的转换器把文件转换一次。

但是在写这篇文章的时候我脑洞大开,有了一个新的方案:

首先,Chokidar.watch 监听的文件越多,越会影响性能,其次,每次改变一个字符,整个文件就会重新编译一次。如果我们能够明确 path 只监听当前编辑的文件,那么性能无疑会提升很多。

其次,就是编译转换,这里应该要使用热更新原理,类似于 Vnode 的实现方案只去更新变动的节点。目前我没有发现市面上存在现成的工具包,期待有志之士来实现这样的工具了

Webpack 插件开发

所有的功能都实现之后我们需要把我们的代码和 Webpack 融合,也就是写成 Webpack 插件的形式。那么 Webpack 的插件开发有什么要注意的呢?

插件开发关键点

其实插件的开发非常简单,只需要注意要定义一个 Apply 去用来监听 Webpack 的各种事件。

MyPlugin.prototype.apply = function(compiler) {}

这个功能主要通过 Compiler 来实现的, Compiler 就是 Webpack 编译器的引用。通过 Compiler 可以实现对 Webpack 的监听:

Webpack 开始编译时候

apply(compiler) {
  compiler.plugin("compile", function (params) {
    console.log("The compiler is starting to compile...");
  });
}

Webpack 编译生成最终资源的时候

apply(compiler) {
   compiler.plugin("emit", function(compilation, callback) { }
}

其实 Webpack 在编译的过程中还会有很多节点,我们都可以通过这个方法去监听 Webpack 。在调用这个方法的时候还可以通过 Compilation 去对编译的对象引用监听。看到这里,不少人会搞晕 CompilerCompilation ,其实它们很好区分:

  • Compiler 代表编译器实体,主要就是编译器上的回调事件。
  • Compilation 代表编译过程也就是我们在编译器中定义的进程

例如:

// compilation('编译器'对'编译ing'这个事件的监听)
compiler.plugin("compilation", function(compilation) {
  console.log("The compiler is starting a new compilation...");
  // 在compilation事件监听中,我们可以访问compilation引用,它是一个代表编译过程的对象引用
  // 我们一定要区分compiler和compilation,一个代表编译器实体,另一个代表编译过程
  // optimize('编译过程'对'优化文件'这个事件的监听)
  compilation.plugin("optimize", function() {
    console.log("The compilation is starting to optimize files...");
  });
});

我们在下面会有详细介绍,最终在文件的结尾我们通过

 module.export = MyPlugin;

把整个函数导出就可以了。

如何介入 Webpack 流程

简单的了解完 Webpack 的插件开发,我们还需要知道 Webpack 的处理流程,因为我们的这个插件需要一个合适的时机进入。这里就是在 Webpack 开始执行就去处理,因为我们转换的产物不是最终的 HTML 而是 Vue 它还需要 Webpack 去处理。

我们希望可以整个过程可以按照下面的流程去实现:

这样做的目的就是希望性能更好,用起来更方便!

所以我们需要简单的了解下 Webpack 的插件机制,这对我们整个功能的开发有着重要的意义,当插件出现问题我们可能够快速的定位。

通过上面这张图我们看到, MD 转 Vue 一定要是同步执行,这里是一个关键,只有当我们把所有的 .md 转换成 .vue 才能在让 Webpack 进行下面的工作。

而 Webpack 本质上是一种串行事件流的机制,它的工作流程就是将各个插件串联起来

实现这一切的核心就是 Tapable。

Tapable

Tapable 是一个类似于 nodejsEventEmitter 的库, 主要是控制钩子函数的发布与订阅。当然,Tapable 提供的 hook 机制比较全面,分为同步和异步两个大类(异步中又区分异步并行和异步串行),而根据事件执行的终止条件的不同,由衍生出了 Bail/Waterfall/Loop 类型。

Webpack 中许多对象扩展自 Tapable 类。Tapable 类暴露了 tap、tapAsync 和 tapPromise 方法,可以根据钩子的同步/异步方式来选择一个函数注入逻辑。

  • tap 同步钩子,同步钩子在使用时不可以包含异步调用。
  • tapAsync 异步钩子,通过 callback 回调告诉 Webpack 异步执行完毕
  • tapPromise 异步钩子,返回一个 Promise 告诉 Webpack 异步执行完毕

什么是 Compiler

compiler 对象代表了完整的 Webpack 环境配置。这个对象在启动 Webpack 时被一次性建立,并配置好所有可操作的设置,包括 optionsloaderplugin。当在 Webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用 compiler 来访问 Webpack 的主环境。它内部实现大体上如下:

class Compiler extends Tapable {
  constructor(context) {
    super();
    this.hooks = {
      /** @type {SyncBailHook<Compilation>} */
      shouldEmit: new SyncBailHook(["compilation"]),
      /** @type {AsyncSeriesHook<Stats>} */
      done: new AsyncSeriesHook(["stats"]),
      /** @type {AsyncSeriesHook<>} */
      additionalPass: new AsyncSeriesHook([]),
      /** @type {AsyncSeriesHook<Compiler>} */
      ......
      ......
      some code
    };
    ......
    ......
    some code
}

可以看到, Compier 继承了 Tapable, 并且在实例上绑定了一个 hook 对象, 使得 Compier 的实例 compier 可以像这样使用

compiler.hooks.compile.tapAsync(
    "afterCompile",
    (compilation, callback) => {
        console.log("This is an example plugin!");
        console.log(
            "Here’s the `compilation` object which represents a single build of assets:",
            compilation
        );
        // 使用 webpack  提供的 plugin API 操作构建结果
        compilation.addModule(/* ... */);
        callback();
    }
);

compiler 对象是 Webpack 的编译器对象,Webpack 的核心就是编译器。

什么是 Compilation

compilation 对象代表了一次资源版本构建。当运行 Webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。它内部实现大体上如下:

class Compilation extends Tapable {
    /**
     * Creates an instance of Compilation.
     * @param {Compiler} compiler the compiler which created the compilation
     */
    constructor(compiler) {
        super();
        this.hooks = {
            /** @type {SyncHook<Module>} */
            buildModule: new SyncHook(["module"]),
            /** @type {SyncHook<Module>} */
            rebuildModule: new SyncHook(["module"]),
            /** @type {SyncHook<Module, Error>} */
            failedModule: new SyncHook(["module", "error"]),
            /** @type {SyncHook<Module>} */
            succeedModule: new SyncHook(["module"]),
            /** @type {SyncHook<Dependency, string>} */
            addEntry: new SyncHook(["entry", "name"]),
            /** @type {SyncHook<Dependency, string, Error>} */
        }
    }
}

简单来说就是 compilation 对象负责生成编译资源。
下面我们在介绍下 Webpack 中 compilercompilation 一些比较重要的事件钩子。

Compiler:

事件钩子 触发时机 参数 类型
entry-option 初始化 option - SyncBailHook
run 开始编译 compiler AsyncSeriesHook
compile 真正开始的编译,在创建 compilation 对象之前 compilation SyncHook
compilation 生成好了 compilation 对象,可以操作这个对象啦 compilation SyncHook
make 从 entry 开始递归分析依赖,准备对每个模块进行 build compilation AsyncParallelHook
after-compile 编译 build 过程结束 compilation AsyncSeriesHook
emit 在将内存中 assets 内容写到磁盘文件夹之前 compilation AsyncSeriesHook
after-emit 在将内存中 assets 内容写到磁盘文件夹之后 compilation AsyncSeriesHook
done 完成所有的编译过程 stats AsyncSeriesHook
failed 编译失败的时候 error SyncHook

Compilation:

事件钩子 触发时机 参数 类型
normal-module-loader 普通模块 loader,真正(一个接一个地)加载模块图(graph)中所有模块的函数。 loaderContext module SyncHook
seal 编译(compilation)停止接收新模块时触发。 - SyncHook
optimize 优化阶段开始时触发。 - SyncHook
optimize-modules 模块的优化 modules SyncBailHook
optimize-chunks 优化 chunk chunks SyncBailHook
additional-assets 为编译(compilation)创建附加资源(asset)。 - AsyncSeriesHook
optimize-chunk-assets 优化所有 chunk 资源(asset)。 chunks AsyncSeriesHook
optimize-assets 优化存储在 compilation.assets 中的所有资源(asset) assets AsyncSeriesHook

可以看到其实就是在 apply 中传入一个 Compiler 实例然后基于该实例注册事件, Compilation 同理, 最后 Webpack 会在各流程执行 call 方法。

其语法是

compileer.hooks.阶段.tap函数('插件名称', (阶段回调参数) => {
  
});

例如:

compiler.hooks.run.tap(pluginName, compilation=>{
           console.log('webpack 构建过程开始'); 
});

我们在 Node 中运行 Webpack 之后就可以看到:

$webpack  ..config webpack .dev.js
webpack  构建开始
hash:f12203213123123123
.....
Done in 4.1s~~~~

我们也可以监听一些 Webpack 定义好的事件,如下。

compiler.plugin('complie', params => {
  console.log('我是同步钩子')
});

总结下上面的内容 Webpack 有很多事件节点,而我们的插件通过在 apply 中就可以监听 Webpack 的过程。在适当的时机插入进去执行想要的事情。

前端功能

一路走来,我们终于把 .md 转换成了 .vue 这种组件的形式,接下来主要是 Vue 方面的开发了,终于到了前端该做的事情了。

路由处理

我们的单页面应用离不开路由,那么我们是怎么管理的呢?
一张分布图,带你了解 NutUI 的结构

上面是我们的主要目录,通过 MD 转 Vue 把 .md 转换的 .vue 文件全部放到
view 这个文件下。把我们 引言等 .md 转换放到 page 里面去。其实这么做主要是为了管理员对它们的区分。

那么我们的 router 怎么管理呢,首先我们的项目在创建时候就会有一个 json
文件里面主要记录组件的一些信息

"sorts": [
    "数据展示",
    "数据录入",
    "操作反馈",
    "导航组件",
    "布局组件",
    "基础组件",
    "业务组件"
  ],
  "packages": [
    {
      "name": "Cell",
      "version": "1.0.0",
      "sort": "4",
      "chnName": "列表项",
      "type": "component",
      "showDemo": true,
      "desc": "列表项,可组合成列表",
      "author": "Frans"
    }
    ]

接下来只要把它和我们定好的目录结合起来就行了。

const routes = [];
list.map((item) => {
    if (item.showDemo === false) return;
    const pkgName = item.name.toLowerCase();
    // 随着转换我们的路径已经可以确定了
    routes.push({
        path: "/" + item.name,
        components: {
            default: Index,
            main: () => import("./view/" + pkgName + ".vue")
        },
        name: item.name
    });
});
const router = new VueRouter({
    routes
});
Vue.use(vueg, router, options);
export default router;

我们的网站还有全屏和复制功能,这些对于一个 Vue 项目来说就在简单不过了,我就不在具体的描述了,只有把每个组件的说明文档通过 mixins 把它们写在一个 js 文件中然后混入就行了。

总结

文章到这就结束了,本文主要介绍了 MD 格式转 Vue 的实现,最终一键生成官网网页。而我们对技术领域的探索并没有结束,通过总结规划,寻找更快的解决方案,是我们每一个开发者对自己领域的执着。NutUI 在未来也会随着使用者的反馈去修改自身的不足,争取让其用户体验更加的优秀,在前端组件库丰富的时代走出一条自己的道路。前路漫漫,我们大家一起去探究吧!


京东设计中心JDC
696 声望1k 粉丝

致力为京东零售消费者提供完美的购物体验。以京东零售体验设计为核心,为京东集团各业务条线提供设计支持, 包括线上基础产品体验设计、营销活动体验设计、品牌创意设计、新媒体传播设计、内外部系统产品设计、企...