应用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.jsuse: [  MiniCssExtractPlugin.loader,  'css-loader',  'sass-loader',  'postcss-loader', // Add this],

增加蕴含/assets/postcss.config.js以下内容的文件:

// /assets/postcss.config.jsmodule.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}  endend

/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  endend

而后$ mix ecto.migrate

接下来关上lib/instagram_clone/accounts/user.ex并将以下内容增加到您的用户架构中:

field :username, :stringfield :full_name, :stringfield :avatar_url, :string, default: "/images/default-avatar.png"field :bio, :stringfield :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  endend

很简略,咱们找到带有会话令牌的以后用户并将其调配回套接字,咱们还订阅了一个 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.exdefmodule InstagramCloneWeb.PageLiveComponent do  use InstagramCloneWeb, :live_componentend

获取表格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_componentend

将以下内容增加到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