写一个Elixir LiveBook智能单元

733 阅读7分钟

什么是LiveBook?它是一个Elixir编程笔记本,它很神奇你可以在本地运行LiveBook,也可以在互联网上托管。我只在本地运行过它。

书的格式是markdown的超集,所以可以很容易地提交到Git repos,并从任何其他LiveBook实例中打开。

下面是我的《2021年代码降临》的一个片段--第7天的LiveBook

Screenshot of an open Elixir LiveBook notebook showing code and data chart打开的Elixir LiveBook笔记本的截图,显示代码和数据图表

什么是LiveBook智能单元?它是一种用户体验,用于处理一些代码模式的自动生成。例如,用给定的主机、用户名和密码创建一个数据库连接。该代码的形状将永远是相同的,但具体的主机、用户名和密码可以改变。

下面是一个新的数据库连接智能单元的例子,它与LiveBook 0.6一起交付。

Screenshot of an Elixir LiveBook smart cell showing fields to configure a database connectionElixir LiveBook智能单元的屏幕截图,显示配置数据库连接的字段

在任何时候,你都可以告诉一个智能单元放弃包装用户体验层,并简单地成为一个硬编码的代码单元,就像其他任何单元一样。

下面是同样的数据库连接智能单元 "栅格化 "为硬代码,只需在LiveBook中点击一个按钮。

opts = [hostname: "localhost", port: 5432, username: "", password: "", database: ""]
{:ok, conn} = Kino.start_child({Postgrex, opts})

这就是智能单元的真正魔力:它只是代码而已一个代码模板,与网络用户体验相联系,用用户的输入来填充代码模板的部分。还有一点,比如处理单元格的生命周期和响应更新的字段,但这就是要点。

让我们来写一个智能单元!

我们的第一个花哨的新智能单元格将非常简单地打印一个神秘的计算机文本。没有互动,没有字段,没有变量。这将是非常好的!

我们希望我们的智能单元产生的代码看起来像这样。从字面上看,输出中没有任何变量。简单地生成这个Elixir代码:

IO.puts "Not ready reading drive A"
IO.puts "Abort, Retry, Fail?"

我们可以在LiveBook中完全内联地做这个例子。但是,让我们额外地做一下吧

$ mix new not_ready_cell
$ cd not_ready_cell

首先:我们有一些模板需要铺设。也许最终智能单元会有一个钩子,但现在我们用手来做这个。

添加lib/application.ex ,以处理一些生命周期的行为,例如将我们的NotReadyCell 与LiveBook中的智能单元集实际注册:

