NutUI官网开发关键技术揭秘

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

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

 

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

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

早期的首页:vue

早期的文档页:node

经过上面两张图咱们能够看到,当初的网站有不少不足乃至成为痛点,具体表现如下几点:webpack

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

针对这些痛点 2.0 做出以下改变:git

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

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

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

在开发这个插件功能中,咱们还用到了一些 Node 操做,和 NPM 依赖包,它和咱们平时的前端开发有很多区别,不一样于页面的交互逻辑,我感受这更有意思,那么我们就一探究竟吧。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 在将来也会随着使用者的反馈去修改自身的不足,争取让其用户体验更加的优秀,在前端组件库丰富的时代走出一条本身的道路。前路漫漫,咱们你们一块儿去探究吧!