Esbuild 入口文件及啓動過程|源碼解讀
前言
又回到了經典的一句話:“先知其然,而後使其然”。相信很多同學都知道了 esbuild,其以飛快的構建速度聞名於衆。並且,esbuild 作者 Evan Wallace 也在官網的 FAQ 專門介紹了爲什麼 esbuild 會這麼快?(有興趣的同學可以自行了解 https://esbuild.github.io/faq/)
那麼,回到今天本文,將會從 esbuild 源碼的目錄結構入手,圍繞以下 2 點和大家一起走進 esbuild 底層的世界:
-
初識 Esbuild 構建的入口
-
Esbuild 構建的入口做了什麼
1 初識 Esbuild 構建的入口
在 Go 中,是以 package
(包)來劃分模塊,每個 Go 的應用程序都需要包含一個入口 package main
,即 main.go 文件。那麼,顯然 esbuild 本身也是一個 Go 應用,即它的入口文件同樣也是 main.go 文件。
而對於 esbuild,它的目錄結構:
|—— cmd
|—— docs
|—— images
|—— internal
|—— lib
|—— npm
|—— pkg
|—— require
|—— scripts
.gitignore
go.mod
go.sum
Makefile
README.md
version.txt
似乎一眼望去,並沒有我們想要的 main.go 文件,那麼我們要怎麼找到整個應用的入口?
學過 C 的同學,應該知道 Make 這個構建工具,它可以用於執行我們定義好的一系列命令,來實現某個構建目標。並且,不難發現的是上面的目錄結構中有一個 Makefile 文件,它則是用來註冊 Make 命令的。
而在 Makefile 文件中註冊規則的基礎語法會是這樣:
<target> : <prerequisites>
[tab] <commands>
這裏,我們來分別認識一下各個參數的含義:
-
target
構建的目標,即使用 Make 命令的目標,例如make 某個目標名
-
prerequisites
前置條件,通常是一些文件對應的路徑,一旦這些文件發生變動,在執行 Make 命令時,就會進行重新構建,反之不會 -
tab
固定的語法格式要求,命令commands
的開始必須爲一個tab
鍵 -
commands
命令,即執行 Make 命令構建某個目標時,對應會執行的命令
那麼,下面我們來看一下 esbuild 中 Makefile 文件中的內容:
ESBUILD_VERSION = $(shell cat version.txt)
# Strip debug info
GO_FLAGS += "-ldflags=-s -w"
# Avoid embedding the build path in the executable for more reproducible builds
GO_FLAGS += -trimpath
esbuild: cmd/esbuild/version.go cmd/esbuild/*.go pkg/*/*.go internal/*/*.go go.mod
CGO_ENABLED=0 go build $(GO_FLAGS) ./cmd/esbuild
test:
make -j6 test-common
# These tests are for development
test-common: test-go vet-go no-filepath verify-source-map end-to-end-tests js-api-tests plugin-tests register-test node-unref-tests
# These tests are for release (the extra tests are not included in "test" because they are pretty slow)
test-all:
make -j6 test-common test-deno ts-type-tests test-wasm-node test-wasm-browser lib-typecheck
....
注意:這裏只是列出了 Makefile 文件中的部分規則,有興趣的同學可以自行查看其他規則~
可以看到,在 Makefile 文件中註冊了很多規則。而我們經常使用的 esbuild
命令,則對應着這裏的 esbuild
目標。
根據上面對 Makefile 的介紹以及結合這裏的內容,我們可以知道的是 esbuild
命令的核心是由 cmd/esbuild/version.go cmd/esbuild/*.go
和 pkg/*/*.go
、internal/*/*.go go.mod
這三部分相關的文件實現的。
那麼,通常執行 make esbuild
命令,其本質上是執行命令:
CGO_ENABLED=0 go build $(GO_FLAGS) ./cmd/esbuild
下面,我們來分別看一下這個命令做了什麼(含義):
CGO_ENABLED=0
CGO_ENABLED
是 Go 的環境(env)信息之一,我們可以用 go env
命令查看 Go 支持的所有環境信息。
而這裏將 CGO_ENABLED
設爲 0
是爲了禁用 cgo
,因爲默認情況下,CGO_ENABLED
爲 1
,也就是開啓 cgo
的,但是 cgo
是會導入一些包含 C 代碼的文件,那麼也就是說最後編譯的結果會包含一些外部動態鏈接,而不是純靜態鏈接。
cgo
可以讓你在 .go 文件中使用 C 的語法,這裏不做詳細的展開介紹,有興趣的同學可以自行了解
那麼,這個時候大家可能會思考外部動態鏈接和靜態鏈接之間的區別是什麼?爲什麼需要純靜態鏈接的編譯結果?
這是因爲外部動態鏈接會打破你最後編譯出的程序對平臺的適應性。因爲,外部動態鏈接存在一定的不確定因素,簡單的說也許你現在構建出來的應用是可以用的,但是在某天外部動態鏈接的內容發生了變化,那麼很可能會對你的程序運行造成影響。
go build $(GO_FLAGS) ./cmd/esbuild
go build $(GO_FLAGS) ./cmd/esbuild
的核心是 go build
命令,它是用於編譯源碼文件、代碼包、依賴包等操作,例如我們這裏是對 ./cmd/esbuild/main.go
文件執行編譯操作。
到這裏,我們就已經知道了 esbuild 構建的入口是 cmd/esbuild/main.go
文件了。那麼,接下來就讓我們看一下構建的入口都做了哪些事情?
2 Esbuild 構建的入口做了什麼?
雖然,Esbuild 構建的入口 cmd/esbuild/main.go
文件的代碼總共才 268 行左右。但是,爲了方便大家理解,這裏我將拆分爲以下 3 點來分步驟講解:
-
基礎依賴的
package
導入 -
--help
的文字提示函數的定義 -
main
函數具體都做了哪些
2.1 基礎依賴的 package
導入
首先,是基礎依賴的 package
導入,總共導入了 8 個 package
:
import (
"fmt"
"os"
"runtime/debug"
"strings"
"time"
"github.com/evanw/esbuild/internal/api_helpers"
"github.com/evanw/esbuild/internal/logger"
"github.com/evanw/esbuild/pkg/cli"
)
這 8 個 package
分別對應的作用:
-
fmt
用於格式化輸出 I/O 的函數 -
os
提供系統相關的接口 -
runtime/debug
提供程序在運行時進行調試的功能 -
strings
用於操作 UTF-8 編碼的字符串的簡單函數 -
time
用於測量和展示時間 -
github.com/evanw/esbuild/internal/api_helpers
用於檢測計時器是否正在使用 -
github.com/evanw/esbuild/internal/logger
用於格式化日誌輸出 -
github.com/evanw/esbuild/pkg/cli
提供 esbuild 的命令行接口
2.2 --help
的文字提示函數的定義
任何一個工具都會有一個 --help
的選項(option),用於告知用戶能使用的具體命令。所以,esbuild 的 --help
文字提示函數的定義也具備同樣的作用,對應的代碼(僞代碼):
var helpText = func(colors logger.Colors) string {
return `
` + colors.Bold + `Usage:` + colors.Reset + `
esbuild [options] [entry points]
` + colors.Bold + `Documentation:` + colors.Reset + `
` + colors.Underline + `https://esbuild.github.io/` + colors.Reset + `
` + colors.Bold
...
}
這裏會用到我們上面提到的 logger
這個 package
的 Colors
結構體,它主要用於美化在終端輸出的內容,例如加粗(Bold
)、顏色(Red
、Green
):
type Colors struct {
Reset string
Bold string
Dim string
Underline string
Red string
Green string
Blue string
Cyan string
Magenta string
Yellow string
}
而使用 Colors
結構體創建的變量會是這樣:
var TerminalColors = Colors{
Reset: "\033[0m",
Bold: "\033[1m",
Dim: "\033[37m",
Underline: "\033[4m",
Red: "\033[31m",
Green: "\033[32m",
Blue: "\033[34m",
Cyan: "\033[36m",
Magenta: "\033[35m",
Yellow: "\033[33m",
}
2.3 main
函數主要都做了哪些
在前面,我們也提及了每個 Go 的應用程序都必須要有一個 main package
,即 main.go 文件來作爲應用的入口。而在 main.go 文件內也必須聲明 main
函數,來作爲 package
的入口函數。
那麼,作爲 esbuild 的入口文件的 main
函數,主要是做這 2 件事:
1. 獲取輸入的選項(option),並進行處理
使用我們上面提到的 os
這個 package
獲取終端輸入的選項,即 os.Args[1:]
。其中 [1:]
表示獲取數組從索引爲 1 到最後的所有元素構成的數組。
然後,會循環 osArgs
數組,每次會 switch
判斷具體的 case
,對不同的選項,進行相應的處理。例如 --version
選項,會輸出當前 esbuild
的版本號以及退出:
fmt.Printf("%s\n", esbuildVersion)
os.Exit(0)
這整個過程對應的代碼會是這樣:
osArgs := os.Args[1:]
argsEnd := 0
for _, arg := range osArgs {
switch {
case arg == "-h", arg == "-help", arg == "--help", arg == "/?":
logger.PrintText(os.Stdout, logger.LevelSilent, os.Args, helpText)
os.Exit(0)
// Special-case the version flag here
case arg == "--version":
fmt.Printf("%s\n", esbuildVersion)
os.Exit(0)
...
default:
osArgs[argsEnd] = arg
argsEnd++
}
}
並且,值得一提的是這裏會重新構造 osArgs
數組,由於選項是可以一次性輸入多個的, 但是 osArgs
會在後續的啓動構建的時候作爲參數傳入,所以這裏處理過的選項會在數組中去掉。
2. 調用 cli.Run(),啓動構建
對於使用者來說,我們切實關注的是使用 esbuild 來打包某個應用,例如使用 esbuild xxx.js --bundle
命令。而這個過程由 main
函數最後的自執行函數完成。
該函數的核心是調用 cli.Run()
來啓動構建過程,並且傳入上面已經處理過的選項。
func() {
...
exitCode = cli.Run(osArgs)
}()
並且,在正式開啓構建之前,會根據繼續處理前面的選項相關的邏輯,具體會涉及到 CPU 跟蹤、堆棧的跟蹤等,這裏不作展開介紹,有興趣的同學自行了解。
結語
好了,到這裏我們就大致過了一遍 esbuild 構建的入口文件相關源碼。站在沒接觸過 Go 的同學角度看可能稍微有點晦澀,並且有些分支邏輯,文中並沒有展開分析,這會在後續的文章中繼續展開。但是,總體上來看,打開一個新的窗戶看到了不一樣的風景,這不就是我們作爲工程師所希望經歷的嘛 😎。最後,如果文中存在表達不當或錯誤的地方,歡迎各位同學提 Issue~
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/lpWf4jTu2xJRtaevjpmqvQ