defmodule NotReadyCell.Application do
  @moduledoc false

  use Application

  @impl true
  def start(_type, _args) do
    Kino.SmartCell.register(NotReadyCell)
    children = []
    opts = [strategy: :one_for_one, name: KinoDB.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

编辑mix.exs ,添加对kino本身的依赖,并声明该应用程序:

defp deps do
  [
    {:kino, "~> 0.6.1"}
  ]
end

接下来让我们为我们的智能单元将表现出的一个行为写一个非常简单的测试案例:test/not_ready_cell_test.exs

defmodule NotReadyCellTest do
  use ExUnit.Case, async: true

  import Kino.Test

  setup :configure_livebook_bridge

  test "supplies its hardcoded source" do
      {_kino, source} = start_smart_cell!(NotReadyCell, %{})

      assert source ==
               """
               IO.puts("Not ready reading drive A")
               IO.puts("Abort, Retry, Fail?")\
               """
  end
end

这个测试不仅失败了,而且出错了,因为我们还没有给我们的智能单元任何行为。让我们来做吧!

编写lib/not_ready_cell.ex 本身来容纳我们的智能单元和其完全空的main.js 资产。

有一些必要的智能单元的行为,这反过来又要求模块实现特定的功能。在这种情况下,我偷看了一些现实生活中的智能单元,并把它们缩减到最小的代码量,仍然符合要求:

defmodule NotReadyCell do
  @moduledoc false

  use Kino.JS
  use Kino.SmartCell, name: "Not Ready Cell"

  @impl true
  def to_source(_) do
    quote do
      IO.puts("Not ready reading drive A")
      IO.puts("Abort, Retry, Fail?")
    end |> Kino.SmartCell.quoted_to_string()
  end

  @impl true
  def to_attrs(_), do: %{}

  asset "main.js" do
    """
    """
  end
end

智能单元是由资产和Elixir代码组成的。至少他们必须提供或声明一个main.js 资产来处理智能单元生命周期的前端部分。即使我们的NotReadyCell ,没有用户互动,它仍然必须跟上作为一个智能单元所需的合同。

to_attrs 函数是将代码转化为Liveview的一部分。大多数智能单元都有一些用户写入数据的输入字段。这些字段需要知道如何转化为属性,以便它们可以被存储在活页中。我们的 "未准备好 "单元格没有字段,所以根本不需要对属性做任何处理。

因为我们的单元格是完全硬编码的,除了写出代码外,没有任何前端交互,我们也可以使用智能单元格行为提供的非常方便的资产功能,声明一个完全空的内联main.js 文件。

真正实际的智能单元代码工作是由to_source 函数完成的。该函数应该将属性和代码模板组装成工作代码。但在我们的单元格中,没有属性,只有代码可以传入智能单元格。

它能测试吗?是的:

$ mix test
.

Finished in 0.06 seconds (0.06s async, 0.00s sync)
1 test, 0 failures

Randomized with seed 379333

但它能工作吗?让我们把它导入到LiveBook中去看看吧!

它能打印吗?破坏者:它没有打印

事情似乎开始得不错。至少我们可以声明依赖关系并完成设置:

The dependency on the “Not Ready” smart cell project installs successfully对 "未准备好 "的智能电池项目的依赖性成功安装了

对上了。我们甚至将 "未准备好的电池 "注册为一个智能电池:

Our “Not Ready” smart cell is selectable from the smart cell menu我们的 "未准备好 "智能单元可以从智能单元菜单中选择

将单元格添加到LiveBook中有点不尽如人意,但我们不能期望太多,对吗?我们没有给我们的智能单元格提供它所期望的任何部分的前端代码

Our “Not Ready” smart cell barely has any cell UX我们的 "未就绪 "智能单元格几乎没有任何单元格用户体验

但是,当我们点击评估时,它会打印吗?

它不会。当我们点击 "Evaluate" 按钮时,什么也没有发生。

也许代码不在那里。让我们来偷看一下引擎盖下面的内容。

Our “Not Ready” cell does at least contain the code we specified我们的 "未准备好 "单元格至少包含我们指定的代码

好吧,嘿,我们至少得到了一些正确的东西。我们可以永久地将其转换为代码单元格,然后它确实按照预期进行评估和打印。

看起来智能单元格的互动比简单地铺设代码要多一点。我们可能需要为我们的智能单元提供一些前端/后端生命周期的钩子,以便它知道何时评估。现在我敢打赌, "评估 "的点击并没有传播到智能单元中。

让我们为我们的智能单元格添加足够的代码,以便它知道何时进行评估

生命周期从哪里来?好吧,在智能单元格的例子中,有一个use Kino.JS.Live 行,我已经明显地忽略了。我敢打赌,这一行一定有什么作用:

use Kino.Js.Live

啊哈,至少在我的文本编辑器中是有效果的。NotReadyCell 模块现在抱怨说 "嘿,你还没有为Kino.JS.Live行为定义一个必要的函数:你需要一个handle_connect/1"

好吧,让我们看看这个例子中的智能单元是什么样子的:

@impl true
def handle_connect(ctx) do
  {:ok, %{text: ctx.assigns.text}, ctx}
end

好的,我想我们可以简化一下,因为在我们的硬编码智能单元中,我们有零(0)个赋值需要担心。所以,让我们尝试一下,基本上什么都不做:

@impl true
def handle_connect(ctx) do
  {:ok, %{}, ctx}
end

嘿,vim现在很高兴。让我们试一试:

Our “Not Ready” cell works and prints out the expected output我们的 "未准备好 "单元格工作了,并打印出了预期的输出结果

呜呼!

我宣布,在这篇简短的文章中,我们已经实现了也许是目前你能实现的最绝对最小的智能单元。没有互动,没有智能,甚至几乎没有任何 "cell"-ness。只是一些硬编码的代码被自动注册为你可以插入到LiveBook中的东西,如果该依赖性被安装的话。

由于显而易见的原因,我不打算将NotReadyCell 到hex。但你可以在github.com/sdball/not_…找到代码供参考。

这意味着如果你真的想把它添加到你的LiveBook中,你就可以了

Mix.install([  {:not_ready_cell, git: "https://github.com/sdball/not_ready_cell"},])

下一次

我在使用Elixir LiveBook智能单元的过程中感到非常愉快。在下一篇文章中,我们将编写一个智能单元,允许向GitHub GraphQLAPI提交GraphQL查询!