首页 > 其他分享 >使用 Phoenix LiveView 构建 Instagram (3)

使用 Phoenix LiveView 构建 Instagram (3)

时间:2023-09-01 09:55:25浏览次数:35  
标签:instagram do end socket Phoenix LiveView live user Instagram

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

在第 2 部分中,我们添加了编辑帐户和上传用户头像的功能,在这部分中,我们将处理用户的个人资料。您可以赶上Instagram 克隆 GitHub Repo。

首先,我们需要路由,lib/instagram_clone_web/router.ex在根范围下打开添加以下路由:browser

scope "/", InstagramCloneWeb do
  pipe_through :browser
  
  live "/", PageLive,  :index
  live "/:username", UserLive.Profile # THIS LINE WAS ADDED
end

然后让我们在lib/instagram_clone_web/live/user_live文件夹中创建实时查看文件:

lib/instagram_clone_web/live/user_live/profile.ex lib/instagram_clone_web/live/user_live/profile.html.leex

在里面添加以下内容lib/instagram_clone_web/live/user_live/profile.ex

defmodule InstagramCloneWeb.UserLive.Profile do
  use InstagramCloneWeb, :live_view
  
  @impl true
  def mount(%{"username" => username}, session, socket) do
    socket = assign_defaults(session, socket)

    {:ok,
      socket
      |> assign(username: username)}
  end

end

里面lib/instagram_clone_web/live/user_live/profile.html.leex

<h1 class="text-5xl"><%= @username %></h1>

打开第 56 行的导航标题,lib/instagram_clone_web/live/header_nav_component.html.leex让我们添加新路线:

<%= live_patch to: Routes.live_path(@socket, InstagramCloneWeb.UserLive.Profile, @current_user.username)  do  %>

我们需要找到用户名参数传递给我们的实时视图的用户,打开lib/instagram_clone/accounts.ex添加一个profile()函数来为我们做到这一点:

...

  @doc """
  Gets the user with the given username param.
  """
  def profile(param) do
    Repo.get_by!(User, username: param)
  end

...

让我们更新里面的挂载函数lib/instagram_clone_web/live/user_live/profile.ex

defmodule InstagramCloneWeb.UserLive.Profile do
  use InstagramCloneWeb, :live_view
  
  @impl true
  def mount(_params, session, socket) do
    socket = assign_defaults(session, socket)

    {:ok, socket}
  end

end

我们需要处理用户名参数,打开宏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
      alias InstagramClone.Accounts # <-- THIS LINE WAS ADDED
      @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
	  @doc """
	    Because we are calling this function in each liveview, 
	    and we needed access to the username param in our profile liveview, 
	    we updated this function for when the username param is present,
	    get the user and assign it along with page title to the socket
	  """
      @impl true
      def handle_params(params, uri, socket) do
        if Map.has_key?(params, "username") do
          %{"username" => username} = params
          user = Accounts.profile(username)
          {:noreply,
          socket
          |> assign(current_uri_path: URI.parse(uri).path)
          |> assign(user: user, page_title: "#{user.full_name} (@#{user.username})")}
        else
          {:noreply,
            socket
            |> assign(current_uri_path: URI.parse(uri).path)}
        end
      end
    end
  end

Repo.get_by!(可查询、子句、选项)

从查询中获取单个结果。Ecto.NoResultsError如果未找到记录或有多个条目,则引发。在生产中,当找不到记录时会出现 404 错误。

在第 57 行打开,lib/instagram_clone_web/live/header_nav_component.htm.leex将以下内容添加到配置文件列表标记中,以在选择时关闭下拉菜单:

<li  @click="open = false"  class="py-2 px-4 hover:bg-gray-50">Profile</li>

里面lib/instagram_clone_web/live/user_live/profile.html.leex

<header class="flex justify-center px-10">
  <!-- Profile Picture Section -->
  <section class="w-1/4">
      <%= img_tag @user.avatar_url,
          class: "w-40 h-40 rounded-full object-cover object-center" %>
  </section>
  <!-- END Profile Picture Section -->

  <!-- Profile Details Section -->
  <section class="w-3/4">
    <div class="flex px-3 pt-3">
        <h1 class="truncate md:overflow-clip text-2xl md:text-2xl text-gray-500 mb-3"><%= @user.username %></h1>
        <span class="ml-11"><button class="py-1 px-5 border-none shadow rounded text-gray-50 hover:bg-light-blue-600 bg-light-blue-500">Follow</button></span>
    </div>

    <div>
      <ul class="flex p-3">
          <li><b>0</b> Posts</li>
          <li class="ml-11"><b>0</b> Followers</li>
          <li class="ml-11"><b>0</b> Following</li>
      </ul>
    </div>

    <div class="p-3">
      <h2 class="text-md text-gray-600 font-bold"><%= @user.full_name %></h2>
      <%= if @user.bio do %>
        <p class="max-w-full break-words"><%= @user.bio %></p>
      <% end %>
      <%= if @user.website do %>
        <%= link display_website_uri(@user.website),
          to: @user.website,
          target: "_blank", rel: "noreferrer",
          class: "text-blue-700" %>
      <% end %>
    </div>
  </section>
  <!-- END Profile Details Section -->
</header>

<section class="border-t-2 mt-5">
  <ul class="flex justify-center text-center space-x-20">
    <li class="pt-4 px-1 text-sm text-gray-600 border-t-2 border-black -mt-0.5">
       POSTS
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      IGTV
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      SAVED
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      TAGGED
    </li>
  </ul>
</section>

打开lib/instagram_clone_web/live/render_helpers.ex添加以下2个函数来显示和获取网站uri:

  def display_website_uri(website) do
    website = website
    |> String.replace_leading("https://", "")
    |> String.replace_leading("http://", "")
    website
  end

现在我们需要在内部创建用户关注组件lib/instagram_clone_web/live/user_live

lib/instagram_clone_web/live/user_live/follow_component.ex

