应用PETAL(Phoenix、Elixir、TailwindCSS、AlpineJS、LiveView)技术栈构建一个简化版的Instagram Web应用程序

<!--more-->

在第 5 局部中,咱们增加了 show-post 页面,在这部分中,咱们将在主页上进行工作。您能够赶上Instagram 克隆 GitHub Repo。

首先,咱们在 posts 上下文中增加一个函数来获取 feed,而后增加另一个函数来获取 feed 的总数 open lib/instagram_clone/posts.ex

  @doc """  Returns the list of paginated posts of a given user id  And posts of following list of given user id  With user and likes preloaded  With 2 most recent comments preloaded with user and likes  User, page, and per_page are given with the socket assigns  ## Examples      iex> get_accounts_feed(following_list, assigns)      [%{photo_url: "", url_id: ""}, ...]  """  def get_accounts_feed(following_list, assigns) do    user = assigns.current_user    page = assigns.page    per_page = assigns.per_page    query =      from c in Comment,      select: %{id: c.id, row_number: over(row_number(), :posts_partition)},      windows: [posts_partition: [partition_by: :post_id, order_by: [desc: :id]]]    comments_query =      from c in Comment,      join: r in subquery(query),      on: c.id == r.id and r.row_number <= 2    Post    |> where([p], p.user_id in ^following_list)    |> or_where([p], p.user_id == ^user.id)    |> limit(^per_page)    |> offset(^((page - 1) * per_page))    |> order_by(desc: :id)    |> preload([:user, :likes, comments: ^{comments_query, [:user, :likes]}])    |> Repo.all()  end  def get_accounts_feed_total(following_list, assigns) do    user = assigns.current_user    Post    |> where([p], p.user_id in ^following_list)    |> or_where([p], p.user_id == ^user.id)    |> select([p], count(p.id))    |> Repo.one()  end

咱们须要以下列表,在外面lib/instagram_clone/accounts.ex增加以下函数:

  @doc """  Returns the list of following user ids  ## Examples      iex> get_following_list(user)      [3, 2, 1]  """  def get_following_list(user) do    Follows    |> select([f], f.followed_id)    |> where(follower_id: ^user.id)    |> Repo.all()  end

在外面lib/instagram_clone_web/live/page_live.ex让咱们调配提要:

  alias InstagramClone.Uploaders.Avatar  alias InstagramClone.Accounts  alias InstagramCloneWeb.UserLive.FollowComponent  alias InstagramClone.Posts  alias InstagramCloneWeb.Live.LikeComponent  @impl true  def mount(_params, session, socket) do    socket = assign_defaults(session, socket)    {:ok,      socket      |> assign(page: 1, per_page: 15),      temporary_assigns: [user_feed: []]}  end  @impl true  def handle_params(_params, _uri, socket) do    {:noreply,      socket      |> assign(live_action: apply_action(socket.assigns.current_user))      |> assign_posts()}  end  defp apply_action(current_user) do    if !current_user, do: :root_path  end  defp assign_posts(socket) do    if socket.assigns.current_user do      current_user = socket.assigns.current_user      following_list = Accounts.get_following_list(current_user)      accounts_feed_total = Posts.get_accounts_feed_total(following_list, socket.assigns)        socket      |> assign(following_list: following_list)      |> assign(accounts_feed_total: accounts_feed_total)      |> assign_user_feed()    else      socket    end  end  defp assign_user_feed(socket, following_list) do    user_feed = Posts.get_accounts_feed(socket.assigns.following_list, socket.assigns)    socket |> assign(user_feed: user_feed)  end

页和每页被调配给咱们的挂载函数中的套接字。咱们正在检查用户是否登录以获取以下列表并将其传递给调配提要函数以返回调配了提要的套接字,咱们在句柄参数函数中执行此操作。

当初让咱们为帖子提要创立一个组件,在咱们的 live 文件夹中增加以下文件:

