Mix 构建工具速通
Elixir 的官方构建工具,对比你熟知的 Clojure Leiningen 和 Java Maven。
这节课回答三个问题:
- Mix 是什么、项目结构长什么样
- 常用命令(compile / test / deps / release / format / credo)
- 跟 Lein / Maven 一一对应(从你已经会的快速迁移心智模型)
所有例子都来自 ex/elixir/ 真实项目。
**预计时间:**45 分钟。
project.clj + lein new + lein deps)或 Java 的 Maven(pom.xml + mvn package)。本课不再讲"为什么需要构建工具",只讲 Elixir 的 Mix 特殊点。一、Mix 是什么?
Mix 是 Elixir / Erlang 生态的官方构建工具,类似:
| 生态 | 工具 | 配置文件 |
|---|---|---|
| Elixir / Erlang | Mix | mix.exs(Elixir 脚本) |
| Clojure | Leiningen | project.clj(Clojure 脚本) |
| Java | Maven | pom.xml(XML) |
| Java | Gradle | build.gradle(Groovy/Kotlin DSL) |
| Node.js | npm / yarn / pnpm | package.json(JSON) |
| Rust | Cargo | Cargo.toml(TOML) |
| Go | (无)go mod | go.mod |
**关键直觉:**Mix 的 mix.exs 是 Elixir 代码(不是 XML/JSON/TOML)——因为它本身是 Elixir 脚本,能写任意逻辑(条件、宏、调用函数)。
二、Mix 创建的项目结构
运行 mix new my_app 生成:
my_app/
├── mix.exs # 项目配置(依赖、版本、aliases)
├── README.md
├── .gitignore
├── lib/
│ ├── my_app.ex # 主模块(默认一个 module)
│ └── my_app/
│ └── ... # 子模块
├── test/
│ ├── test_helper.exs # 测试公共配置
│ └── my_app_test.exs # 一个测试文件
└── config/
├── config.exs # 共享配置
├── dev.exs # dev 环境配置
├── test.exs # test 环境配置
└── prod.exs # prod 环境配置
对比 Lein / Maven:
| 目录 | Mix (Elixir) | Lein (Clojure) | Maven (Java) |
|---|---|---|---|
| 源码 | lib/ | src/ | src/main/java/ |
| 测试 | test/ | test/ | src/test/java/ |
| 资源 | priv/ | resources/ | src/main/resources/ |
| 配置 | config/*.exs | project.clj 里的 :profiles | src/main/resources/ + filter |
| 依赖 | deps/(自动管理) | .m2/(Maven 本地仓) | .m2/(Maven 本地仓) |
| 编译产物 | _build/ | target/ | target/ |
**关键直觉:**Mix 自动建 _build/,类似 Lein/Maven 的 target/,应该 gitignore。
三、Mix.exs 全解剖
项目 ex/elixir/mix.exs 全文:
“
defmodule Pipeline.MixProject do
use Mix.Project
def project do
[
app: :pipeline, # 应用名(atop 上也是这个名字)
version: "0.1.0",
elixir: "~> 1.18", # Elixir 版本要求
start_permanent: Mix.env() == :prod, # prod 启动失败时整个 VM 崩溃(vs dev 抛异常)
deps: deps(), # 函数返回依赖列表
elixirc_paths: elixirc_paths(Mix.env()), # 不同 env 编不同目录
test_paths: ["test"]
]
end
# 测试与 dev 额外编译 test/support
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
def application do
[
extra_applications: [:logger], # 启动时额外跑的 OTP app
mod: {Pipeline.Application, []} # OTP Application 入口模块
]
end
defp deps do
[
{:oban, "~> 2.23"},
{:ecto_sql, "~> 3.14"},
{:postgrex, "~> 0.19"},
{:pgvector, "~> 0.4"},
{:finch, "~> 0.23"},
{:jason, "~> 1.4"},
{:lang_ex, "~> 0.11"},
{:plug, "~> 1.16"},
{:bandit, "~> 1.6"},
{:redix, "~> 1.5"}
]
end
end
三段式:project/0(项目元信息)、application/0(运行时行为)、deps/0(依赖列表)。 对比: | 段 | Mix | Lein | Maven | | --- | --- | --- | --- | | 元信息 | project/0 返回 keyword list | defproject 宏 | <project><groupId/><artifactId/>...</project> | | 依赖 | deps/0 返回 {:pkg, "~> ver"} 列表 | :dependencies [...] | <dependencies> XML | | 运行时 | application/0(mod: 指向 Application 模块) | :main ns.symbol | <build><plugins> 配置 main class | **关键直觉:**Mix 的 mod: {Pipeline.Application, []} 就是”启动时调这个 OTP Application 回调”——比 Java 的 public static void main(String[]) 更结构化(详见 0002)。 ## 四、最常用 12 个 Mix 命令 每个命令给出 + 对应的 Lein / Maven 命令: | 用途 | Mix | Lein | Maven | | --- | --- | --- | --- | | 新建项目 | mix new my_app | lein new app my-app | mvn archetype:generate -DarchetypeArtifactId=maven-archetype-quickstart -DgroupId=com.x -DartifactId=my-app | | 下载依赖 | mix deps.get | lein deps | mvn dependency:resolve | | 编译 | mix compile | lein compile | mvn compile | | 清理 | mix clean | lein clean | mvn clean | | 运行测试 | mix test | lein test | mvn test | | 启动 REPL | iex -S mix | lein repl | (无内置) | | 运行单文件 | mix run my_script.exs | lein run -m my.ns | mvn exec:java -Dexec.mainClass=... | | 生成 release | mix release | lein uberjar | mvn package | | 格式化 | mix format | (无内置) | mvn spotless:apply(插件) | | 代码质量 | mix credo | lein kibit | mvn checkstyle:check | | 静态类型 | mix dialyzer | lein typed | mvn compile(编译期检查) | | DB 迁移(Ecto) | mix ecto.migrate | lein migratus | mvn flyway:migrate |
- 全新跑项目:
mix deps.get && mix compile && mix test - 开发循环:
iex -S mix(REPL + 加载所有依赖) - 生产部署:
MIX_ENV=prod mix release(生成自包含目录)
mix release 是 Elixir 部署的核心,会生成自包含的目录:
MIX_ENV=prod mix release
# 产物在 _build/prod/rel/my_app/
_build/prod/rel/my_app/
├── bin/
│ ├── my_app # 启动脚本(前台 / daemon / remote_console)
│ └── my_app.bat # Windows 启动
├── lib/ # 所有依赖 + VM 编译产物
├── releases/
│ ├── 0.1.0/
│ │ ├── my_app.bat
│ │ ├── my_app.sh
│ │ └── runtime.exs # prod-only 启动配置
│ └── COOKIE
└── ...
vs Lein uberjar / Maven package: | 维度 | mix release | lein uberjar | mvn package | | --- | --- | --- | --- | | 产物 | 目录(bin + lib + releases) | 单一 fat JAR | JAR / WAR | | 包含 BEAM VM? | 可选(混编模式自动嵌入) | N/A(JVM 已装) | N/A | | 运行时 | ./bin/my_app start | java -jar my-app.jar | java -jar my-app.jar | | 远程 console | ./bin/my_app remote(直连 prod 进程) | lein repl :connect | jmx / 调试器 | | 配置注入 | runtime.exs(启动时读 env) | profiles.clj | Spring profile / -Dkey=value | | 冷启动 | 2-3 秒(含 VM) | 3-10 秒(仅应用) | 3-10 秒 |
这是 Elixir + Vultr "按需起 VM" 架构能 work 的技术基础(详见 extensions-serverless-architecture.md)。
mix my_task:
# lib/mix/tasks/deploy.ex
defmodule Mix.Tasks.Deploy do
use Mix.Task
@shortdoc "Deploy to staging via cloud-init"
def run(args) do
Application.ensure_all_started(:pipeline)
# 读环境变量
env = System.get_env("MIX_ENV", "dev")
snapshot_id = System.get_env("VULTR_SNAPSHOT_ID")
# 调 Vultr API
case Vultr.spin_up(snapshot_id, env) do
{:ok, vm} ->
Mix.shell().info("✓ Deployed: #{vm.label} (#{vm.ip})")
{:error, reason} ->
Mix.raise("✗ Deploy failed: #{inspect(reason)}")
end
end
end
# 用法
# mix deploy MIX_ENV=prod VULTR_SNAPSHOT_ID=abc123
关键点: - 文件放 lib/mix/tasks/your_task.ex,模块名 Mix.Tasks.YourTask - 执行 mix your_task(下划线 不用连字符) - 可以 Mix.shell().info/1 输出、Mix.raise/1 抛错、Mix.shell().prompt/1 交互 对比 Lein 的 :hooks / Maven 的 maven-antrun-plugin:更轻量、无需写 plugin descriptor。 ## 七、配置系统(config/*.exs) Elixir 的配置分两层——这点和 Lein 的 profile 不一样,要特别注意: ### 7.1 编译时配置(config.exs) config/config.exs 在编译期被读取,Application.compile_env/2 把值烧进编译产物。改完需要重新 mix compile。
# config/config.exs
import Config
config :pipeline, web_port: 8002
config :pipeline, schema_prefix: ""
# 在 lib/pipeline/feishu_message.ex
@schema_prefix Application.compile_env(:pipeline, :schema_prefix, "") <> "public"
7.2 运行时配置(runtime.exs) release 启动时读 releases/0.1.0/runtime.exs,从环境变量取配置:
# _build/prod/rel/pipeline/releases/0.1.0/runtime.exs
import Config
config :pipeline, llm_url: System.get_env("LLM_URL")
config :pipeline, llm_key: System.get_env("LLM_KEY")
config :pipeline, database_url: System.get_env("DATABASE_URL")
关键原则(Elixir 1.11+ 强制): - 编译时配置 → config/*.exs,只放”不依赖环境的常量” - 运行时配置 → runtime.exs,所有 secrets、URL、key 放这里 - 代码里用 Application.get_env/2,3 或 Application.fetch_env!/2 读运行时值 对比: | 时点 | Elixir | Java Spring | | --- | --- | --- | | 编译时 | config/config.exs + compile_env | application.yml + @Value(编译时占位) | | 运行时 | runtime.exs + env var | 环境变量 + ${ENV_VAR:default} |
compile_env 在 test 隔离场景特别有用:项目里 @schema_prefix Application.compile_env(:pipeline, :schema_prefix, "") <> "public" 在 MIX_ENV=test mix compile 时就烧成 "test_public",测试数据不会污染 prod 表。defp deps do
[
{:oban, "~> 2.23"}, # 兼容 2.23.x(Patch 版本可升)
{:ecto_sql, "~> 3.14"},
{:postgrex, "~> 0.19"},
{:pgvector, "~> 0.4"},
{:finch, "~> 0.23"},
{:jason, "~> 1.4"},
{:lang_ex, "~> 0.11"},
{:plug, "~> 1.16"},
{:bandit, "~> 1.6"},
{:redix, "~> 1.5"},
# Hex 上找不到的,可指定 git:
# {:my_lib, git: "https://github.com/me/my_lib.git", branch: "main"},
# 或本地 path:
# {:my_lib, path: "../my_lib"}
]
end
版本约束语法: | 写法 | 允许范围 | 类比 Lein/Maven | | --- | --- | --- | | "~> 2.23" | 2.23.0 ≤ ver < 3.0 | Lein [2.23]、Maven [2.23,3.0) | | "~> 2.23.0" | 2.23.0 ≤ ver < 2.24 | Maven [2.23.0,2.24.0) | | "2.23.0" | 只能 2.23.0 | Maven [2.23.0] | | ">= 2.23.0" | 2.23.0 及以上 | Maven [2.23.0,) | **锁文件 mix.lock:**类似 Lein 的 :pin 或 Maven 的 dependency:resolve-plugin.lock,记录实际装的版本。应该 git commit,保证团队/CI 装到一样版本。 ## 九、3 个推荐工具(必装) ### 9.1 mix format(项目根 .formatter.exs)
# .formatter.exs
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
line_length: 120
]
类似 gofmt / prettier / clang-format。mix format 改文件,mix format --check-formatted 只检查(CI 必跑)。 ### 9.2 mix credo(代码质量 lint)
# mix.exs deps
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}
# 用法
mix credo # 跑所有检查
mix credo --strict # 严格模式
mix credo MyApp.SomeMod # 跑单个模块
类似 Java 的 Checkstyle / PMD / SpotBugs。Elixir 项目默认必装。 ### 9.3 mix dialyzer(静态类型检查)
# mix.exs deps
{:dialyxir, "~> 1.4", only: [:dev], runtime: false}
# 用法(首次很慢,要建 PLT)
mix dialyzer
# 跑完会报告 type mismatch、unused functions 等
类似 TypeScript 的 tsc --noEmit、Haskell 的 GHC type check。Elixir 动态类型,但 @spec 注释让 dialyzer 能静态推断。 ## 十、Mix 的 4 个特殊概念(Lein/Maven 没有的) ### 10.1 Mix env(环境) 三环境::dev / :test / :prod。通过 MIX_ENV=... 切换:
MIX_ENV=prod mix compile
MIX_ENV=test mix test
MIX_ENV=dev iex -S mix
作用: - 不同环境加载不同 config/{env}.exs - Mix.env() == :prod 时 start_permanent: true → 启动失败直接崩 VM(vs dev 时抛异常) - 代码里 if Mix.env() == :test, do: ... 可写测试专用逻辑(不推荐滥用) ### 10.2 Mix.Project callbacks 你的 mix.exs 必须 use Mix.Project 并实现 3 个 callback(project/0、application/0、deps/0)。这是强制的,编译器知道你的项目需要哪些字段。 ### 10.3 Mix.Config vs Config(API 改名史) Elixir 1.9 之前用 Mix.Config,1.9+ 改用 import Config。你看到的老项目用 use Mix.Config 是历史代码。 ### 10.4 mix new 的 4 种类型
mix new my_app # 普通 library
mix new my_app --sup # 带 Supervisor 模板(OTP 入口)
mix new my_app --umbrella # umbrella 项目(多 app 单仓)
mix new my_app --module MyApp # 指定主模块名
—sup 直接生成 OTP 项目骨架;umbrella 用于微服务(多个 app 共享 deps/)。 ## 十一、测验(12 道)
Mix 的项目配置文件是?
mix deps.get 对应 Maven 什么?
Mix release 的产物是?
启动 Mix release 的 prod 应用的命令?
MIX_ENV=prod mix release 生成的 release 里,运行时配置(secrets 等)从哪读?
~> 2.23 表示什么版本范围?
mix new my_app --sup 比 mix new my_app 多了什么?
mix.lock 应该?
Application.compile_env(:pipeline, :schema_prefix, "") 跟 Application.get_env(:pipeline, :schema_prefix, "") 区别?
mix format --check-formatted 用途?
Mix.exs 三段式是?
mix format 对应 Lein / Maven 的什么?
R1. 读下面 mix.exs,MIX_ENV=prod mix release 后启动 ./bin/pipeline start,Llm URL 从哪读?
R2. 读下面 deps,运行 mix deps.get 装的是哪个版本的 bandito?