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 在將來也會隨着使用者的反饋去修改自身的不足,爭取讓其用戶體驗更加的優秀,在前端組件庫豐富的時代走出一條本身的道路。前路漫漫,咱們你們一塊兒去探究吧!

相關文章
相關標籤/搜索