關注Vue社區,回復「加群」
加入我們一起學習,天天進步
轉轉內部腳手架的 Webpack 部分,是基於 @vue/cli 進行二次封裝的。選擇二次封裝而不是自己搞一套 Webpack 配置,是為了減少維護的成本。比如最近新出的 Vue2.7 版本,如果自行維護 Webpack 配置,可能還要對 vue-loader 進行一些調整。遇到重難點問題,還需要去看 @vue/cli 的源碼作為參考,重新實現一遍它裡面的邏輯。在看任何開源庫的源碼之前,必須先了解它有哪些功能,這樣才能針對性地分模塊閱讀源碼。根據 @vue/cli 的文檔,它大體上分為兩塊功能:
文章將分為兩個部分,第一個部分是對 @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提供了三個參數。
index.js - service 部分
與generator.js類似,index.js同樣導出一個函數。
/***@type{import('@vue/cli-service').ServicePlugin}*/module.exports=functionservice(api,projectOptions){}
在使用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 create
通常使用vue create <project-name>來創建一個新的項目。
在這個流程中,最值得關注的是 @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-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/cli 源碼的一些心得。若有不足之處,歡迎在評論中指出。
❤️ 看完兩件事
如果你覺得這篇內容對你挺有啟發,我想邀請你幫我兩個小忙:
點個「在看」,讓更多的人也能看到這篇內容(喜歡不點在看,都是耍流氓 -_-)
關注公眾號「Vue社區」,每周重點攻克一個前端面試重難點,
公眾號後台回復「電子書」即可免費獲取 27本 精選的前端電子書!
