Bubble Tea Tutorials(Bubble Tea教程)

警告
本文最后更新于 2023-05-06,文中内容可能已过时。

Bubble Tea基础

Bubble Tea是基于The Elm Architecture的功能设计范式,它恰好可以与Go很好地配合。这是一种令人愉快的构建应用程序的方式。

Bubble Tea架构,如下图所示:

Bubble Tea Architecture

本教程假定你对Go有一定的了解。

顺便说一下,这个程序的非注释源代码可以在GitHub上找到。

Enough! Let’s get to it.

在本教程中,我们要做一份购物清单。

首先,我们将定义我们的包并导入一些库。我们唯一的外部导入将是Bubble Tea库,我们将其简称为tea。

1
2
3
4
5
6
7
8
package main

import (
    "fmt"
    "os"

    tea "github.com/charmbracelet/bubbletea"
)

Bubble Tea程序是由一个描述应用状态的模型和该模型上的三个简单方法组成的:

  • Init:一个函数,用于返回应用程序运行的初始命令。
  • Update:一个处理传入事件并相应更新模型的函数。
  • View:一个函数,根据模型中的数据渲染UI。

The Model()

因此,让我们从定义我们的模型开始,它将存储我们应用程序的状态。它可以是任何类型,但结构通常是最合理的。

1
2
3
4
5
type model struct {
    choices  []string           // 购物列表中的物品
    cursor   int                // 我们的光标指向哪个物品
    selected map[int]struct{}   // 哪些物品被选中
}

Initialization(初始化)

接下来,我们将定义我们应用程序的初始状态。在这种情况下,我们要定义一个函数来返回我们的初始模型,然而,我们也可以很容易地在其他地方将初始模型定义为一个变量。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func initialModel() model {
	return model{
		// 我们的购物列表是一张杂货清单
		choices:  []string{"Buy carrots", "Buy celery", "Buy kohlrabi"},

		// 一张表明哪些选择被选中的`map`。
        // 我们像使用数学集合一样使用这个`map`。
        // `key`指的是上面`choices`切片的索引。
		selected: make(map[int]struct{}),
	}
}

接下来,我们定义init方法。init可以返回一个Cmd,它可以执行一些初始I/O。现在,我们不需要做任何的I/O操作,所以对于Cmd,我们将只是返回nil,翻译成 “没有命令”。

1
2
3
4
func (m model) Init() tea.Cmd {
    // 只需返回`nil`,这意味着 "现在没有`I/O``操作"。
    return nil
}

The Update Method(更新函数)

接下来是更新方法(Update)。Update在 “事件发生"时被调用。它的工作是查看已经发生的事情并返回一个更新的模型(Model)作为回应。它还可以返回一个Cmd来使更多的事件发生,但现在不要担心这部分。

在我们的例子中,当用户按下向下方向键时,Update的工作是注意到向下方向键被按下,并相应地移动光标(或不移动)。

“发生的事件"以Msg的形式出现,它可以是任何类型。信息是发生的一些I/O的结果,例如按键、定时器滴答或来自服务器的响应。

我们通常用类型匹配来计算我们收到的Msg的类型,但你也可以使用类型断言。

现在,我们只处理tea.KeyMsg信息,当按键被按下时,这些信息会自动发送给更新函数(Update)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {

    // 按键被按下吗?
    case tea.KeyMsg:

        // Cool,实际按下的是什么键?
        switch msg.String() {

        // 这些键应该是退出程序。
        case "ctrl+c", "q":
            return m, tea.Quit

        // 向上键和"K"键将向上移动光标
        case "up", "k":
            if m.cursor > 0 {
                m.cursor--
            }

        // 向下键和"J"键将向下移动光标
        case "down", "j":
            if m.cursor < len(m.choices)-1 {
                m.cursor++
            }

        // 回车键和空格键(字面意义上的空格)可以切换光标指向的物品的选择状态。
        case "enter", " ":
            _, ok := m.selected[m.cursor]
            if ok {
                delete(m.selected, m.cursor)
            } else {
                m.selected[m.cursor] = struct{}{}
            }
        }
    }

    // 将更新后的模型(`Model`)返回给Bubble Tea运行时进行处理。
    // 请注意,我们并没有返回一个命令。
    return m, nil
}