lib/instagram_clone_web/live/page_post_feed_component.ex lib/instagram_clone_web/live/page_post_feed_component.html.leex

外面lib/instagram_clone_web/live/page_live.html.leex

<%= if @current_user do %>  <section class="flex">    <div id="user-feed" class="w-3/5" phx-update="append">      <%= for post <- @user_feed do %>        <%= live_component @socket,          InstagramCloneWeb.Live.PagePostFeedComponent,          post: post,          id: post.id,          current_user: @current_user %>      <% end %>    </div>  </section>    <div    id="profile-posts-footer"    class="flex justify-center"    phx-hook="ProfilePostsScroll">  </div><% else %>  <%= live_component @socket,    InstagramCloneWeb.PageLiveComponent,    id: 1 %><% end %>

外面lib/instagram_clone_web/live/page_post_feed_component.ex

defmodule InstagramCloneWeb.Live.PagePostFeedComponent do  use InstagramCloneWeb, :live_component  alias InstagramClone.Uploaders.Avatar  alias InstagramClone.Comments  alias InstagramClone.Comments.Comment  @impl true  def mount(socket) do    {:ok,      socket      |> assign(changeset: Comments.change_comment(%Comment{})),      temporary_assigns: [comments: []]}  end  @impl true  def handle_event("save", %{"comment" => comment_param}, socket) do    %{"body" => body} = comment_param    current_user = socket.assigns.current_user    post = socket.assigns.post    if body == "" do      {:noreply, socket}    else      comment = Comments.create_comment(current_user, post, comment_param)      {:noreply,        socket        |> update(:comments, fn comments -> [comment | comments] end)        |> assign(changeset: Comments.change_comment(%Comment{}))}    end  endend

咱们正在设置表单变更集和长期正文,咱们将应用它们来附加新正文。保留句柄性能与咱们在展现页面上应用的性能雷同。

外面lib/instagram_clone_web/live/page_post_feed_component.html.leex

