简介:微赞社区官方论坛的微信小程序案例源码包,面向初学微信小程序或想研究社区类应用的移动开发者,完整还原了基于腾讯小程序框架的论坛社交场景。压缩包共48个文件,包含19张界面PNG图、11个JS逻辑文件、6套WXSS样式、6个WXML页面结构、5个JSON配置以及1个临时资源包,整体大小仅1.53MB,结构精简、便于对照阅读。已有199人学习参考。通过剖析源码可系统掌握app.js全局入口、app.json页面配置、app.wxss全局样式,以及首页、帖子列表、用户中心等页面的WXML/WXSS布局与JS交互逻辑,同时覆盖小程序生命周期函数、双向数据绑定、本地存储及常用API的调用方式。尤其值得关注的是社区场景中用户登录、发帖回帖、点赞收藏、消息通知等功能的实现思路,并涉及懒加载、分包加载等性能优化技巧,对理解完整论坛型小程序的项目分层与常用设计模式很有帮助。
1. 从微赞社区源码看待小程序启动链路与工程边界
拿到这套源码,我第一件事不是去翻论坛首页,而是先把根目录的app.js、app.json和app.wxss打开。微信小程序和普通 Web 项目不同,它的启动入口不是某个 html,而是由app.json里注册的页面路径决定的。微赞社区这套源码把全局逻辑都压在app.js里,globalData挂载用户信息和请求域名,真正页面下的 js 文件只负责单个页面的状态维护。很多新手以为页面目录下的 js 是入口,结果怎么找都找不到首页逻辑,其实只要反推app.json中的pages列表,第一个就是启动页面。这个案例适合想搞懂小程序工程结构、又想完整跑一遍社区交互的开发者,也适合把论坛 UI 搬到自己的业务后台的人。它能帮你理清小程序的启动时序、数据流向和页面通信机制,而不是停留在零散的 wxml 标签上。
2. 全局配置与 WXML/WXSS 的社区版式实现
2.1 app.json 的页面注册顺序决定社区 tab 入口
微赞社区这个工程解压后,根目录是app.js、app.json、app.wxss,再加一个pages目录。小程序启动时先执行app.js,再根据app.json的pages数组把第一个页面当作首屏。我拿到源码后,会先看app.json的pages数组,而不是直接去翻pages/index/index.js。原因很简单:项目代码里可能有多个页面,但只有被注册到pages里的路径才能跳转和访问,微赞社区把首页、论坛、消息、我的四个 tab 统一在app.json里声明,任何页面文件如果没注册进去,编译时也不会进入包体。
{ "pages": [ "pages/index/index", "pages/forum/forum", "pages/message/message", "pages/mine/mine" ], "window": { "navigationBarTitleText": "微赞社区", "navigationBarBackgroundColor": "#1E90FF", "backgroundTextStyle": "light" }, "tabBar": { "color": "#666666", "selectedColor": "#1E90FF", "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/forum/forum", "text": "论坛" }, { "pagePath": "pages/message/message", "text": "消息" }, { "pagePath": "pages/mine/mine", "text": "我的" } ] }, "style": "v2", "sitemapLocation": "sitemap.json" }这段配置里,pages数组顺序就是启动顺序,将pages/forum/forum挪到首位,首屏就会变成论坛页。tabBar里出现的pagePath必须同时出现在pages数组中,否则编译会直接报错。window.navigationBarTitleText是一个全局默认标题,但 tabBar 页面默认不显示导航栏标题,页面自己的 json 里可以再覆盖。另外,每个页面还可以有自己的page.json来覆盖navigationBarTitleText,比如pages/threadDetail/threadDetail.json里设置标题为“帖子详情”,这个优先于window全局配置。tabBar 页面虽然不能动态修改文字,但可以通过wx.setTabBarItem修改角标,微赞社区的消息 tab 就用这个来显示未读数。
2.2 WXML 模板:wx:for 渲染帖子列表
微赞社区的首页 feed 流和论坛版块列表都用到了wx:for。把数据从 js 的data传到 wxml 再渲染,写法上跟 vue 的v-for很像,但语义更接近循环渲染。下面是我从源码里提取出的简化版帖子列表:
<view class="thread-list"> <block wx:for="{{threads}}" wx:key="id"> <view class="thread-item" bindtap="openDetail">.thread-item { display: flex; flex-direction: column; padding: 24rpx 32rpx; border-bottom: 1rpx solid #f0f0f0; } .thread-list { padding-bottom: calc(env(safe-area-inset-bottom) + 120rpx); }env(safe-area-inset-bottom)是 iPhone 从 X 开始的底部安全距离,配合 fixed 的 tabBar 或输入框使用,防止内容被 home indicator 遮挡。不需要手动判断机型,直接写在 padding 或 height 里。不同单位的适用场景可以通过这张表快速确认:
| 单位 | 特点 | 社区页面里的位置 |
|---|---|---|
| rpx | 相对屏幕宽度,750rpx=满屏 | 卡片 padding、字体大小、头像尺寸 |
| px | 固定像素,不随屏宽变化 | 1px 分隔线、box-shadow |
| % | 相对父容器 | 版块封面宽度、弹窗宽度 |
| vh/vw | 相对视口 | 全屏遮罩、半屏弹窗 |
| env(safe-area-inset-bottom) | 读取 iPhone 安全区 | tabBar 上移、底部评论框 |
2.4 公共样式抽到 app.wxss
源码里app.wxss只有几十行,但把所有页面都会用到的通用类放到了这里面,比如card、btn、ellipsis。这样的好处是页面 wxss 只写差异部分,减少重复样式文件体积。我一般也会把单行省略、多行省略抽出来。
.ellipsis { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .card { margin: 24rpx; padding: 32rpx; background: #ffffff; border-radius: 16rpx; box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04); }注意:页面级 wxss 里同名类会覆盖app.wxss,因为页面样式加载顺序在公共样式之后。如果发现公共类没生效,先看是不是在页面里重新定义了同名 class。
3. 页面逻辑与数据流:feed 流、发帖与本地缓存
3.1 Page 生命周期在论坛页面中的应用
微赞社区的核心页面基本上都有四个生命周期动作:onLoad读取页面参数,onShow刷新未读消息,onReachBottom加载下一页,onPullDownRefresh下拉重置列表。以论坛板块页为例:
Page({ data: { threads: [], page: 1, hasMore: true }, onLoad(options) { this.forumId = options.id || 0; this.loadThreads(); }, onShow() { this.syncLikeStatus(); }, onReachBottom() { if (this.data.hasMore) { this.setData({ page: this.data.page + 1 }); this.loadThreads(); } } });onLoad只在页面首次创建时执行,onShow在每次从后台切回或从其他页面返回时执行。所以点赞状态同步放在onShow里,可以保证从详情页返回列表时已经变化的数据立即刷新。注意onReachBottom在内容不满一屏时也会触发,需要靠hasMore判断是否继续请求。四个生命周期的使用场景可以从表里快速对应:
| 生命周期 | 触发时机 | 论坛页面里的用途 |
|---|---|---|
| onLoad | 页面创建 | 读取帖子 id、首次加载列表 |
| onShow | 页面显示/返回 | 刷新点赞状态、未读消息角标 |
| onReachBottom | 滚动到底部 | 分页加载下一页 |
| onPullDownRefresh | 下拉动作 | 重置 page=1,重新拉取列表 |
3.2 setData 的粒度控制
微信小程序逻辑层和渲染层是分离的,setData会把数据通过序列化传到视图层。如果整个列表每次都整体setData,数据量大时卡顿明显。微赞社区的做法是每次请求后拼接列表并一次性更新:
loadThreads() { wx.showLoading({ title: '加载中' }); request('/threads', 'GET', { page: this.data.page, forumId: this.forumId }) .then(res => { const list = this.data.page === 1 ? res.data : this.data.threads.concat(res.data); this.setData({ threads: list, hasMore: res.data.length >= 10 }); }) .finally(() => wx.hideLoading()); }这里用concat生成新数组,而不是 push 后直接赋值原数组,因为setData对数组的更新依赖引用变化。如果你在原数组上 push,再setData同一个引用,视图层可能无法感知变化。参数说明:page 从 1 开始,page=1 时覆盖列表,否则追加;hasMore 用本次返回条数判断,小于每页条数说明没有下一页了。setData每次传入的数据大小建议不超过 1024KB,超过会有警告并拖慢渲染。如果列表项很多,可以优先渲染首屏 10 条,滚动到底部再追加。常见做法是把threads拆成visibleThreads,在onReachBottom时再setData增量。
3.3 发帖:本地存储模拟数据落盘
没有后端时,本地模拟发帖可以用来快速验证页面逻辑。用wx.setStorageSync把新帖子存起来。微赞社区源码里有一个utils/storage.js,专门封装本地缓存操作。
function submitLocalThread(title, content) { const key = 'my_threads'; const myThreads = wx.getStorageSync(key) || []; const newThread = { id: Date.now(), title, content, createdAt: Date.now(), replies: 0 }; myThreads.unshift(newThread); wx.setStorageSync(key, myThreads); return newThread; }参数说明:wx.getStorageSync取不到数据时返回空字符串,所以必须用|| []兜底。wx.setStorageSync有 1MB 的单个 key 限制,只适合存草稿和临时数据;论坛富文本、图片等大内容建议用wx.getFileSystemManager写入本地文件。注意本地存储不可跨设备,用户换手机后草稿不会同步。
3.4 事件冒泡与组件状态隔离
论坛帖子卡片上通常有“点赞”和“进入详情”两个操作,点击点赞不能触发跳转,否则体验很别扭。这里要区分bindtap和catchtap。
<view class="thread-item" bindtap="openDetail">const BASE_URL = 'https://api.weizan.example.com'; function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: `${BASE_URL}${path}`, method, data, header: { 'Content-Type': 'application/json', 'Authorization': wx.getStorageSync('token') || '' }, success(res) { if (res.statusCode >= 200 && res.statusCode < 300) { resolve(res.data); } else if (res.statusCode === 401) { wx.navigateTo({ url: '/pages/login/login' }); reject(res); } else { wx.showToast({ title: `请求失败:${res.statusCode}`, icon: 'none' }); reject(res); } }, fail(err) { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); }BASE_URL放在这里管理,开发时切换环境只需改一个常量。401 统一跳登录页,避免每个页面都写一遍登录判断。注意success里还要判断statusCode,因为wx.request的业务数据在res.data中,请求成功不代表接口成功。开发环境可以通过在 request 里读取 constants 配置文件,来切换 mock 地址和正式地址。但不要把所有 api 域名写死在代码里,否则后期改起来容易漏。微赞社区的做法是在config.js里集中存放在env字段,构建时按环境变量切换。
4.2 wx.login 与自定义登录态
社区登录不是简单调一个接口,而是先用wx.login拿到临时 code,再让后端用 code 去微信服务器换 session_key。微赞社区的pages/login/login.js中处理逻辑如下:
function login() { return new Promise((resolve, reject) => { wx.login({ success: async res => { if (!res.code) return reject(res); try { const data = await request('/login', 'POST', { code: res.code }); wx.setStorageSync('token', data.token); wx.setStorageSync('userId', data.userId); resolve(data); } catch (e) { reject(e); } }, fail: reject }); }); }wx.login的 code 有效期只有 5 分钟,而且只能一次性使用,所以每次登录流程都要重新调用,不能把旧 code 存起来。后端拿到 code 后,通过 openid 和 session_key 生成自定义 token,小程序侧只保存这个 token。别把 openid 直接放到前端,session_key 更不能下发,否则数据有被伪造的风险。
4.3 点赞收藏的乐观更新策略
微赞社区帖子列表里的点赞数量变化很快,如果每次都等接口返回再改 UI,用户会感觉延迟明显。源码里用的是乐观更新:先改本地视图,再发请求,失败再回滚。实现时我会加锁防止重复点击:
toggleLike(threadId) { if (this.locks && this.locks[threadId]) return; this.locks = this.locks || {}; this.locks[threadId] = true; const threads = this.data.threads.map(t => { if (t.id !== threadId) return t; return { ...t, liked: !t.liked, likeCount: t.liked ? t.likeCount - 1 : t.likeCount + 1 }; }); this.setData({ threads }); request(`/threads/${threadId}/like`, 'POST', { liked: true }) .finally(() => { delete this.locks[threadId]; }); }乐观更新对比等接口返回,体验差异很大:
| 方案 | 响应速度 | 风险 | 适用场景 |
|---|---|---|---|
| 同步等待 | 慢,依赖网络 | 数据最准确 | 支付、资金操作 |
| 乐观更新 | 快,即时反馈 | 可能回滚 | 点赞、收藏、浏览量 |
回滚代码没有完全给出,但思路是保存原始状态,catch 里恢复。这里用map生成新数组,不直接原地改,满足setData的引用变化要求。
4.4 私信聊天与 WebSocket
论坛里的私信模块走长连接。微信小程序的wx.connectSocket提供原生 WebSocket 能力,但微赞社区源码里还额外封装了心跳。常见问题是连接处于断开状态时发消息会直接失败,所以我一般会在onSocketClose里标记连接状态,在发送前判断,如果断开就重新连接,然后等 open 后再发。心跳每 30 秒发一个 ping,服务端超时 10 秒不响应就主动重连。避免在onSocketMessage里频繁setData,把消息先 push 到内存数组,再在一帧内统一渲染。社区权限控制通常依赖 token 中的角色字段,比如版主可以删帖。小程序前端无法保证安全,只能做 UI 隐藏,真正的删除权限必须在后端校验。所以分析源码时不要只关注前端交互,请求层对接口返回的拒绝理由也要做统一处理。
5. 用分包加载和渲染控制优化论坛小程序的加载速度
5.1 拆分包与 preloadRule
微赞社区源码在最初版本里所有页面都在主包,后期优化时把帖子详情、发布、搜索这些低频页面移到了分包。分包配置在 app.json 中:
{ "pages": ["pages/index/index"], "subPackages": [ { "root": "pages/forum", "pages": ["threadDetail", "postThread", "search"] } ], "preloadRule": { "pages/index/index": { "network": "wifi", "packages": ["pages/forum"] } } }tabBar 页面必须在主包,所以 index 和 mine 留在主包,论坛详情这类页面放分包。preloadRule的意思是,在 wifi 环境下加载首页后预下载 forum 分包,用户点开详情时不白屏。subPackages里的 root 不能和主包 page 路径重叠。
5.2 列表图片懒加载与失败兜底
论坛列表的封面图最影响首屏速度。image 组件加上lazy-load后,页面滚动到可视区才真正加载图片:
<image wx:for="{{threads}}" wx:key="id" src="{{item.cover}}" lazy-load mode="aspectFill" class="cover" binderror="onImgError" />lazy-load只作用于滚动列表,不能替代压缩图片。mode="aspectFill"保证图片填充且不变形,避免默认的 scaleToFill 把封面压扁。binderror绑定的onImgError可以在图片链接失效时替换为默认占位图。
5.3 骨架屏替代白屏
社区页面数据请求期间,渲染空白更容易让用户觉得卡。用骨架屏占住卡片位置:
<view class="thread-skeleton" wx:if="{{loading}}"> <view class="skeleton-avatar"></view> <view class="skeleton-title"></view> <view class="skeleton-line"></view> </view>loading 为 true 时渲染骨架,请求完成后把 loading 设为 false,同时渲染真实列表。骨架屏的样式用灰色背景和透明度动画模拟加载效果。注意骨架屏组件要避免和真实列表同时渲染,否则会增加一次渲染压力。
5.4 用 wxs 减轻逻辑层压力
列表中的时间格式化如果放在 js 里,每一条都需要从逻辑层传字符串到视图层。把格式化放到 wxs 里,直接在视图层计算更轻:
// utils/format.wxs var pad = function(n) { return n < 10 ? '0' + n : '' + n; }; module.exports = { formatTime: function(ts) { var d = getDate(ts); return pad(d.getHours()) + ':' + pad(d.getMinutes()); } };在 wxml 中这样引用:<wxs src="../../utils/format.wxs" module="fmt" />,然后{{fmt.formatTime(item.createdAt)}}。wxs 是运行在视图层的脚本,不支持 ES6 语法,也不用 js 里那些 dom 操作,适合做 filter 类转换。优化的最终验证方式是打开微信开发者工具的性能面板,看逻辑层 setData 的耗时和视图层渲染耗时,找出变化最大的列表页再进一步做节点裁剪。
本文还有配套的精品资源,点击获取