你可能已经注意到,上面的ctrl+cq返回一个带有模型(Model)的tea.Quit命令。那是一个特殊的命令,它指示Bubble Tea运行时退出,退出程序。

The View Method(渲染函数)

最后,是时候渲染我们的UI了。在所有的方法中,视图是最简单的。我们看一下模型的当前状态,然后用它来返回一个字符串。这个字符串就是我们的UI!

因为视图描述了你的应用程序的整个UI,你不必担心重绘逻辑和类似的东西。Bubble Tea为你解决了这个问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func (m model) View() string {
    // The header
    s := "What should we buy at the market?\n\n"

    // 对我们的购物列表进行迭代。
    for i, choice := range m.choices {

        // 光标是否指着这个选项?
        cursor := " " // 无光标
        if m.cursor == i {
            cursor = ">" // 有光标
        }

        // 这个选项是否被选中?
        checked := " " // 未选中
        if _, ok := m.selected[i]; ok {
            checked = "x" // 选中
        }

        // 渲染该行
        s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
    }

    // The footer
    s += "\nPress q to quit.\n"

    // 发送UI进行渲染
    return s
}

All Together Now

最后一步是简单地运行我们的程序。我们把我们的初始模型传递给tea.NewProgram,然后让它运行:

1
2
3
4
5
6
7
func main() {
    p := tea.NewProgram(initialModel())
    if _, err := p.Run(); err != nil {
        fmt.Printf("Alas, there's been an error: %v", err)
        os.Exit(1)
    }
}

What’s Next?

本教程涵盖了构建交互式终端UI的基础知识,但在现实世界中,你也需要执行I/O。要了解这一点,请看Command Tutorial。这很简单。

也有几个Bubble Tea examples的例子,当然,还有Go Docs

Commands in Bubble Tea(Bubble Tea命令)

这是Bubble Tea的第二个教程,涵盖了涉及I/O的命令。本教程假定你对围棋有一定的了解,并对第一个教程有一定的理解。

你可以在GitHub上找到这个程序的非注释版本。

Let’s Go!

在本教程中,我们要建立一个非常简单的程序,向服务器发出HTTP请求并报告响应的状态代码。

我们将导入一些必要的包,并将我们要检查的URL放在一个常量(const)中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import (
    "fmt"
    "net/http"
    "os"
    "time"

    tea "github.com/charmbracelet/bubbletea"
)

const url = "https://charm.sh/"

The Model

接下来我们将定义我们的模型(Model)。我们唯一需要存储的是HTTP响应的状态代码和一个可能的错误。

1
2
3
4
type model struct {
    status int
    err    error
}

Commands and Messages

Cmds是执行一些I/O然后返回一个Msg的函数。检查时间、定时器的滴答声、从磁盘上读取数据和网络的东西都是I/O,应该通过命令来运行。这可能听起来很苛刻,但它将使你的Bubble Tea程序保持直接和简单。

总之,让我们写一个Cmd,向服务器发出请求并将结果作为Msg返回。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func checkServer() tea.Msg {

    // 创建一个HTTP客户端并发出一个GET请求。
    c := &http.Client{Timeout: 10 * time.Second}
    res, err := c.Get(url)

    if err != nil {
        // 发出我们的请求时有一个错误。把我们收到的错误包在一个消息中并返回。
        return errMsg{err}
    }
    // 我们收到了来自服务器的响应。将HTTP状态代码作为消息返回。
    return statusMsg(res.StatusCode)
}

type statusMsg int

type errMsg struct{ err error }

// 对于包含错误的消息,在消息上实现错误接口往往很方便。
func (e errMsg) Error() string { return e.err.Error() }