<div class="mb-16 shadow" id="post-<%= @post.id %>">  <div class="flex p-4 items-center">    <!-- Post header section -->    <%= live_redirect to: Routes.user_profile_path(@socket, :index, @post.user.username) do %>      <%= img_tag Avatar.get_thumb(@post.user.avatar_url), class: "w-8 h-8 rounded-full object-cover object-center" %>    <% end %>    <div class="ml-3">      <%= live_redirect @post.user.username,        to: Routes.user_profile_path(@socket, :index, @post.user.username),        class: "truncate font-bold text-sm text-gray-500 hover:underline" %>    </div>    <!-- End post header section -->  </div>  <!-- Post Image section -->  <%= img_tag @post.photo_url,          class: "w-full object-contain h-full shadow-sm" %>  <!-- End Post Image section -->  <div class="w-full">    <!-- Action icons section -->    <div class="flex pl-4 pr-2 pt-2">      <%= live_component @socket,          InstagramCloneWeb.Live.LikeComponent,          id: @post.id,          liked: @post,          w_h: "w-8 h-8",          current_user: @current_user %>      <%= live_redirect to: Routes.live_path(@socket, InstagramCloneWeb.PostLive.Show, @post.url_id) do %>        <div class="ml-4 w-8 h-8">          <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="1" 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>        </div>      <% end %>      <div class="ml-4 w-8 h-8 cursor-pointer">        <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="1" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" />        </svg>      </div>      <div class="w-8 h-8 ml-auto cursor-pointer">        <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="1" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />        </svg>      </div>    </div>    <!-- End Action icons section -->    <!-- Description section -->    <button class="px-5 text-xs text-gray-500 font-bold focus:outline-none"><%= @post.total_likes %> likes</button>    <!-- End Description Section -->  </div>  <%= if @post.description do %>    <!-- Description section -->    <div class="flex mt-2">      <div class="px-4 w-11/12">        <%= live_redirect @post.user.username,        to: Routes.user_profile_path(@socket, :index, @post.user.username),        class: "font-bold text-sm text-gray-500 hover:underline" %>        <span class="text-sm text-gray-700">          <p class="inline"><%= @post.description %></p></span>        </span>      </div>    </div>  <!-- End Description Section -->  <% end %>  <%= if @post.total_comments > 2 do %>    <%= live_redirect to: Routes.live_path(@socket, InstagramCloneWeb.PostLive.Show, @post.url_id) do %>      <h6 class="px-5 text-sm text-gray-400">        View all <%= @post.total_comments %> comments      </h6>    <% end %>  <% end %>  <section id="comments" phx-update="append">    <%= for comment <- @post.comments do %>      <div class="flex" id="comment-<%= comment.id %>">        <div class="px-4 w-11/12">          <%= live_redirect comment.user.username,                to: Routes.user_profile_path(@socket, :index, comment.user.username),                class: "truncate font-bold text-sm text-gray-500 hover:underline" %>          <span class="text-sm text-gray-700">            <p class="inline"><%= comment.body %></p>          </span>        </div>        <%= live_component @socket,            InstagramCloneWeb.Live.LikeComponent,            id: comment.id,            liked: comment,            w_h: "w-5 h-5",            current_user: @current_user %>      </div>    <% end %>    <%= for comment <- @comments do %>      <div class="flex" id="comment-<%= comment.id %>">        <div class="px-4 w-11/12">          <%= live_redirect comment.user.username,                to: Routes.user_profile_path(@socket, :index, comment.user.username),                class: "truncate font-bold text-sm text-gray-500 hover:underline" %>          <span class="text-sm text-gray-700">            <p class="inline"><%= comment.body %></p>          </span>        </div>        <%= live_component @socket,            InstagramCloneWeb.Live.LikeComponent,            id: comment.id,            liked: comment,            w_h: "w-5 h-5",            current_user: @current_user %>      </div>    <% end %>  </section>  <h6 class="px-5 py-2 text-xs text-gray-400"><%= Timex.from_now(@post.inserted_at) %></h6>  <!-- Comment input section -->  <%= f = form_for @changeset, "#",    id: @id,    phx_submit: "save",    phx_target: @myself,    class: "p-2 flex items-center mt-3 border-t-2 border-gray-100",    x_data: "{      disableSubmit: true,      inputText: null,      displayCommentBtn: (refs) => {        refs.cbtn.classList.remove('opacity-30')        refs.cbtn.classList.remove('cursor-not-allowed')      },      disableCommentBtn: (refs) => {        refs.cbtn.classList.add('opacity-30')        refs.cbtn.classList.add('cursor-not-allowed')      }    }" %>    <div class="w-full">      <%= textarea f, :body,        class: "w-full border-0 focus:ring-transparent resize-none",        rows: 1,        placeholder: "Add a comment...",        aria_label: "Add a comment...",        autocorrect: "off",        autocomplete: "off",        x_model: "inputText",        "@input": "[          (inputText.length != 0) ? [disableSubmit = false, displayCommentBtn($refs)] : [disableSubmit = true, disableCommentBtn($refs)]        ]" %>    </div>    <div>      <%= submit "Post",        phx_disable_with: "Posting...",        class: "text-light-blue-500 opacity-30 cursor-not-allowed font-bold pb-2 text-sm focus:outline-none",        x_ref: "cbtn",        "@click": "inputText = null",        "x_bind:disabled": "disableSubmit" %>    </div>  </form></div>

咱们应用与显示页面上应用的雷同的表单来增加新评论,并且循环遍历帖子评论和长期评论,以便在增加新评论时可能更新评论。

当咱们喜爱帖子或评论时,咱们须要解决从 Like 组件发送的音讯,咱们还必须解决用钩子触发的事件以加载更多帖子,更新lib/instagram_clone_web/live/page_live.ex为以下内容:

