微信小程序项目实战:从今日美食源码剖析页面路由与本地存储

微信小程序项目实战:从今日美食源码剖析页面路由与本地存储 简介这是「今日美食」微信小程序完整项目实例定位为美食菜谱分享应用适合小程序开发者、移动前端学习者以及正在准备实训作品的学生。资源为 rar 压缩包共60个文件包含7个wxml页面结构、8个wxss样式、9个js逻辑脚本以及11个json配置数据另有png、jpeg等图片素材整体大小仅1.08MB便于在微信开发者工具中直接导入分析。项目功能覆盖首页推荐、分类导航、关键词搜索、菜谱详情、收藏和分享等典型模块wxml负责页面骨架wxss完成视觉样式js控制页面交互和请求逻辑json存储菜谱分类、食材与步骤数据前后端数据交互思路清晰适合逐文件研读。目前已有20290人浏览学习是广受认可的小程序练手项目。通过学习该实例可以掌握小程序目录结构、组件用法、数据绑定、API调用和事件处理等核心技能也能参考其图片资源与配置写法快速搭建自己的美食或内容社区类小程序。1. 微信小程序“今日美食”能复现什么一个jinrimeishi.rar解压后只有几十个文件却能覆盖菜谱列表、搜索、详情、添加和用户页这就是微信小程序项目实例里常被拿来练手的“今日美食”。它没有复杂后端所有菜品数据都存在前端代码里用 JSON 组织、通过本地存储做收藏和浏览记录正好适合想搞懂微信小程序页面路由和数据处理的人拿来做复现。你不需要一台服务器只需要微信开发者工具导入项目改一改app.json和页面里的 WXML/WXSS就能跑起来。这个项目的价值不在菜谱本身而在于它把小程序的页面栈、数据绑定和本地 API 串成了一条完整链路新手能从中学会 tabBar 之外的普通页面怎么跳转、参数怎么传熟手也能借此顺一遍小程序的启动流程和 local storage 边界。2. 从 app.json 到 pages今日美食的目录结构与页面路由2.1 压缩包里的文件逐项拆解先把jinrimeishi.rar解压得到jinrimeishi目录典型的小程序工程结构如下我直接把每个文件对应的角色标出来文件/目录作用关键点app.js小程序入口逻辑App({ onLaunch() {} })里可做全局初始化app.json全局配置pages数组决定哪些文件会参与编译app.wxss全局样式公共按钮、容器样式放这里pages/index首页展示推荐美食通常是列表入口pages/logs日志页调试用的示例页可删可留pages/detailFood菜品详情接收index页传来的菜品 idpages/addFood添加菜品表单页往本地存储里追加数据pages/select分类选择按菜系或难度筛选pages/user个人中心展示收藏和最近浏览pages/searchList搜索列表根据关键词过滤菜谱utils/util.js工具函数格式化时间、生成 id、模拟数据project.config.json项目配置设置 appid、编译条件、本地设置project.private.config.json私有配置不提交到仓库的本地配置sitemap.json索引配置控制哪些页面可以被微信索引常见做法是先把logs页删掉用detailFood、addFood这些真正的业务页替换但这不影响我们先理解路由。pages数组的第一项就是小程序的启动页这里项目默认是pages/index/index所以一打开进入首页。2.2 app.jsonpages 数组、window 和 tabBar 怎么配app.json是全局配置的入口。pages数组里每个路径都对应一个不含扩展名的 js/wxml/wxss/json 文件组。示例配置如下{ pages: [ pages/index/index, pages/detailFood/detailFood, pages/addFood/addFood, pages/select/select, pages/user/user, pages/searchList/searchList, pages/logs/logs ], window: { navigationBarBackgroundColor: #ff6b35, navigationBarTitleText: 今日美食, navigationBarTextStyle: white, backgroundColor: #f5f5f5 }, style: v2 }这段配置里pages数组的顺序是有讲究的排在最前面的页面会作为小程序启动后的第一个页面。window配置的是所有页面的导航栏外观navigationBarTitleText如果不在单页面的 json 里重写就会统一显示成“今日美食”。如果你想要首页底部出现导航栏还需要加tabBar字段但“今日美食”这个项目里index和user适合做 tabBar其他页面走普通跳转。我一般建议把index和user作为tabBar的两个 tab因为用户最常用的就是浏览和查看个人收藏。project.config.json里的appid默认是测试号你在微信开发者工具里导入项目时如果提示 appid 无效可以用测试号编译等真正上线前再去微信公众平台申请正式 appid。sitemap.json可以控制索引配置为{rules:[{action:disallow,page:*}]}表示不允许被搜索索引开发阶段可以这样设置避免调试页被爬到。2.3 页面跳转从 index 到 detailFood 的路径参数传递小程序页面跳转最直接的是wx.navigateTo它会保留当前页面压入新页面栈。示例代码// pages/index/index.js goDetail(event) { const id event.currentTarget.dataset.id; wx.navigateTo({ url: /pages/detailFood/detailFood?id${id} }); }对应的 WXML 里需要给点击的菜品卡片加上>view classfood-card wx:for{{foodList}} wx:keyid>onLoad(options) { const id options.id; // 从本地数据源里找到对应菜品 this.setData({ foodDetail: getFoodDetailById(id) }); }这里有个容易踩的坑wx.navigateTo的 URL 长度有限制不要通过它传递大段 JSON只传id这样的短参数然后在目标页根据id去查数据源。如果页面之间需要传递复杂对象用getApp().globalData或者wx.setStorageSync临时存一下更稳妥。3. WXML WXSS菜谱卡片、搜索页与详情页的界面实现3.1 用 wx:for 渲染首页菜谱列表首页的核心是把菜品数组渲染成卡片。先看 WXML 代码view classpage-container view classfood-list view classfood-item wx:for{{foodList}} wx:keyid bindtapgoDetail>.food-list { padding: 24rpx; } .food-item { display: flex; background: #fff; border-radius: 16rpx; padding: 20rpx; margin-bottom: 20rpx; box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05); } .food-image { width: 160rpx; height: 160rpx; border-radius: 12rpx; margin-right: 20rpx; background: #f0f0f0; }rpx是小程序的响应式像素单位不需要手动适配不同机型宽度设计稿按 750 来做就行。图片加载失败时background: #f0f0f0兜底至少不会显示空白破图。3.2 searchList 页的搜索逻辑bindinput setData搜索页用bindinput监听输入框每次输入都会触发回调再把结果渲染到列表。看代码// pages/searchList/searchList.js Page({ data: { keyword: , resultList: [] }, onInput(e) { const keyword e.detail.value.trim(); this.setData({ keyword }); const results searchFood(keyword); // 从数据源过滤 this.setData({ resultList: results }); } });WXML 部分input classsearch-input placeholder输入菜名或食材 value{{keyword}} bindinputonInput confirm-typesearch / view classresult-list view classresult-item wx:for{{resultList}} wx:keyid text{{item.name}}/text text{{item.materials}}/text /view /viewe.detail.value是输入框当前内容setData会同步更新视图。这里有个性能细节每次输入都触发整个searchFood过滤如果本地数据量很小没问题但菜谱超过几百条时建议做防抖。可以用setTimeout延迟 300 毫秒再执行过滤避免连续输入导致 UI 卡顿。实际项目中searchFood可以写在utils/util.js里类似这样function searchFood(keyword) { const allFood getFoodData(); return allFood.filter(item { return item.name.includes(keyword) || item.materials.some(m m.includes(keyword)); }); }includes是 ES6 方法小程序开发者工具默认支持不需要额外 polyfill。materials是数组some只要有一项食材匹配就返回 true。3.3 详情页和添加页的输入绑定详情页需要展示食材和步骤数据结构我用一个二维数组来表示步骤每一步包含text和可选image。示例 WXMLview classmaterials text classsection-title食材配料/text view classmaterial-item wx:for{{foodDetail.materials}} wx:key*this text{{item}}/text /view /view view classsteps text classsection-title制作步骤/text view classstep-item wx:for{{foodDetail.steps}} wx:keyindex text classstep-num{{index 1}}/text text classstep-text{{item}}/text /view /viewwx:key*this适用于数组内容本身就是字符串的场景wx:keyindex则是用数组索引做 key不推荐但用于静态步骤数据时可以接受。因为这里的步骤不会增删排用索引作为 key 不会产生问题。添加页addFood里表单控件的双向绑定靠bindinput配合setData实现很像 React 里的受控组件。例如input value{{name}}>onFieldInput(e) { const field e.currentTarget.dataset.field; this.setData({ [field]: e.detail.value }); }这里用>// utils/util.js const foodData [ { id: f001, name: 红烧排骨, category: 家常菜, difficulty: 中级, time: 40, image: /images/paigu.png, tags: [下饭, 经典], materials: [排骨, 姜, 蒜, 酱油, 糖], steps: [排骨焯水, 炒糖色, 下排骨翻炒, 加水炖煮, 收汁出锅] } ]; function getFoodData() { return foodData; } function getFoodDetailById(id) { return foodData.find(item item.id id); } module.exports { getFoodData, getFoodDetailById };这样做的优点是不用处理异步请求页面加载时同步拿数据。但要注意module.exports导出的数据是引用类型如果页面里直接修改item会污染全局数据源。所以我会在getFoodData返回JSON.parse(JSON.stringify(foodData))做一层深拷贝防止误操作。4.2 用 wx.setStorageSync 实现收藏与最近浏览本地存储是小程序保存用户状态的常用手段。收藏功能用wx.setStorageSync同步写入简单可靠。看这段代码// pages/detailFood/detailFood.js toggleFavorite() { const foodDetail this.data.foodDetail; let favorites wx.getStorageSync(favorites) || []; const index favorites.findIndex(item item.id foodDetail.id); if (index -1) { favorites.splice(index, 1); wx.showToast({ title: 已取消收藏, icon: none }); } else { favorites.push(foodDetail); wx.showToast({ title: 收藏成功, icon: success }); } wx.setStorageSync(favorites, favorites); this.setData({ isFavorite: index -1 }); }关键 API 是wx.getStorageSync(key)和wx.setStorageSync(key, data)key 是字符串data 会自动序列化。这里有几个边界情况需要注意坑点表现处理方式key 不存在getStorageSync返回空字符串用 数据超 1MB写入失败只存 id 数组详情通过 id 重新查修改数组内容原数组被直接改用splice或重新赋值整体数组wx.showToast的icon参数只支持success、error、none提示“已取消收藏”时不能写info否则无效。4.3 封装 request从本地 JSON 到真实 API 的平滑过渡项目里的util.js还可以封装一套异步请求方便以后接入真实后端。常见做法是把wx.request包成 Promisefunction request(url, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: url, method: method, data: data, header: { Content-Type: application/json }, success(res) { if (res.statusCode 200 res.statusCode 300) { resolve(res.data); } else { reject(new Error(请求失败状态码 ${res.statusCode})); } }, fail(err) { reject(err); } }); }); }用的时候可以这样async loadData() { wx.showLoading({ title: 加载中 }); try { const data await request(/api/food/list); this.setData({ foodList: data }); } catch (err) { wx.showToast({ title: err.message, icon: none }); } finally { wx.hideLoading(); } }async/await让代码看起来像同步但小程序基础库需要支持 Promise旧版本要引入 polyfill。wx.showLoading和wx.hideLoading必须配对使用否则真机上会在页面留下一个转圈层。封装的header里Content-Type用application/json如果你的后端接口接收表单格式要改成application/x-www-form-urlencoded。在“今日美食”这个阶段你完全可以不调用request直接用util.js里的同步数据但这个封装已经为后续替换成云开发或真实 API 留好了接口。到时候只要把getFoodData内部从return foodData改成return await request(/api/food)调用方不用动。5. 运营级收尾分享菜单、合法域名和加载页改造5.1 onShareAppMessage 分享菜谱让用户把菜谱转发到微信好友或群聊需要两个动作配合在 WXML 按钮上设置open-typeshare在页面的Page配置里实现onShareAppMessagebutton classshare-btn open-typeshare分享这道菜/buttononShareAppMessage() { const food this.data.foodDetail; return { title: 今日美食${food.name}, path: /pages/detailFood/detailFood?id${food.id}, imageUrl: food.image ? food.image : /images/default-share.png }; }path里带id微信会模拟用户从这个路径重新打开小程序转发后对方点开直接定位到那道菜。imageUrl不传时默认截取当前页面顶部区域作为分享图片但截的图经常很丑建议指定一个 5:4 比例的图片。还有一个参数success和fail回调现在基本不需要在回调里做太多事因为微信限制不能自定义分享成功后的奖励逻辑。5.2 project.config.json 与合法域名调试开发者工具里经常碰到“不在以下 request 合法域名列表中”的报错那是因为微信真机环境不允许往非 HTTPS 域名发请求。开发阶段可以在project.config.json的setting里临时打开校验开关{ setting: { urlCheck: false, es6: true, enhance: true, postcss: true, minified: true } }urlCheck: false会让工具和真机调试跳过域名校验但注意这只能在开发阶段用上线前必须到微信公众平台「开发管理—服务器域名」里配置好request合法域名。如果你的后端没有备案域名可以用云开发的wx.cloud.callFunction绕过域名限制那又是一套不同的配置。遇到局域网接口联调时开启urlCheck: false同时把详情里的“不校验合法域名”勾上就可以用http://192.168.x.x:8080直接访问。5.3 细节技巧修改刚进入的加载页面小程序启动时会先经历app.js的onLaunch然后加载启动页。很多人觉得初始白屏很难看实际上可以在index页面的onLoad里做一次自定义 loadingonLoad() { wx.showLoading({ title: 今日美食准备中, mask: true }); // 模拟异步数据加载 setTimeout(() { const data getFoodData(); this.setData({ foodList: data }); wx.hideLoading(); }, 500); }mask: true会挡住用户点击避免在数据还没渲染时误触。但wx.showLoading在真机上如果超过 10 秒不隐藏会自动消失所以setTimeout的时长不要超过这个限制。更彻底的做法是给index页的 WXML 加一个骨架屏比如用灰色方块画出列表占位等setData完成后切换成真实数据这也是很多“微信小程序项目实例”里面没有写出来的优化点。如果你想让初次加载看起来更专业可以把setTimeout里的逻辑替换成真正的数据请求loading 提示始终在接口返回后才关闭。本文还有配套的精品资源点击获取