Phoenix 1.7.0 发布:内置 Tailwind、验证路由、LiveView 流和未来展望
发布于 2023 年 2 月 24 日,作者 Chris McCord
Phoenix 1.7 的最终版本发布了!Phoenix 1.7 包含了许多期待已久的新功能,例如验证路由、Tailwind 支持、LiveView 身份验证生成器、统一的 HEEx 模板、LiveView 流用于优化集合等等。这是一个向后兼容的版本,包含一些弃用功能。大多数用户只需更改几个依赖项即可更新。
**注意**:要生成一个新的 1.7 项目,你需要从 hex 安装 phx.new
生成器
mix archive.install hex phx_new
验证路由
验证路由用基于符号的 (~p
)、编译时验证的方法取代了路由助手。
**注意**:验证路由利用了 Elixir 1.14 的新编译器功能。Phoenix 仍然支持旧版本的 Elixir,但你需要更新才能享受新的编译时验证功能。
实际上,这意味着之前你使用的是自动生成的函数,例如
# router
get "/oauth/callbacks/:id", OAuthCallbackController, :new
# usage
MyRouter.Helpers.o_auth_callback_path(conn, :new, "github")
# => "/oauth/callbacks/github"
MyRouter.Helpers.o_auth_callback_url(conn, :new, "github")
# => "http://localhost:4000/oauth/callbacks/github"
现在你可以使用
# router
get "/oauth/callbacks/:id", OAuthCallbackController, :new
# usage
~p"/oauth/callbacks/github"
# => "/oauth/callbacks/github"
url(~p"/oauth/callbacks/github")
# => "http://localhost:4000/oauth/callbacks/github"
这有许多优点。不再需要猜测哪个函数被词形转换了 - 是 Helpers.oauth_callback_path
还是 o_auth_callback_path
等等。你也不再需要在 99% 的时间你都知道应该使用哪个端点配置的情况下,在每个地方都包含 %Plug.Conn{}
或 %Phoenix.Socket{}
或端点模块。
现在,你编写的路由与使用 ~p
调用它们的方式之间存在一对一映射。你只需像在应用程序中到处硬编码字符串一样编写它 - 除了你不会遇到硬编码字符串带来的维护问题。我们可以在易用性和维护性方面获得双赢,因为 ~p
在编译时会针对路由器中的路由进行验证。
例如,假设我们输入了一个错误的路由
<.link href={~p"/userz/profile"}>Profile</.link>
编译器将在编译时将所有 ~p
与你的路由器进行比较,并在找不到匹配路由时通知你。
warning: no route path for AppWeb.Router matches "/postz/#{post}"
lib/app_web/live/post_live.ex:100: AppWeb.PostLive.render/1
动态的“命名参数”也像普通字符串一样简单地进行插值,而不是使用任意函数参数。
~p"/posts/#{post.id}"
此外,插值的 ~p
值通过 Phoenix.Param
协议进行编码。例如,应用程序中的 %Post{}
结构可以派生 Phoenix.Param
协议,以生成基于 slug 的路径,而不是基于 ID 的路径。这允许你使用 ~p"/posts/#{post}"
而不是 ~p"/posts/#{post.slug}"
在整个应用程序中。
查询字符串也支持验证路由,无论是传统的查询字符串形式
~p"/posts?page=#{page}"
还是作为关键字列表或值映射
params = %{page: 1, direction: "asc"}
~p"/posts?#{params}"
与路径段一样,查询字符串参数也经过适当的 URL 编码,可以直接插值到 ~p
字符串中。
一旦你尝试了这个新功能,你就不会再想使用路由助手了。新的 phx.gen.html|live|json|auth
生成器使用验证路由。
基于组件的 Tailwind 生成器
Phoenix 1.7 默认情况下包含 TailwindCSS,无需依赖系统上的 nodejs。在我 20 年的 Web 开发生涯中,TailwindCSS 是我发现的最佳界面样式工具。它的实用优先方法比我使用过的任何 CSS 系统或框架都更易于维护和高效。它的共同定位方法也与函数组件和 LiveView 环境完美契合。
Tailwind 团队还慷慨地设计了新项目登录页面、CRUD 页面和新项目的身份验证系统页面,为您提供一流且完善的应用程序构建起点。
一个新的 phx.new
项目将包含一个 CoreComponents
模块,其中包含一组核心 UI 组件,例如表格、模态框、表单和数据列表。Phoenix 生成器套件 (phx.gen.html|live|json|auth
) 利用这些核心组件。这有许多好处。
首先,你可以根据自己的需求、设计和喜好来定制核心 UI 组件。如果你想使用 Bulma 或 Bootstrap 而不是 Tailwind - 没问题!只需用你的框架/UI 特定实现替换 core_components.ex
中的函数定义,生成器将继续为新功能提供良好的起点,无论你是初学者还是经验丰富的专家,都可以构建定制的产品功能。
实际上,生成器会提供使用核心组件的模板,如下所示
<.header>
New Post
<:subtitle>Use this form to manage post records in your database.</:subtitle>
</.header>
<.simple_form for={@form} action={~p"/posts"}>
<input field={@form[:title]} type="text" label="Title" />
<input field={@form[:views]} type="number" label="Views" />
<:actions>
<.button>Save Post</.button>
</:actions>
</.simple_form>
<.back navigate={~p"/posts"}>Back to posts></.back>
我们非常喜欢 Tailwind 团队为新应用程序设计的内容,但我们也期待看到社区发布他们自己的针对各种框架的 core_components.ex
替换。
控制器和 LiveView 之间统一的函数组件
HEEx 提供的函数组件,具有声明式的赋值和插槽,是我们编写 Phoenix 项目中 HTML 的方式的一次重大变革。函数组件提供 UI 构建块,允许将功能封装起来,并在之前的 Phoenix.View
模板方法中更好地扩展。你将获得一种更自然的方式来编写动态标记,可重复使用的 UI 可被调用者扩展,以及编译时功能,使编写基于 HTML 的应用程序成为一种真正一流的体验。
函数组件带来了编写 Phoenix 中 HTML 应用程序的新方法,以及新的约定集。此外,用户一直都在努力将基于控制器的 Phoenix.View
功能与应用程序中的 Phoenix.LiveView
功能结合起来。用户发现自己会在基于控制器的模板中编写 render("table", user: user)
,而他们的 LiveView 则使用新的 <.table rows={@users}>
功能。没有很好的方法可以在应用程序中共享这些方法。
出于这些原因,Phoenix 团队统一了来自控制器请求或 LiveView 的 HTML 渲染方法。这种转变还使我们能够重新审视约定,并与将模板和应用程序代码放在一起的 LiveView 方法保持一致。
新的应用程序(以及 phx 生成器)将 Phoenix.View
作为依赖项删除,转而使用新的 Phoenix.Template
依赖项,该依赖项使用函数组件作为框架中所有渲染的基础。
你的控制器看起来仍然一样
defmodule AppWeb.UserController do
use MyAppWeb, :controller
def index(conn, _params) do
users = ...
render(conn, :index, users: users)
end
end
但是,控制器不再调用 AppWeb.UserView.render("index.html", assigns)
,现在我们首先会在视图模块中查找 index/1
函数组件,如果存在则调用它进行渲染。此外,我们还将词形转换后的视图模块重命名为 AppWeb.UserHTML
或 AppWeb.UserJSON
等等,以便对每个格式进行视图渲染。所有这些操作都以向后兼容的方式完成,并且是基于对 use Phoenix.Controller
的选项的可选行为。
然后,所有 HTML 渲染都基于函数组件,这些组件可以直接在模块中编写,或者使用 Phoenix.Component
提供的新 embed_templates
宏从外部文件嵌入。新应用程序中的 PageHTML
模块看起来像这样
defmodule AppWeb.PageHTML do
use AppWeb, :html
embed_templates "page_html/*"
end
新的目录结构看起来像这样
lib/app_wb
├── controllers
│ ├── page_controller.ex
│ ├── page_html.ex
│ ├── error_html.ex
│ ├── error_json.ex
│ └── page_html
│ └── home.html.heex
├── live
│ ├── home_live.ex
├── components
│ ├── core_components.ex
│ ├── layouts.ex
│ └── layouts
│ ├── app.html.heex
│ └── root.html.heex
├── endpoint.ex
└── router.ex
你现在基于控制器的渲染或基于 LiveView 的渲染都共享相同的函数组件和布局。无论是运行 phx.gen.html
、phx.gen.live
还是 phx.gen.auth
,新生成的模板都将使用你的 components/core_components.ex
定义。
此外,我们还将视图模块与它们的控制器文件放在一起。这带来了 LiveView 共同定位的相同好处 - 高度耦合的文件放在一起。必须一起更改的文件现在放在一起,无论编写的是 LiveView 功能还是控制器功能。
这些更改都是为了更好地编写基于 HTML 的应用程序,但也简化了渲染其他格式,例如 JSON。例如,基于 JSON 的视图模块遵循相同的约定 - Phoenix 首先会在渲染 index 模板时查找 index/1
函数,然后再尝试 render/2
。这使我们能够简化一般的 JSON 渲染,并摆脱诸如 Phoenix.View.render_one|render_many
之类的概念。
例如,这是由 phx.gen.json
生成的 JSON 视图
defmodule AppWeb.PostJSON do
alias AppWeb.Blog.Post
@doc """
Renders a list of posts.
"""
def index(%{posts: posts}) do
%{data: for(post <- posts, do: data(post))}
end
@doc """
Renders a single post.
"""
def show(%{post: post}) do
%{data: data(post)}
end
defp data(%Post{} = post) do
%{
id: post.id,
title: post.title
}
end
end
注意,它只是一个普通的 Elixir 函数 - 应该是这样!
这些功能为未来的应用程序提供了一个统一的渲染模型,并提供了一种新的改进的 UI 编写方式,但它们与之前的做法有所不同。大多数大型、成熟的应用程序最好继续依赖 Phoenix.View
。
LiveView 流
LiveView 现在包含一个流接口,用于在 UI 中管理大型集合,而无需在服务器上将集合存储在内存中。只需调用几个函数,你就可以将新项插入 UI,动态追加或预置,或者重新排序项,而无需在服务器上重新加载项。
Phoenix 1.7 中的 phx.gen.live
实时 CRUD 生成器使用流来管理你的项目列表。这允许数据输入、更新和删除,而无需在初始加载后重新获取项目列表。让我们看看它是如何实现的。
当你运行 mix phx.gen.live Blog Post posts title views:integer
时,会生成以下 PostLive.Index
模块。
defmodule DemoWeb.PostLive.Index do
use DemoWeb, :live_view
alias Demo.Blog
alias Demo.Blog.Post
@impl true
def mount(_params, _session, socket) do
{:ok, stream(socket, :posts, Blog.list_posts())}
end
...
end
注意,我们不是使用普通的 assign(socket, :posts, Blog.list_posts())
,而是使用了新的 stream/3
接口。这将使用初始帖子集合设置流。然后在生成的 index.html.heex
模板中,我们使用流来渲染帖子表格
<.table
id="posts"
rows={@streams.posts}
row_click={fn {_id, post} -> JS.navigate(~p"/posts/#{post}") end}
>
<:col :let={{_id, post}} label="Title"><%= post.title %></:col>
<:col :let={{_id, post}} label="Views"><%= post.views %></:col>
<:action :let={{_id, post}}>
<div class="sr-only">
<.link navigate={~p"/posts/#{post}"}>Show</.link>
</div>
<.link patch={~p"/posts/#{post}/edit"}>Edit</.link>
</:action>
<:action :let={{id, post}}>
<.link
phx-click={JS.push("delete", value: %{id: post.id}) |> hide("##{id}")}
data-confirm="Are you sure?"
>
Delete
</.link>
</:action>
</.table>
这看起来与旧模板非常相似,但我们不是访问裸露的 @posts
赋值,而是将 @stream.posts
传递给我们的表格。使用流时,我们也会被传递流的 DOM ID 和项目。
回到服务器,我们可以看到将新项目插入表格是多么简单。当我们生成的 FormComponent
通过表单更新或保存帖子时,我们会向父级 PostLive.Index
LiveView 发送一个消息包,告知新帖子或更新的帖子
PostLive.FormComponent
:
defmodule DemoWeb.PostLive.FormComponent do
...
defp save_post(socket, :new, post_params) do
case Blog.create_post(post_params) do
{:ok, post} ->
notify_parent({:saved, post})
{:noreply,
socket
|> put_flash(:info, "Post created successfully")
|> push_patch(to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign_form(socket, changeset)}
end
end
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
end
然后,我们在 PostLive.Index
handle_info
子句中接收该消息
@impl true
def handle_info({DemoWeb.PostLive.FormComponent, {:saved, post}}, socket) do
{:noreply, stream_insert(socket, :posts, post)}
end
因此,表单告诉我们它保存了一个帖子,我们只需在流中 stream_insert
帖子。就是这样!如果帖子已存在于 UI 中,它将在适当的位置更新。否则,它将默认追加到容器中。你也可以使用 stream_insert(socket, :posts, post, at: 0)
进行预置,或将任何索引传递给 :at
以进行任意项目插入或重新排序。
流是我们迈向 LiveView 1.0 的最后一块拼图之一,我很高兴我们最终实现了它。
新的表单字段数据结构
我们都熟悉 <.form for={@changeset}>
的 Phoenix.HTML 表单原语,其中表单接受实现 Phoenix.HTML.FormData
协议的数据结构,并返回 %Phoenix.HTML.Form{}
。我们方法的一个问题是表单数据结构无法跟踪单个表单字段的更改。这使得 LiveView 中的优化变得不可能,因为我们必须在任何单个更改时重新渲染并重新发送表单。随着 Phoenix.HTML.FormData.to_form
和 Phoenix.Component.to_form
的引入,我们现在有了 %Phoenix.HTML.FormField{}
数据结构来处理单个字段更改。
新的 phx.gen.live
生成器和您的 core_components.ex
利用了这些新添加的功能。
Phoenix 和 LiveView 的下一步
Phoenix 生成器利用了 LiveView 的最新功能,并且会继续扩展。使用流式集合作为默认值,我们可以为 phx.gen.live
中的实时 CRUD 生成器提供更高级的开箱即用功能。例如,我们计划为资源引入同步 UI。生成的 Phoenix 表单功能将随着新的 to_form
接口的添加而继续发展。
对于 LiveView,to_form
允许我们提供优化的表单基础。现在,对一个表单字段的单个更改将产生一个优化的差异。
在完成此优化工作后,LiveView 1.0 的主要剩余功能是扩展的表单 API,它更好地支持动态表单输入、向导式表单以及将表单输入委派给子 LiveComponents。
替代 Web 服务器支持
多亏了 Mat Trudel 的努力,我们现在在 Plug 和 Phoenix 中有了对一流 Web 服务器的支持,允许在 Phoenix 中使用其他 Web 服务器(如 Bandit),同时享受 WebSockets、Channels 和 LiveView 等所有功能。如果您对纯 Elixir HTTP 服务器感兴趣,请继续关注 Bandit 项目,或者在您自己的 Phoenix 项目中尝试一下!
下一步
与往常一样,分步升级指南 将帮助您将现有的 1.6.x 应用程序升级到 1.7。
完整的更改日志可以在 此处找到。
如果您遇到问题,请在 elixir slack 或 论坛 上联系我们。
编码愉快!
–Chris