close

關注Vue社區,回復「加群」

加入我們一起學習,天天進步

轉轉內部腳手架的 Webpack 部分,是基於 @vue/cli 進行二次封裝的。選擇二次封裝而不是自己搞一套 Webpack 配置,是為了減少維護的成本。比如最近新出的 Vue2.7 版本,如果自行維護 Webpack 配置,可能還要對 vue-loader 進行一些調整。遇到重難點問題,還需要去看 @vue/cli 的源碼作為參考,重新實現一遍它裡面的邏輯。在看任何開源庫的源碼之前,必須先了解它有哪些功能,這樣才能針對性地分模塊閱讀源碼。根據 @vue/cli 的文檔,它大體上分為兩塊功能:

項目模板生成 npm 包@vue/cli是這個功能的入口包,提供了vue create命令生成項目模板。
開發階段功能 這個功能對應 npm 包@vue/cli-service,包含了一些開發時實用的命令。
vue-cli-service serve用於啟動開發服務器。
vue-cli-service lint命令對代碼進行 lint。
vue-cli-service inspect查看被所有 cli 插件修改後的 Webpack 配置。

文章將分為兩個部分,第一個部分是對 @vue/cli plugin 和 preset 的介紹,第二個部分是@vue/cli 的關鍵部分源碼實現,包括插件系統實現,Webpack 配置處理等內容。

plugin 插件cli 插件的組成

@vue/cli 設計了插件系統,一個插件是一個 npm 包,總共由 generator (模板) 和 service (服務) 兩個部分組成。一個簡單的插件目錄是這樣的:

.├──generator.js├──index.js├──package.json├──pnpm-lock.yaml

generator.js文件對應上文的 generator 部分,負責說明該插件希望對生成的模板做出哪些改動。index.js文件對應上文的 service 部分,可以為vue-cli-service這個主命令註冊新的副命令,或者對 @vue/cli 自帶的一些命令做出修改。

cli 生成項目模板流程

@vue/cli 在生成項目時,會在目標目錄下新建一個package.json文件,並在devDependencies中列出所有使用到的 cli 插件。此時會執行第一次npm install,來安裝 cli 插件,@vue/cli 會調用這些插件的generator.js,得到最終輸出到目標目錄的項目結構,並寫入硬盤。由於 cli 插件會向package.json中聲明一些新的依賴(比如 vue、vue-router),所以此時 @vue/cli 會執行第二次npm install,確保這些依賴被全部安裝。此時項目已經基本上創建完成,@vue/cli 調用每個插件 tempalte 部分註冊的onCreateComplete鈎子函數,執行一些項目創建完成後的邏輯。創建流程到此就結束了。

generator.js - generator 部分

接下來簡單介紹generator.js該怎麼寫,它的簽名如下:

