工作中的很多專案都是基於 umi 開發的,所以最近學了一下 umi 的原始碼,對這個框架的好感又多了一些~。如果你也感興趣的話,歡迎跟我一起來學習or溫習一下。
這篇文章會帶你從專案執行開始切入,循序漸進地瞭解 umi 核心的部分。
我們建立好 umi 專案之後,第一步一般是使用 yarn start
命令去執行它,執行的是 umi dev
,也就是 umi 命令,所以先來看看 umi 命令是怎麼定義的。
下面提到的原始碼目錄在 umi 的原始碼倉庫 /packages 目錄下。
umi 命令
umi 命令的定義在 /umi/bin
目錄,預設執行 /umi/src/cli.ts
,邏輯是這樣的:
1. 引數規範化
使用 yargs-parser
庫處理命令列引數,處理 version
、 help
命令的邏輯。
2. 【dev】啟動新的程式
這裡說一下 dev
和 build
的區別,開發環境會額外啟動新的程式來執行服務,並做一些事件監聽、處理通訊的工作。
這部分程式碼在 /umi/src/utils/fork.ts
,核心邏輯主要有三部分:
##### 2.1 處理埠號
預設埠是 8000,如果被佔用會自動加 1。
##### 2.2 啟動新的程式
使用 child_process
的 fork
建立新的子程式,執行的是 /umi/src/forkedDev.ts
,這個檔案裡的邏輯就是下面的第3步和第4步了。
##### 2.3 處理通訊
建立好子程式之後,會監聽這個子程式的事件來傳遞訊息,這裡會處理兩種事件: RESTART
重啟 和 UPDATE_PORT
更新埠。
主程式(執行 umi 命令的當前程式)會監聽退出等事件從而 kill 掉子程式以及觸發外掛的 onExit
事件。
3. 初始化 webpack
這部分程式碼在 /umi/src/initWebpack.ts
。
初始化之前會先去配置檔案裡找有沒有 webpack5
或 mfsu
的配置,有的話初始化 webpack5,沒有就初始化 webpack4。
這裡提一個重要的點,umi 將類似 webpack 的依賴封裝在了 deps
, 之前的 《Umi 4 設計思路 》 演講裡有提到中間商的概念,umi 會做一些預打包的工作來解決版本穩定性的問題。
4. 構造 Service 物件
終於講到 umi 的核心部分了,程式碼很短但很重要。
await new Service({ ...params }).run({
name: 'dev', // or 'build'
args,
});
這裡有個特別的處理,初始化使用的 Service
做了個小小的封裝,預設內建了 preset-built-in
,這個 preset 就是 外掛的擴充套件方法 的實現部分。
import { IServiceOpts, Service as CoreService } from '@umijs/core';
class Service extends CoreService {
constructor(opts: IServiceOpts) {
...
super({
...opts,
presets: [
require.resolve('@umijs/preset-built-in'),
...(opts.presets || []),
],
plugins: [require.resolve('./plugins/umiAlias'), ...(opts.plugins || [])],
});
}
}
plugin 是 umi 設計中非常重要的部分,外掛化的思想使得 umi 可以輕鬆自如的控制流程和實現定製,這種思想有一個學術名稱 微核心架構
。
微核心架構
這部分就不展開說啦,我也是查資料摘了一些重要的點,重點了解一下核心系統的設計思想, Service
類就是 umi 的核心繫統。
核心類 Service
程式碼在 /core/src/Service/Service.ts
,這個類的結構非常簡單,只有兩部分:建構函式 和 run()
方法。
constructor 建構函式
初始化階段的主要工作是收集配置,從這裡能看出來作者是如此的心細,環境變數的優先順序,配置檔案的優先順序,就連 page(s)
目錄名這麼細節的點都安排的明明白白?。
初始化的屬性從圖裡可以清晰的看到,就不贅述了,這裡只列出來了(我覺得)重要的部分。
初始化 presets 和 plugin 的同時,會把所有外掛的資訊記錄下來,也就是 外掛登錄檔
,以便於管理和執行外掛。
run()
跑起來~
總結來說,這部分就是按生命週期前進:
- 初始化 presets 和 plugins
- 設定一些鉤子
- 最後執行
runCommand()
執行 umi 命令相關的具體邏輯
Service 有9個生命週期,作用是後續外掛內執行一些動作的判斷依據。
export enum ServiceStage {
uninitialized,
constructor,
init,
initPresets,
initPlugins,
initHooks,
pluginReady,
getConfig,
getPaths,
run,
}
presets 和 plugins 的初始化邏輯是一樣的,摘錄幾行重要的虛擬碼如下:
const api = this.getPluginAPI({ id, key, service: this })
// 獲取 PluginAPI 物件,用來傳遞給 plugin 本身,也就是外掛的實現規範
this.registerPlugin(preset)
// 外掛登錄檔,執行的程式碼是 this.plugins[preset.id] = preset
const { presets, plugins } = await this.applyAPI({ api, apply: preset.apply })
// 執行外掛內部的 apply 方法,即 return apply()(api)
PluginAPI
這個類裡定義了 Plugin的核心方法,umi 文件已經介紹的很詳細了。
applyPlugin()
外掛執行的核心方法,引數中的 type
決定了執行外掛的邏輯, add 和 modify 會新增或修改 initialValue,並在執行完後將結果返回,event 顧名思義作為事件存在。
tapable(webpack) 我也還在學習總結中,有優秀的文章歡迎推薦給我~。
runCommand()
這裡主要看一下 dev
命令相關的邏輯,程式碼在 preset-built-in/src/plugins/commands/dev/dev.ts
。
preset-built-in
這是 umi 預設的外掛集,實現的功能主要有五部分:
registorMethods
統一註冊方法。
route
routes 配置的實現。
寫臨時檔案
src/.umi
目錄裡的檔案生成過程都在這裡,包括專案執行的入口、路由、外掛等等。配置
umi 的文件 配置 裡的實現。
commands
命令具體的實現邏輯,以及 webpack 配置修改相關。
就分享到這裡吧,如有錯誤歡迎指正。