Esbuild 入口文件及啓動過程|源碼解讀

前言

又回到了經典的一句話:「先知其然,然後使其然」。相信不少同窗都知道了 esbuild,其以飛快的構建速度聞名於衆。而且,esbuild 做者 Evan Wallace 也在官網的 FAQ專門介紹了爲何 esbuild 會這麼快?(有興趣的同窗能夠自行了解 https://esbuild.github.io/faq/前端

那麼,回到今天本文,將會從 esbuild 源碼的目錄結構入手,圍繞如下 2 點和你們一塊兒走進 esbuild 底層的世界:node

  • 初識 Esbuild 構建的入口
  • Esbuild 構建的入口作了什麼

1 初識 Esbuild 構建的入口

在 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/*.gopkg/*/*.gointernal/*/*.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_ENABLED1,也就是開啓 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 這個 packageColors 結構體,它主要用於美化在終端輸出的內容,例如加粗(Bold)、顏色(RedGreen):

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~

點贊 👍

經過閱讀本篇文章,若是有收穫的話,能夠點個贊,這將會成爲我持續分享的動力,感謝~

我是五柳,喜歡創新、搗鼓源碼,專一於源碼(Vue 三、Vite)、前端工程化、跨端等技術學習和分享。此外,個人全部文章都會收錄在 https://github.com/WJCHumble/Blog,歡迎 Watch Or Star!
相關文章
相關標籤/搜索