使用PETAL(Phoenix、Elixir、TailwindCSS、AlpineJS、LiveView)技术栈构建一个简化版的Instagram Web应用程序
更好的学习方法是亲自动手构建东西,让我们使用很棒的 PETAL(Phoenix、Elixir、TailwindCSS、AlpineJS、LiveView)堆栈构建一个简化版的 Instagram Web 应用程序,并深入了解函数式的黑暗世界编程和最热门的孩子在凤凰框架与LiveView。
我不认为自己是一名老师,也不是任何方面的专家,我只是一个像你一样的普通人。任何人都可以遵循,即使您可能会被整个堆栈吓倒,这是一种新技术,不是很流行,而且没有很多资源和材料。如果您是一位经验丰富的开发人员,那么您不会有任何问题,这并不意味着如果您是初学者,您就无法跟上,我会尽力使其对初学者友好,但我不会详细介绍堆栈的每个基础知识或网络开发,所以你已经被警告了。
Elixir 是我有幸学习和尝试的最好的语言之一,我想与世界分享我的热情,我希望其他人能感受到我对这门语言的感受。
免责声明: Elixir、函数式编程、Phoenix 框架,可能听起来、看起来很困难和复杂,但它根本不是,比其他任何东西都容易,它可能不适合每个人,因为我们的想法并不相同,但对于那些认为就像我尝试的感觉一样。TailwindCSS 可能有些固执己见,看起来不值得尝试,我知道,因为我也是这么感觉的,但只要尝试一下,你用得越多,它就会变得越有意义,你就会越喜欢它,它让 CSS 变得不复杂,让你不放弃前端开发,CSS仍然会很痛苦,作为开发者,我们没有耐心把UI弄好,但它是一股新鲜空气。
我们不会在本文中完成整个项目,这将是一系列文章,因此这将是第 1 部分。我假设您有自己的开发环境,安装了 Elixir,我的开发环境是在带有 WSL 的 Windows 10 上。我们将尽力尽可能详细,但保持简单,这仅用于学习目的,因此它不会是精确的副本,也不会具有所有功能,我们将尽可能接近真实的东西,也我们不会专注于使网站具有响应能力,我们只会使其适用于大屏幕。
让我们首先转到终端并使用 LiveView 创建一个新的 Phoenix 应用程序。
$ mix phx.new instagram_clone --live
安装并获取所有依赖项后。
$ cd instagram_clone && mix ecto.create
我创建了一个 GitHub 存储库,您可以在此处访问Instagram 克隆 GitHub 存储库,您可以随意使用代码,欢迎贡献。
让我们运行服务器以确保一切正常。
$ iex -S mix phx.server
如果没有错误,当您访问 http://localhost:4000/ 时,您应该会看到默认的 Phoenix 框架主页
我使用 Visual Studio Code,因此我将使用以下命令打开项目文件夹。
$ code .
现在让我们在 mix.exs 文件中添加混合依赖项。
elixir
# mix.exs file
defp deps do
[
{:phoenix, "~> 1.5.6"},
{:phoenix_ecto, "~> 4.1"},
{:ecto_sql, "~> 3.4"},
{:postgrex, ">= 0.0.0"},
{:floki, ">= 0.27.0", only: :test},
{:phoenix_html, "~> 2.11"},
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:phoenix_live_dashboard, "~> 0.3 or ~> 0.2.9"},
{:telemetry_metrics, "~> 0.4"},
{:telemetry_poller, "~> 0.4"},
{:gettext, "~> 0.11"},
{:jason, "~> 1.0"},
{:plug_cowboy, "~> 2.0"},
{:phoenix_live_view, "~> 0.15.4", override: true},
{:timex, "~> 3.6"},
{:faker, "~> 0.16.0"}
]
end
我们将 :phoenix_live_view 更新到 15.4 版本,并添加了 timex 来处理时间,并在需要测试数据时添加 faker。
设置 TailwindCSS 和 AlpineJS
确保拥有最新的 Node 和 npm 版本。
$ cd assets
$ npm i tailwindcss postcss autoprefixer postcss-loader@4.2 --save-dev
接下来让我们配置 Webpack、PostCSS 和 TailwindCSS。
elixir
// /assets/webpack.config.js
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
'postcss-loader', // Add this
],
添加包含/assets/postcss.config.js
以下内容的文件:
elixir
// /assets/postcss.config.js
module.exports = {
plugins: {
"postcss-import": {},
tailwindcss: {},
autoprefixer: {}
}
}
创建 TailwindCSS 配置文件。
$ npx tailwindcss init
将以下配置添加到该文件中:
elixir
const colors = require('tailwindcss/colors')
module.exports = {
purge: {
enabled: process.env.NODE_ENV === "production",
content: [
"../lib/**/*.eex",
"../lib/**/*.leex",
"../lib/**/*_view.ex"
],
options: {
whitelist: [/phx/, /nprogress/]
}
},
theme: {
extend: {
colors: {
'light-blue': colors.lightBlue,
cyan: colors.cyan,
},
},
},
variants: {
extend: {
borderWidth: ['hover'],
}
},
plugins: [require('@tailwindcss/forms')],
}
我们配置要清除的文件,添加自定义颜色和自定义表单插件。现在让我们将自定义表单添加到 npm 依赖项中。
$ npm i @tailwindcss/forms --save-dev
对于自定义组件冲突,让我们添加 postcss-import 插件。
$ npm i postcss-import --save-dev
转到/assets/css/app.scss
文件顶部并添加以下内容:
elixir
/* This file is for your main application css. */
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
删掉/assets/css/phoenix.css
我们就不需要了
让我们退出资产文件夹$ cd ..
并运行我们的服务器$ iex -S mix phx.server
通过/lib/instagram_clone_web/live/page_live.html.leex
删除所有内容并添加以下内容来测试它:
elixir
<h1 class="text-red-500 text-5xl font-bold text-center">Instagram Clone</h1>
我们的主页上应该有一个红色的大标题。
删除/lib/instagram_clone_web/live/page_live.ex
所有内容,因为我们的主页中不需要任何内容,然后添加以下内容:
elixir
defmodule InstagramCloneWeb.PageLive do
use InstagramCloneWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
end
去/lib/instagram_clone_web/templates/layout/root.html.leex
删除默认的phoenix标头,该文件上应该有以下内容:
elixir
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<%= csrf_meta_tag() %>
<%= live_title_tag assigns[:page_title] || "InstagramClone", suffix: " · Phoenix Framework" %>
<link phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
<script defer phx-track-static type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
</head>
<body>
<!-- Remove Everything Above Here -->
<%= @inner_content %>
</body>
</html>
现在让我们用 tailwind 自定义我们的主容器,转到/lib/instagram_clone_web/templates/layout/live.html.leex
主标签并将以下类添加到主标签中:
elixir
<main role="main" class="container mx-auto max-w-full md:w-11/12 2xl:w-6/12 pt-24"> <!-- This the class that we added -->
<p class="alert alert-info" role="alert"
phx-click="lv:clear-flash"
phx-value-key="info"><%= live_flash(@flash, :info) %></p>
<p class="alert alert-danger" role="alert"
phx-click="lv:clear-flash"
phx-value-key="error"><%= live_flash(@flash, :error) %></p>
<%= @inner_content %>
</main>
添加 AlpineJS
TailwindCSS 准备就绪后,让我们添加 AlpineJS。让我们再次进入我们的$ cd assets
文件夹并运行以下命令:
$ npm i alpinejs@2.8.2
打开找到的app.js文件/assets/js/app.js
并添加以下内容,这样我们就不会与LiveView自己的DOM修补发生任何冲突:
elixir
import Alpine from "alpinejs"
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
params: { _csrf_token: csrfToken },
dom: {
onBeforeElUpdated(from, to) {
if (from.__x) { Alpine.clone(from.__x, to) }
}
}
})
让我们退出资产文件夹$ cd ..
并运行我们的服务器$ iex -S mix phx.server
测试一下购买并将/lib/instagram_clone_web/live/page_live.html.leex
以下内容添加到我们的文件顶部:
elixir
<div x-data="{ open: false }">
<button @click="open = true">Open Dropdown</button>
<ul
x-show="open"
@click.away="open = false"
>
Dropdown Body
</ul>
</div>
如果我们进入主页,我们应该有一个可点击的下拉菜单,如下例所示:
Phx.Gen.Auth
完成这些设置后,真正的乐趣就开始了。让我们使用 phx.gen.auth 包添加用户身份验证。
让我们将包添加到我们的mix.exs
文件中。
elixir
defp deps do
[
{:phoenix, "~> 1.5.6"},
{:phoenix_ecto, "~> 4.1"},
{:ecto_sql, "~> 3.4"},
{:postgrex, ">= 0.0.0"},
{:floki, ">= 0.27.0", only: :test},
{:phoenix_html, "~> 2.11"},
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:phoenix_live_dashboard, "~> 0.3 or ~> 0.2.9"},
{:telemetry_metrics, "~> 0.4"},
{:telemetry_poller, "~> 0.4"},
{:gettext, "~> 0.11"},
{:jason, "~> 1.0"},
{:plug_cowboy, "~> 2.0"},
{:phoenix_live_view, "~> 0.15.4", override: true},
{:timex, "~> 3.6"},
{:faker, "~> 0.16.0"},
{:phx_gen_auth, "~> 0.7", only: [:dev], runtime: false}
]
end
安装并编译依赖项
$ mix do deps.get, deps.compile
使用以下命令安装身份验证系统:
$ mix phx.gen.auth Accounts User users
生成所有文件后运行以下命令:
$ mix deps.get && mix ecto.migrate
现在我们需要通过运行以下命令向用户表添加一些字段:
$ mix ecto.gen.migration add_to_users_table
然后打开生成的文件priv/repo/migrations/20210409223611_add_to_users_table.exs
并添加以下内容:
elixir
defmodule InstagramClone.Repo.Migrations.AddToUsersTable do
use Ecto.Migration
def change do
alter table(:users) do
add :username, :string
add :full_name, :string
add :avatar_url, :string
add :bio, :string
add :website, :string
end
end
end
然后$ mix ecto.migrate
接下来打开lib/instagram_clone/accounts/user.ex
并将以下内容添加到您的用户架构中:
elixir
field :username, :string
field :full_name, :string
field :avatar_url, :string, default: "/images/default-avatar.png"
field :bio, :string
field :website, :string
下载上面的默认头像图像并将其重命名为default-avatar.png
并将该图像添加到priv/static/images
现在我们需要为新用户架构添加验证,因此lib/instagram_clone/accounts/user.ex
再次打开并将其更改registration_changeset
为以下内容:
elixir
def registration_changeset(user, attrs, opts \\ []) do
user
|> cast(attrs, [:email, :password, :username, :full_name, :avatar_url, :bio, :website])
|> validate_required([:username, :full_name])
|> validate_length(:username, min: 5, max: 30)
|> validate_format(:username, ~r/^[a-zA-Z0-9_.-]*$/, message: "Please use letters and numbers without space(only characters allowed _ . -)")
|> unique_constraint(:username)
|> validate_length(:full_name, min: 4, max: 30)
|> validate_email()
|> validate_password(opts)
end
此外,我们需要更改我们的validate_password
函数,以便在更新用户帐户时,我们不需要验证或散列密码,因此将其更改为以下内容:
elixir
defp validate_password(changeset, opts) do
register_user? = Keyword.get(opts, :register_user, true)
if register_user? do
changeset
|> validate_required([:password])
|> validate_length(:password, min: 6, max: 80)
# |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character")
# |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character")
# |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
|> maybe_hash_password(opts)
else
changeset
end
end
更新用户帐户时,我们将向register_user: false
变更集发送一个选项。此外,出于开发目的,最小密码长度已更改为 6,但应在生产中更改。
让我们运行我们的服务器$ iex -S mix phx.server
并打开/lib/instagram_clone_web/live/page_live.html.leex
我们的主页样式以添加注册表单。
在执行此操作之前,我们必须删除 phx.gen.auth 自动生成的身份验证链接,因此请转到正文顶部/lib/instagram_clone_web/templates/layout/root.html.leex
并将其删除。<%= render "_user_menu.html", assigns %>
elixir
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<%= csrf_meta_tag() %>
<%= live_title_tag assigns[:page_title] || "InstagramClone", suffix: " · Phoenix Framework" %>
<link phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
<script defer phx-track-static type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
</head>
<body>
<%= render "_user_menu.html", assigns %><!-- REMOVE IT -->
<%= @inner_content %>
</body>
</html>
最后,删除/lib/instagram_clone_web/templates/layout/_user_menu.html.eex
部分文件,我们不需要它。
好的,现在回来/lib/instagram_clone_web/live/page_live.html.leex
添加以下内容:
elixir
<section class="w-1/2 border-2 shadow-lg flex flex-col place-items-center mx-auto p-6">
<h1 class="text-4xl font-bold italic text-gray-600">InstagramClone</h1>
<p class="text-gray-400 font-semibold text-lg my-6">Sign up to see photos and videos from your friends.</p>
</section>
我们需要添加一个表单,因此将/lib/instagram_clone_web/live/page_live.ex
mount 函数更改为以下内容:
elixir
alias InstagramClone.Accounts
alias InstagramClone.Accounts.User
@impl true
def mount(_params, _session, socket) do
changeset = Accounts.change_user_registration(%User{})
{:ok,
socket
|> assign(changeset: changeset)}
end
让我们通过编辑来添加表单和新样式/lib/instagram_clone_web/live/page_live.html.leex
:
elixir
<section class="w-1/2 border-2 shadow flex flex-col place-items-center mx-auto p-6">
<h1 class="text-4xl font-bold italic text-gray-700">InstagramClone</h1>
<p class="text-gray-500 font-semibold text-lg mt-6 text-center px-8">Sign up to see photos and videos from your friends.</p>
<%= f = form_for @changeset, "#",
phx_change: "validate",
phx_submit: "save",
phx_trigger_action: @trigger_submit,
class: "flex flex-col space-y-4 w-full px-6" %>
<div class="flex flex-col">
<%= label f, :email, class: "text-gray-400" %>
<%= email_input f, :email, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
<%= error_tag f, :email, class: "text-red-700 text-sm" %>
</div>
<div class="flex flex-col">
<%= label f, :full_name, class: "text-gray-400" %>
<%= text_input f, :full_name, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
<%= error_tag f, :full_name, class: "text-red-700 text-sm" %>
</div>
<div class="flex flex-col">
<%= label f, :username, class: "text-gray-400" %>
<%= text_input f, :username, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
<%= error_tag f, :username, class: "text-red-700 text-sm" %>
</div>
<div class="flex flex-col">
<%= label f, :password, class: "text-gray-400" %>
<%= password_input f, :password, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
<%= error_tag f, :password, class: "text-red-700 text-sm" %>
</div>
<div>
<%= submit "Sign up", phx_disable_with: "Saving...", class: "block w-full py-2 border-none shadow rounded font-semibold text-sm text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 cursor-pointer" %>
</div>
</form>
<p class="text-sm px-10 text-center mt-6 text-gray-400 font-semibold">By signing up, you agree to our Terms , Data Policy and Cookies Policy .</p>
</section>
<section class="w-1/2 border-2 shadow flex justify-center mx-auto p-6 mt-6">
<p class="text-lg text-gray-600">Have an account? <%= link "Log in", to: Routes.user_session_path(@socket, :new), class: "text-light-blue-500 font-semibold" %></p>
</section>
error_tag()
在继续之前,我们需要稍微调整一下我们的辅助函数,这样我们就可以向其中添加类,打开lib/instagram_clone_web/views/error_helpers.ex
文件并将函数更改为以下内容:
elixir
def error_tag(form, field, class \\ [class: "invalid-feedback"]) do
Enum.map(Keyword.get_values(form.errors, field), fn error ->
content_tag(:span, translate_error(error),
class: Keyword.get(class, :class),
phx_feedback_for: input_id(form, field)
)
end)
end
这将作为我们的基础登陆和注册页面。我们需要添加我们的validation()
和save()
函数,并分配trigger_submit
给 mount 函数中的套接字,以便能够通过 HTTP 触发表单,将表单直接发送到我们的/lib/instagram_clone_web/live/page_live.ex
liveview 模块上的注册控制器,以便我们可以注册我们的用户,所以让我们这样做下一个。
elixir
@impl true
def mount(_params, _session, socket) do
changeset = Accounts.change_user_registration(%User{})
{:ok,
socket
|> assign(changeset: changeset)
|> assign(trigger_submit: false)}
end
@impl true
def handle_event("validate", %{"user" => user_params}, socket) do
changeset =
%User{}
|> User.registration_changeset(user_params)
|> Map.put(:action, :validate)
:timer.sleep(9000)
{:noreply, socket |> assign(changeset: changeset)}
end
def handle_event("save", _, socket) do
{:noreply, assign(socket, trigger_submit: true)}
end
为了处理和显示错误,我们必须编辑我们的注册用户页面,因为它们是由控制器处理的常规 Phoenix 视图,所以它应该看起来像我们的实时视图。在此之前,我们必须向容器中的主标记添加一个类以用于常规视图,打开lib/instagram_clone_web/templates/layout/app.html.eex
:
elixir
<main role="main" class="container mx-auto max-w-full md:w-11/12 2xl:w-6/12 pt-24">
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<%= @inner_content %>
</main>
然后使用以下内容打开lib/instagram_clone_web/templates/user_registration/new.html.eex
并编辑该文件:
elixir
<section class="w-1/2 border-2 shadow flex flex-col place-items-center mx-auto p-6">
<h1 class="text-4xl font-bold italic text-gray-700">InstagramClone</h1>
<p class="text-gray-500 font-semibold text-lg mt-6 text-center px-8">Sign up to see photos and videos from your friends.</p>
<%= form_for @changeset, Routes.user_registration_path(@conn, :create), [class: "flex flex-col space-y-4 w-full px-6"], fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<div class="flex flex-col">
<%= label f, :email, class: "text-gray-400" %>
<%= email_input f, :email, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
<%= error_tag f, :email, class: "text-red-700 text-sm" %>
</div>
<div class="flex flex-col">
<%= label f, :full_name, class: "text-gray-400" %>
<%= text_input f, :full_name, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
<%= error_tag f, :full_name, class: "text-red-700 text-sm" %>
</div>
<div class="flex flex-col">
<%= label f, :username, class: "text-gray-400" %>
<%= text_input f, :username, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
<%= error_tag f, :username, class: "text-red-700 text-sm" %>
</div>
<div class="flex flex-col">
<%= label f, :password, class: "text-gray-400" %>
<%= password_input f, :password, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
<%= error_tag f, :password, class: "text-red-700 text-sm" %>
</div>
<div>
<%= submit "Sign up", phx_disable_with: "Saving...", class: "block w-full py-2 border-none shadow rounded font-semibold text-sm text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 cursor-pointer" %>
</div>
<% end %>
<p class="text-sm px-10 text-center mt-6 text-gray-400 font-semibold">By signing up, you agree to our Terms , Data Policy and Cookies Policy .</p>
</section>
<section class="w-1/2 border-2 shadow flex justify-center mx-auto p-6 mt-6">
<p class="text-lg text-gray-600">Have an account? <%= link "Log in", to: Routes.user_session_path(@conn, :new), class: "text-light-blue-500 font-semibold" %></p>
</section>
让我们设计登录页面并登录。打开lib/instagram_clone_web/templates/user_session/new.html.eex
并添加以下内容:
elixir
<section class="w-1/2 border-2 shadow flex flex-col place-items-center mx-auto p-6">
<h1 class="text-4xl font-bold italic text-gray-700">InstagramClone</h1>
<%= form_for @conn, Routes.user_session_path(@conn, :create), [as: :user, class: "flex flex-col space-y-4 w-full px-6"], fn f -> %>
<%= if @error_message do %>
<div class="alert alert-danger">
<p><%= @error_message %></p>
</div>
<% end %>
<div class="flex flex-col">
<%= label f, :email, class: "text-gray-400" %>
<%= email_input f, :email, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
<%= error_tag f, :email, class: "text-red-700 text-sm" %>
</div>
<div class="flex flex-col">
<%= label f, :password, class: "text-gray-400" %>
<%= password_input f, :password, value: input_value(f, :password), class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
<%= error_tag f, :password, class: "text-red-700 text-sm" %>
</div>
<div>
<%= submit "Log In", phx_disable_with: "Saving...", class: "block w-full py-2 border-none shadow rounded font-semibold text-sm text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 cursor-pointer" %>
</div>
<% end %>
<p class="text-sm px-10 text-center mt-6 text-gray-400 font-semibold"><%= link "Forgot password?", to: Routes.user_reset_password_path(@conn, :new) %></p>
</section>
<section class="w-1/2 border-2 shadow flex justify-center mx-auto p-6 mt-6">
<p class="text-lg text-gray-600">Don't have an account? <%= link "Sign up", to: Routes.user_registration_path(@conn, :new), class: "text-light-blue-500 font-semibold" %></p>
</section>
它应该如下图所示。
事情变得非常有趣和令人兴奋,但我们没有办法在实时视图中获取当前登录的用户,我们必须在每个实时视图安装上手动获取它,我们可以手动执行此操作,但我们很懒,所以我们将添加一个我们可以调用并有权访问当前用户的辅助函数。
将新文件添加到lib/instagram_clone_web/live
名为的文件夹中lib/instagram_clone_web/live/live_helpers.ex
并添加以下内容:
elixir
defmodule InstagramCloneWeb.LiveHelpers do
import Phoenix.LiveView
alias InstagramClone.Accounts
alias InstagramClone.Accounts.User
alias InstagramCloneWeb.UserAuth
def assign_defaults(session, socket) do
if connected?(socket), do: InstagramCloneWeb.Endpoint.subscribe(UserAuth.pubsub_topic())
socket =
assign_new(socket, :current_user, fn ->
find_current_user(session)
end)
socket
end
defp find_current_user(session) do
with user_token when not is_nil(user_token) <- session["user_token"],
%User{} = user <- Accounts.get_user_by_session_token(user_token),
do: user
end
end
很简单,我们找到带有会话令牌的当前用户并将其分配回套接字,我们还订阅了一个 pubsub 主题,以便在使用套接字登录时注销所有实时当前会话,接下来让我们创建该函数,打开并lib/instagram_clone_web/controllers/user_auth.ex
添加下列的:
elixir
# Added to the top of our file
@pubsub_topic "user_updates"
def pubsub_topic, do: @pubsub_topic
# We changed a line on this function
def log_out_user(conn) do
user_token = get_session(conn, :user_token)
Accounts.log_out_user(user_token) #Line changed
if live_socket_id = get_session(conn, :live_socket_id) do
InstagramCloneWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
end
conn
|> renew_session()
|> delete_resp_cookie(@remember_me_cookie)
|> redirect(to: "/")
end
现在让我们将log_out_user()
函数添加到我们的Accounts
上下文中,打开lib/instagram_clone/accounts.ex
添加:
elixir
...
alias InstagramCloneWeb.UserAuth
...
def log_out_user(token) do
user = get_user_by_session_token(token)
# Delete all user tokens
Repo.delete_all(UserToken.user_and_contexts_query(user, :all))
# Broadcast to all liveviews to immediately disconnect the user
InstagramCloneWeb.Endpoint.broadcast_from(
self(),
UserAuth.pubsub_topic(),
"logout_user",
%{
user: user
}
)
end
...
现在我们必须在实时视图中提供辅助函数,打开lib/instagram_clone_web.ex
并添加以下内容:
elixir
def live_view do
quote do
use Phoenix.LiveView,
layout: {InstagramCloneWeb.LayoutView, "live.html"}
unquote(view_helpers())
# Added Start
import InstagramCloneWeb.LiveHelpers
alias InstagramClone.Accounts.User
@impl true
def handle_info(%{event: "logout_user", payload: %{user: %User{id: id}}}, socket) do
with %User{id: ^id} <- socket.assigns.current_user do
{:noreply,
socket
|> redirect(to: "/")
|> put_flash(:info, "Logged out successfully.")}
else
_any -> {:noreply, socket}
end
end
# Added END
end
end
我们还添加了handle_info()
自动对所有实时视图中的注销消息做出反应的功能。
打开/lib/instagram_clone_web/live/page_live.ex
并将挂载函数更改为以下内容:
elixir
@impl true
def mount(_params, session, socket) do
socket = assign_defaults(session, socket)
changeset = Accounts.change_user_registration(%User{})
{:ok,
socket
|> assign(changeset: changeset)
|> assign(trigger_submit: false)}
end
然后打开/lib/instagram_clone_web/live/page_live.html.leex
文件并将其更改为以下内容:
elixir
<%= if @current_user do %>
<%= link "Log Out", to: Routes.user_session_path(@socket, :delete), method: :delete %>
<h1>User Logged In Homepage</h1>
<% else %>
<section class="w-1/2 border-2 shadow flex flex-col place-items-center mx-auto p-6">
<h1 class="text-4xl font-bold italic text-gray-700">InstagramClone</h1>
<p class="text-gray-500 font-semibold text-lg mt-6 text-center px-8">Sign up to see photos and videos from your friends.</p>
<%= f = form_for @changeset, Routes.user_registration_path(@socket, :create),
phx_change: "validate",
phx_submit: "save",
phx_trigger_action: @trigger_submit,
class: "flex flex-col space-y-4 w-full px-6" %>
<div class="flex flex-col">
<%= label f, :email, class: "text-gray-400" %>
<%= email_input f, :email, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
<%= error_tag f, :email, class: "text-red-700 text-sm" %>
</div>
<div class="flex flex-col">
<%= label f, :full_name, class: "text-gray-400" %>
<%= text_input f, :full_name, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
<%= error_tag f, :full_name, class: "text-red-700 text-sm" %>
</div>
<div class="flex flex-col">
<%= label f, :username, class: "text-gray-400" %>
<%= text_input f, :username, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
<%= error_tag f, :username, class: "text-red-700 text-sm" %>
</div>
<div class="flex flex-col">
<%= label f, :password, class: "text-gray-400" %>
<%= password_input f, :password, value: input_value(f, :password), class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
<%= error_tag f, :password, class: "text-red-700 text-sm" %>
</div>
<div>
<%= submit "Sign up", phx_disable_with: "Saving...", class: "block w-full py-2 border-none shadow rounded font-semibold text-sm text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 cursor-pointer" %>
</div>
</form>
<p class="text-sm px-10 text-center mt-6 text-gray-400 font-semibold">By signing up, you agree to our Terms , Data Policy and Cookies Policy .</p>
</section>
<section class="w-1/2 border-2 shadow flex justify-center mx-auto p-6 mt-6">
<p class="text-lg text-gray-600">Have an account? <%= link "Log in", to: Routes.user_session_path(@socket, :new), class: "text-light-blue-500 font-semibold" %></p>
</section>
<% end %>
此时,您应该重新加载服务器并通过登录和注销来测试您的主页。一切都应该工作正常,但是让我们组件化我们的主页,在下面lib/instagram_clone_web/live
创建 2 个文件,lib/instagram_clone_web/live/page_live_component.ex
然后lib/instagram_clone_web/live/page_live_component.html.leex
elixir
#lib/instagram_clone_web/live/page_live_component.ex
defmodule InstagramCloneWeb.PageLiveComponent do
use InstagramCloneWeb, :live_component
end
获取表格lib/instagram_clone_web/live/page_live.html.leex
并将其添加到lib/instagram_clone_web/live/page_live_component.html.leex
:
elixir
<section class="w-1/2 border-2 shadow flex flex-col place-items-center mx-auto p-6">
<h1 class="text-4xl font-bold italic text-gray-700">InstagramClone</h1>
<p class="text-gray-500 font-semibold text-lg mt-6 text-center px-8">Sign up to see photos and videos from your friends.</p>
<%= f = form_for @changeset, Routes.user_registration_path(@socket, :create),
phx_change: "validate",
phx_submit: "save",
phx_trigger_action: @trigger_submit,
class: "flex flex-col space-y-4 w-full px-6" %>
<div class="flex flex-col">
<%= label f, :email, class: "text-gray-400" %>
<%= email_input f, :email, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
<%= error_tag f, :email, class: "text-red-700 text-sm" %>
</div>
<div class="flex flex-col">
<%= label f, :full_name, class: "text-gray-400" %>
<%= text_input f, :full_name, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
<%= error_tag f, :full_name, class: "text-red-700 text-sm" %>
</div>
<div class="flex flex-col">
<%= label f, :username, class: "text-gray-400" %>
<%= text_input f, :username, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
<%= error_tag f, :username, class: "text-red-700 text-sm" %>
</div>
<div class="flex flex-col">
<%= label f, :password, class: "text-gray-400" %>
<%= password_input f, :password, value: input_value(f, :password), class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
<%= error_tag f, :password, class: "text-red-700 text-sm" %>
</div>
<div>
<%= submit "Sign up", phx_disable_with: "Saving...", class: "block w-full py-2 border-none shadow rounded font-semibold text-sm text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 cursor-pointer" %>
</div>
</form>
<p class="text-sm px-10 text-center mt-6 text-gray-400 font-semibold">By signing up, you agree to our Terms , Data Policy and Cookies Policy .</p>
</section>
<section class="w-1/2 border-2 shadow flex justify-center mx-auto p-6 mt-6">
<p class="text-lg text-gray-600">Have an account? <%= link "Log in", to: Routes.user_session_path(@socket, :new), class: "text-light-blue-500 font-semibold" %></p>
</section>
现在您的lib/instagram_clone_web/live/page_live.html.leex
示例应该如下所示:
elixir
<%= if @current_user do %>
<h1>User Logged In Homepage</h1>
<% else %>
<%= live_component @socket, InstagramCloneWeb.PageLiveComponent, changeset: @changeset, trigger_submit: @trigger_submit %>
<% end %>
这有助于我们清理代码,以便稍后我们开始为登录用户处理主页时。
最后让我们添加一个标题导航菜单组件。首先我们需要知道当前的 URL 路径,我们将使用宏为所有实时视图执行此操作,打开lib/instagram_clone_web.ex
添加以下函数live_view()
:
elixir
@impl true
def handle_params(_unsigned_params, uri, socket) do
{:noreply,
socket
|> assign(current_uri_path: URI.parse(uri).path)}
end
current_uri_path
这将使我们能够访问分配给套接字的所有实时视图的当前 URL 路径。因此,在我们的lib/instagram_clone_web.ex
文件中,新更新的内容live_view()
应如下所示:
elixir
...
def live_view do
quote do
use Phoenix.LiveView,
layout: {InstagramCloneWeb.LayoutView, "live.html"}
unquote(view_helpers())
import InstagramCloneWeb.LiveHelpers
alias InstagramClone.Accounts.User
@impl true
def handle_info(%{event: "logout_user", payload: %{user: %User{id: id}}}, socket) do
with %User{id: ^id} <- socket.assigns.current_user do
{:noreply,
socket
|> redirect(to: "/")
|> put_flash(:info, "Logged out successfully.")}
else
_any -> {:noreply, socket}
end
end
@impl true
def handle_params(_unsigned_params, uri, socket) do
{:noreply,
socket
|> assign(current_uri_path: URI.parse(uri).path)}
end
end
end
...
现在在下面lib/instagram_clone_web/live
添加 2 个文件,header_nav_component.ex
以及header_nav_component.html.leex
. 添加lib/instagram_clone_web/live/header_nav_component.ex
以下内容:
elixir
defmodule InstagramCloneWeb.HeaderNavComponent do
use InstagramCloneWeb, :live_component
end
将以下内容添加到lib/instagram_clone_web/live/header_nav_component.html.leex
:
elixir
<div class="h-14 border-b-2 flex fixed w-full bg-white z-50">
<header class="flex items-center container mx-auto max-w-full md:w-11/12 2xl:w-6/12">
<%= live_patch to: Routes.page_path(@socket, :index) do %>
<h1 class="text-2xl font-bold italic">#InstagramClone</h1>
<% end %>
<div class="w-2/5 flex justify-end"><input type="search" placeholder="Search" class="h-7 bg-gray-50 shadow-sm border-gray-300 focus:ring-gray-300 focus:ring-opacity-50 focus:border-gray-400 px-0.5 rounded-sm"></div>
<nav class="w-3/5 relative">
<ul x-data="{open: false}" class="flex justify-end">
<%= if @current_user do %>
<li class="w-7 h-7 text-gray-600">
<%= live_patch to: Routes.page_path(@socket, :index) do %>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
<% end %>
</li>
<li class="w-7 h-7 ml-6 text-gray-600">
<%= live_patch to: "" do %>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<% end %>
</li>
<li class="w-7 h-7 ml-6 text-gray-600">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
</li>
<li class="w-7 h-7 ml-6 text-gray-600">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
</svg>
</li>
<li class="w-7 h-7 ml-6 text-gray-600">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
</svg>
</li>
<li
@click="open = true"
class="w-7 h-7 ml-6 shadow-md rounded-full overflow-hidden cursor-pointer"
>
<%= img_tag @current_user.avatar_url,
class: "w-full h-full object-cover object-center" %>
</li>
<ul class="absolute top-14 w-56 bg-white shadow-md text-sm -right-8"
x-show="open"
@click.away="open = false"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 transform scale-90"
x-transition:enter-end="opacity-100 transform scale-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 transform scale-100"
x-transition:leave-end="opacity-0 transform scale-90"
>
<%= live_patch to: "" do %>
<li class="py-2 px-4 hover:bg-gray-50">Profile</li>
<% end %>
<li class="py-2 px-4 hover:bg-gray-50">Saved</li>
<%= live_patch to: "" do %>
<li class="py-2 px-4 hover:bg-gray-50">Settings</li>
<% end %>
<li class="border-t-2 py-2 px-4 hover:bg-gray-50"><%= link "Log Out", to: Routes.user_session_path(@socket, :delete), method: :delete %></li>
</ul>
<% else %>
<li>
<%= link "Log In", to: Routes.user_session_path(@socket, :new), class: "md:w-24 py-1 px-3 border-none shadow rounded text-gray-50 hover:bg-blue-600 bg-blue-500 font-semibold" %>
</li>
<li>
<%= link "Sign Up", to: Routes.user_registration_path(@socket, :new), class: "md:w-24 py-1 px-3 border-none text-blue-500 hover:text-blue-600 font-semibold" %>
</li>
<% end %>
</ul>
</nav>
</header>
</div>
现在要显示该组件,请打开lib/instagram_clone_web/templates/layout/live.html.leex
并将其添加到文件顶部:
elixir
<%= if @current_user do %>
<%= live_component @socket, InstagramCloneWeb.HeaderNavComponent, current_user: @current_user %>
<% else %>
<%= if @current_uri_path !== "/" do %>
<%= live_component @socket, InstagramCloneWeb.HeaderNavComponent, current_user: @current_user %>
<% end %>
<% end %>
这将检查用户是否已登录以显示标题导航,如果没有,则不是主页,例如我们稍后将创建的个人资料或帖子页面,它也会显示它,否则如果主页且未登录,则不会显示得到显示。
对于第 1 部分来说,这已经足够了,我们还有很长的路要走,还有很多真正令人兴奋和有趣的事情要做,我在撰写本系列文章时正在构建它,因此如果有任何错误或错误,我们会在我们进行过程中得到修复,请在下面的评论中告诉我您的想法,您可以为Instagram 克隆 GitHub 存储库做出贡献。