查看源代码 依赖项和伞形项目

本章我们将讨论如何在 Mix 中管理依赖项。

我们的 kv 应用程序已完成,现在该实现服务器来处理我们在第一章中定义的请求了。

CREATE shopping
OK

PUT shopping milk 1
OK

PUT shopping eggs 3
OK

GET shopping milk
1
OK

DELETE shopping eggs
OK

然而,我们不会在 kv 应用程序中添加更多代码,而是会将 TCP 服务器构建为另一个应用程序,该应用程序是 kv 应用程序的客户端。由于整个运行时和 Elixir 生态系统都面向应用程序,因此将我们的项目分解成更小的应用程序,这些应用程序共同工作,而不是构建一个大型的单体应用程序,这是很有意义的。

在创建我们的新应用程序之前,我们必须讨论 Mix 如何处理依赖项。实际上,我们通常使用两种依赖项:内部依赖项和外部依赖项。Mix 支持使用这两种依赖项的机制。

外部依赖项

外部依赖项是不与您的业务领域绑定的依赖项。例如,如果您需要为您的分布式 KV 应用程序使用 HTTP API,则可以使用 Plug 项目作为外部依赖项。

安装外部依赖项很简单。最常见的是,我们使用 Hex 包管理器,通过在我们的 mix.exs 文件中的 deps 函数中列出依赖项来实现。

def deps do
  [{:plug, "~> 1.0"}]
end

此依赖项引用了已推送到 Hex 的 1.x.x 版本系列中的最新版本 Plug。这是由版本号之前的 ~> 指示的。有关指定版本要求的更多信息,请参阅 Version 模块的文档。

通常,稳定版本会推送到 Hex。如果您想要依赖于仍在开发中的外部依赖项,Mix 也能够管理 Git 依赖项。

def deps do
  [{:plug, git: "https://github.com/elixir-lang/plug.git"}]
end

您会注意到,当您将依赖项添加到项目中时,Mix 会生成一个 mix.lock 文件,该文件保证可重复构建。该锁定文件必须签入到您的版本控制系统中,以确保每个使用该项目的人都将使用与您相同的依赖项版本。

Mix 提供了许多用于处理依赖项的任务,您可以在 mix help 中看到。

$ mix help
mix deps              # Lists dependencies and their status
mix deps.clean        # Deletes the given dependencies' files
mix deps.compile      # Compiles dependencies
mix deps.get          # Gets all out of date dependencies
mix deps.tree         # Prints the dependency tree
mix deps.unlock       # Unlocks the given dependencies
mix deps.update       # Updates the given dependencies

最常见的任务是 mix deps.getmix deps.update。获取后,依赖项会自动为您编译。您可以通过键入 mix help deps 以及在 Mix.Tasks.Deps 模块的文档中了解更多关于 deps 的信息。

内部依赖项

内部依赖项是特定于您的项目的依赖项。它们通常在您的项目/公司/组织的范围之外没有意义。大多数情况下,您希望将它们保持私有,无论出于技术、经济还是商业原因。

如果您有内部依赖项,Mix 支持两种方法来处理它们:Git 存储库或伞形项目。

例如,如果您将 kv 项目推送到 Git 存储库,您需要在您的 deps 代码中列出它以便使用它。

def deps do
  [{:kv, git: "https://github.com/YOUR_ACCOUNT/kv.git"}]
end

但是,如果该存储库是私有的,您可能需要指定私有 URL [email protected]:YOUR_ACCOUNT/kv.git。无论哪种情况,只要您拥有正确的凭据,Mix 都能够为您获取它。

在 Elixir 中,不建议将 Git 存储库用于内部依赖项。请记住,运行时和 Elixir 生态系统已经提供了应用程序的概念。因此,我们期望您经常将代码分解成可以逻辑组织的应用程序,即使在一个项目中也是如此。

但是,如果您将每个应用程序作为单独的项目推送到 Git 存储库,您的项目可能变得非常难以维护,因为您将花费大量时间管理这些 Git 存储库,而不是编写代码。

为此,Mix 支持“伞形项目”。伞形项目用于构建在单个存储库中一起运行的应用程序。这正是我们将在接下来的部分中探讨的风格。