defmodule InstagramCloneWeb.PageLive do  use InstagramCloneWeb, :live_view  alias InstagramClone.Uploaders.Avatar  alias InstagramClone.Accounts  alias InstagramCloneWeb.UserLive.FollowComponent  alias InstagramClone.Posts  alias InstagramCloneWeb.Live.LikeComponent  @impl true  def mount(_params, session, socket) do    socket = assign_defaults(session, socket)    {:ok,      socket      |> assign(page: 1, per_page: 15),      temporary_assigns: [user_feed: []]}  end  @impl true  def handle_params(_params, _uri, socket) do    {:noreply,      socket      |> assign(live_action: apply_action(socket.assigns.current_user))      |> assign_posts()}  end  @impl true  def handle_event("load-more-profile-posts", _, socket) do    {:noreply, socket |> load_posts}  end  defp load_posts(socket) do    total_posts = socket.assigns.accounts_feed_total    page = socket.assigns.page    per_page = socket.assigns.per_page    total_pages = ceil(total_posts / per_page)    if page == total_pages do      socket    else      socket      |> update(:page, &(&1 + 1))      |> assign_user_feed()    end  end  @impl true  def handle_info({LikeComponent, :update_comment_likes, _}, socket) do    {:noreply, socket}  end  @impl true  def handle_info({LikeComponent, :update_post_likes, post}, socket) do    post_feed = Posts.get_post_feed!(post.id)    {:noreply,      socket      |> update(:user_feed, fn user_feed -> [post_feed | user_feed] end)}  end  defp apply_action(current_user) do    if !current_user, do: :root_path  end  defp assign_posts(socket) do    if socket.assigns.current_user do      current_user = socket.assigns.current_user      following_list = Accounts.get_following_list(current_user)      accounts_feed_total = Posts.get_accounts_feed_total(following_list, socket.assigns)      socket      |> assign(following_list: following_list)      |> assign(accounts_feed_total: accounts_feed_total)      |> assign_user_feed()    else      socket    end  end  defp assign_user_feed(socket) do    user_feed = Posts.get_accounts_feed(socket.assigns.following_list, socket.assigns)    socket |> assign(user_feed: user_feed)  endend

让咱们对 Like 组件进行一些更改,因为咱们在帖子和评论之间共享它,将文件挪动到 post_live 文件夹内部的 live 文件夹并将模块重命名为以下内容:

lib/instagram_clone_web/live/like_component.ex

defmodule  InstagramCloneWeb.Live.LikeComponent  do

lib/instagram_clone_web/live/post_live/show.html.leex在第 70 行外部重命名该组件:

...        InstagramCloneWeb.Live.LikeComponent,...

第 24 行外部lib/instagram_clone_web/live/post_live/comment_component.html.leex也重命名了该组件:

...        InstagramCloneWeb.PostLive.LikeComponent,        ...

在外部lib/instagram_clone_web/live/like_component.ex,让咱们更新send_msg()以将 like 作为变量发送,而不仅仅是 id:

...  defp send_msg(liked) do    msg = get_struct_msg_atom(liked)    send(self(), {__MODULE__, msg, liked})  end...

同样在 外部lib/instagram_clone_web/live/like_component.ex,让咱们删除该liked?()函数,而后检查用户 ID 是否在第 61 行的用户 ID 列表中:

...    if assigns.current_user.id in assigns.liked.likes do # LINE 61  # DELETE THIS FUNCTION WE WON"T NEED ANYMORE  # Enum.any?(likes, fn l ->  #   l.user_id == user_id  # end)...

在第 30 行,咱们更新以查看数据库:

...    if Likes.liked?(current_user.id, liked.id) do...

咱们新更新的文件应如下所示:

