NutUI 是一款很是優秀的移動端組件庫, GitHub 上已得到 1.8k 的 star,NPM 下載量超過 13k。公司內部已賦能支持 40+ 個項目,外部接入使用項目達到 20+ 個。使用者將會得到以下收益:css
NutUI 的歷史已經有 2 年了,2017 年的版本是 v1.0 ,那是一個造輪子和摸索的過程。最開始的組件大都是來源於業務,項目中通過抽離封裝作成的組件。html
那仍是一我的人均可以提交組件的年代,當時最樸素的一個想法就是複用,先把業務中組件數量積累下來。好比一個地址配送組件一個同窗開發花了2-3人日,在另外一個購物場景項目中也會有,若是每人都去開發一個必然浪費。最先組件庫僅僅是在公司內部使用,那時候的 NutUi是下面這個樣子:前端
早期的首頁:vue
早期的文檔頁:node
經過上面兩張圖咱們能夠看到,當初的網站有不少不足乃至成爲痛點,具體表現如下幾點:webpack
針對這些痛點 2.0 做出以下改變:git
對比 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 的官方網站的需求是什麼呢?
瞭解了具體需求,下面就能夠開發功能了,其中最重要的就是選擇一條對的路。
這裏我選擇的是經過 .md
轉換成 .vue
,爲何呢?
.vue
模版開發正好能夠把 .md
轉換過來的 HTML 直接嵌套到 template 中。咱們的 Style 不多使用 Class ,而是基於標籤選擇去作樣式處理:
h1,h2,p { color: #333333; } h1 { font-size: 30px; font-weight: 700; margin: 10px 0 20px; }
這樣的好處就是咱們在編寫文檔時不用去關心這些,只須要記住簡單的幾個 MD 語法就能夠寫出一篇相對完整的文檔。
基本書寫格式以下:
首先感謝 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
,其中有兩個屬性分別是 enter
和 leave
。它們分別表明監聽遍歷的進入階段和離開階段。一般咱們只須要定義 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
暴露出了一個叫 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 ,例如:heading
、code
等等。能夠經過一個 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 件事:
mark
把 .md
文件 轉換成了 HTML 語言,並插入了咱們想要定製化的代碼結構。fs.writeFile
生成一個新的 .vue
文件 。這就完成了咱們 .md 轉 .vue
轉換的第一個功能,把 MD 語言轉換成 Vue語言。
下面的內容比較枯燥無味,不過它倒是這個插件中不可或缺的部分,沒有它整個轉換過程將會變得奇慢無比。
有了上面的基礎,接下來咱們就須要藉助 Node 去進行文件的讀寫了,其實做爲一個前端開發人員,我對於這塊的掌握開始是 0 ,不過憑藉着看過代碼無數,心中天然有數的看片定律,經過 Node 官方文檔的學習,我把 get 到的知識分享給你們,接下來獻醜了。
老樣子,開車以前先找路,理清思路,事半功倍!
.md
的文件。要求就是無論這個 .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 的插件開發有什麼要注意的呢?
其實插件的開發很是簡單,只須要注意要定義一個 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
去對編譯的對象引用監聽。看到這裏,很多人會搞暈 Compiler
和 Compilation
,其實它們很好區分:
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 開始執行就去處理,由於咱們轉換的產物不是最終的 HTML 而是 Vue 它還須要 Webpack 去處理。
咱們但願能夠整個過程能夠按照下面的流程去實現:
這樣作的目的就是但願性能更好,用起來更方便!
因此咱們須要簡單的瞭解下 Webpack 的插件機制,這對咱們整個功能的開發有着重要的意義,當插件出現問題咱們可可以快速的定位。
經過上面這張圖咱們看到, MD 轉 Vue 必定要是同步執行,這裏是一個關鍵,只有當咱們把全部的 .md
轉換成 .vue
才能在讓 Webpack 進行下面的工做。
而 Webpack 本質上是一種串行事件流的機制,它的工做流程就是將各個插件串聯起來
實現這一切的核心就是 Tapable。
Tapable
是一個相似於 nodejs
的 EventEmitter
的庫, 主要是控制鉤子函數的發佈與訂閱。固然,Tapable
提供的 hook
機制比較全面,分爲同步和異步兩個大類(異步中又區分異步並行和異步串行),而根據事件執行的終止條件的不一樣,由衍生出了 Bail/Waterfall/Loop 類型。
Webpack 中許多對象擴展自 Tapable
類。Tapable
類暴露了 tap、tapAsync 和 tapPromise 方法,能夠根據鉤子的同步/異步方式來選擇一個函數注入邏輯。
compiler
對象表明了完整的 Webpack 環境配置。這個對象在啓動 Webpack 時被一次性創建,並配置好全部可操做的設置,包括 options
,loader
和 plugin
。當在 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
對象表明了一次資源版本構建。當運行 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 中 compiler
和 compilation
一些比較重要的事件鉤子。
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 在將來也會隨着使用者的反饋去修改自身的不足,爭取讓其用戶體驗更加的優秀,在前端組件庫豐富的時代走出一條本身的道路。前路漫漫,咱們你們一塊兒去探究吧!