defmodule InstagramCloneWeb.UserLive.FollowComponent do
  use InstagramCloneWeb, :live_component

  def render(assigns) do
    ~L"""
    <button
      phx-target="<%= @myself %>"
      phx-click="toggle-status"
      class="<%= @follow_btn_styles? %>"><%= @follow_btn_name? %></button>
    """
  end

  def handle_event("toggle-status", _params, socket) do
    follow_btn_name? = get_follow_btn_name?(socket.assigns.follow_btn_name?)
    follow_btn_styles? = get_follow_btn_styles?(socket.assigns.follow_btn_name?)

    :timer.sleep(200)
    {:noreply,
      socket
      |> assign(follow_btn_name?: follow_btn_name?)
      |> assign(follow_btn_styles?: follow_btn_styles?)}
  end

  defp get_follow_btn_name?(name) when name == "Follow" do
    "Unfollow"
  end
  defp get_follow_btn_name?(name) when name == "Unfollow" do
    "Follow"
  end

  defp get_follow_btn_styles?(name) when name == "Follow"  do
    "py-1 px-2 text-red-500 border-2 rounded font-semibold hover:bg-gray-50 focus:outline-none"
  end
  defp get_follow_btn_styles?(name) when name == "Unfollow" do
    "py-1 px-5 border-none shadow rounded text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 focus:outline-none"
  end
end

在我们的渲染函数中,我们只有按钮和一个将在组件内部获取句柄的单击函数。它有 2 个分配,一个用于按钮名称,另一个用于样式,这些将在我们的配置文件 LiveView 中设置,然后在我们的事件函数中,我们将它们分配回套接字。

现在让我们在配置文件模板中使用我们的组件,打开lib/instagram_clone_web/live/user_live/profile.html.leex文件并将其更新为以下内容:

<header class="flex justify-center px-10">
  <!-- Profile Picture Section -->
  <section class="w-1/4">
      <%= img_tag @user.avatar_url,
          class: "w-40 h-40 rounded-full object-cover object-center" %>
  </section>
  <!-- END Profile Picture Section -->

  <!-- Profile Details Section -->
  <section class="w-3/4">
    <div class="flex px-3 pt-3">
        <h1 class="truncate md:overflow-clip text-2xl md:text-2xl text-gray-500 mb-3"><%= @user.username %></h1>
        <span class="ml-11">
	      <!-- THE BUTTON WAS REPLACED FOR THE COMPONENT -->
          <%= cond do %>
            <% @current_user && @current_user == @user -> %>
              <%= live_patch "Edit Profile",
                to: Routes.live_path(@socket, InstagramCloneWeb.UserLive.Settings),
                class: "py-1 px-2 border-2 rounded font-semibold hover:bg-gray-50" %>

            <% @current_user -> %>
              <%= live_component @socket,
                InstagramCloneWeb.UserLive.FollowComponent,
                id: @user.id,
                follow_btn_name?: @follow_btn_name?,
                follow_btn_styles?: @follow_btn_styles? %>

            <% true -> %>
              <%= link "Follow", to: Routes.user_session_path(@socket, :new), class: "py-1 px-5 border-none shadow rounded text-gray-50 hover:bg-light-blue-600 bg-light-blue-500" %>
          <% end %>
          <!-- ALL THIS UNTIL HERE WAS ADDED -->
        </span>
    </div>

    <div>
      <ul class="flex p-3">
          <li><b>0</b> Posts</li>
          <li class="ml-11"><b>0</b> Followers</li>
          <li class="ml-11"><b>0</b> Following</li>
      </ul>
    </div>

    <div class="p-3">
      <h2 class="text-md text-gray-600 font-bold"><%= @user.full_name %></h2>
      <%= if @user.bio do %>
        <p class="max-w-full break-words"><%= @user.bio %></p>
      <% end %>
      <%= if @user.website do %>
        <%= link display_website_uri(@user.website),
          to: @user.website,
          target: "_blank", rel: "noreferrer",
          class: "text-blue-700" %>
      <% end %>
    </div>
  </section>
  <!-- END Profile Details Section -->
</header>

<section class="border-t-2 mt-5">
  <ul class="flex justify-center text-center space-x-20">
    <li class="pt-4 px-1 text-sm text-gray-600 border-t-2 border-black -mt-0.5">
       POSTS
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      IGTV
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      SAVED
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      TAGGED
    </li>
  </ul>
</section>

我们创建了一个条件来向用户显示正确的按钮,当登录并且用户在他的个人资料上时,它将获得一个编辑个人资料链接,当登录和任何其他个人资料时,我们显示该组件,当未登录时,只是一个链接到登录页面。现在我们需要发送到组件的分配,就在组件显示时,打开lib/instagram_clone_web/live/user_live/profile.ex文件并将其更新为以下内容:

defmodule InstagramCloneWeb.UserLive.Profile do
  use InstagramCloneWeb, :live_view

  alias InstagramClone.Accounts

  @impl true
  def mount(%{"username" => username}, session, socket) do
    socket = assign_defaults(session, socket)
    current_user = socket.assigns.current_user
    user = Accounts.profile(username)

    get_assigns(socket, current_user, user)
  end

  defp get_follow_btn_name? do
    "Follow"
  end

  defp get_follow_btn_styles? do
    "py-1 px-5 border-none shadow rounded text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 focus:outline-none"
  end

  defp get_assigns(socket, current_user, user) do
    if current_user && current_user !== user do
      follow_btn_name? = get_follow_btn_name?()
      follow_btn_styles? = get_follow_btn_styles?()

      {:ok,
        socket
        |> assign(follow_btn_name?: follow_btn_name?)
        |> assign(follow_btn_styles?: follow_btn_styles?)}
    else
      {:ok, socket}
    end
  end

end

让我们创建一个关注模式来处理终端中的关注者:

$ mix phx.gen.schema Accounts.Follows accounts_follows follower_id:references:users followed_id:references:users

打开生成的迁移并添加以下内容:

defmodule  InstagramClone.Repo.Migrations.CreateAccountsFollows  do
  use Ecto.Migration
  
  def  change  do
    create table(:accounts_follows)  do
      add :follower_id,  references(:users,  on_delete:  :delete_all)
      add :followed_id,  references(:users,  on_delete:  :delete_all)

      timestamps()
    end
	
    create index(:accounts_follows,  [:follower_id])
    create index(:accounts_follows,  [:followed_id])
  end