并注意到我们定义了两个新的Msg类型。它们可以是任何类型,甚至是一个空结构。我们将在后面的更新函数(Update)中再来讨论它们。首先,让我们写一下我们的初始化函数。

The Initialization Method

初始化方法非常简单:我们返回我们之前做的Cmd。请注意,我们并没有调用该函数;Bubble Tea运行时将在合适的时候进行调用。

1
2
3
func (m model) Init() (tea.Cmd) {
    return checkServer
}

The Update Method

在内部,Cmds在一个goroutine中异步运行。它们返回的Msg被收集起来,并被发送到我们的更新函数(Update)中进行处理。还记得我们之前在做checkServer命令时做的那些消息类型吗?我们在这里处理它们。这使得处理许多异步操作变得非常容易。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {

    case statusMsg:
        // 服务器返回了一条状态信息。把它保存到我们的模型中。
        // 同时告诉Bubble Tea运行时我们要退出,因为我们没有其他事情可做了。
        // 我们仍然可以用我们的状态信息渲染一个最终视图。
        m.status = int(msg)
        return m, tea.Quit

    case errMsg:
        // 有一个错误。在模型中注意它。并告诉运行时我们已经完成了,想要退出。
        m.err = msg
        return m, tea.Quit

    case tea.KeyMsg:
        // Ctrl+c退出。即使是短时运行的程序,有一个退出键也是很好的,
        // 以防止你的逻辑出现问题。如果用户不能退出,他们会非常恼火的。
        if msg.Type == tea.KeyCtrlC {
            return m, tea.Quit
        }
    }

    // 如果我们碰巧收到任何其他信息,不要做任何事情。
    return m, nil
}

The View Function

我们的视图(View)是非常直接的。我们看一下当前的模型(Model),并据此建立一个字符串:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func (m model) View() string {
    // 如果有错误,就把它打印出来,不要再做其他事情。
    if m.err != nil {
        return fmt.Sprintf("\nWe had some trouble: %v\n\n", m.err)
    }

    // 告诉用户我们正在做的事情。
    s := fmt.Sprintf("Checking %s ... ", url)

    // 当服务器响应一个状态时,将其添加到当前行。
    if m.status > 0 {
        s += fmt.Sprintf("%d %s!", m.status, http.StatusText(m.status))
    }

    // 渲染UI
    return "\n" + s + "\n\n"
}

Run the program

唯一要做的就是运行这个程序,所以让我们来做吧! 在这种情况下,我们的初始模型根本不需要任何数据,我们只是把它初始化为一个带有默认值的结构。

1
2
3
4
5
6
func main() {
    if _, err := tea.NewProgram(model{}).Run(); err != nil {
        fmt.Printf("Uh oh, there was an error: %v\n", err)
        os.Exit(1)
    }
}

就是这样。不过,还有一件事有助于了解 Cmds。

One More Thing About Commands

Cmds在Bubble Tea中被定义为type Cmd func() Msg。所以它们只是不接受任何参数的函数,并返回一个MsgMsg可以是任何类型。如果你需要给一个命令传递参数,你只需制作一个返回命令的函数。比如说:

1
2
3
4
5
func cmdWithArg(id int) tea.Cmd {
    return func() tea.Msg {
        return someMsg{id: id}
    }
}

一个更现实的例子看起来像:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func checkSomeUrl(url string) tea.Cmd {
    return func() tea.Msg {
        c := &http.Client{Timeout: 10 * time.Second}
        res, err := c.Get(url)
        if err != nil {
            return errMsg{err}
        }
        return statusMsg(res.StatusCode)
    }
}

总之,只要确保你在最里面的函数中做尽可能多的事情,因为那是异步运行的函数。

Now What?

做完本教程和上一教程后,你应该准备好建立一个属于你自己的Bubble Tea程序。我们还推荐你看一下Bubble Tea的example programs以及Bubbles,一个Bubble Tea的组件库。

当然,也可以看看Go Docs

Additional Resources

Buy Me a Coffee ~~
hiifong 支付宝支付宝
hiifong 微信微信
0%