圖解 VueLoader : .vue 文件是如何被打包的?

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"一、整體概述"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用過 Vue 的同學,對於 .vue 單文件文件組件類型的文件(下文簡稱 "},{"type":"text","marks":[{"type":"strong"}],"text":"SFC"},{"type":"text","text":")應該不會陌生。SFC 文件需要通過構建工具(本文以 Webpack4 爲例)打包成一個 Bundle,才能被識別和使用。那麼這中間經歷了什麼、不同的代碼塊是如何被其他規則識別的、最終生成了什麼?帶着這些問題,且看下文一一道來。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/11\/11a8470319adcf582e884e509fb3f0de.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"▲圖1. SFC 經過 Webpack 打包後的產物是什麼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1. SFC的輸入和輸出"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Webpack 需要增加 vue-loader 【1】和 vueLoaderPlugin 對 SFC 進行支持。我們首先聚焦到 vue-loader 的代碼:入口文件爲 lib\/index.js ,入參 source 是 SFC 源碼,經過處理邏輯後,輸出 export default 的代碼字符串。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"【1】:vue-loader:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic"}],"text":"https:\/\/github.com\/vuejs\/vue-loader\/blob\/master\/lib\/index.js#L32"}]},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\n\/\/ vue-loader lib\/index.js\n\n\/\/ source 是我們寫的 SFC 源碼\nmodule.exports = function (source) {\n ...\n \/\/ 返回值 code 是一段 ESModule 代碼字符串\n let code = `...`;\n code += `\\n export default component.exports`;\n return code;\n};"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用實際的例子操作一下, demo.vue 經過運行後得到下圖的輸出。經過觀察可以發現,原有的 template ,變成了 import ... from '.\/demo.vue?vue&type=template' ,其他代碼塊也發生了類似的變化。 ?vue&type=template 新增的 vue 和 type 參數的作用是什麼?我們繼續往下看。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/bd\/bd1c28e3b1d9165dd74ce4e28f834bf9.png","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"▲圖2. SFC 被 vue-loader 轉化後的結果"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2. template、script、style 代碼塊切分"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上個小節中,可以看到 template、script、style 代碼塊在輸出結果中已經轉化爲對應的 import 邏輯。這一步是 vue-loader 調用了 @vue\/component-compiler-utils 的 parse 函數進行解析後,分別生成了對應的 import 邏輯,相關源碼如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\n\/\/ vue-loader lib\/index.js\nconst { parse } = require('@vue\/component-compiler-utils');\n\nmodule.exports = function (source) {\n\n \/\/ 解析源碼,得到描述符\n const descriptor = parse({ source, ... });\n\n \/\/ 如果 template 塊存在\n if (descriptor.template) { ... }\n \/\/ 如果 script 塊存在\n if (descriptor.script) { ... }\n \/\/ 如果 style 塊存在(支持多 style 塊)\n if (descriptor.styles.length) { ... }\n \/\/ Vue 還支持自定義塊\n if (descriptor.customBlocks && descriptor.customBlocks.length) { ... }\n\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下圖是 demo.vue 被轉化的流程圖解:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/26\/26244a7773393f36aa8f278a7cd89d28.png","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"▲圖3. 各個代碼塊被分別轉化爲相應的 import 邏輯"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"二、VueLoaderPlugin 的作用"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面的章節留了一個疑問, ?vue&type=template 的作用是什麼?可以從 VueLoaderPlugin 【2】中找出答案,首先我們先了解 Webpack 中的 Plugin 能做什麼。"}]},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"【2】:VueLoaderPlugin:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic"}],"text":"https:\/\/github.com\/vuejs\/vue-loader\/blob\/master\/lib\/plugin-webpack4.js"}]},{"type":"horizontalrule"},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Plugin 的特性"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Plugin 的作用,主要有以下兩條:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"能夠 hook 到在每個編譯(compilation)中觸發的所有關鍵事件。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在插件實例的 apply 方法中,"},{"type":"text","marks":[{"type":"strong"}],"text":"可以通過 compiler.options 獲取 Webpack 配置,並進行修改"},{"type":"text","text":"。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"VueLoaderPlugin 通過第二個特性,在初始化階段,對 module.rules 進行動態修改。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"VueLoaderPlugin 預處理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"VueLoaderPlugin 的處理流程中,修改了 module.rules,在原來的基礎上加入了 pitcher 和 cloneRules 。這一步的作用是:新增的 rule ,能識別形如 ?vue&type=template 的 querystring,讓不同語言的代碼塊匹配到對應的 rule。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\nclass VueLoaderPlugin {\n apply (compiler) {\n \/\/ 標識 VueLoaderPlugin 已被加載\n \/\/ 可用於 vue-loader 運行時,檢測 VueLoaderPlugin 的加載信息\n if (webpack4) { ... } esle { ... }\n\n \/\/ 重頭戲,對 Webpack 配置進行修改\n const rawRules = compiler.options.module.rules;\n const { rules } = new RuleSet(rawRules);\n ...\n \/\/ 替換初始 module.rules,在原有 rule 上,增加 pitcher、clonedRules\n compiler.options.module.rules = [\n pitcher,\n ...clonedRules,\n ...rules\n ];\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":"br"}},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"【3】:對資源的 querystring 進行匹配:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic"}],"text":"https:\/\/v4.webpack.docschina.org\/configuration\/module\/#rule-resourcequery"}]},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/f3\/f34191f0127855024665a25c055a3c96.png","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"▲圖4. VueLoaderPlugin 對 module.rules 的修改"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"三、回到 Loader"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上一節梳理了 VueLoaderPlugin 在初始化階段的預處理,這一節我們繼續回到構建階段中,看看以 VueLoader 爲中心如何協調其它 Loader ,得到每個代碼塊的構建結果。同樣地,我們先了解一下 Webpack 的 loader 特性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"Webpack 的 loader 運行順序"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於 loader ,我們知道它們的執行是有順序的,如果是這樣的配置,運行的順序將是 c-loader -> b-loader -> a-loader。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"module.exports = {\n module: {\n rules: [{\n ...\n use: ['a-loader', 'b-loader', 'c-loader'],\n }],\n },\n};"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不過,在實際(從右到左)執行 loader 之前,會先從左到右調用 loader 上的 pitch 方法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"|- a-loader `pitch`\n |- b-loader `pitch`\n |- c-loader `pitch`\n |- requested module is picked up as a dependency\n |- c-loader normal execution\n |- b-loader normal execution\n|- a-loader normal execution"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"並且在 loader 的 pitch 方法中,如果有實際的返回值,將會跳過後續的 loader,比如在 b-loader 的 pitch 中,如果返回了實際值,將會產生下面的執行順序。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\n# 注意 a-loader 依然會正常執行,跳過的是 c-loader\n|- a-loader `pitch`\n |- b-loader `pitch` returns a module\n|- a-loader normal execution"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"知道這個特性,有利於我們理解 SFC 中各代碼塊在 loader 中的處理順序。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2. SFC 轉化流程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"還記得第一節生成的編譯結果嗎?每個代碼塊都導出了對應邏輯,我們以 script 塊爲例,結合第二節的 PitcherLoader 再次進行轉化,轉化後的結果爲:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/49\/499f55f9320a332929ebf90dec26d66b.png","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"▲圖5. script 塊的轉化流程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後的 import 語句,使用了內聯方式的 import 語法【4】,我們拆分一下便於理解。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"【4】:import 語法:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic"}],"text":"https:\/\/webpack.docschina.org\/concepts\/loaders\/#inline"}]},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"# 原import\n-!..\/..\/node_modules\/babel-loader\/lib\/index.js??ref--2-\n0!..\/..\/node_modules\/vue-loader\/lib\/index.js??vue-loader-options!.\/demo.vue?\nvue&type=script&lang=js&\";\n\n# Part1 -! \n# 將禁用所有已配置的 preLoader 和 loader,但是不禁用 postLoaders\n\n# Part2 ..\/..\/node_modules\/babel-loader\/lib\/index.js??ref--2-0\n# 參考小節:VueLoaderPlugin 的預處理,demo.vue 會自動添加 .js 後綴,以匹配所有 js 的 Rule,這裏使用 babel-loader 處理 js 模塊\n\n# Part3 ..\/..\/node_modules\/vue-loader\/lib\/index.js??vue-loader-options \n# \/\\.vue$\/ 規則匹配到 demo.vue,並使用 vue-loader 處理 .vue 後綴\n\n# Part4 .\/demo.vue?vue&type=script&lang=js&"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3. PitchLoader"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述的轉化發生在 PitchLoader 中,對 PitchLoader 的實現邏輯感興趣的同學,可以閱讀 loader\/pitcher.js 的源碼:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ vue-loader lib\/loaders\/pitcher.js\n\n\/\/ PitcherLoader.pitch 方法,所有帶 ?vue 的模塊請求,都會走到這裏\nmodule.exports.pitch = function (remainingRequest) {\n \/\/ 如 .\/demo?vue&type=script&lang=js\n \/\/ 此時,loaders 是所有能處理 .vue 和 .xxx 的 loader 列表\n let loaders = this.loaders;\n ...\n \/\/ 得到 -!babel-loader!vue-loader!\n const genRequest = loaders => { ... };\n\n \/\/ 處理 style 塊 和 template 塊,支持\n if (query.type === 'style') { ... }\n if (query.type === 'template') { ... }\n\n \/\/ 處理 script 塊和 custom 塊\n return `import mod from ${request}; export default mod; export * from ${request}`;\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4. 再次執行 VueLoader"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"細心的同學可能發現了,在 PitchLoader 的轉化結果中,還是會以 vue-loader 作爲第一個處理的 loader,但 vue-loader 不是一開始就轉化過了嗎 ?與第一次不同的是,這次 vue-loader 的作用,僅僅是"},{"type":"text","marks":[{"type":"strong"}],"text":"把 SFC 中語法塊的源碼提取出來,並交給後面的 loader 進行處理"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ec\/ec40d37782beaef834e24446c9c29912.png","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"▲圖6. 第二次進入 vue-loader"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"\n\/\/ vue-loader lib\/index.js\nconst { parse } = require('@vue\/component-compiler-utils');\n\nmodule.exports = function (source) {\n \/\/ 如果querystring 包含了 type 參數,則直接返回該塊的代碼\n if (incomingQuery.type) {\n return selectBlock( ... );\n }\n};\n\n\/\/ vue-loader lib\/select.js\nmodule.exports = function selectBlock (...) {\n \/\/ 選擇不同語法塊的內容進行返回\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"至此,vue-loader 裏面的處理邏輯基本已經梳理完成。各部分代碼塊也傳入後續的 loader 中進行解析和轉化。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"四、總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們再用一張完整的處理流程圖總結一下 SFC 構建流程吧:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/5e\/5eebb6837a1140bda0f3dcc04e550d70.png","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"頭圖:Unsplash"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作者:尹佳"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文:https:\/\/mp.weixin.qq.com\/s\/FJzDRLchG_DWA80Wp141Vg"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文:圖解 VueLoader : .vue 文件是如何被打包的?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"來源:雲加社區 - 微信公衆號 [ID:QcloudCommunity]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"轉載:著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章