end

另外,让我们在用户表中添加 2 个新字段,以跟踪总关注者和关注者,回到我们的终端:

$ mix ecto.gen.migration adds_follower_followings_count_to_users_table

打开生成的迁移并添加以下内容:

defmodule InstagramClone.Repo.Migrations.AddsFollowerFollowingsCountToUsersTable do
  use Ecto.Migration

  def change do
    alter table(:users) do
      add :followers_count, :integer, default: 0
      add :following_count, :integer, default: 0
    end
  end
end

返回终端运行$ mix ecto.migrate

现在打开在该文件内生成的新架构,lib/instagram_clone/accounts/follows.ex添加以下内容:

defmodule InstagramClone.Accounts.Follows do
  use Ecto.Schema

  alias InstagramClone.Accounts.User

  schema "accounts_follows" do
    belongs_to :follower, User
    belongs_to :followed, User

    timestamps()
  end

end

里面lib/instagram_clone/accounts/user.ex添加以下内容:

  alias InstagramClone.Accounts.Follows

  @derive {Inspect,  except:  [:password]}
  schema "users"  do
    field :email,  :string
    field :password,  :string,  virtual:  true
    field :hashed_password,  :string
    field :confirmed_at,  :naive_datetime
    field :username,  :string
    field :full_name,  :string
    field :avatar_url,  :string,  default:  "/images/default-avatar.png"
    field :bio,  :string
    field :website,  :string
    field :followers_count, :integer, default: 0
    field :following_count, :integer, default: 0
    has_many :following, Follows,  foreign_key:  :follower_id
    has_many :followers, Follows,  foreign_key:  :followed_id
    timestamps()
  end

  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)
    |> unsafe_validate_unique(:username, InstagramClone.Repo)
    |> validate_length(:full_name, min: 4, max: 255)
    |> validate_format(:full_name,  ~r/^[a-zA-Z0-9 ]*$/,  message:  "Please use letters and numbers")
    |> validate_website_schemes()
    |> validate_website_authority()
    |> validate_email()
    |> validate_password(opts)
  end

  defp validate_website_schemes(changeset) do
    validate_change(changeset, :website, fn :website, website ->
      uri = URI.parse(website)
      if uri.scheme, do: check_uri_scheme(uri.scheme), else: [website: "Enter a valid website"]
    end)
  end

  defp validate_website_authority(changeset) do
    validate_change(changeset, :website, fn :website, website ->
      authority = URI.parse(website).authority
      if String.match?(authority, ~r/^[a-zA-Z0-9.-]*$/) do
        []
      else
        [website: "Enter a valid website"]
      end
    end)
  end

  defp check_uri_scheme(scheme) when scheme == "http", do: []
  defp check_uri_scheme(scheme) when scheme == "https", do: []
  defp check_uri_scheme(_scheme), do: [website: "Enter a valid website"]

lib/instagram_clone/accounts.ex在文件的底部和顶部添加以下函数alias InstagramClone.Accounts.Follows

  @doc """
  Creates a follow to the given followed user, and builds
  user association to be able to preload the user when associations are loaded,
  gets users to update counts, then performs 3 Repo operations,
  creates the follow, updates user followings count, and user followers count,
  we select the user in our updated followers count query, that gets returned
  """
  def create_follow(follower, followed, user) do
    follower = Ecto.build_assoc(follower, :following)
    follow = Ecto.build_assoc(followed, :followers, follower)
    update_following_count = from(u in User, where: u.id == ^user.id)
    update_followers_count = from(u in User, where: u.id == ^followed.id, select: u)

    Ecto.Multi.new()
    |> Ecto.Multi.insert(:follow, follow)
    |> Ecto.Multi.update_all(:update_following, update_following_count, inc: [following_count: 1])
    |> Ecto.Multi.update_all(:update_followers, update_followers_count, inc: [followers_count: 1])
    |> Repo.transaction()
    |> case do
      {:ok,   %{update_followers: update_followers}} ->
        {1, user} = update_followers
        hd(user)
    end
  end

  @doc """
  Deletes following association with given user,
  then performs 3 Repo operations, to delete the association,
  update followings count, update and select followers count,
  updated followers count gets returned
  """
  def unfollow(follower_id, followed_id) do
    follow = following?(follower_id, followed_id)
    update_following_count = from(u in User, where: u.id == ^follower_id)
    update_followers_count = from(u in User, where: u.id == ^followed_id, select: u)

    Ecto.Multi.new()
    |> Ecto.Multi.delete(:follow, follow)
    |> Ecto.Multi.update_all(:update_following, update_following_count, inc: [following_count: -1])
    |> Ecto.Multi.update_all(:update_followers, update_followers_count, inc: [followers_count: -1])
    |> Repo.transaction()
    |> case do
      {:ok,   %{update_followers: update_followers}} ->
        {1, user} = update_followers
        hd(user)
    end
  end

  @doc """
  Returns nil if not found
  """
  def following?(follower_id, followed_id) do
    Repo.get_by(Follows, [follower_id: follower_id, followed_id: followed_id])
  end

  @doc """
  Returns all user's followings
  """
  def list_following(user) do
    user = user |> Repo.preload(:following)
    user.following |> Repo.preload(:followed)
  end

  @doc """
  Returns all user's followers
  """
  def list_followers(user) do
    user = user |> Repo.preload(:followers)
    user.followers |> Repo.preload(:follower)
  end

现在让我们更新我们的文件lib/instagram_clone_web/live/user_live/profile.ex

defmodule InstagramCloneWeb.UserLive.Profile do
  use InstagramCloneWeb, :live_view

  alias InstagramClone.Accounts
  alias InstagramCloneWeb.UserLive.FollowComponent

  @impl true
  def mount(_params, session, socket) do
    socket = assign_defaults(session, socket)

    {:ok, socket}
  end

  @impl true
  def handle_info({FollowComponent, :update_totals, updated_user}, socket) do
    {:noreply, socket |> assign(user: updated_user)}
  end

