又回到了经典的一句话:“先知其然,然后使其然”。相信不少同窗都知道了 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!