
简介一套完整的仿豆瓣评分微信小程序课程设计项目面向移动终端开发新手可作为课程设计或期末练手的实战素材。项目用 JavaScript 处理业务逻辑用 JSON 管理数据与配置通过 URL 请求豆瓣相关接口实现评分、评论等核心功能wxml 与 wxss 分别完成页面结构和样式整体难度适中适合新手逐步仿写。压缩包共 62 个文件包含 14 个 js、14 个 json、12 个 wxss、11 个 wxml、10 张项目运行截图及 1 份课程设计报告大小仅 1.36MBapp/pages/components/utils 等目录划分清晰便于对照源码理解小程序工程结构每个页面均配套对应的 js、json、wxml 与 wxss 文件可按模块逐页对照学习。附带的课程设计报告涵盖需求分析、设计思路、技术选型与实现过程运行图片可直观看到界面效果特别适合零基础学习者按图索骥。已有 539 人学习资源虽小却覆盖了小程序开发的主要环节能有效帮助读者打通从页面布局到数据交互、从调试到部署的完整链路。1. 拿到仿豆瓣评分源码包先按 app.json 顺序理清项目结构一份 rar 解压之后如果直接双击project.config.json让微信开发者工具识别项目会先看到全项目骨架app.js、app.json、app.wxss、project.config.json、pages、components、utils、sitemap.json这些都覆盖了一个微信小程序从启动到渲染的完整链路。仿豆瓣评分这个题目最有学习价值的地方不在于界面多复杂而在于它把 tab 导航、列表加载、评分交互、自定义组件四件事都串了起来正好对应小程序新手最需要理解的 js 与 json 协作方式。下面按实际拆项目的顺序来写先讲配置层为什么这样设计再带上utils/request.js封装和评分组件实现最后是上线前容易翻车的细节处理。源码包里的多张运行图和课程设计报告文档建议留到代码跑通后再对照看比一开始就翻图更有效率。2. 微信小程序目录结构与 app.json 路由注册实战2.1 根目录四个文件的作用边界在微信小程序里app.js负责定义全局 App 实例和 globalDataapp.json负责声明页面与窗口配置app.wxss负责全局样式project.config.json保存开发者工具的项目级设置。第一次导入仿豆瓣源码时如果工具提示找不到app.json多半是把这些文件放进了子目录而不是项目根目录把路径收敛一下就能解决。我习惯拿到源码先打开project.config.json看appid字段。课程设计源码经常带着原作者的 appid直接编译就会弹出“登录用户不是该小程序的开发者”把 appid 删除或替换成自己的测试号即可。文件里如果存在miniprogramRoot字段它表示小程序代码的实际根目录以后移动目录位置前要先同步改这里。另外注意 pages 下的组织方式每个页面推荐一个文件夹内部放同名四个文件.js、.json、.wxml、.wxss。这是微信官方推荐结构实际也能不同名但 pages 和 tabBar 注册的是文件路径编辑器不会自动纠正大小写。Windows 下大小写不敏感macOS 或云构建却会严格区分建议一律用小写命名避免换机器后白屏。2.2 app.json 的 pages 注册与 window 参数解析{ pages: [ pages/home/home, pages/detail/detail, pages/mine/mine ], window: { navigationBarBackgroundColor: #ffffff, navigationBarTextStyle: black, navigationBarTitleText: 仿豆瓣评分, backgroundColor: #f5f5f5, backgroundTextStyle: dark }, sitemapLocation: sitemap.json }这段配置里pages数组的顺序不是摆设排第一的页面是整个小程序的入口也是冷启动后第一个渲染的页面。很多初学者往数组里追加页面路径后直接保存工具报page pages/xxx not found回头才发现页面文件根本还没创建。正确顺序应该是先建目录和四个文件再来这里注册顺序反了会浪费不少排查时间。window里的字段控制全局窗口。navigationBarBackgroundColor只接受十六进制色值navigationBarTextStyle只允许 black 或 white两个字段组合决定导航栏外观自由度比 CSS 低很多。backgroundTextStyle控制下拉刷新时小圆点的明暗取值只有 light 和 dark。sitemapLocation指向 sitemap.json 路径这个文件不是必须创建但路径写错会让编译日志出现 sitemap warning虽然不影响预览看着总是别扭。建议根目录保留一份基础配置{ rules: [ { action: allow, page: * } ] }rules里action为 allow 表示允许被微信搜索索引page写*代表全部页面。课程设计场景没有收录诉求把 action 改成 disallow 也行还能避免测试数据被索引。2.3 tabBar 的多页面导航配置仿豆瓣首页天然是一个分栏应用底部导航在app.json里用tabBar节点定义tabBar: { color: #999999, selectedColor: #42bd56, backgroundColor: #ffffff, borderStyle: black, list: [ { pagePath: pages/home/home, text: 热映 }, { pagePath: pages/mine/mine, text: 我的 } ] }color是未选中文字颜色selectedColor是选中态颜色borderStyle只接受 black 和 white。list最少 2 项、最多 5 项pagePath必须在 pages 数组中提前注册。iconPath和selectedIconPath是 tab 图标的路径只能引用项目内图片不能使用网络 url。这里故意不写 icon纯文字 tab 在小型课程设计中足够清晰还能避开图标文件超过 40kb 导致真机显示异常的问题。提示自定义组件建议按页面维度注册。在pages/home/home.json里加usingComponents只影响当前页面放到app.json的usingComponents则是全局注册组件会随主包一起加载页面少时看不出差别页面一多启动速度会受影响。组件路径以/开头表示从项目根目录解析写成相对路径也能运行但页面目录层级一变相对路径就失效。这一点和现代前端框架的代码分割思路一致组件属于谁、由谁加载越精确越不容易出问题。3. 豆瓣数据请求utils/request.js 封装与 JSON 分层转换3.1 用 Promise 封装 wx.request原生wx.request的成功结果放在 success 回调里每个接口都要重复处理状态码和错误提示所以最常规的做法是在 utils 目录封装一个request.js。它把 url、data、method、header 统一收口并返回 Promise页面层可以用 async/await 或 then 链式调用。// utils/request.js const BASE_URL https://api.example.com/v2/movie; function request(url, data {}, method GET) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL url, data: data, method: method, timeout: 10000, header: { content-type: application/json }, success(res) { // 2xx 状态码才进入 resolve其余交给 reject 统一处理 if (res.statusCode 200 res.statusCode 300) { resolve(res.data); } else { reject({ code: res.statusCode, msg: res.data }); } }, fail(err) { reject(err); } }); }); } module.exports { request };timeout参数建议从一开始就显式指定微信客户端默认超时是 60 秒接口没响应时用户会一直看着 loading 转圈。改成 10 秒并配合页面里的失败提示体验会好很多。data在 GET 请求中会自动拼接到 url 的查询串里传入对象中如果带、或中文微信会按 URL 编码规则处理成%3D这类字符。这是标准行为不是 bug后端解码后自然会还原。如果你在服务端收到了%3D而不是检查服务端是不是做了重复 decode。豆瓣官方 API 目前需要 apikey而且小程序后台必须把接口域名加入 request 合法域名列表生产环境才能访问。课程设计拿不到 apikey 的情况下我通常改用本地 JSON 模拟数据先把页面交互跑通之后再把“数据源切换”做成一个独立配置项。3.2 电影榜单的 JSON 字段设计与 mock 切换仿豆瓣的列表数据建议按豆瓣惯例组织每部电影包含标题、评分对象、类型、演职人员、封面图和详情页 id{ title: 流浪地球2, rating: { max: 10, average: 8.3, stars: 45 }, genres: [科幻, 冒险], casts: [{ name: 吴京 }, { name: 刘德华 }], image: https://example.com/poster.jpg, id: movie_123 }这里的rating.stars是 0 到 50 的字符串这是豆瓣评分体系里表示星星数量的方式每满 10 对应一颗实心星。JS 里字符串参与比较时会自动转数字但渲染星星时最好显式parseInt一次避免模板表达式隐式转换带来边界错误。id字段不能省它既作为wx:key的取值也可以作为详情页跳转的 query 参数。原始外链url字段在小程序里不能直接用于页面跳转要在页面逻辑中映射成/pages/detail/detail?idmovie_123这种内部路径。本地模拟数据时常见做法是把 JSON 文件放到utils/mock/目录再在请求模块前面加一个开关// utils/config.js module.exports { USE_MOCK: true, API_BASE_URL: https://api.example.com };请求模块里判断USE_MOCK为 true 就走本地 require 的数据并延迟 resolve这样前后端联调前后页面代码不用大改。注意require的模块会被真实打包进代码包mock 数据文件体积要控制封面图尽量用网络图而不是 base64 内嵌否则主包体积很快超标。3.3 列表页加载状态机与上下拉刷新首页最常用的加载状态有三个变量当前页号、是否加载中、是否到底。对应的代码骨架如下// pages/home/home.js const { request } require(../../utils/request); Page({ data: { movies: [], page: 0, pageSize: 20, isLoading: false, isFinished: false }, onLoad() { this.loadMovies(true); }, onPullDownRefresh() { this.loadMovies(true); }, onReachBottom() { if (!this.data.isFinished !this.data.isLoading) { this.loadMovies(false); } }, loadMovies(reset) { if (this.data.isLoading) return; this.setData({ isLoading: true }); const page reset ? 0 : this.data.page 1; request(/in_theaters, { start: page * this.data.pageSize, count: this.data.pageSize }) .then((res) { const items (res.subjects || []).map((item) { const avg item.rating ? Number(item.rating.average) : 0; return { ...item, rating: { ...item.rating, average: avg } }; }); const movies reset ? items : this.data.movies.concat(items); this.setData({ movies, page, isFinished: items.length this.data.pageSize }); }) .catch(() { wx.showToast({ title: 加载失败, icon: none }); }) .finally(() { this.setData({ isLoading: false }); wx.stopPullDownRefresh(); }); } });isLoading是互斥锁快速滚动触底时会阻止连续发起请求isFinished在返回条数小于 pageSize 时置为 true后续不再触发加载。数据映射里顺手做了Number()转换把后端返回的字符串型 average 修正为数值传递给评分组件时才不会出现类型比较错误。finally中必须调用wx.stopPullDownRefresh()否则下拉动画不会自动收回。注意setData单次提交数据有 1MB 上限。榜单接口返回的字段很多先过滤掉不需要的字段再 setData能有效缓解长列表在低端机上的主线程传输压力。4. 仿豆瓣评分组件的 observers 监听与 wx:key 渲染优化4.1 自定义评分组件用 properties observers 响应变化源码包的 components 目录里通常能找到评分相关组件比如 movie-rating。自定义组件的 JS 中properties是对外暴露的属性observers用于监听属性变化并同步内部状态。评分组件的实现可以这样拆解// components/movie-rating/movie-rating.js Component({ properties: { value: { type: Number, value: 0 }, max: { type: Number, value: 5 }, readonly: { type: Boolean, value: false } }, data: { stars: [] }, observers: { value, max: function (value, max) { const stars []; for (let i 1; i max; i) { const diff value - i; stars.push({ filled: diff 0, half: diff -0.5 diff 0 }); } this.setData({ stars: stars }); } }, methods: { onTap(e) { if (this.data.readonly) return; const index Number(e.currentTarget.dataset.index); this.triggerEvent(change, { value: index 1 }); } } });observers在组件初始化时执行一次以后每次 value 或 max 变化也会重新执行正好覆盖“列表加载完成后把分数传进组件”的场景。计算逻辑里用diff -0.5判断半星当 value 为 4.5 时前四颗为整星区间、第四颗半星、第五颗为空。用数值区间判断而不是字符串匹配可以避免不同接口返回精度不一致造成的显示错误。readonly控制是否可点击详情页展示历史评分时传 true评分区域传 false同一个组件承担两种职责正是组件化最直接的收益。对应 wxml 里渲染星星的写法view classrating-box view wx:for{{stars}} wx:keyindex >wx.setNavigationBarTitle({ title: this.data.movie.title - 豆瓣评分 });调用时机建议放在onShow而不是onLoad。onLoad阶段数据通常是异步返回的标题设置可能还没生效就被页面栈覆盖onShow里执行能确保每次回到页面都刷新标题。如果开发者在工具的“编译模式”里看到标题偶尔没变那是编译模式缓存的问题重新编译一次即可恢复。5.2 自定义导航栏时胶囊位置的计算一旦在app.json里把navigationStyle改成custom导航栏完全由页面接管右上角胶囊按钮的悬浮位置需要手动适配。常见的兼容写法是同时读取状态栏高度和胶囊按钮位置const menu wx.getMenuButtonBoundingClientRect(); const statusBarHeight wx.getSystemInfoSync().statusBarHeight; this.setData({ navBarHeight: menu.bottom menu.top - statusBarHeight, statusBarHeight: statusBarHeight });menu.bottom与menu.top的和减去状态栏高度就是自定义导航栏的实际高度。这个值在 iPhone 全面屏与普通安卓机上差异很大写死 44px 会出现标题与胶囊重叠。页面样式里用padding-top: {{statusBarHeight}}px留出安全区再按navBarHeight撑起导航内容是目前兼容性最好的处理方式。最后还有一个容易被忽略的环节开发者工具“详情 - 本地设置 - 不校验合法域名”只对开发环境生效提交预览或真机扫码时开关会强制关闭。凡是请求非 https 域名或域名未在小程序后台配置都会直接走 fail 回调。答辩现场出现这种情况非常被动所以提前在 mp 后台把接口域名加入 request 合法域名同时确认证书不是自签名。配置完回来点“清缓存 - 清除全部缓存”再重新编译避免旧分包资源干扰判断。打开开发者工具 Network 面板刷新列表页看到请求状态码 200 且响应体为正常 JSON 即可出现url not in domain list则说明域名配置还没同步生效。真机预览后从首页进入详情页点击评分组件触发 change 事件控制台打印出对应 score 日志整个仿豆瓣评分小程序就具备交付条件了。本文还有配套的精品资源点击获取