纯前端复刻小红书界面:从瀑布流到移动端交互的基本功训练
用纯前端复刻一个小红书界面我把它当成一次基本功训练“高仿小红书HTML前端网站”这种项目说实话已经不算什么新鲜话题了很多初学前端的人都会拿它练手。但大多数模仿作品都停留在“看着像”的层面真正点开之后交互逻辑、滚动性能、适配细节统统经不起推敲。我个人认为这类仿站项目的价值并不在于“仿”本身而在于你能否把一个看似简单的高颜值界面拆解成可落地的前端工程问题。这篇文章不打算给你一份完整的源代码而是把我在做类似项目时的完整思路、模块拆解、核心实现方案、以及踩过的坑一条条讲清楚。它适合三类人一是刚学完HTML/CSS/JS基础、正准备找项目练手的新手二是想系统整理前端布局与交互经验的开发者三是需要快速搭建移动端风格展示页的从业者。先说结论高仿一个交互级的移动端界面比很多人想象中更考验基本功。你不需要框架不需要构建工具甚至不需要后端但你需要对布局、滚动、事件、适配都有清晰的认知。1. 整体设计思路先定框架再扣细节1.1 这类项目的核心目标到底是什么很多人一上来就拿图开始切边切边写写到一半发现布局乱了、交互不对、代码也改不动了。根本原因是没有提前想清楚“这个项目到底要解决什么问题”。我理解的高仿小红书HTML前端网站目标不是1:1还原像素而是在纯前端的技术栈下复现其核心视觉特征和关键交互体验。核心特征包括瀑布流双列卡片布局、顶部搜索栏、底部Tab导航、发布按钮的视觉引导、详情页弹层、以及点击态和滑动效果的还原。明确了这个目标后面做技术选型时就知道该往哪儿使劲了。这里还有一个边界问题需要想清楚我们只做前端展示与交互模拟不涉及真实账号体系、真实UGC数据、推荐算法。数据全部用本地静态数据模拟。这样既能让项目完整跑起来又完全规避合规风险。1.2 技术选型为什么不用框架反而全用原生三件套我在做这个项目时刻意选择了纯HTML CSS JavaScript没有引入任何框架或构建工具。原因有三第一这类仿站项目对DOM结构和CSS还原度要求极高原生CSS的表达力足够且更直接如果用框架反而多了很多间接层。第二原生实现更便于新手理解“浏览器到底是怎么工作的”离开框架的舒适区才能看清布局、样式、事件之间的关系。第三纯静态项目部署极其方便任何人都能双击打开运行不需要安装任何依赖。当然这不代表框架没用。如果你已经熟练原生想快速迭代也可以用Vue或React来做组件化拆分。但第一版我个人强烈建议原生实现。1.3 布局架构从设计稿到页面结构的三层拆法一个典型的小红书式页面从视觉上可以拆成三个层次顶层是状态栏搜索栏中间是可滚动的内容流底部是固定的Tab栏。这个结构看着简单实际动手时最容易出问题的是“滚动区域”和“固定区域”的关系。我采用的拆法是页面高度锁定为100%即viewport高度整体使用flex纵向布局顶部栏和底部Tab作为固定的flex成员中间内容区作为flex:1的独立滚动容器。这样做的好处是中间内容区的滚动完全独立不会带动顶部和底部区域滚动这也是移动端App的常见交互模式。有一个很容易犯的错是把整个body当作滚动容器然后给底部Tab设置position: fixed。这种情况下虽然Tab看起来固定在底部但滚动条出现在body上中间内容区域的滚动触底逻辑会变得非常绕。所以从一开始就要明确谁是滚动容器。2. 核心模块拆解每一个细节都有存在的理由2.1 顶部分区状态栏、搜索框、分类标签顶部区域看起来只是几个元素的堆叠实际上包含了大量适配细节。状态栏是模拟手机系统状态条的显示信号、时间、电量等信息。这里要做的并不是真去读取系统状态而是用纯HTML写一个等宽分布的横条。关键点是字号要小、颜色要浅不能喧宾夺主高度也要控制好一般是44px左右。搜索框的设计核心是“视觉上的圆角胶囊灰色背景”。内嵌一个放大镜图标加占位文字点击后跳转搜索页。做这个交互时有一个细节值得注意搜索框并不需要单独处理焦点态直接绑定点击事件跳转页面就可以了如果真做了focus弹键盘反而会暴露这是网页而不是App的现实。分类标签横向排列小红书以前的版本是顶部tab关注、发现、附近、直播这些。我的做法是做一个可横向滚动的标签栏每个标签对应的内容区切换显示。这里有一个实现技巧让分类标签用overflow-x: auto横向滚动但隐藏滚动条视觉上就是原生App的滑动标签效果。.hot-tabs { display: flex; overflow-x: auto; scrollbar-width: none; } .hot-tabs::-webkit-scrollbar { display: none; }2.2 瀑布流双列卡片最考验布局基本功的部分小红书最显著的视觉特征就是双列瀑布流。高仿项目能不能让人一眼觉得“像”瀑布流占了很大的权重。实现瀑布流的方案有好几种我对比一下它们的优劣方案实现难度适用场景缺点CSS多列布局低纯静态展示内容纵向顺序被打乱点击映射逻辑复杂flex双列手动分配中数据量中等需要自己维护两条列数组JS动态计算最短列中高数据量动态变化需要处理图片加载时序网格布局grid中高度规律的内容高度差异大时无法自动填充空隙我在项目中用的是第三种JS动态计算最短列。核心思路是准备两个容器左列和右列每来一条数据比较当前左列和右列的累计高度把卡片插入到高度较短的那一列中。这样能保证两列高度尽量均衡视觉上就是自然的瀑布流效果。为什么不用CSS columns因为在columns布局下内容是按列排布的文字的阅读顺序会从左列到底再回到右列这和实际的数据顺序不一致。如果后续你要做“点击某条内容、打开对应详情页”的交互索引映射会绕很多弯。而手动分配双列每张卡片都在真实的DOM顺序中数据索引清晰后续扩展十分方便。卡片内部的结构也有讲究图片区域、标题文字、作者信息、点赞量。图片使用aspect-ratio: 3 / 4的固定比例或者根据图片真实宽高动态设置避免加载过程中出现高度跳动。这里强烈建议占位高度预留否则瀑布流刚加载时会出现明显的塌陷和跳动。2.3 图片墙比例、裁剪、懒加载的三重处理图片是小红书内容的核心表达媒介几乎每一张卡片的视觉重量都集中在图片上。图片处理不到位整个界面质感就毁了。第一层是比例处理。瀑布流之所以看起来有呼吸感是因为卡片图片高度不完全一致。但如果每张图片高矮差别过大又显得杂乱。我采用的做法是为每张图片预设两种比例3:4和1:1用随机或数据字段指定的方式分配。这样视觉上既有节奏感又不会失控。第二层是裁剪。用object-fit: cover让图片按比例居中裁剪超出部分隐藏。这里有一个细节很多新手直接把 标签的高度设为固定值然后不管图片原始比例结果图片被拉伸变形。正确做法是用一个容器包裹img容器设aspect-ratioimg设width: 100%; height: 100%; object-fit: cover。第三层是懒加载。我是用IntersectionObserver实现的图片进入视口前不加载真实地址只显示一个占位色块进入视口后再加载。这个操作对瀑布流特别重要因为一次渲染几十张图片如果全部加载首屏的加载时间会非常感人。const observer new IntersectionObserver((entries) { entries.forEach(entry { if (entry.isIntersecting) { const img entry.target; img.src img.dataset.src; observer.unobserve(img); } }); }); document.querySelectorAll(img[data-src]).forEach(img observer.observe(img));2.4 底部Tab栏和发布按钮从形到神的交互还原底部Tab是用户最常触碰的区域还原度会直接影响“像不像”的第一印象。一个标准的底部Tab栏由4-5个icon和对应的文字标签组成当前选中的项目高亮显示。我实现的Tab切换逻辑比较简单点击某一项时切换高亮class同时切换中间内容区的显示。注意切换时保持滚动位置最好不要让切换导致内容区重新从顶部开始这个细节很影响体验。解决办法是在切换前记录每个Tab对应内容容器的scrollTop切换后恢复。发布按钮看似只是一个居中的大按钮实际上它是整个视觉层级中的核心焦点。我用了一个相对定位的Tab栏容器加一个绝对定位的圆形按钮设置在Tab栏上方半悬空的效果。发布按钮点击后会弹出一个从底部滑出的半屏面板上面有几个模拟功能入口比如“拍视频”“拍图文”“直播”点击后仅仅是弹出提示。底部弹出层这个交互核心在于两点一是过渡动画二是遮罩层。动画用transform: translateY配合transition实现不要用top或margin去移动因为transform不触发重排性能更好。遮罩层点击要能关闭弹层这是用户的基本预期必须处理。2.5 详情页弹层点击卡片之后的世界点击瀑布流中的任意一张卡片应该弹出一个类似App详情页的全屏覆盖层。这个弹层主要有三个部分顶部图片区、中间标题与描述、底部点赞收藏操作栏。实现上我用了全屏fixed覆盖层自带上下滑入过渡。关闭按钮放在左上角或右上角点击后弹层向下滑出并移除。弹层的滚动独立于主页面避免出现背景穿透滚动的问题这里就需要在弹层出现时锁定body滚动。锁定滚动的标准做法是给body添加overflow: hidden并将body的position设为fixed同时记录并恢复原来的滚动位置。这里有一个细节如果直接设overflow: hidden在某些浏览器上滚动位置会跳动所以推荐用JavaScript记录scrollY再在关闭时恢复。// 打开弹层 const scrollY window.scrollY; document.body.style.position fixed; document.body.style.top -${scrollY}px; document.body.style.left 0; document.body.style.right 0; // 关闭弹层 document.body.style.position ; document.body.style.top ; window.scrollTo(0, scrollY);2.6 点赞、收藏、关注交互反馈的节奏感高仿项目中的点赞、收藏、关注这类操作因为没有后端实际上只是本地状态切换但反馈的节奏很重要。点赞按钮我做了两个层次的变化第一层是图标的形状变化从空心变为实心第二层是颜色变化从灰白色变为主题红。同时加一个小幅缩放的动画效果让点击反馈更“有手感”。缩放动画用CSS的transform: scale配合transition实现关键是要快一般150ms到200ms太快没感觉太慢又拖沓。收藏的效果类似但icon和颜色的变化走向不同。关注按钮是文字按钮点击后变成“已关注”这里的逻辑要注意不能只改颜色和文字还要考虑重复点击时可以取消关注状态来回切换要顺畅。这里有一个非常多人忽略的点状态切换后卡片上的点赞数字要不要变我个人建议做。点赞1再点取消-1这样才形成一个合格的交互闭环。否则用户点了之后数字没变化心里会嘀咕这个页面是不是假的。3. 实操过程从零搭建一个可复现的完整项目3.1 项目目录结构与静态数据准备开始之前先把目录结构规划好后面写代码的时候心里才不慌。我建议采用这种简单清晰的结构xiaohongshu-clone/ ├── index.html ├── css/ │ ├── reset.css │ ├── main.css │ └── modal.css ├── js/ │ ├── data.js │ ├── waterfall.js │ ├── tabs.js │ └── interaction.js └── images/ ├── avatar/ └── feed/index.html是唯一页面入口所有功能都在这个页面内完成不需要额外的路由跳转。css目录下分了三个文件reset做基础样式重置main管布局和组件样式modal单独管理弹层相关样式这样职责清楚、维护方便。数据文件data.js是整个模拟数据的中枢。我构造了一个数组每个对象代表一条笔记内容字段包括id、作者名、作者头像、图片地址、标题、点赞数、收藏数、是否已关注等。数据尽量贴近真实的展示格式但内容完全虚构。这里想提醒一下生成数据时要保证图片地址真实有效建议用本地图片或稳定图床不要用外链防盗链的图片否则加载失败会影响整个页面的体验。3.2 HTML骨架一屏三段式结构HTML的结构我直接锁定为三段式div classapp !-- 顶部区域 -- header classapp-header div classstatus-bar.../div div classsearch-bar.../div div classcategory-tabs.../div /header !-- 内容滚动区 -- main classapp-content div classfeed-column left/div div classfeed-column right/div /main !-- 底部Tab -- nav classapp-tabbar div classtab-item首页/div div classtab-item购物/div div classtab-publish/div div classtab-item消息/div div classtab-item我/div /nav /div这里需要特别提醒的是class的命名。很多新手喜欢用“left-column”“right-column”这样表意不清晰的命名后期改样式时非常痛苦。我建议类名基于模块职责来取比如feed-column就说明它是内容流中的列容器tab-publish就表明是发布按钮。清晰命名可能在项目初期看起来多敲了几个字母但后期维护的收益远大于投入。3.3 瀑布流的JavaScript实现五步搞定瀑布流的JS逻辑我整理成了五个步骤第一步读取data数组第二步计算当前两列的高度用scrollHeight或offsetHeight第三步找出高度较小的列第四步动态创建卡片DOM并插入该列第五步更新该列高度值。卡片DOM的创建我这里不推荐用模板字符串拼HTML再插入这种方式虽然它简单直观但数据量大时性能略差而且容易产生XSS隐患。更稳的是用document.createElement动态创建节点把每个文本内容用textContent赋值。项目虽然只是模拟数据但这个习惯要从一开始就养成。卡片内点击冒泡问题也值得注意整张卡片有跳转详情事件但卡片内部的点赞按钮不应该触发跳转。解决办法是在点赞按钮上调用event.stopPropagation()阻止事件冒泡。这个坑我确实踩过第一版做的时候根本没加点一次点赞直接跳到了详情页用户会很懵。3.4 下拉刷新、触底加载让页面“活”起来一个静态写死几十条数据的页面浏览完就没有了交互感很弱。为了让页面更接近真实App的状态我加了两个能力下拉刷新和触底加载。下拉刷新实现的核心是监听滚动容器的scroll事件当scrollTop为0时记录下拉距离并且当距离超过阈值时触发刷新动作。实际实现中有很多要注意的地方比如下拉时顶部要有视觉反馈一个转动的loading图标手指松开后要复位还要防止下拉手势和浏览器原生滚动手势冲突。触底加载就更常用了当滚动容器的scrollTop clientHeight scrollHeight - 阈值 时触发加载新数据。我设置阈值为200px也就是距离底部200px时就开始预加载这样体验比较顺滑能避免用户看到白屏等待。新加载的数据同样通过瀑布流算法动态分配追加到对应列的末尾。加载完成后需要重新计算列高度保证后续插入位置的准确性。这里还有一个细节加载更多时应该有一个loading提示避免用户以为没信号了反复下拉。3.5 样式细节从字号到圆角的一整套规范仿站之所以像细节占了八成。我这里有几个关键样式参数直接给出参考值主色调红色系我是用#ff2442作为主红这是视觉冲击力最强的点。搜索栏背景#f5f5f5圆角16px内部字号14px颜色#999。卡片圆角8px到12px之间图片做到8px整体卡片12px更接近现在的审美。文字层级标题字号14-15px作者信息字号12px点赞量字号12px。卡片间距列内卡片上下间距8-12px两列之间的间距10-16px。这些数值看着零散其实背后有统一的规律全站以12px、14px、15px为文字基准以8px的倍数为间距基准以主红色为唯一的强调色。这样整体页面才有统一的节奏感而不是东一榔头西一棒子。在写样式时还有一类容易被忽视的元素头像。头像统一用圆形裁剪width和height设置为32pxobject-fit: cover。用户头像旁边有时会显示他正在关注的关系状态这个小元素对还原度提升非常明显一定要在视觉上做出来。4. 常见问题与排查技巧实录4.1 图片加载闪烁、高度跳动怎么办图片问题是仿站项目中出现频率最高的问题。最常见的症状是图片加载完的瞬间卡片高度突然变化导致整个瀑布流跳动甚至出现错位。原因很简单图片在加载完成前没有占据空间。解决思路也很明确为图片容器设置固定比例或者给img设置一个预设高度。我推荐给图片外层容器设置aspect-ratio并让图片绝对填充。还有一种情况是图片加载完成前先显示了默认图标碎图片图标这个一般是src为空或路径错误导致的。排查时先看控制台的网络请求确认图片路径是否正确其次确认是否有跨域限制。4.2 点击Tab后滚动位置丢失底部Tab切换后内容区回到顶部这个体验非常差但排查起来往往不容易发现原因。因为默认情况下你切换visible状态时滚动容器直接重建scrollTop自然归零。解决办法是切换前记录scrollTop切换后恢复。我的实现方式是为每个Tab对应的内容区创建一个实例对象保存其scrollTop值在切换事件中先把当前容器的scrollTop存起来再切换显示另一个容器最后把新容器的scrollTop从缓存里取出来恢复。还有一个连带问题如果Tab对应的内容区存在不同长度的数据切换后恢复的scrollTop可能超出新容器的滚动范围浏览器会自动限制为最大值。这个属于正常现象不用额外处理。4.3 点击发布按钮但遮罩层和弹层顺序不对这个现象通常是z-index层级问题。遮罩层、弹层、底部Tab、页面内容这四者的层级必须有清晰的规划。我的推荐关系是页面内容z-index最低默认0或1底部Tab其次z-index: 100遮罩层z-index: 200和弹层z-index: 300在最上方。这样设计后任何弹层和遮罩都能盖住Tab栏符合App的交互习惯。如果你发现弹层打开后底部的Tab仍然可见十有八九是Tab的z-index比遮罩层大。这种情况在移动端尤其明显因为用户手指很容易碰到Tab区域导致误触。4.4 快速排查清单我把日常排查问题的顺序列成清单按这个顺序查一般能快速定位问题打开浏览器控制台先看有没有红色的报错尤其是JS语法错误。JS报错会直接导致交互全部失灵。检查网络请求中图片等静态资源的状态码404是路径问题403通常是防盗链。核对每个容器的宽高特别是使用flex布局时子元素是否出现了溢出或收缩异常。确认事件绑定是否生效可以在绑定回调里console.log一条测试信息看点击后是否触发。检查滚动容器是否是预期对象有没有出现页面整体滚动和容器滚动同时存在的双滚动条问题。4.5 性能优化为什么滚动会卡顿高仿页面的卡顿大多数时候不是因为你写的JS太慢而是因为触发了太多机制。最典型的几个坑一是布局抖动。每次向瀑布流里插入卡片时强制读取列高度再插入这个过程叫做强制同步布局。如果频繁读取offsetHeight再写DOM浏览器会反复重新计算布局非常消耗性能。二是图片体积过大。移动端网络环境下一张几兆的图片会拖慢整个页面的加载速度。建议用适当压缩后的图片控制在200KB以内甚至用WebP格式。这里应尽量避免直接塞原图。三是没有使用will-change或者transform来引导合成层。滚动容器加上will-change: scroll-position弹层动画元素加上will-change: transform能明显降低滚动时的卡顿感尤其是低端机表现会更明显。不过这里也要提醒一句will-change不能滥用每个使用它的元素都会独立占用合成层用多了反而导致性能下降。5. 安全与合规注意事项仿站的边界在哪里这是一个绕不开的话题也是每个做仿站项目的人都必须想清楚的问题模仿到什么程度算合理练习什么程度会踩线。我的原则是只模仿界面布局、交互模式、视觉风格这类UI层面的东西不照抄具体的商标标识、专属图标、人格化的品牌元素。页面标题可以写“仿小红书风格Demo”“小红书风格前端练习”这样的描述性名称但不能直接自称是官方或正式版本的页面。品牌官方名称在代码注释和页面内容中尽量少出现避免误导。具体到代码中要注意三点第一数据全部用自己构造的虚构内容不要抓取或搬运真实UGC数据第二一些带有品牌识别度的独特设计元素比如标志性的Logo图形不要完整复刻用类似风格的占位标识即可第三如果项目需要开源或公开展示在README中明确说明这是一个纯前端练习项目与任何商业产品或品牌无关。这些边界不是限制创造力而是保护你自己。一个练习项目能展示的能力应该体现在布局还原度、交互完成度、代码组织能力这些方面而不是靠蹭品牌标识来获得虚假的“像”。6. 扩展思路这个项目还能往哪些方向走做完第一版的高仿页面后这个项目的价值才刚刚开始释放。我试着把后续可以扩展的方向列出来供大家参考。第一个方向是接入真实数据。通过公开的API或自建后端接口把静态数据换成动态数据实现内容的实时加载。这样可以顺便练习fetch、axios、Promise、async/await这些异步编程知识。瀑布流的排序、分页、加载状态管理也会变得更复杂更有挑战。第二个方向是增加用户交互深度。比如登录面板的模拟、注册表单的校验、个人信息页的编辑甚至本地缓存浏览记录。这些功能涉及localStorage、sessionStorage的使用还能顺便理解浏览器存储机制。第三个方向是响应式和跨端适配。目前的布局是按照移动端起点的但如果要让它在手机和PC上都能自然展示就需要引入媒体查询甚至对不同分辨率做布局切换。这个方向能把CSS的响应式知识补得很完整。第四个方向是性能优化专项。用Lighthouse跑一轮分数然后针对Performance、Accessibility、Best Practices去优化。图片压缩、懒加载策略、渲染性能、语义化标签每一样都能写出一篇独立的经验文章。第五个方向是组件化重构。如果第一版用的是原生JS那第二版可以把它整体改成Vue或React组件化结构把瀑布流、Tab栏、卡片分别封装成独立组件通过props和state管理数据流。经过这个重构过程你对框架的理解会远超单纯看文档。每个方向都是独立的练习项目而且难度是逐级递增的。建议从当前页面出发一次只扩展一个方向把每一步做透不要贪多。7. 常见问题速查表这里把文中提到的高频问题汇总成一张速查表平时遇到问题可以直接对照排查。症状可能原因解决方案卡片图片加载后高度跳动图片未预留占位空间外层容器设置aspect-ratioimg用object-fit: cover点击卡片误触发点赞事件冒泡未阻止点赞按钮上调用event.stopPropagation()切换Tab后滚动位置丢失滚动容器被重建或切换切换前记录scrollTop切换后恢复弹层盖不住底部Tabz-index层级规划不清按“内容 Tab 遮罩 弹层”的顺序设置层级弹层打开后背景还能滚动未锁定body滚动用position: fixed加记录scrollY锁定关闭时恢复下拉刷新与滚动冲突事件监听对象错误确认滚动容器是内部main而非整个body页面加载慢图片未压缩或未懒加载本地压缩图片用IntersectionObserver实现懒加载触底加载反复触发阈值判断进入循环加载期间用isLoading锁完成后再解锁双列列高不均衡未动态计算最短列插入前分别读取两列高度选择较矮的列插入8. 最后的几点个人体会这类高仿项目的价值不取决于它看起来多像原版而在于你为了“像”付出了多少思考。布局、色彩、交互、动画、性能、合规意识每一环都在给你上课。我在做这个项目时最有感触的一个点是处理瀑布流高度跳动问题的过程。一开始只是简单地把卡片塞进去没有考虑图片加载时序结果每次刷新页面都要忍受视觉跳跃。后来花了几个小时调整结构从固定比例、懒加载到动态高度更新一步步解决最终页面丝滑地滚动起来时那种满足感远远超过“页面像小红书”这件事本身。最后再分享一个小技巧做这类高仿页面时准备一个截图工具把原版界面截图后放在设计稿层然后在浏览器里开着开发者工具对比你的页面和原版的差异。逐像素对比虽然费时间但确实是快速提升还原度的最好方法。颜色差一点、圆角差一点、间距差一点积累起来就是“像”和“不像”的天壤之别。