umi3 原始碼學習

Jing發表於2022-02-10

工作中的很多專案都是基於 umi 開發的,所以最近學了一下 umi 的原始碼,對這個框架的好感又多了一些~。如果你也感興趣的話,歡迎跟我一起來學習or溫習一下。

這篇文章會帶你從專案執行開始切入,循序漸進地瞭解 umi 核心的部分。


我們建立好 umi 專案之後,第一步一般是使用 yarn start 命令去執行它,執行的是 umi dev,也就是 umi 命令,所以先來看看 umi 命令是怎麼定義的。

下面提到的原始碼目錄在 umi 的原始碼倉庫 /packages 目錄下。

umi 命令

umi 命令的定義在 /umi/bin 目錄,預設執行 /umi/src/cli.ts,邏輯是這樣的:

image.png

1. 引數規範化

使用 yargs-parser庫處理命令列引數,處理 versionhelp 命令的邏輯。

2. 【dev】啟動新的程式

這裡說一下 devbuild 的區別,開發環境會額外啟動新的程式來執行服務,並做一些事件監聽、處理通訊的工作。

這部分程式碼在 /umi/src/utils/fork.ts,核心邏輯主要有三部分:

##### 2.1 處理埠號

預設埠是 8000,如果被佔用會自動加 1。

##### 2.2 啟動新的程式

使用 child_processfork 建立新的子程式,執行的是 /umi/src/forkedDev.ts,這個檔案裡的邏輯就是下面的第3步和第4步了。

##### 2.3 處理通訊

建立好子程式之後,會監聽這個子程式的事件來傳遞訊息,這裡會處理兩種事件: RESTART 重啟 和 UPDATE_PORT 更新埠。

主程式(執行 umi 命令的當前程式)會監聽退出等事件從而 kill 掉子程式以及觸發外掛的 onExit 事件。

3. 初始化 webpack

這部分程式碼在 /umi/src/initWebpack.ts

初始化之前會先去配置檔案裡找有沒有 webpack5mfsu 的配置,有的話初始化 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 的核心繫統。

image.png

核心類 Service

程式碼在 /core/src/Service/Service.ts ,這個類的結構非常簡單,只有兩部分:建構函式 和 run() 方法。

constructor 建構函式

image.png

初始化階段的主要工作是收集配置,從這裡能看出來作者是如此的心細,環境變數的優先順序,配置檔案的優先順序,就連 page(s) 目錄名這麼細節的點都安排的明明白白?。

初始化的屬性從圖裡可以清晰的看到,就不贅述了,這裡只列出來了(我覺得)重要的部分。

初始化 presets 和 plugin 的同時,會把所有外掛的資訊記錄下來,也就是 外掛登錄檔,以便於管理和執行外掛。

run()

跑起來~

image.png

總結來說,這部分就是按生命週期前進:

  1. 初始化 presets 和 plugins
  2. 設定一些鉤子
  3. 最後執行 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 文件已經介紹的很詳細了。

image.png

applyPlugin()

外掛執行的核心方法,引數中的 type 決定了執行外掛的邏輯, add 和 modify 會新增或修改 initialValue,並在執行完後將結果返回,event 顧名思義作為事件存在。

tapable(webpack) 我也還在學習總結中,有優秀的文章歡迎推薦給我~。

image.png

runCommand()

這裡主要看一下 dev 命令相關的邏輯,程式碼在 preset-built-in/src/plugins/commands/dev/dev.ts

image.png

preset-built-in

image.png

這是 umi 預設的外掛集,實現的功能主要有五部分:

  1. registorMethods

    統一註冊方法。

  2. route

    routes 配置的實現。

  3. 寫臨時檔案

    src/.umi 目錄裡的檔案生成過程都在這裡,包括專案執行的入口、路由、外掛等等。

  4. 配置

    umi 的文件 配置 裡的實現。

  5. commands

    命令具體的實現邏輯,以及 webpack 配置修改相關。


就分享到這裡吧,如有錯誤歡迎指正。

參考

《螞蟻前端研發最佳實踐》文字稿

《Umi 4 設計思路 - 雲謙》視訊 & 文字版

《umi原始碼》知乎專欄

umi原始碼-幕布

相關文章