/***@type{import('@vue/cli').GeneratorPlugin}*/module.exports=functiongenerator(api,pluginOptions,preset){//這裡寫插件的代碼}

generator.js文件的邏輯很簡單,只需要導出一個函數即可。@vue/cli 為generator.js提供了三個參數。

api 是 @vue/cli 內部一個名為 GeneratorAPI 的類的實例,提供了各種操作模板的方法。
pluginOptions @vue/cli 有一個預設的概念,預設可以指定每個 cli 插件的選項。詳見下文 preset 部分。
preset 預設對象,詳見下文 preset 部分。其中 api 這個參數最為關鍵。它是一個對象,常用的屬性和方法有:
api.extendPackage()對package.json文件進行擴展和修改。
api.render()將一個文件夾 render 到創建項目的目錄。可以簡單地理解為,將一個文件夾複製到目標文件夾中。與複製不同的是,render的對象支持 ejs 語法,可以在裡面寫部分 JS 邏輯。比如希望根據pluginOptions選項,來 render 不同的內容。

index.js - service 部分

與generator.js類似,index.js同樣導出一個函數。

/***@type{import('@vue/cli-service').ServicePlugin}*/module.exports=functionservice(api,projectOptions){}

api @vue/cli 內部名為 ServiceAPI 的類的實例。需要注意與 GeneratorAPI 進行區分。
projectOptions 即vue.config.js文件中的選項。api 參數常用的方法有:
api.configureWepback使用webpack-merge修改 Webpack 配置
api.chainConfig使用webpack-chain修改 Webpack 配置
api.registerCommand為vue-cli-service註冊新的命令。
preset 預設

在使用vue create命令創建項目時,需要使用者做出幾個選擇,包含 Vue 版本、是否使用 TS 和 Babel 等選項。這些選項會被合併成一個對象,@vue/cli 將這個對象稱為 preset。如果你曾經使用 @vue/cli 創建過項目,並選擇將選項保存為一個預設,那麼可以通過cat ~/.vuerc命令來找到保存的配置。這個配置一般長這樣:

{//是否使用淘寶源"useTaobaoRegistry":false,//使用 cli 創建項目時,使用哪個包管理器安裝依賴。"packageManager":"npm",//被保存的cli預設"presets":{"vue3-preset":{"useConfigFiles":true,//創建模板時,使用哪些 cli 插件。"plugins":{// key 為插件的名稱,value 是插件的配置。"@vue/cli-plugin-babel":{},"@vue/cli-plugin-typescript":{"classComponent":false,"useTsWithBabel":true},"@vue/cli-plugin-router":{"historyMode":false},"@vue/cli-plugin-vuex":{},"@vue/cli-plugin-eslint":{"config":"prettier","lintOn":["save"]}},//新項目使用vue2還是vue3"vueVersion":"3",//新項目使用什麼css預處理器"cssPreprocessor":"less"}},//@vue/cli的最新版本"latestVersion":"5.0.8",//上次檢查@vue/cli最新版本的時間"lastChecked":1657541617415}

如果~/.vuerc文件中保存了歷史預設,下次使用vue create時,就可以選擇這些預設,跳過一堆問題的選擇。如果希望對預設有更深入的定製,可以仿照.vuerc文件的格式,將預設的內容寫在一個 json 文件中。比如這樣一份文件:

{"useConfigFiles":true,"plugins":{//為了自定義@vue/cli而編寫的插件"@zz-common/vue-cli-plugin-zz":{"version":"^0.0.7"},"@vue/cli-plugin-babel":{},"@vue/cli-plugin-typescript":{"classComponent":false,"useTsWithBabel":true},"@vue/cli-plugin-router":{"historyMode":true},"@vue/cli-plugin-vuex":{},"@vue/cli-plugin-eslint":{"config":"prettier","lintOn":["save"]}},"vueVersion":"2","cssPreprocessor":"dart-sass"}

假設這個文件的名字是vueCliPreset.json,那麼可以通過vue create <project-name> --preset ./vueCliPreset.json命令,來使用這個預設文件,創建對應的項目模板。

@vue/cli 運行流程倉庫概覽

vue-cli 是一個基於 yarn 的 monorepo,核心包都位於packages/@vue文件夾下,包含:

@vue/cli 核心包
@vue/cli-service 核心包
@vue/cli-plugin-babel 插件
@vue/cli-plugin-typescript 插件
@vue/cli-plugin-vuex 插件
@vue/cli-plugin-router 插件 其中,@vue/cli對外暴露了vue命令,並統籌各個 cli 插件的運作,可以將其稱為入口包。它負責命令行交互、預設存取、插件統籌等工作。

@vue/cli 包含這些功能:

vue create 創建一個模板項目
vue invoke 調用某個 cli 插件的 generator 部分
vue add 添加一個 cli 插件
vue upgrade 升級一個 cli 插件
vue inspect 查看當前項目的 Webpack 配置

@vue/cli-service 包含這些功能:

vue-cli-service serve
vue-cli-service build 由於篇幅的原因,這裡僅介紹vue create和vue-cli-service build/serve這兩個核心功能。

vue create

通常使用vue create <project-name>來創建一個新的項目。

Creator 類:@vue/cli 包中使用commander這個包,聲明了create命令和對應的參數。項目創建由名為Creator的 class 負責,Creator實例中的create方法,接收了commander傳遞的所有命令行選項,進行項目的創建。
prompt: 在上文提到,vue create支持使用一個 json 文件作為預設。如果使用 json 文件預設,那麼create命令的 prompt 詢問階段會被跳過,否則會問使用者一些問題,來生成 preset 對象。
包管理器:@vue/cli 支持使用命令行參數指定包管理器。如果沒有指定,則會依次降級到.vuerc文件、yarn、pnpm、npm。另外由於創建項目的過程中,與包管理器相關的命令調用非常多,且需要抹平不同包管理器之間的區別,所以源碼中使用PackageManager這個類來封裝包管理器操作。這是值得學習的一點。
第一次安裝依賴:在一次項目創建的過程中,需要使用各種官方和非官方的插件,所以 cli 會首先根據 preset 對象中指定的插件名,來創建package.json文件,並使用PackageManager.install方法,來進行第一次安裝。
調用 cli 插件的 generator 部分:在第一次安裝完成後,所有的 cli 插件都被安裝了。@vue/cli 此時會調用所有插件的 generator 部分,生成最終需要輸出到硬盤的文件內容。
調用 hook 函數:cli 會調用插件註冊的一些函數,在項目創建完成後運行。
完成創建

在這個流程中,最值得關注的是 @vue/cli 與 cli 插件的交互部分。在一個擁有插件系統的設計中,有插件容器和插件兩個部分。容器需要將上下文內容和用戶選項,提供給插件,讓插件實現它的功能。所以於上下文和用戶選項的整合尤為關鍵。以插件的 generator 部分為例,@vue/cli 使用單獨的類GeneratorAPI,為插件提供 render 文件夾、擴展 package.json 等各種實用的功能。@vue/cli 使用 files 對象來記錄最終輸出到硬盤的文件內容,key 是文件路徑,value 是文件內容。GeneratorAPI.render作用是將插件指定的文件夾,render 到最終生成的項目中去。這個 API 實質是在讀取 render 方法指定的文件夾,使用 ejs 模板引擎處理源文件內容,並將處理後的內容記錄在 files 對象中。最後只需要根據 files 對象,將文件一一寫入硬盤即可。除了 files 對象,cli 中還有一個 pkg 對象來記錄 package.json 中的內容,當插件調用GeneratorAPI.extendPackage時,實際上是在修改 pkg 對象。之所以 pkg 不在 files 對象中,是因為 package.json 與其它文件差異較大,cli 插件需要對它有更細粒度的操作。

總結下插件的交互部分,一共有三個關鍵點:

合適的數據結構 在 @vue/cli 是兩個對象,也可視情況採用 Set Map WeakMap WeakSet 等。
操作數據結構的接口 這個情景下對應的是 GeneratorAPI,其中的方法都是在用不同的方式操作數據結構。事實上這裡有兩種選擇,一種是直接將 files 對象暴露給插件,讓插件自由發揮。優點是插件的上限更高,可以完成更複雜的功能;缺點是操作不便,files 對象的 value 是字符串,通常需要使用正則或者 ast 來操作。如果希望使用 ast,還需要根據字符串的內容,來選擇不同的 parser。另一種是對 files 對象操作的方法進行封裝,就像GeneratorAPI這個類一樣。這樣做的優點是,插件的代碼量大大下降,出 bug 的幾率更低,行為更加統一。@vue/cli 則同時採用了這兩種做法,向插件傳遞GeneratorAPI實例的同時,也暴露了 files 對象。
hook 設計 理論上來說,插件容器暴露的 hook 數量越多,插件的上限就越高。@vue/cli 中插件暴露的 hook 並不多,比如 eslint 等配置文件的轉換、項目創建結束後的 hook 等。原因之一是創建工程模板這個需求的複雜度不夠高,也就不需要過多的 hook。良好的插件容器,需要將內部所有關鍵流程的都暴露給插件,比如配置的合併策略、插件的執行順序、數據結構的便捷修改方法、原始數據結構等。

vue-cli-service build/serve

build 與 serve 的原理是類似的,它們都由@vue/cli-service這個包實現。@vue/cli-service是一個官方的 @vue/cli 插件,它通過ServiceAPI.registerCommand註冊了serve和build命令,處理 Webpack 相關的操作。這同時體現了插件系統的好處,可以將打包邏輯提取到單獨的插件中,不必與 @vue/cli 的代碼放在同一個包中。build 的主體邏輯比較簡單,加載vue.config.js文件,調用 cli 插件,得到修改後的 Webpack 配置,並使用 Webpack 進行打包。有一個點是,@vue/cli 支持 modern 模式的構建。當 modern 模式開啟時,它會進行兩次構建,第一次構建會通過 script 標籤進行模塊加載,第二次構建基於瀏覽器模塊系統(type="module" VS nomodule)。

@vue/cli 的不足之處

@vue/cli 是一個優秀的腳手架,但仍有一些令人遺憾的設計存在。比如它對 JS API 的支持度較差,配置與 vue.config.js 文件強綁定。在進行 modern 模式的打包時,它的內部使用子進程的形式,遞歸地調用自身來完成功能。這會導致通過 JS API 傳入的參數,被vue.config.js文件內容覆蓋,造成意料外的行為。

vue.config.js 不支持 ts 寫法,需要使用類型注釋,來獲得類型提示。如果希望使用 esm 格式,需要使用.mjs後綴,且通過環境變量傳入vue.config.mjs,來覆蓋默認的文件名。

另外,插件的 service 函數部分,返回的 Promise 沒有被 await。基於 Promise 的 API 都不適合在 插件的 service 部分使用,比如fs.readFilefs.writeFile,需要使用同步版本的 API 代替。如果這個部分使用 cjs 代碼編寫,依賴了一個 esm 格式的庫,那麼這個庫需要使用import()函數來導入。由於 top level await 的存在,import()函數是一個異步函數,可能導致一部分 cli 插件代碼,實際上被沒有被執行完,但 @vue/cli 卻誤認為它已經執行完了,從而產生報錯。並且報錯的信息通常和 Webpack 相關,不容易注意到這是一個異步相關的問題。

最後

儘管文章中提到了 @vue/cli 的一些設計缺陷,但多少有些吹毛求疵的成分。如果將時間倒回到 @vue/cli 被創建的時間點,這樣一個

擁有插件系統
採用了最佳實踐的同時,仍保留高度自定義 Webpack 配置的能力的腳手架,給 Vue 開發者一個方便、快捷啟動項目的途徑,已經十分優秀且值得借鑑了。

本文是筆者在實現公司內部腳手架的 Webpack 部分時,看 @vue/cli 源碼的一些心得。若有不足之處,歡迎在評論中指出。

❤️ 看完兩件事

如果你覺得這篇內容對你挺有啟發,我想邀請你幫我兩個小忙:

點個「在看」,讓更多的人也能看到這篇內容(喜歡不點在看,都是耍流氓 -_-)

關注公眾號「Vue社區」,每周重點攻克一個前端面試重難點,

公眾號後台回復「電子書」即可免費獲取 27本 精選的前端電子書!

點個在看支持我吧,轉發就更好
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()