让我们创建一个新的 Mix 项目。我们将在创意上将其命名为 kv_umbrella,这个新项目将包含现有的 kv 应用程序和新的 kv_server 应用程序。目录结构将如下所示

+ kv_umbrella
  + apps
    + kv
    + kv_server

这种方法的有趣之处在于,Mix 为处理此类项目提供了许多便利,例如能够使用单个命令编译和测试 apps 内的所有应用程序。但是,即使它们都在 apps 中列出,它们仍然彼此独立,因此您可以根据需要单独构建、测试和部署每个应用程序。

让我们开始吧!

伞形项目

让我们使用 mix new 启动一个新项目。这个新项目将命名为 kv_umbrella,我们需要在创建它时传递 --umbrella 选项。不要在现有的 kv 项目内创建这个新项目!

$ mix new kv_umbrella --umbrella
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating apps
* creating config
* creating config/config.exs

从打印的信息中,我们可以看到生成的少得多的文件。生成的 mix.exs 文件也不同。让我们看一下(注释已删除)

defmodule KvUmbrella.MixProject do
  use Mix.Project

  def project do
    [
      apps_path: "apps",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  defp deps do
    []
  end
end

使这个项目与之前的项目不同的是项目定义中的 apps_path: "apps" 条目。这意味着这个项目将充当一个伞形项目。此类项目没有源文件或测试,尽管它们可以有自己的依赖项。每个子应用程序必须在 apps 目录内定义。

让我们进入 apps 目录并开始构建 kv_server。这一次,我们将传递 --sup 标志,它将告诉 Mix 为我们自动生成一个监督树,而不是像我们在之前的章节中那样手动构建一个。

$ cd kv_umbrella/apps
$ mix new kv_server --module KVServer --sup

生成的与我们最初为 kv 生成的文件类似,但也有一些差异。让我们打开 mix.exs

defmodule KVServer.MixProject do
  use Mix.Project

  def project do
    [
      app: :kv_server,
      version: "0.1.0",
      build_path: "../../_build",
      config_path: "../../config/config.exs",
      deps_path: "../../deps",
      lockfile: "../../mix.lock",
      elixir: "~> 1.14",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  # Run "mix help compile.app" to learn about applications
  def application do
    [
      extra_applications: [:logger],
      mod: {KVServer.Application, []}
    ]
  end

  # Run "mix help deps" to learn about dependencies
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
      # {:sibling_app_in_umbrella, in_umbrella: true},
    ]
  end
end

首先,由于我们在 kv_umbrella/apps 内生成了这个项目,Mix 自动检测到伞形项目结构,并在项目定义中添加了四行

build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
lockfile: "../../mix.lock",

这些选项意味着所有依赖项都将检出到 kv_umbrella/deps,并且它们将共享相同的构建、配置和锁定文件。我们还没有讨论配置,但从这里我们可以直观地理解所有配置和依赖项都在一个伞形项目中的所有项目之间共享,而不是每个应用程序。

第二个变化是在 mix.exs 内的 application 函数中

def application do
  [
    extra_applications: [:logger],
    mod: {KVServer.Application, []}
  ]
end

因为我们传递了 --sup 标志,Mix 自动添加了 mod: {KVServer.Application, []},指定 KVServer.Application 是我们的应用程序回调模块。 KVServer.Application 将启动我们的应用程序监督树。

事实上,让我们打开 lib/kv_server/application.ex

defmodule KVServer.Application do
  # See https://hexdocs.erlang.ac.cn/elixir/Application.html
  # for more information on OTP Applications
  @moduledoc false

  use Application

  @impl true
  def start(_type, _args) do
    # List all child processes to be supervised
    children = [
      # Starts a worker by calling: KVServer.Worker.start_link(arg)
      # {KVServer.Worker, arg},
    ]

    # See https://hexdocs.erlang.ac.cn/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: KVServer.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

请注意,它定义了应用程序回调函数 start/2,并且没有定义一个名为 KVServer.Supervisor 的监督程序,该监督程序使用 Supervisor 模块,而是方便地在内联中定义了监督程序!您可以通过阅读 Supervisor 模块文档了解更多关于此类监督程序的信息。

我们现在可以尝试我们的第一个伞形子项目。我们可以在 apps/kv_server 目录内运行测试,但这不会很有趣。相反,转到伞形项目的根目录并运行 mix test

$ mix test

它有效!

由于我们希望 kv_server 最终使用我们在 kv 中定义的功能,因此我们需要将 kv 添加为应用程序的依赖项。

伞形项目内的依赖项

伞形项目中应用程序之间的依赖项仍然必须明确定义,Mix 使这变得很容易。打开 apps/kv_server/mix.exs 并将 deps/0 函数更改为以下内容

defp deps do
  [{:kv, in_umbrella: true}]
end

上面的行使 :kv:kv_server 中可用,并在服务器启动之前自动启动 :kv 应用程序。

最后,将我们到目前为止构建的 kv 应用程序复制到我们新伞形项目中的 apps 目录中。最终的目录结构应与我们之前提到的结构相匹配

+ kv_umbrella
  + apps
    + kv
    + kv_server

现在我们需要修改 apps/kv/mix.exs 以包含我们在 apps/kv_server/mix.exs 中看到的伞形项目条目。打开 apps/kv/mix.exs 并添加到 project/0 函数中

build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
lockfile: "../../mix.lock",

现在,您可以使用 mix test 从伞形项目的根目录运行这两个项目的测试。很棒!

不要喝酷饮

伞形项目是一种便利,可以帮助您组织和管理多个应用程序。虽然它在应用程序之间提供了一定程度的分离,但这些应用程序并没有完全解耦,因为它们共享相同的配置和相同的依赖项。

在同一个存储库中保留多个应用程序的模式被称为“单体存储库”。伞形项目通过提供方便地一次编译、测试和运行多个应用程序来最大限度地利用这种模式。

如果您发现自己处于需要为同一依赖项在每个应用程序中使用不同配置或使用不同依赖项版本的情况,那么您的代码库很可能已经超出了伞形项目能够提供的范围。

好消息是,分解伞形项目非常简单,您只需要将应用程序从伞形项目 apps/ 目录中移出,并将项目的 mix.exs 文件更新为不再设置 build_pathconfig_pathdeps_pathlockfile 配置。您可以通过多种方式依赖于伞形项目外部的私有项目

  1. 将其移动到同一个存储库内的单独文件夹中,并使用路径依赖项指向它(单体存储库模式)
  2. 将存储库移动到单独的 Git 存储库中,并依赖于它
  3. 将项目发布到私有 Hex.pm 组织

总结

在本章中,我们深入了解了 Mix 依赖关系和伞形项目。虽然我们可以不依赖服务器运行 kv,但我们的 kv_server 直接依赖于 kv。通过将它们分解成单独的应用程序,我们在开发和测试方面获得了更多控制权。

在使用伞形应用程序时,在它们之间建立明确的边界非常重要。我们即将开发的 kv_server 只能访问在 kv 中定义的公共 API。将你的伞形应用程序视为任何其他依赖项,甚至视为 Elixir 本身:你只能访问公开且有文档记录的内容。访问依赖项中的私有功能是一种糟糕的做法,最终会导致你的代码在更新到新版本时出现问题。

伞形应用程序还可以用作从代码库中提取应用程序的垫脚石。例如,想象一个需要向用户发送“推送通知”的 Web 应用程序。整个“推送通知系统”可以作为伞形项目中的一个独立应用程序开发,具有自己的监督树和 API。如果你遇到了另一个项目需要推送通知系统的这种情况,可以将系统移至私有存储库或 Hex 包

最后,请记住,伞形项目中的应用程序共享相同的配置和依赖项。如果你的伞形项目中的两个应用程序需要以截然不同的方式配置相同的依赖项,甚至使用不同的版本,那么你可能已经超出了伞形项目带来的好处。请记住,你可以打破伞形项目,仍然可以利用“单一仓库”背后的优势。

我们的伞形项目已经启动并运行,现在该开始编写我们的服务器了。