这是我参与2022首次更文挑战的第29天,活动详情查看:2022首次更文挑战
本文为译文,原文链接:www.calhoun.io/creating-a-…
我试图为开发设置一个docker容器,并遇到了一个问题-无论我尝试什么,文件更改事件没有被传播到容器,所以我不能得到热/实时重新加载工作。
你可以想象,这非常令人沮丧;在开发过程中必须手动停止、重建和重新启动应用程序,这已经够耗时的了,但是再加上一个docker容器和一些额外的步骤,突然之间,这个过程就会消耗掉你一整天的时间。这对我来说行不通。
我是一个相当务实的人,所以在这一点上,我的下一步通常是最小化浪费的时间,并恢复到一个我知道会有用的工具。在过去,一直在本地运行modd,而不是为我的本地Go应用程序使用一个容器。这已经证明对我来说工作得很好,因为modd可以处理我需要完成的大多数准备任务,也很适合在文件更改后运行测试。双赢!
不幸的是,在这种情况下,这是行不通的。很多人都知道,我创建了编程课程,我如此热衷于让这个本地docker设置工作的原因之一是,我想为我所有的新课程提供docker-compose配置。这有助于避免任何支持问题,在一个操作系统上一切正常,但在另一个操作系统上有一些微妙的bug(我在看你,Windows!),同时也使新用户更容易得到东西和快速运行-你只需要安装Docker。所有这一切都意味着我需要一个解决方案,不能回到我以前的方式。😭
我检查了我的选项,最终决定只编写一个使用轮询的自定义实时加载程序。我认为库是最好的,然后我可以写一个快速的(约50行)主程序main.go
。在将来的每个项目中使用它。
我意识到还有其他的实时重新加载工具,以及许多支持轮询,但我仍然觉得编写自己的工具是这种情况下的最佳选择,因为它可以在相对较短的时间内获得我所需要的内容。
本文的其余部分将记录编写名为pitstop
的实时重载库的过程,然后讨论我是如何让它与我的Go应用程序一起工作的。之后,我将讨论一些额外的想法和库的一些潜在问题。
Breaking the code into steps 将代码分解成步骤
我做的第一件事就是把我想要走的步骤分开。历史告诉我,这可能是错误的,但我喜欢在开始之前有一个大致的想法,我想在这里与大家分享。
-
检测文件变化的函数。
-
一个函数来构建和运行我的应用程序。
-
一个外部循环,调用这些函数,并使用用户提供的配置变量在必要时休眠。
最终版本采取了比这更多的步骤,但这些都是一个很好的起点。
步骤一-检测文件变化
我需要一个函数,它可以递归地查看目录中的所有文件,查找自给定时间戳以来发生过更改的所有文件。第一个版本并不需要任何特别的东西,比如可以忽略的扩展。我只需要一个概念证明,这样我就可以继续进行剩下的步骤,并让某些东西工作起来。
幸运的是,Go有一个函数可以帮我完成大部分工作:filepath.Walk
在filepath.Walk
,我们只需要提供一个filepath.WalkFunc,它将递归地遍历目录中的所有文件,同时为我们提供一个os.FileInfo,幸运的是,它有一个ModTime()
方法。这意味着我们真正需要的是一种检测变化的方法,这可以通过一个闭包来完成,该闭包为我们的DidChange
函数提供了以下代码。
func DidChange(dir string, since time.Time) bool {
var changed bool
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if info.ModTime().After(since) {
changed = true
}
return nil
})
return changed
}
源代码链接:github.com/joncalhoun/…
步骤二:构建和运行应用程序
接下来,我需要一种方法来构建和运行应用程序。在许多实时加载程序中,这只是一个bash命令,但我没有看到任何真正的理由,为什么我必须限制自己只有bash命令。毕竟,这是一个库,所以我可以接受任何函数作为构建或运行步骤。
我决定如下:
type BuildFunc func() error
type RunFunc func() (stop func(), err error)
正如我前面提到的,许多构建步骤往往是像安装这样的bash命令,所以我也想让它更容易实现。为了适应这一点,我添加了以下帮助程序。
// BuildCommand works similar to exec.Command, but rather than returning an
// exec.Cmd it returns a BuildFunc that can be reused.
func BuildCommand(command string, args ...string) BuildFunc {
return func() error {
cmd := exec.Command(command, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
return fmt.Errorf("error building: "%s %s": %w", command, strings.Join(args, " "), err)
}
return nil
}
}
// RunCommand works similar to exec.Command, but rather than returning an
// exec.Cmd it returns a RunFunc that can be reused.
func RunCommand(command string, args ...string) RunFunc {
return func() (func(), error) {
cmd := exec.Command(command, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Start()
if err != nil {
return nil, fmt.Errorf("error running: "%s %s": %w", command, strings.Join(args, " "), err)
}
return func() {
cmd.Process.Kill()
}, nil
}
}
现在在我的库中,任何地方我想要一个BuildFunc
,我可以提供一个典型的bash命令行,就像这样:
pitstop.BuildCommand("go", "install")
pitstop.BuildCommand("go", "test", "./...")
相同的,我也可以用这种方式提供一个RunFunc
:
pitstop.RunCommand("server", "--prod")
为了简单起见,我在这里做的一个权衡是,任何由RunCommand
或BuildCommand
创建的东西都将写入os.Stdout
和os.Stderr
,不能自定义。如果任何构建命令退出时带有任何非零状态码,我还选择返回一个错误,我们很快就会看到,这意味着任何go test ./...
失败导致构建被停止。最后,我选择只使用cmd.Process.Kill()
来终止命令。我的理由是,从技术上讲,任何使用这个库的人都可以编写他们自己的自定义BuildFunc
,它不使用这个默认行为,但对我来说,这是我90%的时间都想要的,我不想总是配置这些助手。
最后,我用一个Run
函数将所有这些放在一起。
// Run will run all pre BuildFuncs, then the RunFunc, and then finally the post
// BuildFuncs. Any errors encountered will be returned, and the build process
// halted. If RunFunc has been called, stop will also be called so that it is
// guaranteed to not be running anytime an error is returned.
func Run(pre []BuildFunc, run RunFunc, post []BuildFunc) (func(), error) {
for _, fn := range pre {
err := fn()
if err != nil {
return nil, err
}
}
stop, err := run()
if err != nil {
return nil, err
}
for _, fn := range post {
err := fn()
if err != nil {
stop()
return nil, err
}
}
return stop, nil
}
旁注:我考虑了一个可变参数的post参数,但决定不这样做,因为它会使我们如何传入pre和post不同,尽管事实是,这些基本上是相同的,除了当他们运行。
编写并测试了所有的代码后,我已经准备好将其整合在一起并开始使用它了!
本篇先到这里啦,下节继续。