defmodule InstagramCloneWeb.Live.LikeComponent do  use InstagramCloneWeb, :live_component  alias InstagramClone.Likes  @impl true  def update(assigns, socket) do    get_btn_status(socket, assigns)  end  @impl true  def render(assigns) do    ~L"""    <button      phx-target="<%= @myself %>"      phx-click="toggle-status"      class="<%= @w_h %> focus:outline-none">      <%= @icon %>    </button>    """  end  @impl true  def handle_event("toggle-status", _params, socket) do    current_user = socket.assigns.current_user    liked = socket.assigns.liked    if Likes.liked?(current_user.id, liked.id) do      unlike(socket, current_user.id, liked)    else      like(socket, current_user, liked)    end  end  defp like(socket, current_user, liked) do    Likes.create_like(current_user, liked)    send_msg(liked)    {:noreply,      socket      |> assign(icon: unlike_icon(socket.assigns))}  end  defp unlike(socket, current_user_id, liked) do    Likes.unlike(current_user_id, liked)    send_msg(liked)    {:noreply,      socket      |> assign(icon: like_icon(socket.assigns))}  end  defp send_msg(liked) do    msg = get_struct_msg_atom(liked)    send(self(), {__MODULE__, msg, liked})  end  defp get_btn_status(socket, assigns) do    if assigns.current_user.id in assigns.liked.likes do      get_socket_assigns(socket, assigns, unlike_icon(assigns))    else      get_socket_assigns(socket, assigns, like_icon(assigns))    end  end  defp get_socket_assigns(socket, assigns, icon) do    {:ok,      socket      |> assign(assigns)      |> assign(icon: icon)}  end  defp get_struct_name(struct) do    struct.__struct__    |> Module.split()    |> List.last()    |> String.downcase()  end  defp get_struct_msg_atom(struct) do    name = get_struct_name(struct)    update_struct_likes = "update_#{name}_likes"    String.to_atom(update_struct_likes)  end  defp like_icon(assigns) do    ~L"""    <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="1" 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>    """  end  defp unlike_icon(assigns) do    ~L"""    <svg class="text-red-600" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">      <path fill-rule="evenodd" d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z" clip-rule="evenodd" />    </svg>    """  endend

当初,当咱们预加载点赞时,咱们只需发送 id 列表、关上,lib/instagram_clone/posts.ex并且在咱们收到帖子的每个性能上,咱们必须更新预加载点赞的形式:

defmodule InstagramClone.Posts do  @moduledoc """  The Posts context.  """  import Ecto.Query, warn: false  alias InstagramClone.Repo  alias InstagramClone.Posts.Post  alias InstagramClone.Accounts.User  alias InstagramClone.Comments.Comment  alias InstagramClone.Likes.Like  @doc """  Returns the list of posts.  ## Examples      iex> list_posts()      [%Post{}, ...]  """  def list_posts do    Repo.all(Post)  end  @doc """  Returns the list of paginated posts of a given user id.  ## Examples      iex> list_user_posts(page: 1, per_page: 10, user_id: 1)      [%{photo_url: "", url_id: ""}, ...]  """  def list_profile_posts(page: page, per_page: per_page, user_id: user_id) do    Post    |> select([p], map(p, [:url_id, :photo_url]))    |> where(user_id: ^user_id)    |> limit(^per_page)    |> offset(^((page - 1) * per_page))    |> order_by(desc: :id)    |> Repo.all  end  @doc """  Returns the list of paginated posts of a given user id  And posts of following list of given user id  With user and likes preloaded  With 2 most recent comments preloaded with user and likes  User, page, and per_page are given with the socket assigns  ## Examples      iex> get_accounts_feed(following_list, assigns)      [%{photo_url: "", url_id: ""}, ...]  """  def get_accounts_feed(following_list, assigns) do    user = assigns.current_user    page = assigns.page    per_page = assigns.per_page    query =      from c in Comment,      select: %{id: c.id, row_number: over(row_number(), :posts_partition)},      windows: [posts_partition: [partition_by: :post_id, order_by: [desc: :id]]]    comments_query =      from c in Comment,      join: r in subquery(query),      on: c.id == r.id and r.row_number <= 2    likes_query = Like |> select([l], l.user_id)    Post    |> where([p], p.user_id in ^following_list)    |> or_where([p], p.user_id == ^user.id)    |> limit(^per_page)    |> offset(^((page - 1) * per_page))    |> order_by(desc: :id)    |> preload([:user, likes: ^likes_query, comments: ^{comments_query, [:user, likes: likes_query]}])    |> Repo.all()  end  @doc """  Gets a single post.  Raises `Ecto.NoResultsError` if the Post does not exist.  ## Examples      iex> get_post!(123)      %Post{}      iex> get_post!(456)      ** (Ecto.NoResultsError)  """  def get_post!(id) do    likes_query = Like |> select([l], l.user_id)    Repo.get!(Post, id)    |> Repo.preload([:user, likes: likes_query])  end  def get_post_feed!(id) do    query =      from c in Comment,      select: %{id: c.id, row_number: over(row_number(), :posts_partition)},      windows: [posts_partition: [partition_by: :post_id, order_by: [desc: :id]]]    comments_query =      from c in Comment,      join: r in subquery(query),      on: c.id == r.id and r.row_number <= 2    likes_query = Like |> select([l], l.user_id)    Post    |> preload([:user, likes: ^likes_query, comments: ^{comments_query, [:user, likes: likes_query]}])    |> Repo.get!(id)  end  def get_post_by_url!(id) do    likes_query = Like |> select([l], l.user_id)    Repo.get_by!(Post, url_id: id)    |> Repo.preload([:user, likes: likes_query])  end  @doc """  Creates a post.  ## Examples      iex> create_post(%{field: value})      {:ok, %Post{}}      iex> create_post(%{field: bad_value})      {:error, %Ecto.Changeset{}}  """  def create_post(%Post{} = post, attrs \\ %{}, user) do    post = Ecto.build_assoc(user, :posts, put_url_id(post))    changeset = Post.changeset(post, attrs)    update_posts_count = from(u in User, where: u.id == ^user.id)    Ecto.Multi.new()    |> Ecto.Multi.update_all(:update_posts_count, update_posts_count, inc: [posts_count: 1])    |> Ecto.Multi.insert(:post, changeset)    |> Repo.transaction()  end  # Generates a base64-encoding 8 bytes  defp put_url_id(post) do    url_id = Base.encode64(:crypto.strong_rand_bytes(8), padding: false)    %Post{post | url_id: url_id}  end  @doc """  Updates a post.  ## Examples      iex> update_post(post, %{field: new_value})      {:ok, %Post{}}      iex> update_post(post, %{field: bad_value})      {:error, %Ecto.Changeset{}}  """  def update_post(%Post{} = post, attrs) do    post    |> Post.changeset(attrs)    |> Repo.update()  end  @doc """  Deletes a post.  ## Examples      iex> delete_post(post)      {:ok, %Post{}}      iex> delete_post(post)      {:error, %Ecto.Changeset{}}  """  def delete_post(%Post{} = post) do    Repo.delete(post)  end  @doc """  Returns an `%Ecto.Changeset{}` for tracking post changes.  ## Examples      iex> change_post(post)      %Ecto.Changeset{data: %Post{}}  """  def change_post(%Post{} = post, attrs \\ %{}) do    Post.changeset(post, attrs)  endend

咱们还必须对正文执行雷同的操作,关上lib/instagram_clone/comments.ex文件并将其更新为以下内容:

defmodule InstagramClone.Comments do  @moduledoc """  The Comments context.  """  import Ecto.Query, warn: false  alias InstagramClone.Repo  alias InstagramClone.Likes.Like  alias InstagramClone.Comments.Comment  @doc """  Returns the list of comments.  ## Examples      iex> list_comments()      [%Comment{}, ...]  """  def list_comments do    Repo.all(Comment)  end  def list_post_comments(assigns, public: public) do    user = assigns.current_user    post_id = assigns.post.id    per_page = assigns.per_page    page = assigns.page    likes_query = Like |> select([l], l.user_id)    Comment    |> where(post_id: ^post_id)    |> get_post_comments_sorting(public, user)    |> limit(^per_page)    |> offset(^((page - 1) * per_page))    |> preload([:user, likes: ^likes_query])    |> Repo.all  end  defp get_post_comments_sorting(module, public, user) do    if public do      order_by(module, asc: :id)    else      order_by(module, fragment("(CASE WHEN user_id = ? then 1 else 2 end)", ^user.id))    end  end  @doc """  Gets a single comment.  Raises `Ecto.NoResultsError` if the Comment does not exist.  ## Examples      iex> get_comment!(123)      %Comment{}      iex> get_comment!(456)      ** (Ecto.NoResultsError)  """  def get_comment!(id) do    likes_query = Like |> select([l], l.user_id)    Repo.get!(Comment, id)    |> Repo.preload([:user, likes: likes_query])  end  @doc """  Creates a comment.  ## Examples      iex> create_comment(%{field: value})      {:ok, %Comment{}}      iex> create_comment(%{field: bad_value})      {:error, %Ecto.Changeset{}}  """  def create_comment(user, post, attrs \\ %{}) do    update_total_comments = post.__struct__ |> where(id: ^post.id)    comment_attrs = %Comment{} |> Comment.changeset(attrs)    comment =      comment_attrs      |> Ecto.Changeset.put_assoc(:user, user)      |> Ecto.Changeset.put_assoc(:post, post)    Ecto.Multi.new()    |> Ecto.Multi.update_all(:update_total_comments, update_total_comments, inc: [total_comments: 1])    |> Ecto.Multi.insert(:comment, comment)    |> Repo.transaction()    |> case do      {:ok, %{comment: comment}} ->        likes_query = Like |> select([l], l.user_id)        comment |> Repo.preload(likes: likes_query)    end  end  @doc """  Updates a comment.  ## Examples      iex> update_comment(comment, %{field: new_value})      {:ok, %Comment{}}      iex> update_comment(comment, %{field: bad_value})      {:error, %Ecto.Changeset{}}  """  def update_comment(%Comment{} = comment, attrs) do    comment    |> Comment.changeset(attrs)    |> Repo.update()  end  @doc """  Deletes a comment.  ## Examples      iex> delete_comment(comment)      {:ok, %Comment{}}      iex> delete_comment(comment)      {:error, %Ecto.Changeset{}}  """  def delete_comment(%Comment{} = comment) do    Repo.delete(comment)  end  @doc """  Returns an `%Ecto.Changeset{}` for tracking comment changes.  ## Examples      iex> change_comment(comment)      %Ecto.Changeset{data: %Comment{}}  """  def change_comment(%Comment{} = comment, attrs \\ %{}) do    Comment.changeset(comment, attrs)  endend

外部lib/instagram_clone_web/live/post_live/show.ex更新第 6 行:

...  alias InstagramCloneWeb.Live.LikeComponent...

外面lib/instagram_clone_web/live/post_live/show.html.leex更新第70行和行:

...              InstagramCloneWeb.Live.LikeComponent,...

外部lib/instagram_clone_web/live/post_live/comment_component.html.leex更新第 24 行:

...        InstagramCloneWeb.Live.LikeComponent,...

更新lib/instagram_clone/likes.ex如下:

defmodule InstagramClone.Likes do  import Ecto.Query, warn: false  alias InstagramClone.Repo  alias InstagramClone.Likes.Like  def create_like(user, liked) do    user = Ecto.build_assoc(user, :likes)    like = Ecto.build_assoc(liked, :likes, user)    update_total_likes = liked.__struct__ |> where(id: ^liked.id)    Ecto.Multi.new()    |> Ecto.Multi.insert(:like, like)    |> Ecto.Multi.update_all(:update_total_likes, update_total_likes, inc: [total_likes: 1])    |> Repo.transaction()  end  def unlike(user_id, liked) do    like = liked?(user_id, liked.id)    update_total_likes = liked.__struct__ |> where(id: ^liked.id)    Ecto.Multi.new()    |> Ecto.Multi.delete(:like, like)    |> Ecto.Multi.update_all(:update_total_likes, update_total_likes, inc: [total_likes: -1])    |> Repo.transaction()  end  # Returns nil if not found  def liked?(user_id, liked_id) do    Repo.get_by(Like, [user_id: user_id, liked_id: liked_id])  endend