end

我们将在组件内设置关注按钮,然后向父级实时视图发送消息以更新计数。

在里面lib/instagram_clone_web/live/user_live/profile.html.leex更新第 20 行和第 27 行的分配名称:

<%  @current_user ->  %>
  <%= live_component @socket,
    InstagramCloneWeb.UserLive.FollowComponent,
    id:  @user.id,
    user:  @user,
    current_user:  @current_user %>

<%  true  ->  %>
  <%= link "Follow",  to: Routes.user_session_path(@socket,  :new),  class:  "user-profile-follow-btn"  %>
  

打开assets/css/app.scss并添加以下内容:

/* This file is for your main application css. */
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@import "../node_modules/nprogress/nprogress.css";

@layer components {
  .user-profile-unfollow-btn {
    @apply  py-1  px-2  text-red-500  border-2  rounded  font-semibold  hover:bg-gray-50
  }

  .user-profile-follow-btn {
    @apply  py-1  px-5  border-none  shadow  rounded  text-gray-50  hover:bg-light-blue-600  bg-light-blue-500
  }
}

/* Styles for handling buttons click events */
.while-submitting  {  display: none;  }

.phx-click-loading  {
  .while-submitting  {  display: inline;  }
  .btns  {  display: none;  }
}

打开lib/instagram_clone_web/live/user_live/follow_component.ex文件并将其更新为以下内容:

