使用方法
- 复制安装命令,在自己的智能体环境安装前先检查源码包。
- 从原始文档核对所需工具、凭据和支持的宿主。
- 说明输入材料与期望交付物,并对照来源检查输出。
npx skills add https://github.com/TerminalSkills/skills --skill phoenix开始前需了解
- 安装不包含模型订阅、API 额度或访问文件的权限。
- 仓库星数和安装量反映活跃情况,不等于独立质量或安全测试。
按源仓库内容呈现,保留标题、案例、代码、表格、链接以及原文引用的演示图片。
Phoenix
Overview
Phoenix leverages the Erlang VM (BEAM) for massive concurrency and fault tolerance. LiveView enables rich, real-time UIs with server-rendered HTML, with little custom JavaScript. This skill targets Phoenix 1.8 (1.8.16 checked) with LiveView 1.x and Ecto 3.
Instructions
Installation
mix archive.install hex phx_new # asks to confirm; add --force to skip the prompt
mix phx.new blog --database postgres # PostgreSQL is the default
cd blog
mix setup # deps.get, ecto.setup, assets.setup, assets.build
mix phx.server # http://localhost:4000 (dev server binds to localhost)On Linux install inotify-tools for live reload. The examples below use the app name my_app; substitute your own.
Ecto Schema and Migration
# lib/my_app/articles/article.ex — Ecto schema
defmodule MyApp.Articles.Article do
use Ecto.Schema
import Ecto.Changeset
schema "articles" do
field :title, :string
field :slug, :string
field :body, :string
field :published, :boolean, default: false
belongs_to :author, MyApp.Accounts.User
timestamps()
end
def changeset(article, attrs) do
article
|> cast(attrs, [:title, :body, :published, :author_id])
|> validate_required([:title, :body, :author_id])
|> validate_length(:title, max: 200)
|> unique_constraint(:slug)
|> generate_slug()
end
defp generate_slug(changeset) do
case get_change(changeset, :title) do
nil -> changeset
title -> put_change(changeset, :slug, slugify(title))
end
end
defp slugify(title) do
title |> String.downcase() |> String.replace(~r/[^a-z0-9]+/u, "-") |> String.trim("-")
end
end# priv/repo/migrations/20261024120000_create_articles.exs — migration
defmodule MyApp.Repo.Migrations.CreateArticles do
use Ecto.Migration
def change do
create table(:articles) do
add :title, :string, null: false, size: 200
add :slug, :string, null: false
add :body, :text, null: false
add :published, :boolean, default: false
add :author_id, references(:users, on_delete: :delete_all), null: false
timestamps()
end
create unique_index(:articles, [:slug])
end
endContext Module
# lib/my_app/articles.ex
defmodule MyApp.Articles do
import Ecto.Query
alias MyApp.Repo
alias MyApp.Articles.Article
def list_published do
Article
|> where(published: true)
|> order_by(desc: :inserted_at)
|> preload(:author)
|> Repo.all()
end
def get_published_by_slug!(slug) do
Article |> where(slug: ^slug, published: true) |> preload(:author) |> Repo.one!()
end
def search(term) do
Article
|> where([a], a.published and ilike(a.title, ^"%#{term}%"))
|> order_by(desc: :inserted_at)
|> preload(:author)
|> Repo.all()
end
def create_article(attrs) do
%Article{}
|> Article.changeset(attrs)
|> Repo.insert()
end
endRouter
# lib/my_app_web/router.ex — routing
defmodule MyAppWeb.Router do
use MyAppWeb, :router
# pipeline :browser (session, flash, root layout, CSRF) is generated by mix phx.new
pipeline :api do
plug :accepts, ["json"]
end
scope "/", MyAppWeb do
pipe_through :browser
live "/articles", ArticleLive.Index, :index
live "/articles/:slug", ArticleLive.Show, :show # Show: see mix phx.gen.live
end
scope "/api", MyAppWeb.API do
pipe_through :api
resources "/articles", ArticleController, only: [:index, :create]
end
endLiveView
# lib/my_app_web/live/article_live/index.ex
defmodule MyAppWeb.ArticleLive.Index do
use MyAppWeb, :live_view
alias MyApp.Articles
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, articles: Articles.list_published(), search: "")}
end
@impl true
def handle_event("search", %{"query" => query}, socket) do
{:noreply, assign(socket, articles: Articles.search(query), search: query)}
end
end<div>
<h1>Articles</h1>
<form phx-change="search">
<input type="text" name="query" value={@search} phx-debounce="300" placeholder="Search..." />
</form>
<div :for={article <- @articles}>
<h2><.link navigate={~p"/articles/#{article.slug}"}>{article.title}</.link></h2>
<p>By {article.author.email}</p>
</div>
</div>JSON Controller
# lib/my_app_web/controllers/api/article_controller.ex — API controller
defmodule MyAppWeb.API.ArticleController do
use MyAppWeb, :controller
alias MyApp.Articles
def index(conn, _params) do
json(conn, %{data: Enum.map(Articles.list_published(), &article_json/1)})
end
def create(conn, %{"article" => params}) do
case Articles.create_article(params) do
{:ok, article} -> conn |> put_status(:created) |> json(%{data: article_json(article)})
{:error, changeset} -> conn |> put_status(422) |> json(%{errors: format_errors(changeset)})
end
end
defp article_json(a), do: %{id: a.id, title: a.title, slug: a.slug}
defp format_errors(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
end
endChannels (Real-time)
# lib/my_app_web/channels/user_socket.ex — authenticate before any channel joins
defmodule MyAppWeb.UserSocket do
use Phoenix.Socket
channel "room:*", MyAppWeb.RoomChannel
@impl true
def connect(%{"token" => token}, socket, _connect_info) do
case Phoenix.Token.verify(MyAppWeb.Endpoint, "user socket", token, max_age: 86_400) do
{:ok, user_id} -> {:ok, assign(socket, :user_id, user_id)}
{:error, _} -> :error
end
end
def connect(_params, _socket, _connect_info), do: :error
@impl true
def id(socket), do: "user_socket:#{socket.assigns.user_id}"
end
# lib/my_app_web/channels/room_channel.ex
defmodule MyAppWeb.RoomChannel do
use MyAppWeb, :channel
def join("room:" <> _room_id, _params, socket), do: {:ok, socket}
def handle_in("message", %{"body" => body}, socket) do
broadcast!(socket, "message", %{body: body, user: socket.assigns.user_id})
{:noreply, socket}
end
endRegister the socket in endpoint.ex: socket "/socket", MyAppWeb.UserSocket, websocket: true, longpoll: false.
Testing
defmodule MyApp.ArticlesTest do
use MyApp.DataCase
import MyApp.AccountsFixtures # created by `mix phx.gen.auth Accounts User users`
test "create_article/1 builds a slug" do
attrs = %{title: "Launch notes", body: "Content", author_id: user_fixture().id}
assert {:ok, article} = MyApp.Articles.create_article(attrs)
assert article.slug == "launch-notes"
end
endKey Commands
mix phx.gen.live Articles Article articles title:string body:text generates a LiveView CRUD with migration and tests; mix ecto.migrate, mix phx.routes, mix test, iex -S mix phx.server (dev server with REPL), mix phx.gen.secret (value for SECRET_KEY_BASE).
Examples
Example 1: "Add a blog section with search"
Run mix phx.gen.auth Accounts User users for authors, add the schema, migration and context above, run mix ecto.migrate, and add the live routes. Typing in the search box sends a search event (debounced 300 ms) and the list re-renders without a page load.
Example 2: "Give me a JSON endpoint for mobile"
Add the /api scope and ArticleController. curl -s -X POST localhost:4000/api/articles -H 'content-type: application/json' -d '{"article":{"title":"Launch notes","body":"Hello","author_id":1}}' returns 201 with {"data":{"id":1,"title":"Launch notes","slug":"launch-notes"}}; a missing title returns 422 with {"errors":{"title":["can't be blank"]}}.
Guidelines
- Production config is read from
config/runtime.exs: setSECRET_KEY_BASE,DATABASE_URL,PHX_HOSTandPHX_SERVER=trueas environment variables; build withMIX_ENV=prod mix release. - Authenticate sockets and check authorization in every
handle_event/handle_in; clients can send any event name and payload. - Pass only whitelisted fields to
cast/3(nevercast(attrs, Map.keys(attrs))) and never cast privileged fields such asis_admin. - Keep the
:browserpipeline's CSRF protection; skip it only in:apipipelines that authenticate with tokens. - Use
phx-debounceon inputs that fire on every keystroke, andstream/3instead of assigning large lists to the socket.
Key Patterns
- Business logic lives in context modules; controllers and LiveViews only delegate to them.
- Validate through changesets; Ecto never lazy-loads, so
preloadexplicitly. - Use
Ecto.Multifor multi-step transactions andPhoenix.PubSub.broadcastto push updates across LiveView processes.