让咱们增加一个蕴含 5 个随机用户倡议的侧边栏,在外面lib/instagram_clone/accounts.ex增加以下函数:

  def random_5(user) do    following_list = get_following_list(user)    User    |> where([u], u.id not in ^following_list)    |> where([u], u.id != ^user.id)    |> order_by(desc: fragment("Random()"))    |> limit(5)    |> Repo.all()  end

在外面lib/instagram_clone_web/live/page_live.ex增加一个handle_info()并将公有assign_posts()函数更新为以下内容:

...  @impl true  def handle_info({FollowComponent, :update_totals, _}, socket) do    {:noreply, socket}  end    defp assign_posts(socket) do    if socket.assigns.current_user do      current_user = socket.assigns.current_user      random_5_users = Accounts.random_5(current_user)      socket      |> assign(users: random_5_users)      |> assign_user_feed()    else      socket    end  end

当初要显示带有随机用户的侧边栏,请将 Inside 更新lib/instagram_clone_web/live/page_live.html.leex为以下内容:

<%= if @current_user do %>  <section class="flex">    <div id="user-feed" class="w-3/5" phx-update="append">      <%= for post <- @user_feed do %>        <%= live_component @socket,          InstagramCloneWeb.Live.PagePostFeedComponent,          post: post,          id: post.id,          current_user: @current_user %>      <% end %>    </div>    <div>      <sidebar class="fixed w-1/4">        <section class=" ml-auto pl-8">          <div class="flex items-center">            <!-- Post header section -->            <%= live_redirect to: Routes.user_profile_path(@socket, :index, @current_user.username) do %>              <%= img_tag Avatar.get_thumb(@current_user.avatar_url), class: "w-14 h-14 rounded-full object-cover object-center" %>            <% end %>            <div class="ml-3">              <%= live_redirect @current_user.username,                to: Routes.user_profile_path(@socket, :index, @current_user.username),                class: "truncate font-bold text-sm text-gray-500 hover:underline" %>              <h2 class="text-sm text-gray-500"><%= @current_user.full_name %></h2>            </div>            <!-- End post header section -->          </div>          <h1 class="text-gray-500 mt-5">Suggestions For You</h1>          <%= for user <- @users do %>            <div class="flex items-center p-3">              <!-- Post header section -->              <%= live_redirect to: Routes.user_profile_path(@socket, :index, user.username) do %>                <%= img_tag Avatar.get_thumb(user.avatar_url), class: "w-10 h-10 rounded-full object-cover object-center" %>              <% end %>              <div class="ml-3">                <%= live_redirect user.username,                  to: Routes.user_profile_path(@socket, :index, user.username),                  class: "truncate font-bold text-sm text-gray-500 hover:underline" %>                <h2 class="text-xs text-gray-500">Suggested for you</h2>              </div>              <span class="ml-auto">                <%= live_component @socket,                  InstagramCloneWeb.UserLive.FollowComponent,                  id: user.id,                  user: user,                  current_user: @current_user %>              </span>              <!-- End post header section -->            </div>          <% end %>        </section>      </sidebar>    </div>  </section>  <div    id="profile-posts-footer"    class="flex justify-center"    phx-hook="ProfilePostsScroll">  </div><% else %>  <%= live_component @socket,    InstagramCloneWeb.PageLiveComponent,    id: 1 %><% end %>

当初就是这样,您能够通过将以下列表发送到后续组件来进步代码效率,从而无需拜访数据库即可设置按钮,从而改良代码。

转自:Elixirprogrammer