defmodule InstagramCloneWeb.UserLive.FollowComponent do
  use InstagramCloneWeb, :live_component

  alias InstagramClone.Accounts

  @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="focus:outline-none">

      <span class="while-submitting">
        <span class="<%= @follow_btn_styles %> inline-flex items-center transition ease-in-out duration-150 cursor-not-allowed">
          <svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-gray-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
            <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
            <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
          </svg>
          Saving
        </span>
      </span>

      <span class="<%= @follow_btn_styles %>"><%= @follow_btn_name %><span>
    </button>
    """
  end

  @impl true
  def handle_event("toggle-status", _params, socket) do
    current_user = socket.assigns.current_user
    user = socket.assigns.user

    :timer.sleep(300)
    if Accounts.following?(current_user.id, user.id) do
      unfollow(socket, current_user.id, user.id)
    else
      follow(socket, current_user, user)
    end
  end

  defp get_btn_status(socket, assigns) do
    if Accounts.following?(assigns.current_user.id, assigns.user.id) do
      get_socket_assigns(socket, assigns, "Unfollow", "user-profile-unfollow-btn")
    else
      get_socket_assigns(socket, assigns, "Follow", "user-profile-follow-btn")
    end
  end

  defp get_socket_assigns(socket, assigns, btn_name, btn_styles) do
    {:ok,
      socket
      |> assign(assigns)
      |> assign(follow_btn_name: btn_name)
      |> assign(follow_btn_styles: btn_styles)}
  end

  defp follow(socket, current_user, user) do
    updated_user = Accounts.create_follow(current_user, user, current_user)
    # Message sent to the parent liveview to update totals
    send(self(), {__MODULE__, :update_totals, updated_user})
    {:noreply,
      socket
      |> assign(follow_btn_name: "Unfollow")
      |> assign(follow_btn_styles: "user-profile-unfollow-btn")}
  end

  defp unfollow(socket, current_user_id, user_id) do
    updated_user = Accounts.unfollow(current_user_id, user_id)
    # Message sent to the parent liveview to update totals
    send(self(), {__MODULE__, :update_totals, updated_user})
    {:noreply,
      socket
      |> assign(follow_btn_name: "Follow")
      |> assign(follow_btn_styles: "user-profile-follow-btn")}
  end

end

内部lib/instagram_clone_web/live/user_live/profile.html.leex更新第 37、38 行:

<li class="ml-11"><b><%=  @user.followers_count %></b> Followers</li>
<li class="ml-11"><b><%=  @user.following_count %></b> Following</li>

一切都应该工作正常,但是出现了一个问题,您可以在下面的 gif 图片中看到它。

instagram-phoenix-p3-1.gif

当关注按钮被触发并且我们导航到我们的个人资料时,我们仍然在同一条路线上,我们没有得到正确的编辑个人资料按钮,因为我们在标题导航中使用,所以当我们转到我们的个人资料时,live_patch/2有没有变化,唯一改变的是@user在我们的模板中,我们用来显示右侧按钮的情况永远不会被调用。

link/2redirect/2重新加载整页

live_redirect/2push_redirect/2重新加载 LiveView 但保留当前布局

live_patch/2push_patch/2更新当前的 LiveView 并仅发送最小的差异

当您想要导航到当前的 LiveView 时,必须使用“修补”操作,只需更新 URL 和当前参数,而不需要挂载新的 LiveView。使用补丁时,将调用handle_params/3回调并将最小的更改集发送到客户端。请参阅下一节了解更多信息。

一个简单的经验法则是坚持使用 live_redirect/2push_redirect/2 并仅在您希望在同一 LiveView 中导航时最大程度地减少发送的数据量的情况下使用补丁帮助程序(例如,如果您想更改对表进行排序,同时更新 URL)。

我们能想到的唯一可以解决该问题的解决方案是live_redirect/2在标题导航中使用重新加载 LiveView。在第 56 行打开,lib/instagram_clone_web/live/header_nav_component.html.leex执行以下操作:

<%= live_redirect to: Routes.live_path(@socket, InstagramCloneWeb.UserLive.Profile,  @current_user.username)  do  %>

现在,LiveView 重新加载,我们会显示正确的按钮。我还注意到为什么我们需要使用该handle_params()函数来分配用户,因为当我们使用live_patch/2任何内容时都没有更改并且用户分配没有重新加载,但现在 LiveView 重新加载,因此我们可以在 LiveViewmount()函数中设置用户,这是冲突的。

打开函数lib/instagram_clone_web/live/user_live/profile.ex并将其更新mount()为以下内容:

  @impl true
  def mount(%{"username" => username}, session, socket) do
    socket = assign_defaults(session, socket)
    user = Accounts.profile(username)

    {:ok,
      socket
      |> assign(user: user)
      |> assign(page_title: "#{user.full_name} (@#{user.username})")}
  end

打开lib/instagram_clone_web.ex并在我们的live_view()宏中将函数更新handle_params()为以下内容:

      @impl true
      def handle_params(_params, uri, socket) do
        {:noreply,
          socket
          |> assign(current_uri_path: URI.parse(uri).path)}
      end

另外,当我们在个人资料页面上时,我们不再需要在选择时关闭下拉菜单,因此lib/instagram_clone_web/live/header_nav_component.htm.leex在第 57 行打开,删除 AlpineJS 指令:

<li class="py-2 px-4 hover:bg-gray-50">Profile</li>

让我们坚持只live_patch/2在我们只想显示使用 URLS 参数更新的内容时使用,这不会触发任何操作或更改任何状态。

我还决定处理 LiveView 中关注按钮的条件,这是个人喜好,您可以选择任何一种方式或您感觉舒服的方式。打开lib/instagram_clone_web/live/user_live/profile.ex并添加以下私有函数:

defp get_action(user, current_user) do
  cond do
    current_user && current_user == user -> :edit_profile
    current_user -> :follow_component
    true -> :login_btn
  end
end

然后lib/instagram_clone_web/live/user_live/profile.ex在我们的 mount 函数中,让我们分配my_action

  @impl true
  def mount(%{"username" => username}, session, socket) do
    socket = assign_defaults(session, socket)
    user = Accounts.profile(username)
    my_action = get_action(user, socket.assigns.current_user)

    {:ok,
      socket
      |> assign(my_action: my_action)
      |> assign(user: user)
      |> assign(page_title: "#{user.full_name} (@#{user.username})")}
  end

然后打开lib/instagram_clone_web/live/user_live/profile.html.leex并将条件更新为以下内容:

<header class="flex justify-center px-10">
  <!-- Profile Picture Section -->
  <section class="w-1/4">
      <%= img_tag @user.avatar_url,
          class: "w-40 h-40 rounded-full object-cover object-center" %>
  </section>
  <!-- END Profile Picture Section -->

  <!-- Profile Details Section -->
  <section class="w-3/4">
    <div class="flex px-3 pt-3">
        <h1 class="truncate md:overflow-clip text-2xl md:text-2xl text-gray-500 mb-3"><%= @user.username %></h1>
        <span class="ml-11">
          <%= if @my_action in [:edit_profile] do %>
            <%= live_patch "Edit Profile",
                to: Routes.live_path(@socket, InstagramCloneWeb.UserLive.Settings),
                class: "py-1 px-2 border-2 rounded font-semibold hover:bg-gray-50" %>
          <% end %>

          <%= if @my_action in [:follow_component] do %>
            <%= live_component @socket,
                InstagramCloneWeb.UserLive.FollowComponent,
                id: @user.id,
                user: @user,
                current_user: @current_user %>
          <% end %>

          <%= if @my_action in [:login_btn] do %>
            <%= link "Follow", to: Routes.user_session_path(@socket, :new), class: "user-profile-follow-btn" %>
          <% end %>
        </span>
    </div>

    <div>
      <ul class="flex p-3">
          <li><b>0</b> Posts</li>
          <li class="ml-11"><b><%= @user.followers_count %></b> Followers</li>
          <li class="ml-11"><b><%= @user.following_count %></b> Following</li>
      </ul>
    </div>

    <div class="p-3">
      <h2 class="text-md text-gray-600 font-bold"><%= @user.full_name %></h2>
      <%= if @user.bio do %>
        <p class="max-w-full break-words"><%= @user.bio %></p>
      <% end %>
      <%= if @user.website do %>
        <%= link display_website_uri(@user.website),
          to: @user.website,
          target: "_blank", rel: "noreferrer",
          class: "text-blue-700" %>
      <% end %>
    </div>
  </section>
  <!-- END Profile Details Section -->
</header>

<section class="border-t-2 mt-5">
  <ul class="flex justify-center text-center space-x-20">
    <li class="pt-4 px-1 text-sm text-gray-600 border-t-2 border-black -mt-0.5">
       POSTS
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      IGTV
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      SAVED
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      TAGGED
    </li>
  </ul>
</section>

让我们致力于显示以下内容和关注者,我们将为此使用模态,我们需要使用handle_params()每个 LiveView 上的宏中定义的函数,因此让我们采用该函数并正确分配它。

打开并从我们的宏中lib/instagram_clone_web.ex删除:handle_params() live_view()

	  # DELETE THIS FUNCTION AND MOVE IT TO: 
	  #lib/instagram_clone_web/live/user_live/settings.ex
	  #lib/instagram_clone_web/live/user_live/pass_settings.ex
	  #lib/instagram_clone_web/live/page_live.ex
      @impl true
      def handle_params(_params, uri, socket) do
        {:noreply,
          socket
          |> assign(current_uri_path: URI.parse(uri).path)}
      end

现在,在我们找到另一种方法之前,我们需要current_uri_path在每个实时视图上手动分配 ,所以让我们记住这一点,我们必须记住它。

打开lib/instagram_clone_web/router.ex

	scope "/", InstagramCloneWeb do
	  pipe_through :browser
	  
	  live "/", PageLive,  :index
	  live "/:username", UserLive.Profile, :index # THIS LINE WAS UPDATED
	end

	scope "/", InstagramCloneWeb do
	  pipe_through [:browser, :require_authenticated_user]

	  get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
	  live "/accounts/edit", UserLive.Settings
	  live "/accounts/password/change", UserLive.PassSettings
	  live "/:username/following", UserLive.Profile, :following  # THIS LINE WAS ADDED
	  live "/:username/followers", UserLive.Profile, :followers # THIS LINE WAS ADDED
	end

打开第 56 行的导航标题,lib/instagram_clone_web/live/header_nav_component.html.leex让我们更新路线:

<%= live_patch to: Routes.user_profile_path(@socket, :index, @current_user.username)  do  %>

更新lib/instagram_clone_web/live/user_live/profile.ex

defmodule InstagramCloneWeb.UserLive.Profile do
  use InstagramCloneWeb, :live_view

  alias InstagramClone.Accounts
  alias InstagramCloneWeb.UserLive.FollowComponent

  @impl true
  def mount(%{"username" => username}, session, socket) do
    socket = assign_defaults(session, socket)
    user = Accounts.profile(username)

    {:ok,
      socket
      |> assign(user: user)
      |> assign(page_title: "#{user.full_name} (@#{user.username})")}
  end

  @impl true
  def handle_params(_params, uri, socket) do
    socket = socket |> assign(current_uri_path: URI.parse(uri).path)
    {:noreply, apply_action(socket, socket.assigns.live_action)}
  end

  @impl true
  def handle_info({FollowComponent, :update_totals, updated_user}, socket) do
    {:noreply, apply_msg_action(socket, socket.assigns.live_action, updated_user)}
  end

  defp apply_msg_action(socket, :follow_component, updated_user) do
    socket |> assign(user: updated_user)
  end

  defp apply_msg_action(socket, _, _updated_user) do
    socket
  end

  defp apply_action(socket, :index) do
	live_action = get_live_action(socket.assigns.user, socket.assigns.current_user)
	
    socket |> assign(live_action: live_action)
  end

  defp apply_action(socket, :following) do
    following = Accounts.list_following(socket.assigns.user)
    socket |> assign(following: following)
  end

  defp apply_action(socket, :followers) do
    followers = Accounts.list_followers(socket.assigns.user)
    socket |> assign(followers: followers)
  end

  defp get_live_action(user, current_user) do
    cond do
      current_user && current_user == user -> :edit_profile
      current_user -> :follow_component
      true -> :login_btn
    end
  end

end

更新lib/instagram_clone_web/live/user_live/profile.html.leex

<%= if @live_action == :following do %>
  <%= live_modal @socket, InstagramCloneWeb.UserLive.Profile.FollowingComponent,
    id: @user.id || :following,
    width:  "w-1/4",
    current_user: @current_user,
    following: @following,
    return_to: Routes.user_profile_path(@socket, :index, @user.username) %>
<% end %>

<%= if @live_action == :followers do %>
  <%= live_modal @socket, InstagramCloneWeb.UserLive.Profile.FollowersComponent,
    id: @user.id || :followers,
    width:  "w-1/4",
    current_user: @current_user,
    followers: @followers,
    return_to: Routes.user_profile_path(@socket, :index, @user.username) %>
<% end %>

<header class="flex justify-center px-10">
  <!-- Profile Picture Section -->
  <section class="w-1/4">
      <%= img_tag @user.avatar_url,
          class: "w-40 h-40 rounded-full object-cover object-center" %>
  </section>
  <!-- END Profile Picture Section -->

  <!-- Profile Details Section -->
  <section class="w-3/4">
    <div class="flex px-3 pt-3">
        <h1 class="truncate md:overflow-clip text-2xl md:text-2xl text-gray-500 mb-3"><%= @user.username %></h1>
        <span class="ml-11">
          <%= if @live_action == :edit_profile do %>
            <%= live_patch "Edit Profile",
                to: Routes.live_path(@socket, InstagramCloneWeb.UserLive.Settings),
                class: "py-1 px-2 border-2 rounded font-semibold hover:bg-gray-50" %>
          <% end %>

          <%= if @live_action == :follow_component do %>
            <%= live_component @socket,
                InstagramCloneWeb.UserLive.FollowComponent,
                id: @user.id,
                user: @user,
                current_user: @current_user %>
          <% end %>

          <%= if @live_action == :login_btn do %>
            <%= link "Follow", to: Routes.user_session_path(@socket, :new), class: "user-profile-follow-btn" %>
          <% end %>
        </span>
    </div>

    <div>
      <ul class="flex p-3">
          <li><b>0</b> Posts</li>
          <%= live_patch to: Routes.user_profile_path(@socket, :followers, @user.username) do %>
            <li class="ml-11"><b><%= @user.followers_count %></b> Followers</li>
          <% end %>
          <%= live_patch to: Routes.user_profile_path(@socket, :following, @user.username) do %>
            <li class="ml-11"><b><%= @user.following_count %></b> Following</li>
          <% end %>
      </ul>
    </div>

    <div class="p-3">
      <h2 class="text-md text-gray-600 font-bold"><%= @user.full_name %></h2>
      <%= if @user.bio do %>
        <p class="max-w-full break-words"><%= @user.bio %></p>
      <% end %>
      <%= if @user.website do %>
        <%= link display_website_uri(@user.website),
          to: @user.website,
          target: "_blank", rel: "noreferrer",
          class: "text-blue-700" %>
      <% end %>
    </div>
  </section>
  <!-- END Profile Details Section -->
</header>

<section class="border-t-2 mt-5">
  <ul class="flex justify-center text-center space-x-20">
    <li class="pt-4 px-1 text-sm text-gray-600 border-t-2 border-black -mt-0.5">
       POSTS
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      IGTV
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      SAVED
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      TAGGED
    </li>
  </ul>
</section>

打开lib/instagram_clone_web/live/render_helpers.ex并添加以下内容以帮助我们显示模态:

  import Phoenix.LiveView.Helpers

      @doc """
  Renders a component inside the `LiveviewPlaygroundWeb.ModalComponent` component.

  The rendered modal receives a `:return_to` option to properly update
  the URL when the modal is closed.
  The rendered modal also receives a `:width` option for the style width

  ## Examples

      <%= live_modal @socket, LiveviewPlaygroundWeb.PostLive.FormComponent,
        id: @post.id || :new,
        width: "w-1/2",
        post: @post,
        return_to: Routes.post_index_path(@socket, :index) %>
  """
  def live_modal(socket, component, opts) do
    path = Keyword.fetch!(opts, :return_to)
    width = Keyword.fetch!(opts,  :width)
    modal_opts = [id: :modal, return_to: path, width: width, component: component, opts: opts]
    live_component(socket, InstagramCloneWeb.ModalComponent, modal_opts)
  end

创建模态组件lib/instagram_clone_web/live/modal_component.ex

defmodule InstagramCloneWeb.ModalComponent do
  use InstagramCloneWeb, :live_component

  @impl true
  def render(assigns) do
    ~L"""
    <div
      class="fixed top-0 left-0 flex items-center justify-center w-full h-screen bg-black bg-opacity-40 z-50"
      phx-capture-click="close"
      phx-window-keydown="close"
      phx-key="escape"
      phx-target="<%= @myself %>"
      phx-page-loading>

      <div class="<%= @width %> h-auto bg-white rounded-xl shadow-xl">
        <%= live_patch raw("&times;"), to: @return_to, class: "float-right text-gray-500 text-4xl px-4" %>
        <%= live_component @socket, @component, @opts %>
      </div>
    </div>
    """
  end

    @impl true
  def handle_event("close", _, socket) do
    {:noreply, push_patch(socket, to: socket.assigns.return_to)}
  end
end

创建关注者组件lib/instagram_clone_web/live/user_live/followers_component.ex

defmodule InstagramCloneWeb.UserLive.Profile.FollowersComponent do
  use InstagramCloneWeb, :live_component

  alias InstagramClone.Uploaders.Avatar
end

和模板lib/instagram_clone_web/live/user_live/followers_component.html.leex

<header class="bg-gray-50 p-2 border-b-2 rounded-t-xl">
  <h1 class="flex justify-center text-xl font-semibold">Followers</h1>
</header>

<%= for follow <- @followers do %>
  <div class="p-4">
    <div class="flex items-center">
      <%= live_redirect to: Routes.user_profile_path(@socket, :index, follow.follower.username) do %>
        <%= img_tag Avatar.get_thumb(follow.follower.avatar_url), class: "w-10 h-10 rounded-full object-cover object-center" %>
      <% end %>

      <div class="ml-3">
        <%= live_redirect  follow.follower.username,
          to: Routes.user_profile_path(@socket, :index, follow.follower.username),
          class: "font-semibold text-sm truncate text-gray-700 hover:underline" %>
        <h6 class="font-semibold text-sm truncate text-gray-400">
          <%= follow.follower.full_name %>
        </h6>
      </div>
      <%= if @current_user !== follow.follower do %>
        <span class="ml-auto">
          <%= live_component @socket,
            InstagramCloneWeb.UserLive.FollowComponent,
            id: follow.follower.id,
            user: follow.follower,
            current_user: @current_user %>
        </span>
      <% end %>
    </div>

  </div>
<% end %>

创建以下组件lib/instagram_clone_web/live/user_live/following_component.ex

defmodule InstagramCloneWeb.UserLive.Profile.FollowingComponent do
  use InstagramCloneWeb, :live_component

  alias InstagramClone.Uploaders.Avatar
end

和模板lib/instagram_clone_web/live/user_live/following_component.html.leex

<header class="bg-gray-50 p-2 border-b-2 rounded-t-xl">
  <h1 class="flex justify-center text-xl font-semibold">Following</h1>
</header>
<%= for follow <- @following do %>
  <div class="p-4">
    <div class="flex items-center">
      <%= live_redirect to: Routes.user_profile_path(@socket, :index, follow.followed.username) do %>
        <%= img_tag Avatar.get_thumb(follow.followed.avatar_url), class: "w-10 h-10 rounded-full object-cover object-center" %>
      <% end %>

      <div class="ml-3">
        <%= live_redirect  follow.followed.username,
          to: Routes.user_profile_path(@socket, :index, follow.followed.username),
          class: "font-semibold text-sm truncate text-gray-700 hover:underline" %>
        <h6 class="font-semibold text-sm truncate text-gray-400">
          <%= follow.followed.full_name %>
        </h6>
      </div>
      <%= if @current_user !== follow.followed do %>
        <span class="ml-auto">
          <%= live_component @socket,
            InstagramCloneWeb.UserLive.FollowComponent,
            id: follow.followed.id,
            user: follow.followed,
            current_user: @current_user %>
        </span>
      <% end %>
    </div>
  </div>
<% end %>

此外,我们还需要对头像上传器、打开lib/instagram_clone_web/live/uploaders/avatar.ex和以下内容进行一些小调整:

  # This was added to return the default image when no avatar uploaded
  def get_thumb(avatar_url) when avatar_url == "/images/default-avatar.png" do
    avatar_url
  end

  def get_thumb(avatar_url) do
    file_name = String.replace_leading(avatar_url, "/uploads/", "")
    ["/#{@upload_directory_name}", "thumb_#{file_name}"] |> Path.join()
  end

instagram-phoenix-p3-2.gif

让我们对标题导航菜单进行一些小的调整,以便不必在每个 LiveView 上分配 current_uri_path,并对我们的page_live组件进行一些小的调整,以在组件内部而不是父 LiveView 中设置表单。

打开lib/instagram_clone_web/live/page_live.ex文件并将其更新为以下内容:

defmodule InstagramCloneWeb.PageLive do
  use InstagramCloneWeb, :live_view

  @impl true
  def mount(_params, session, socket) do
    socket = assign_defaults(session, socket)
    {:ok, socket}
  end

  @impl true
  def handle_params(_params, _uri, socket) do
    {:noreply,
      socket
      |> assign(live_action: apply_action(socket.assigns.current_user))}
  end

  defp apply_action(current_user) do
    if !current_user, do: :root_path
  end
end

更新lib/instagram_clone_web/live/page_live_component.ex如下:

defmodule InstagramCloneWeb.PageLiveComponent do
  use InstagramCloneWeb, :live_component

  alias InstagramClone.Accounts
  alias InstagramClone.Accounts.User

  @impl true
  def mount(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)
    {:noreply, socket |> assign(changeset: changeset)}
  end

  def handle_event("save", _, socket) do
    {:noreply, assign(socket, trigger_submit: true)}
  end
end

在第 5 行内部lib/instagram_clone_web/live/page_live_component.html.leex向表单添加一个目标:

  <%= f = form_for @changeset, Routes.user_registration_path(@socket, :create),
    phx_change: "validate",
    phx_submit: "save",
    phx_target: @myself, # <-- THIS LINE WAS ADDED
    phx_trigger_action: @trigger_submit,
    class: "flex flex-col space-y-4 w-full px-6" %>

更新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,
    id: 1 %>
<% end %>

打开lib/instagram_clone_web/templates/layout/live.html.leex顶部逻辑并将其更新为以下内容:

<%= if @current_user do %>
  <%= live_component @socket, InstagramCloneWeb.HeaderNavComponent, current_user: @current_user %>

<% else %>
  <%= if @live_action !== :root_path do %>
    <%= live_component @socket, InstagramCloneWeb.HeaderNavComponent, current_user: @current_user %>
  <% end %>
<% end %>

现在我们不需要@curent_uri_path在每个liveview上,我们可以从lib/instagram_clone_web/live/user_live/profile.ex第20行里面删除它handle_params()

  @impl true
  def handle_params(_params, uri, socket) do
    socket = socket |> assign(current_uri_path: URI.parse(uri).path) # <-- DELETE THIS LINE
    {:noreply, apply_action(socket, socket.assigns.live_action)}
  end

制作这一部分比我预期的要困难,它变得有点具有挑战性,这个应用程序并不像我们想象的那么容易做好。我的完美主义战胜了我,这就是为什么我花了更长的时间才发布它,我对一些我必须弄清楚的事情一无所知,但毫无疑问,我很享受这个过程并学到了很多东西。在下一部分中,我们将处理用户的帖子。

转自:Elixirprogrammer

标签:instagram,do,end,socket,Phoenix,LiveView,live,user,Instagram
From: https://www.cnblogs.com/markhoo/p/17671009.html

相关文章

  • 使用 Phoenix LiveView 构建 Instagram (1)
    使用PETAL(Phoenix、Elixir、TailwindCSS、AlpineJS、LiveView)技术栈构建一个简化版的InstagramWeb应用程序更好的学习方法是亲自动手构建东西,让我们使用很棒的PETAL(Phoenix、Elixir、TailwindCSS、AlpineJS、LiveView)堆栈构建一个简化版的InstagramWeb应用程序,并深入了......
  • 『MdOI R4』Phoenix 官解(也许)更清晰的阐释
    \[\large(\sum\limits_{i=1}^n|s_i|)-(\sum\limits_{i=1}^{n-1}|s_{p_i}\bigcaps_{p_{i+1}}|)=|\bigcup\limits_{i=1}^ns_i|\]观察题目中式子,不难想到如果对二进制拆位,那么相当于要求对于每个二进制位,包含这一位的集合必须排列在一段区间内,因为左式中每一位至少出现一次,而右......
  • 怎么给hbase的表加二级索引映射到phoenix
    在HBase表中添加二级索引映射到Phoenix在大数据应用中,HBase是一个开源的分布式数据库,而Phoenix是一个基于HBase的SQL层。HBase提供了高性能的读写能力,而Phoenix则使得对HBase表的查询更加简单和直观,类似于传统的关系型数据库。然而,HBase自身并不支持二级索引,这对于一些需要高效查......
  • 跨境盒子:类似Instagram的海外社交平台有哪些?
    Instagram是海外最受欢迎的社交媒体平台之一。根据Statista的数据,Instagram的月度活跃用户超过5亿,约占全球人口的1/10。作为一家老牌社交媒体公司,Instagram的成功在很大程度上要归功于其品牌定位和用户定位。跨境盒子——一个集合了跨境所需各种资源的平台为您选出了以下几个:1. ......
  • CF1515G Phoenix and Odometers
    有点神仙的。题意给定一张\(n\)个点\(m\)条边的有向图,有边权,进行\(q\)次询问(\(n,m,q\le2\times10^5\),边权为不超过\(10^9\)的正整数)。每次询问给定三个参数\(v,s,t(0\les<t\le10^9)\),你需要回答是否存在一条起点终点均为\(v\)的路径,满足\(\text{路径长}+s\equi......
  • Codeforces 1515I - Phoenix and Diamonds(值域倍增+线段树)
    首先\(c\)很大,因此复杂度跟\(c\)有关的项肯定只能是\(\logc\)之类的。类比IOI2021dungeons的套路,我们对值域进行分层,假设\(c\in[2^{\omega-1},2^{\omega})\),考虑令重量在\(\ge2^{\omega-1}\)的物品为“重物品”,其他物品为“轻物品”,那么一个显然的性质是我们最多只......
  • Grids for mac(Instagram客户端工具)v8.5.5免注册版
    GridsforMac是一款允许您在桌面上访问Instagram的应用程序。使用网格,您可以像在手机上一样查看您的Instagram提要、故事和探索页面。您还可以发布照片和视频,对帖子点赞和评论,以及向其他用户发送直接消息。Grids具有简洁、直观的界面,易于使用。它还提供了多种自定义选项,包......
  • Springboot 系列 (29) - Springboot+HBase 大数据存储(七)| Springboot 项目通过 Phoeni
    Phoenix是HBase的开源SQL皮肤,通过Phoenix可以使用标准JDBCAPI代替HBase客户端API来创建表,插入数据和查询HBase数据。Phoenix会把SQL编译成一系列的Hbase的scan操作,然后把scan结果生成标准的JDBC结果集,其底层由于使用了Hbase的API,协处理器,过滤器。Pho......
  • Instagram为何如此受欢迎?
    曾问了自己无数次为什么有那么多图片分享应用,那么多竞争对手而Instagram却如此受欢迎?在355天内就获得超过1000万的用户,或许因为它是iPhone平台的第一款图片分享应用,或许是因为它的界面太漂亮,又或许是因为上边有太多太好的艺术作品。理性分析了一番,总结起来,它汇集了专注、艺术气......
  • 【macOS游戏】Phoenix Point
    原文来源于黑果魏叔官网,转载需注明出处。(下载请直接百度黑果魏叔)X-COM创作者著名的PhoenixPoint策略现已发布完整版,其中包括所有四年开发的所有添加、内容更新、游戏改进。现在有了支持模组!土地被占领了。外星人威胁突变危及人类的残余。只有凤凰计划,一个由最好的科学家和最勇敢的......