应用 PETAL(Phoenix、Elixir、TailwindCSS、AlpineJS、LiveView)技术栈构建一个简化版的 Instagram Web 应用程序
<!–more–>
更好的学习办法是亲自动手构建货色,让咱们应用很棒的 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 文件中增加混合依赖项。
# 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。
// /assets/webpack.config.js
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
'postcss-loader', // Add this
],
增加蕴含 /assets/postcss.config.js
以下内容的文件:
// /assets/postcss.config.js
module.exports = {
plugins: {"postcss-import": {},
tailwindcss: {},
autoprefixer: {}}
}
创立 TailwindCSS 配置文件。
$ npx tailwindcss init
将以下配置增加到该文件中:
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
文件顶部并增加以下内容:
/* 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
删除所有内容并增加以下内容来测试它:
<h1 class="text-red-500 text-5xl font-bold text-center">Instagram Clone</h1>
咱们的主页上应该有一个红色的大题目。
删除 /lib/instagram_clone_web/live/page_live.ex
所有内容,因为咱们的主页中不须要任何内容,而后增加以下内容:
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 标头,该文件上应该有以下内容:
<!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
主标签并将以下类增加到主标签中:
<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 修补产生任何抵触:
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
以下内容增加到咱们的文件顶部:
<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
文件中。
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
并增加以下内容:
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
并将以下内容增加到您的用户架构中:
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
为以下内容:
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
函数,以便在更新用户帐户时,咱们不须要验证或散列明码,因而将其更改为以下内容:
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 %>
<!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
增加以下内容:
<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 函数更改为以下内容:
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
:
<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
文件并将函数更改为以下内容:
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 模块上的注册控制器,以便咱们能够注册咱们的用户,所以让咱们这样做下一个。
@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
:
<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
并编辑该文件:
<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
并增加以下内容:
<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
并增加以下内容:
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
增加下列的:
# 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
增加:
...
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
并增加以下内容:
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
并将挂载函数更改为以下内容:
@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
文件并将其更改为以下内容:
<%= 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
#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
:
<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
示例应该如下所示:
<%= 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()
:
@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()
应如下所示:
...
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
以下内容:
defmodule InstagramCloneWeb.HeaderNavComponent do
use InstagramCloneWeb, :live_component
end
将以下内容增加到lib/instagram_clone_web/live/header_nav_component.html.leex
:
<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
并将其增加到文件顶部:
<%= 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 存储库做出奉献。
转自:Elixirprogrammer