关于elixir:使用-Phoenix-LiveView-构建-Instagram-1

7次阅读

共计 29052 个字符,预计需要花费 73 分钟才能阅读完成。

应用 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

正文完
 0