又回到了經典的一句話:「先知其然,然後使其然」。相信不少同窗都知道了 esbuild,其以飛快的構建速度聞名於衆。而且,esbuild 做者 Evan Wallace 也在官網的 FAQ專門介紹了爲何 esbuild 會這麼快?(有興趣的同窗能夠自行了解 https://esbuild.github.io/faq/)前端
那麼,回到今天本文,將會從 esbuild 源碼的目錄結構入手,圍繞如下 2 點和你們一塊兒走進 esbuild 底層的世界:node
在 Go 中,是以 package
(包)來劃分模塊,每一個 Go 的應用程序都須要包含一個入口 package main
,即 main.go 文件。那麼,顯然 esbuild 自己也是一個 Go 應用,即它的入口文件一樣也是 main.go 文件。git
而對於 esbuild,它的目錄結構:github
|—— cmd |—— docs |—— images |—— internal |—— lib |—— npm |—— pkg |—— require |—— scripts .gitignore go.mod go.sum Makefile README.md version.txt
彷佛一眼望去,並無咱們想要的 main.go 文件,那麼咱們要怎麼找到整個應用的入口?shell
學過 C 的同窗,應該知道 Make 這個構建工具,它能夠用於執行咱們定義好的一系列命令,來實現某個構建目標。而且,不難發現的是上面的目錄結構中有一個 Makefile 文件,它則是用來註冊 Make 命令的。npm
而在 Makefile 文件中註冊規則的基礎語法會是這樣:前端工程化
<target> : <prerequisites> [tab] <commands>
這裏,咱們來分別認識一下各個參數的含義:api
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
目標。bash
根據上面對 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
文件了。那麼,接下來就讓咱們看一下構建的入口都作了哪些事情?
雖然,Esbuild 構建的入口 cmd/esbuild/main.go
文件的代碼總共才 268 行左右。可是,爲了方便你們理解,這裏我將拆分爲如下 3 點來分步驟講解:
package
導入--help
的文字提示函數的定義main
函數具體都作了哪些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 的命令行接口--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", }
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~
經過閱讀本篇文章,若是有收穫的話,能夠點個贊,這將會成爲我持續分享的動力,感謝~
我是五柳,喜歡創新、搗鼓源碼,專一於源碼(Vue 三、Vite)、前端工程化、跨端等技術學習和分享。此外,個人全部文章都會收錄在 https://github.com/WJCHumble/Blog,歡迎 Watch Or Star!