微信小程序源码实战:从今日头条Demo读懂页面结构与数据交互
简介这是一套面向微信小程序初学者的今日头条风格新闻客户端demo覆盖首页信息流展示、新闻分类切换、详情预览等典型场景适合正在学习小程序页面开发、想理解WXML与WXSS协同方式以及JS逻辑控制的开发者参考使用。资源包共11个文件以3个WXSS样式文件、3个JS逻辑文件、2个WXML页面结构文件和1个JSON配置为主另有1个说明文档和1张演示截图整体压缩包仅10KB结构简洁清晰便于逐文件对照学习。已有1051人学习使用内容得到初步验证。借助该源码读者可以掌握小程序目录组织、全局与页面样式分工、基础事件绑定及数据渲染等关键写法通过app.json可查看页面注册方式通过app.wxss能分析公共样式通过pages下各页面文件能对比私有样式与逻辑从而理清一个完整小项目的实现思路同时可依据截图直接核对页面效果适合作为课设仿写或功能二次开发的轻量模板。1. 微信小程序demo的源码先跑通再谈理解一个只包含源代码和截图的小程序 demo最暴露功底的时刻是把压缩包解压后拖进微信开发者工具的那几十秒。页面白屏还是正常渲染、tab 切过去跳不跳、截图里那条红色的“热”字角标对应哪一行 WXSS这些问题在真机预览之前就要心里有数。把“今日头条 demo”当项目实例来做重点不在模仿它的信息流样式而是看它怎么组织页面、放 mock 数据、处理加载状态。我用一套能落地的顺序讲完先对照截图把页面骨架对齐再把数据挂上列表渲染然后补上分类 tab、下拉刷新和上拉加载三个高频交互最后给一套截图和运行结果对不上时的排查路径。适合正在找练手工程的开发者也适合把这类源代码改造成毕业设计或面试作品的场景。2. 读源代码前先对齐页面骨架从截图反推工程结构2.1 从截图反推目录tabBar 与独立页面拿到一个微信小程序 demo 的源代码包第一件事不是打开所有 JS 文件从头读而是先在开发者工具里新建一个项目把代码文件导入跑起来。如果解压后只有pages、utils和三个app.*文件说明这是一个没有分包的标准工程。我一般先对照截图画出页面清单。截图里有几屏工程里就至少有几个页面。一个典型的今日头条 demo 截图组通常包含三屏首页信息流、新闻详情、我的或个人中心其中详情页往往不放在 tabBar 里。对应到代码里是这个结构wechat-news-demo/ ├── app.js ├── app.json ├── app.wxss ├── pages/ │ ├── index/ # 首页信息流tabBar 页 │ │ ├── index.js │ │ ├── index.json │ │ ├── index.wxml │ │ └── index.wxss │ ├── detail/ # 新闻详情页非 tabBar 页 │ │ ├── detail.js │ │ ├── detail.json │ │ ├── detail.wxml │ │ └── detail.wxss │ └── mine/ # 个人中心tabBar 页 │ ├── mine.js │ ├── mine.json │ ├── mine.wxml │ └── mine.wxss └── utils/ └── data.js # 本地 mock 数据截图上的元素对应文件常见代码位置顶部导航栏标题pages/index/index.jsonnavigationBarTitleText横滑分类 tabpages/index/index.wxmlscroll-view标签内新闻卡片列表pages/index/index.wxmlwx:for循环块红色“热”字角标pages/index/index.wxss.hot-tag类底部 tab 栏app.jsontabBar.list正文加图片来源pages/detail/detail.wxmlimage与text节点每个页面目录下四个文件的命名必须完全一致这是微信小程序的硬约束。看到index.js对应index.wxml修改index.wxss也只对这个页面生效。和 HBuilderX 开发 uniapp 时那种单文件组件模式不同原生小程序的“页面四件套”把视图、逻辑、样式、配置拆开demo 拆得越规律后面改造越省事。2.2 app.json 的 pages 顺序与第一屏渲染app.json是整个小程序的入口配置其中pages数组的第一项就是启动后渲染的第一屏。很多演示 demo 会故意把pages/index/index放在第一位而把后续页面写在后面这是最标准的工程顺序{ pages: [ pages/index/index, pages/detail/detail, pages/mine/mine ], window: { navigationBarBackgroundColor: #d43d3d, navigationBarTitleText: 今日头条 demo, navigationBarTextStyle: white, enablePullDownRefresh: true, backgroundTextStyle: dark }, tabBar: { position: top, color: #666666, selectedColor: #d43d3d, list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/mine/mine, text: 我的 } ] } }pages顺序写错是白屏的第一大原因如果第一项配成了不存在的路径开发者工具会直接报错但某些旧版本基础库只提示page not found新手很容易忽略。window节点里的navigationBarBackgroundColor控制顶部栏背景色今日头条类 demo 普遍使用接近#d43d3d的红色对应截图上那一抹品牌色。tabBar的position设为top时tab 栏会出现在页面顶部并且不显示图标这适合教学 demo省去准备 icon 切图的工作量。底部 tab 是默认值但要配iconPath和selectedIconPath否则控制台会警告。注意如果网页里看到的 tab 在顶部说明源代码里用了position: top不要误以为缺少图标文件。2.3 首页与详情页列表页加详情页的分工首页index的职责是从数据源取列表并渲染卡片详情页detail的职责是接收路由参数再展示正文。看源码时先抓住这两个页面的onLoad生命周期业务逻辑基本都从那里开始。首页经常通过wx.navigateTo跳详情页并把id作为参数!-- pages/index/index.wxml -- view classnews-card wx:for{{newsList}} wx:keyid >// pages/index/index.js Page({ goDetail: function (e) { var id e.currentTarget.dataset.id wx.navigateTo({ url: /pages/detail/detail?id id }) } })>.hot-tag { display: inline-block; background-color: #e02e24; color: #ffffff; font-size: 20rpx; padding: 4rpx 10rpx; border-radius: 6rpx; margin-right: 12rpx; }搜索时直接在index.wxss里搜#e02e24或hot比按页面逐行读更快。用这种“截图元素反查样式名”的方式半小时内就能把一个陌生 demo 的关键文件全部标出来顶部分类栏、新闻列表、底部来源时间、详情页的图片空隙。截图如果带红色遮罩或蒙层通常是页面级view的position: fixed叠加不需要在代码里逐行找优先定位最外层的容器类名即可。3. 数据层源代码mock JSON、wx:for 渲染与 setData 粒度3.1 mock 数据放 utils/data.js避开域名白名单绝大多数“今日头条 demo”不会真的接入线上 API而是把新闻条目写在本地 JS 文件里。常见做法是在utils/data.js里维护一个数组每个元素包含id、title、source、comments、category、imageUrl等字段// utils/data.js var newsList [ { id: 1, title: 微信小程序云开发上线新能力开发者可以更低成本起步, source: 科技快讯, comments: 128, category: 推荐, imageUrl: /images/news-1.png }, { id: 2, title: 一线城市通勤报告发布地铁客流恢复明显, source: 本地生活, comments: 56, category: 本地, imageUrl: } ] module.exports { newsList: newsList }在页面里引入并赋值// pages/index/index.js var data require(../../utils/data.js) Page({ data: { newsList: [] }, onLoad: function () { this.setData({ newsList: data.newsList }) } })require的路径是相对路径../../utils/data.js表示从pages/index/往上两级到项目根目录再进入utils。这里建议保持字段名与组件绑定变量一致比如页面里用了{{item.title}}数据源里就用title避免在模板里做二次映射。本地 mock 的另一个好处是绕开 request 合法域名校验如果改成wx.request拉线上数据需要在开发者工具里勾选“不校验合法域名”否则真机预览会失败。截图里如果数据瞬间出现说明走的是本地 require文件加载是同步的如果出现加载动画说明页面里模拟了异步请求。3.2 wx:for 渲染列表与 key 的选取首页信息流在 wxml 层只有一块循环逻辑wx:for是列表渲染的核心指令。写法和 Vue 的v-for类似但注意关键词是item和index默认数组当前项叫item下标叫indexview classnews-list view classnews-card wx:for{{newsList}} wx:keyid wx:for-itemnews wx:for-indexidx view classnews-title{{news.title}}/view view classnews-meta text{{news.source}}/text text classcomments{{news.comments}} 评论/text /view /view /viewwx:key是官方推荐的列表标识用于渲染 diff 时精确复用节点。性能角度讲优先用数据的id不要用下标index。用下标做 key 时如果列表中间插入一条新闻微信会按位置对比可能导致后面的节点全部重建表现为滚动位置抖动或图片闪烁。如果 mock 数据没有数字 id 字段也可以使用字符串字段只要在列表里唯一即可。如果数据字段名恰好叫item会和默认值冲突此时用wx:for-item改名。截图里的新闻卡片如果出现上一条数据残影多半是wx:key缺失或者用了 index优先检查这里。3.3 setData 的路径更新只改一条状态不要重绘整页setData把数据从逻辑层传到视图层是一项昂贵的操作。新手常犯的错误是把整个newsList原样 set 回去即便只是想把某一条新闻标记为“已读”// 低效写法把整个列表重新赋值 this.setData({ newsList: this.data.newsList }) // 推荐写法按路径更新 this.setData({ [newsList[ index ].read]: true })动态路径是setData的高级用法把数组下标拼进 key 字符串里。这样微信只会 diff 那一条数据对应的节点而不是比较整个列表。代码里[newsList[ index ].read]的写法等价于newsList[3].read当处理点击收藏、点击阅读状态时非常常见。注意 index 必须是从wx:for里透传过来的下标不能自己用 id 去换算否则可能越界。参数层面还要关注setData的回调第二参是回调函数在视图层渲染完成后触发适合做“加载更多后恢复按钮状态”这类操作。另外setData单次数据量不要超过 1MB超过时工具会警告。源码头像newsList达到几百条后仍然整页 set会把调试模式拖得很卡这就是后续要上分页的原因。4. 今日头条信息流三大功能分类 Tab、下拉刷新与上拉加载4.1 分类 Tabscroll-view 横滑加 filter 筛选首页顶部那排可以左右滑的分类栏拆开看只有两个要点横向滚动的scroll-view和分类筛选逻辑。scroll-view需要显式声明scroll-x并配合white-space: nowrap去干掉换行scroll-view scroll-xtrue classtab-scroll view classtab-item {{currentCategory 推荐 ? active : }} wx:for{{categories}} wx:key*this >switchCategory: function (e) { var category e.currentTarget.dataset.category this.setData({ currentCategory: category }) if (category 推荐) { this.setData({ newsList: data.newsList }) } else { this.setData({ newsList: data.newsList.filter(function (item) { return item.category category }) }) } }filter是数组的遍历方法返回一个满足条件的新数组不会修改原数据。这里要注意推荐分类通常映射全部数据而其他分类要精确匹配item.category。截图里如果点击分类后列表变短说明 mock 数据里分类字段没对所有条目补齐补数据比改筛选逻辑更省事。这个功能还有一个容易被忽略的点当onReachBottom触发加载更多时要把page重置为 1否则切完分类再上拉会翻到下一页。4.2 下拉刷新onPullDownRefresh 与 stopPullDownRefresh 配对下拉刷新不需要自己监听 touch 事件微信在小程序页面生命周期里内置了onPullDownRefresh。开启它的前提是app.json或者页面的index.json里把enablePullDownRefresh设为true{ enablePullDownRefresh: true, backgroundTextStyle: dark }页面 JS 里实现对应的生命周期函数onPullDownRefresh: function () { var self this setTimeout(function () { self.setData({ newsList: data.newsList, currentCategory: 推荐 }) wx.stopPullDownRefresh() }, 500) }wx.stopPullDownRefresh()必须和onPullDownRefresh成对出现否则顶部 loading 动画会一直转。500ms 的setTimeout是刻意模拟网络延迟同时让用户感知到刷新动作。如果源代码里没有 stop 调用真机上会表现为下拉后无法关闭。backgroundTextStyle取dark时下拉 loading 的小点是深色的配合浅色背景如果导航栏是红色建议设成light。这个参数在截图上看不出来只有真机下拉才能观察。注意刷新完还应该重置分页页码很多 demo 只重置了列表数据导致下拉后再上拉会从第 2 页开始拿数据出现重复条目。4.3 上拉加载更多onReachBottom 与 mock 分页上拉加载的核心是页面的onReachBottom生命周期当页面滚动到底部时自动触发。它只在页面级滚动时生效如果列表套在scroll-view里并设置为纵向滚动这个函数就不会触发这是一个非常高频的坑。data: { newsList: [], page: 1, pageSize: 5, isLoading: false, hasMore: true }, onReachBottom: function () { if (this.data.isLoading || !this.data.hasMore) { return } this.setData({ isLoading: true }) var nextPage this.data.page 1 var start (nextPage - 1) * this.data.pageSize var end start this.data.pageSize var moreList data.newsList.slice(start, end) var self this setTimeout(function () { if (moreList.length 0) { self.setData({ newsList: self.data.newsList.concat(moreList), page: nextPage, isLoading: false }) } else { self.setData({ hasMore: false, isLoading: false }) } }, 400) }状态字段值行为isLoadingtrue阻止重复触发避免一次滚动加载多次hasMorefalse数据取完不再发起新请求page递增记录当前已加载到第几页concat在源码中很常见它返回新数组不会污染this.data.newsList的引用。slice的start和end控制的是截取范围pageSize 5表示每页 5 条。mock 数据总量有限一旦end超过数组长度moreList为空数组此时把hasMore置为false接下来的上拉操作直接被拦截避免无意义的分页请求。isLoading这个锁非常重要。在快速滚动时onReachBottom可能被连续触发多次如果没有锁页面会一次性发出多个请求列表出现大量重复项。源码里出现这样的判断逻辑说明作者处理过实际问题如果没有建议自己补上。4.4 加载状态与无更多状态体验差异在边界信息流页面的体验差距往往不在正常情况而在空数据、加载中、无更多三种边界状态。demo 如果只有列表渲染没有状态提示快速滑动到末尾时会表现为“点了没反应”。常见做法是在列表底部加一个条件渲染的提示view classlist-footer wx:if{{hasMore}} text wx:if{{isLoading}}加载中.../text text wx:else上拉加载更多/text /view view classlist-footer wx:else text没有更多了/text /viewwx:if和wx:else是互斥渲染同一时间只显示一块。这个底部文案不需要截图也建议保留真机上用户对“能不能继续滑”的感知全靠它。注意wx:else必须紧跟在wx:if的兄弟节点后面中间不能插入注释或其他标签否则编译不通过。5. 截图对不上的排查顺序从 Navigation 到 vConsole 再到 setData拿到源代码后最常遇到的场景是截图和运行结果不一致比如截图显示“推荐”tab 高亮红色自己运行却是黑色。这时不要急着改 JS先打开开发者工具的调试器按下面顺序核对。第一看app.json的pages数组第一位是不是pages/index/index。如果第一位写成了pages/detail/detail那启动后直接进详情页和截图的首屏信息流完全对不上。这个排查只要几秒却是白屏和页面错乱的头号原因。想修改刚进入的加载页面同样改这里调整数组顺序即可。第二看导航栏高度和胶囊按钮是否重叠。截图里如果导航栏是自定义的需要在对应页面的index.json中设置navigationStyle: custom并自己计算状态栏高度var menuRect wx.getMenuButtonBoundingClientRect() var statusBarHeight wx.getWindowInfo().statusBarHeight var navBarHeight (menuRect.top - statusBarHeight) * 2 menuRect.heightwx.getMenuButtonBoundingClientRect()返回胶囊按钮的位置和尺寸statusBarHeight是状态栏高度两者相减得到导航栏上下 padding。基础库 2.20.1 以下没有wx.getWindowInfo可以改用wx.getSystemInfoSync().statusBarHeight。顶部导航栏高度是自定义导航栏页面最常调的参数截图对不上时先量这个值。第三真机预览跑出来的数据和模拟器不一致时打开 vConsole。用真实手机预览时点右上角胶囊按钮一次菜单里会出现“打开调试”再回到页面就能看到绿色的 vConsole 悬浮窗。所有console.log和数据结构都会显示在那里。在onLoad里加一行console.log(this.data.newsList)确认数据源是否完整如果newsList为空数组回头看require(../../utils/data.js)的路径是否多写或少写了一层目录。第四Network 面板负责验证数据请求。如果 demo 用的是本地 mockNetwork 里不会出现异步请求如果跑了wx.request去 Network 看响应结构中的字段名与页面绑定的是否一致。图片裂开时右键图片地址看是否指向本地不存在的路径比如/images/news-1.png只在截图里出现过但目录缺少该文件。最后一个小技巧把一条新闻评论数改成88888保存后看模拟器是否即时刷新。这个操作能快速验证数据流路径是否完整如果数字没变说明setData的路径更新没有命中优先在 JS 里搜索comments字段的全部出现位置。本文还有配套的精品资源点击获取