You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Phoenix LiveView中仅从RSS Feed追加新增HEEX条目?

解决Phoenix LiveView中RSS Feed新增条目的追加与前置展示问题

问题场景

  • 在mount/3中首次拉取RSS Feed并渲染所有条目
  • 通过Process.send_after/4在handle_info/2中每5秒重新拉取同一Feed,返回结果包含原始条目+新增条目
  • 当前痛点:
    1. 用stream的话,无法保留历史条目状态,没法判断哪些是新增内容
    2. 用assign存储所有条目,数据量大会有性能问题,且新增条目默认追加在列表底部,用户得手动滚动查看

方案一:优化Assign实现,新增条目前置+自动滚动

核心思路是只在socket的assign里存已处理过的条目标识(比如RSS条目的唯一ID),每次拉取新Feed时筛选出未展示过的新增条目,把它们放在列表最前面,同时用LiveView的前端事件自动滚动到顶部。

  1. 初始化状态
    在mount/3里拉取初始条目后,存两个assign:一个是当前展示的条目列表,另一个是已展示过的条目标识集合

    def mount(_params, _session, socket) do
      rss_items = fetch_rss_feed()
      seen_ids = Enum.map(rss_items, & &1.id) |> MapSet.new()
    
      socket = assign(socket, rss_items: rss_items, seen_item_ids: seen_ids)
      Process.send_after(self(), :fetch_rss, 5000)
      {:ok, socket}
    end
    
  2. 处理定时拉取
    在handle_info/2里筛选新增条目,前置到现有列表头部,同时更新已见ID集合,最后触发滚动事件

    def handle_info(:fetch_rss, socket) do
      new_rss_items = fetch_rss_feed()
      # 筛出没展示过的新增条目
      new_items = Enum.reject(new_rss_items, fn item -> MapSet.member?(socket.assigns.seen_item_ids, item.id) end)
      # 更新已见ID集合
      updated_seen_ids = MapSet.union(socket.assigns.seen_item_ids, Enum.map(new_items, & &1.id) |> MapSet.new())
      # 新增条目前置到列表头部
      updated_items = new_items ++ socket.assigns.rss_items
    
      socket = socket
               |> assign(rss_items: updated_items, seen_item_ids: updated_seen_ids)
               |> push_event(socket, "scroll_to_top", %{})
      Process.send_after(self(), :fetch_rss, 5000)
      {:noreply, socket}
    end
    
  3. HEEX模板与前端滚动
    渲染条目列表,通过Phoenix Hook监听滚动事件实现自动置顶

    <div id="rss-list" phx-hook="ScrollToTop">
      <%= for item <- @rss_items do %>
        <div class="rss-item" id={"item-#{item.id}"}>
          <h3><%= item.title %></h3>
          <p><%= item.description %></p>
        </div>
      <% end %>
    </div>
    
    <script>
      Hooks.ScrollToTop = {
        mounted() {
          this.handleEvent("scroll_to_top", () => {
            document.getElementById("rss-list").scrollTop = 0;
          });
        }
      }
    </script>
    

方案二:结合Stream与持久化状态,实现增量更新

Stream本身是临时的,但可以配合assign存储的已见ID集合,筛选出新增条目后插入到Stream头部,利用Stream的增量DOM更新特性避免全量渲染。

  1. 初始化状态
    在mount/3里拉取初始条目,用stream/3渲染,同时存储已见ID集合

    def mount(_params, _session, socket) do
      rss_items = fetch_rss_feed()
      seen_ids = Enum.map(rss_items, & &1.id) |> MapSet.new()
    
      socket = socket
               |> assign(seen_item_ids: seen_ids)
               |> stream(:rss_items, rss_items, at: 0)
      Process.send_after(self(), :fetch_rss, 5000)
      {:ok, socket}
    end
    
  2. 处理定时拉取
    筛选新增条目,用stream_insert/3的at: 0参数把新增内容插入到Stream最前面,同时更新已见ID集合

    def handle_info(:fetch_rss, socket) do
      new_rss_items = fetch_rss_feed()
      new_items = Enum.reject(new_rss_items, fn item -> MapSet.member?(socket.assigns.seen_item_ids, item.id) end)
      updated_seen_ids = MapSet.union(socket.assigns.seen_item_ids, Enum.map(new_items, & &1.id) |> MapSet.new())
    
      socket = socket
               |> assign(seen_item_ids: updated_seen_ids)
               |> Enum.reduce(new_items, fn item, acc -> stream_insert(acc, :rss_items, item, at: 0) end)
      Process.send_after(self(), :fetch_rss, 5000)
      {:noreply, socket}
    
  3. HEEX模板渲染
    直接遍历Stream条目即可,Stream会自动处理DOM的增量更新

    <div id="rss-list">
      <%= for {dom_id, item} <- @streams.rss_items do %>
        <div id={dom_id} class="rss-item">
          <h3><%= item.title %></h3>
          <p><%= item.description %></p>
        </div>
      <% end %>
    </div>
    

额外优化建议

  • 优先用RSS Feed自带的guid字段作为条目的唯一标识,如果没有可以用「标题+发布时间」的组合生成,避免误判新增条目
  • 如果Feed返回的条目数量极大,可以定期清理seen_item_ids集合和旧条目(比如只保留最近100条),防止内存占用过高

内容的提问来源于stack exchange,提问作者HBCondo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 10:33:09