bubu智聘App亮点详解(4)-瀑布流+事件总线的实战心得

继上篇项目详解文章,我来接着分享一下智能招聘App的广场页开发,其中实现了两个核心技术亮点:基于mock数据的瀑布流展示和mitt事件总线实现的点赞通知系统。整个过程充满挑战但也收获满满,下面详细分享我的实现思路和技术细节。

同样的,我们先来看看具体效果:

这里我们订阅了每个喜欢图标的点击事件,一但点击,封装的微标组件就会在喜欢的头部弹出,当点进喜欢列表,就会清零,现在我来分享一下:

瀑布流文章列表的核心实现

瀑布流的核心在于"滚动加载更多"机制。我使用IntersectionObserver API监听页面底部元素,当用户滚动到底部时自动加载新数据:

scss 复制代码
// ArticleWaterfall.jsx中的关键代码
const bottomRef = useRef(null)
useEffect(() => {
  const observer = new IntersectionObserver(([entry]) => {
    if (entry.isIntersecting) {
      fetchArticle() // 触发加载更多
    }
  })
  
  if (bottomRef.current) observer.observe(bottomRef.current)
  return () => observer.disconnect()
}, [])

在页面底部放置一个观察点元素:

xml 复制代码
<div ref={bottomRef} className={styles.loading}>
  {isLoading && <Loading type='ball' />}
</div>

在该系列的文章第一篇我就介绍了一下瀑布流的模拟,里面有详细的讲解:bubu智聘App亮点详解(1)瀑布流、图片懒加载与Mock数据的完美结合 大家感兴趣可以去看看

数据管理:Zustand状态库实战

我使用Zustand管理文章数据和加载状态,它的API简洁高效:

dart 复制代码
// useArticleStore.js
export const useArticleStore = create((set, get) => ({
  articles: [], // 文章列表
  page: 1,      // 当前页码
  isLoading: false,
  initialLoading: true,
  
  fetchArticle: async () => {
    if (get().isLoading) return
    
    set({ isLoading: true })
    const res = await getArticle(get().page) // API请求
    
    set(state => ({
      articles: [...state.articles, ...res.data], // 追加新数据
      page: state.page + 1,
      isLoading: false,
      initialLoading: false
    }))
  }
}))

getArticle(get().page) // API请求是封装好的请求mock数据的API,会把在mock模拟的数据返回给我们,因为我们的项目是一个纯前端项目,没有真正的后端接口

跨组件通信:mitt事件总线

项目中最精彩的部分是使用mitt实现的事件总线。传统React中父子组件通信容易形成"prop钻取"问题,而事件总线让任意组件间通信变得简单。

1. 创建事件总线

javascript 复制代码
// ToastController.jsx
import mitt from 'mitt'

export const toastEvents = mitt() // 创建事件总线

// 触发事件的方法
export function showToast() {
  toastEvents.emit('show') // 点赞时触发
}

export function decreaseToast() {
  toastEvents.emit('decrease') // 取消点赞时触发
}

export function clearToast() {
  toastEvents.emit('clear') // 清除通知
}

2. 发布事件(在点赞操作时)

scss 复制代码
// ArticleWaterfall.jsx中的点赞处理
const handleLike = (article) => {
  const isNowLiked = toggleLike(article)
  
  if (isNowLiked) {
    showToast() // 点赞时加一
  } else {
    decreaseToast() // 取消点赞时减一
  }
}

3. 订阅事件(在通知组件中)

dart 复制代码
// Toast.jsx
useEffect(() => {
  const show = () => setCount(prev => prev + 1)
  const decrease = () => setCount(prev => Math.max(0, prev - 1))
  const clear = () => setCount(0)
  
  // 订阅事件
  toastEvents.on('show', show)
  toastEvents.on('decrease', decrease)
  toastEvents.on('clear', clear)
  
  // 组件卸载时取消订阅
  return () => {
    toastEvents.off('show', show)
    toastEvents.off('decrease', decrease)
    toastEvents.off('clear', clear)
  }
}, [])

界面细节优化技巧

骨架屏加载效果

数据加载时展示骨架屏提升用户体验:

css 复制代码
{initialLoading ? (
  <div style={{ backgroundColor: '#f5f5f5', minHeight: '100vh' }}>
    {/* 搜索区域骨架 */}
    <div style={{ padding: '20px', backgroundColor: 'white' }}>
      <Skeleton title={false} avatar={{ size: 30 }} row={1} />
    </div>
    
    {/* 标签页骨架 */}
    <div style={{ backgroundColor: 'white', marginBottom: '10px' }}>
      <Skeleton title={false} row={1} style={{ height: '40px' }} />
    </div>
    
    {/* 文章列表骨架 */}
    <div style={{ padding: '0 20px' }}>
      <Skeleton title avatar row={3} style={skeletonStyle} />
      <Skeleton title avatar row={2} style={skeletonStyle} />
      <Skeleton title avatar row={4} style={skeletonStyle} />
    </div>
  </div>
) : (/* 实际内容 */)}

标签页中的通知徽章

在"喜欢"标签上显示点赞数量:

xml 复制代码
<Tabs.TabPane title={
  <div className={styles.tabWithBadge} onClick={clearToast}>
    <div className={styles.badgeContainer}>
      喜欢
      <Toast /> {/* 通知徽章组件 */}
    </div>
  </div>
}>
  <LikeArticle />
</Tabs.TabPane>

样式设计的实用技巧

CSS Module的使用

避免样式冲突的最佳实践:

css 复制代码
/* Square.module.css */
.HeaderSearch {
  display: flex;
  justify-content: space-around;
  background: #81ECEC49;
}

.HeaderSearchInput {
  width: 60%;
  height: 70px;
  align-items: center;
  background: white;
  border-radius: 30px;
}

/* 徽章定位 */
.badgeContainer {
  position: relative;
  padding: 20px;
}

.badge {
  position: absolute;
  top: -1px;
  right: -1px
}

文章卡片样式

css 复制代码
/* ArticleWaterfall.module.css */
.articleItem {
  background: white;
  margin: 15px 15px 0 15px;
  border-radius: 20px;
  overflow: hidden;
}

.articleUser {
  display: flex;
  align-items: center;
}

.articleImages {
  display: flex;
  margin-top: 15px;
}

.articleImages img {
  width: 200px;
  height: 250px;
  border-radius: 20px;
}

项目亮点总结

  1. 高效的状态管理
    Zustand以极简API解决了复杂状态共享问题,比Redux更轻量,比Context性能更好
  2. 优雅的跨组件通信
    mitt事件总线实现了解耦的组件通信,点赞操作和通知显示完全分离
  3. 用户体验优化
    骨架屏 + 滚动加载的组合拳,让长列表浏览体验流畅自然
  4. 样式隔离方案
    CSS Module确保组件样式独立,避免全局污染
  5. 功能完备的点赞系统
    支持点赞/取消、状态持久化、实时通知计数

踩坑心得

  1. IntersectionObserver的陷阱

    初始实现时忘记在组件卸载时disconnect observer,导致内存泄漏。解决方案:

    javascript 复制代码
    return () => observer.disconnect()
  2. 事件总线内存泄漏

    组件内订阅事件后,必须在卸载时取消订阅,否则会导致多次触发

  3. Zustand状态更新

    直接修改状态不会触发更新,必须使用set方法返回新状态

  4. 骨架屏的平衡点

    骨架屏元素需要与实际DOM结构高度匹配,否则会出现布局跳动

这个项目让我深刻体会到React生态系统的强大。通过组合Zustand、mitt等轻量级库,我们可以在不引入庞大框架的情况下,构建出高交互性的现代Web应用。事件总线的设计尤其让我眼前一亮,它像是在组件间架起了无形的通信桥梁,让数据流动更加自由。

相关推荐
前端搬运侠10 分钟前
📝从零到一封装 React 表格:基于 antd Table 实现多条件搜索 + 动态列配置,代码可直接复用
前端
歪歪10012 分钟前
Vue原理与高级开发技巧详解
开发语言·前端·javascript·vue.js·前端框架·集成学习
zabr12 分钟前
我让AI一把撸了个算命网站,结果它比我还懂玄学
前端·aigc·ai编程
xianxin_14 分钟前
CSS Fonts(字体)
前端
用户25191624271114 分钟前
Canvas之画图板
前端·javascript·canvas
快起来别睡了40 分钟前
前端设计模式:让代码更优雅的“万能钥匙”
前端·设计模式
EndingCoder1 小时前
Next.js API 路由:构建后端端点
开发语言·前端·javascript·ecmascript·全栈·next.js·api路由
2301_810970391 小时前
wed前端第三次作业
前端
程序猿阿伟1 小时前
《深度解构:React与Redux构建复杂表单的底层逻辑与实践》
前端·react.js·前端框架
酒酿小圆子~1 小时前
【Agent】ReAct:最经典的Agent设计框架
前端·react.js·前端框架