前言
又回到了经典的一句话:“先知其然,而后使其然 ”。置信很多同学都晓得了 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~
点赞 👍
通过浏览本篇文章,如果有播种的话,能够 点个赞,这将会成为我继续分享的能源,感激~
我是五柳,喜爱翻新、捣鼓源码,专一于源码(Vue 3、Vite)、前端工程化、跨端等技术学习和分享,欢送关注我的 微信公众号:Code center。