ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Vue的uni-app小程序开发:农家书屋项目全解析

基于Vue的uni-app小程序开发:农家书屋项目全解析 简介基于Vue框架的农家书屋小程序设计源码是一套面向小程序开发者和前端学习者的完整工程适合用Vue技术栈构建乡村数字化阅读服务场景。资源共含941个文件压缩包约25.94MB主要涵盖248个JavaScript脚本、229个Vue组件、176个JSON配置、129个Markdown说明文档、86个PNG图片及SCSS/CSS样式文件等覆盖页面结构、交互逻辑、路由配置、样式设计和文档说明能支撑从环境配置到功能实现的全流程学习。已有201人学习下载。读者可从中获取系统的Vue组件化开发思路、小程序配置文件组织方式、样式布局与资源管理方法以及真实项目中的目录结构与工程化细节适合希望以完整项目为参照、提升小程序开发与前端工程能力的开发者。1. 把 Vue 框架和农家书屋放在一起说的是什么微信原生小程序并不接受 Vue 的模板语法所谓“基于 Vue 框架的小程序源码”落地时基本走 uni-app 或 Taro 两条路。农家书屋这类业务选 uni-app 更常见项目规模小、团队往往只有一两个人Vue 写法和 H5 端复用能把维护成本压到最低。书屋分布在乡镇和村庄藏书以农技、童书、大众读物为主借阅量不大但书目查询、预约借阅这类轻交互很刚性做成小程序村民不用跑到屋里扑空管理员也能少做手工台账。这篇面向两类读者接乡村信息化项目的外包工程师以及想用 Vue 认真写一遍小程序的新手。前者能快速出活后者能看清数据、页面、配置在 uni-app 里是如何被组织起来的。2. 技术选型与工程结构用 uni-app 搭出农家书屋小程序2.1 为什么选 uni-app而不是直接写原生 Pageuni-app 属于编译时框架开发时写 Vue 组件template被编译成小程序的 WXMLscript setup里的逻辑编译进 Page 的 methods 和 data最终产物和原生小程序没有区别。这套机制决定了两件事第一组件复用、状态管理、computed 这些 Vue 心智模型可以继续用第二小程序的限制也原样保留比如没有 window 和 document不能直接操作 DOMref 拿到的只是组件实例而不是真实节点。选 uni-app 还有一个现实理由农家书屋的线上化往往不止一个小程序村委会或者承接项目的人通常后续还要一个 H5 展示页甚至要挂一个简单的图书购买入口。同一套 Vue 组件编译到 H5 和微信端比维护两套原生代码省事得多。如果后续要扩展成小程序商城的形态直接加一个商品模块的页面购物车和订单接口往 api 目录里补就行。如果团队里有人提出用 Taro也说得通。Taro 的 React 写法和更细的多端适配在大型项目里更有优势但书屋源码的典型形态是页面少、逻辑浅、依赖轻uni-app 的报错更直白周边插件也更贴近国内小程序生态。我的建议是别为了“更先进”选 Taro除非团队主力是 React 背景。2.2 工程目录源码里哪些文件是核心一个标准 uni-app 工程解压后大概长这样my-book-app/ ├── pages/ │ ├── index/ # 书屋首页书架与推荐 │ ├── books/ # 图书列表与筛选 │ ├── detail/ # 图书详情与借阅 │ └── mine/ # 我的借阅记录 ├── components/ # 自定义组件书架卡、空状态 ├── api/ # 接口定义与请求封装 ├── utils/ # 本地缓存、日期格式化 ├── static/ # 图片等静态资源 ├── App.vue # 应用生命周期与全局样式 ├── main.js # Vue 实例入口 ├── pages.json # 页面路由与 tabBar 配置 ├── manifest.json # 各端 appid 与应用配置 └── uni.scss # 全局样式变量其中 pages.json 和 manifest.json 是 Vue 项目里没有的两个配置文件。pages.json 是路由表加页面样式的混合体小程序没有 Vue Router页面之间的跳转靠的是路径字符串新增页面必须在 pages 数组里注册。manifest.json 则统一管理微信端 appid、H5 端路由模式这些平台差异信息小程序端要改 AppID 或权限声明都去这里找。api 和 utils 这两个目录容易被新手忽略但它们决定了后续能不能快速把 mock 数据换成真实接口。约定是组件里不允许直接出现网络请求代码所有请求都从 api 目录导出这样替换接口、加日志、统一鉴权都只动一处。2.3 pages.json 里的书架导航配置首页、图书列表、我的借阅三个页面用 tabBar 放在底部是最直观的做法。一个常见配置{ pages: [ { path: pages/index/index, style: { navigationBarTitleText: 农家书屋 } }, { path: pages/books/books, style: { navigationBarTitleText: 全部图书 } }, { path: pages/detail/detail, style: { navigationBarTitleText: 图书详情 } }, { path: pages/mine/mine, style: { navigationBarTitleText: 我的借阅 } } ], globalStyle: { navigationBarTextStyle: white, navigationBarBackgroundColor: #3A7D44, backgroundColor: #F5F5F5 }, tabBar: { color: #666666, selectedColor: #3A7D44, list: [ { pagePath: pages/index/index, text: 书架 }, { pagePath: pages/books/books, text: 找书 }, { pagePath: pages/mine/mine, text: 我的 } ] } }navigationBarBackgroundColor 建议和书屋的品牌色统一绿色系最容易和“书屋”联想。globalStyle 里的配置是全部页面的默认值单页要覆盖时在 pages 数组对应项的 style 里写同名属性即可。tabBar 的 list 最多配五个每个 pagePath 都必须已经出现在 pages 数组里顺序不一致时编译直接报错。detail 页不要放进 tabBar详情页从列表跳进来再挂一个底部 tab 会很奇怪。提示tabBar 的 iconPath 和 selectedIconPath 是可选的不配置时底部只显示文字适合先跑通逻辑、后补设计的阶段。如果要配图标图片必须是 png 且体积在 40KB 以内否则部分基础库会静默丢失图标。3. 书架首页的数据与渲染从 mock 数据到真实列表3.1 先定义图书的数据模型小程序端的数据不建议一开始就接后端先把静态数据跑通后面只换接口。图书模型用一组 mock 就够id 是传给后端的唯一标识cover 是封面路径stock 是当前可借册数。// utils/book-data.js export const books [ { id: 1, title: 水稻高产栽培技术, category: 农技, author: 省农科院专家组, stock: 5, cover: /static/covers/rice.png, summary: 从育秧到收割的田间管理手册适合本地区气候条件 }, { id: 2, title: 幼儿睡前故事集, category: 儿童, author: 儿童文学出版社, stock: 8, cover: , summary: 适合 3-6 岁亲子共读的短篇故事合集 }, { id: 3, title: 常见病家庭护理, category: 健康, author: 乡村卫生室整理, stock: 2, cover: , summary: 村民日常健康知识问答通俗易懂 } ]字段设计上要刻意避开那些“看起来很全但用不上”的属性比如出版社、ISBN 都先不建。书屋的书目量级通常只有几百本一本书对应多条记录的关联表结构在小程序端没必要展开。任何你不想在小程序里维护的字段就不要出现在这个模型里。3.2 首页用组合式 API 拉取并渲染页面代码同时包含模板、样式和逻辑template view classcontainer view classcategory-bar view v-forcat in categories :keycat classcategory-item :class{ active: cat currentCategory } clickswitchCategory(cat) {{ cat }}/view /view view classbook-list view v-forbook in filteredBooks :keybook.id classbook-card image classbook-cover :srcbook.cover || /static/covers/default.png modeaspectFill / view classbook-info text classbook-title{{ book.title }}/text text classbook-meta{{ book.author }} · 可借 {{ book.stock }} 本/text /view /view /view /view /template script setup import { ref, computed } from vue import { onLoad } from dcloudio/uni-app import { books as mockBooks } from /utils/book-data.js const categories [全部, 农技, 儿童, 健康] const currentCategory ref(全部) const bookList ref([]) const filteredBooks computed(() { if (currentCategory.value 全部) return bookList.value return bookList.value.filter((book) book.category currentCategory.value) }) function switchCategory(cat) { currentCategory.value cat } onLoad(() { bookList.value mockBooks }) /scriptonLoad 是 uni-app 从微信生命周期映射过来的钩子页面创建后立即执行适合做初始化。filteredBooks 是 computed 计算属性分类切换时只改 currentCategory 这一个响应式变量列表重新计算不需要手动操作 DOM。这里刻意没有在模板里写 v-if 来判断空数据是因为空状态用统一组件处理后面要换成“暂无书目”时只改一处。image 的 mode 设为 aspectFill封面图不会因为尺寸不一致而变形。3.3 上拉加载、下拉刷新要配着设置小程序原生提供 onReachBottom 和 onPullDownRefresh但两个机制默认都不开。onPullDownRefresh 需要在页面 style 里开启{ path: pages/index/index, style: { navigationBarTitleText: 农家书屋, enablePullDownRefresh: true, onReachBottomDistance: 80 } }onReachBottomDistance 单位是 px表示距离底部多少像素时触发触底。数值太小手指还没划到就没了加载提示太大读者还没看完当前内容就开始请求下一页。书屋这类信息流页面我一般取 80。onPullDownRefresh 只在 json 里开启还不够页面逻辑里必须调用 uni.stopPullDownRefresh() 手动收尾否则下拉动画会一直转。页面里的对接逻辑import { onPullDownRefresh, onReachBottom } from dcloudio/uni-app import { getBooks } from /api/books.js const page ref(1) const pageSize 10 const loading ref(false) async function loadBooks(reset false) { if (loading.value) return loading.value true const next reset ? 1 : page.value const res await getBooks({ page: next, pageSize }) bookList.value reset ? res.list : [...bookList.value, ...res.list] page.value next 1 loading.value false if (reset) uni.stopPullDownRefresh() } onPullDownRefresh(() loadBooks(true)) onReachBottom(() loadBooks(false))组合式 API 的生命周期钩子直接在 setup 顶层调用不需要嵌套在 onLoad 里这是 Vue3 写法和 Vue2 选项式最大的习惯差异。loading 锁防止触底事件和下拉刷新同时触发导致列表重复reset 参数决定是整体替换还是追加。getBooks 就是下一步要做的接口封装。4. 接口层与本地缓存弱网环境下也能查书4.1 用 uni.request 封装一个请求实例小程序没有 fetch统一走 uni.request。直接写业务请求会把超时、错误提示、loading 逻辑散落在每个页面所以一般会在 api 目录里放一个基础封装// api/request.js const BASE_URL https://api.example.com export function request(path, options {}) { return new Promise((resolve, reject) { uni.request({ url: ${BASE_URL}${path}, method: options.method || GET, data: options.data || {}, timeout: 8000, header: { Content-Type: application/json, ...options.header }, success: (res) { if (res.statusCode 200 res.statusCode 300) { resolve(res.data) } else if (res.statusCode 401) { uni.navigateTo({ url: /pages/login/login }) reject(res) } else { uni.showToast({ title: 请求失败(${res.statusCode}), icon: none }) reject(res) } }, fail: (err) { // 弱网或断网时兜底走本地缓存由调用方决定 reject(err) } }) }) }timeout 在农家书屋场景里要重视村委会的 Wi-Fi 和乡镇移动网络抖动都很常见默认 60 秒超时会让页面卡住很久。8 秒是一个相对平衡的值接口没到缓存还没有页面就显示骨架接口到了再刷新。statusCode 的判断不能省略uni.request 的 success 分支只代表 HTTP 层完成不代表业务成功。401 分支单独处理是为了将来接入读者证鉴权时不用回改每个页面。4.2 书屋业务的接口设计接口路径按资源命名后端只要跟着这份约定实现前端就能直接对接| 功能 | 方法 | 路径 | 主要参数 | 返回字段 | | 书目查询 | GET | /api/books | keyword, category, page, pageSize | list, total, hasMore | | 借阅登记 | POST | /api/borrow | bookId, readerId | borrowId, dueDate | | 归还登记 | POST | /api/return | borrowId | returnedAt | | 我的借阅 | GET | /api/borrows | readerId, status | list |keyword 是书名模糊搜索category 对应页面顶部分类。hasMore 是比 total 更实用的字段它直接告诉前端“后面还有没有”省去前端用 page*pageSize 和 total 比较的边界处理。借阅接口的返回里带 dueDate前端能直接用它计算倒计时不用自己维护借期规则。页面调用时只关心数据不关心 HTTP// api/books.js import { request } from ./request export const getBooks (params) request(/api/books, { method: GET, data: params }) export const borrowBook (bookId, readerId) request(/api/borrow, { method: POST, data: { bookId, readerId } })4.3 本地缓存兜底没有网也要能看到上一批书目缓存策略是书屋小程序比商城小程序更该做的一件事。读者可能站在书屋门口信号只有一格这时候书目列表已经是缓存的了他至少能查到自己想借的书在不在架。基础写法是function getCached(key) { const raw uni.getStorageSync(key) if (!raw) return null const { data, expire } raw if (Date.now() expire) { uni.removeStorageSync(key) return null } return data } function setCached(key, data, ttlMinutes 60 * 24) { uni.setStorageSync(key, { data, expire: Date.now() ttlMinutes * 60 * 1000 }) }缓存有效期设为 24 小时的理由书屋书目变动频率很低新书到馆一般以周为单位一天刷新一次足够。ttl 参数保留出来是因为借阅记录这类敏感数据不适合长时间缓存调用时传 10 分钟即可。缓存的 key 建议带版本号比如books_v1将来数据结构变了旧缓存读出来会直接因字段不符合预期而报错带上版本号可以自然失效。具体使用时在 getBooks 的 fail 分支里先 getCached(books_v1)有值就 resolve 缓存没有才 reject。5. 源码上线的最后一公里编译、备案与常见报错5.1 从源码到微信开发者工具uni-app 项目编译到微信小程序需要三步安装依赖、编译、导入。命令行方式最通用npm install npm run build:mp-weixin产物默认在dist/build/mp-weixin目录。打开微信开发者工具选择“导入项目”目录指向这个文件夹AppID 填小程序的 appid没有就先选测试号。npm run dev:mp-weixin是开发模式改动代码自动重新编译适合连着开发者工具调试build 是压缩产物提交审核前必须再过一遍。如果拿到的是 HBuilderX 工程目录里没有 package.json直接用 HBuilderX 打开项目根目录在“运行”菜单里选“运行到小程序模拟器”效果一样。导入后第一件事是确认“详情-本地设置”里的“不校验合法域名”是否打开。开发阶段这个选项能避免频繁被拦截但上线前必须关闭它并在小程序后台配置 request 的合法域名。域名必须备案且只支持 HTTPS。冷启动默认展示的页面就是 pages.json 里第一个页面如果想先放一个欢迎引导页把引导页路径挪到 pages 数组首位即可这是最省事的“修改刚进入的加载页面”的做法。注意编译成功不代表能正常运行。常见情况是开发者工具控制台没有报错页面空白先打开调试面板看 network 请求绝大多数白屏来自域名校验失败。5.2 小程序备案的备注怎么填备案是上线绕不开的环节。农家书屋的性质要在“服务内容”里写清楚别图省事只填两个字“阅读”。建议写“面向农村读者的图书借阅、书目查询和阅读活动信息发布”十几秒的事后台审核的语义理解会准确很多。主办者类型按实际主体选。以书屋名义备案证件就填单位的登记证书或营业执照封面照片拍清楚即可以个人名义备案需要个人身份证和相关使用证明。类目选择上“生活服务-图书馆”或者“教育-其他”都能覆盖图书借阅审核通过率差别不大关键是和备注里的描述保持一致。5.3 Vue 打包后布局异常的排查顺序“vue 打包后布局异常”是出现频率很高的搜索词实际上多数和小程序运行时有关和 Vue 本身无关。第一类图片拉伸或错位。小程序端 image 组件默认是 320px 高不设置 height 就会把封面拉变形。解决方式是给 image 设置固定宽高并加 modeaspectFill这在第 3 章的模板里已经示范过。第二类rpx 和 px 混用。rpx 在不同机型上等比缩放px 是固定物理像素混用容易出现“开发工具里正常真机上右移一截”的情况。建议栅格和间距全部用 rpx边框用 px写一条约定进项目 README比事后排查成本低。第三类白屏后下拉刷新恢复。多发生在 tabBar 页面因为 tabBar 页面在启动时会被提前创建onLoad 里的初始化如果依赖了另一个页面的数据时序就不可控。对策是把初始化事件挂到 onShow 上并加一个 beforeInit 的脏标记只执行一次。6. 提升成品感动态标题、骨架屏与分享卡片6.1 动态修改页面标题书目详情页根据书名设置标题避免所有页面都叫“农家书屋”uni.setNavigationBarTitle({ title: book.title })页面 config 里配置的 navigationBarTitleText 是默认值运行时调用这个 API 会覆盖它。要特别注意执行时机必须在数据加载完成后调用否则拿到的 book 还是空对象标题会被设成 undefined。6.2 给书架列表加骨架屏列表加载的几百毫秒里骨架屏比 loading 转圈更有反馈感。用纯 CSS 实现不引入组件库.skeleton-card { display: flex; padding: 24rpx; background: #fff; margin-bottom: 16rpx; } .skeleton-block { background: linear-gradient(90deg, #eee 25%, #f5f5f5 50%, #eee 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; }配合 v-for 生成 6 个骨架卡片数据返回后 bookList 有值骨架就消失。首页交互完全依赖 Vue 的条件渲染不需要额外状态管理。颜色用 #eee 和 #f5f5f5 这种低对比度组合太亮的灰色在真机上会显得页面“脏”。6.3 配置分享卡片onShareAppMessage 是微信给页面的“自来水”入口配置 title 和 pathonShareAppMessage(() ({ title: 我正在农家书屋借《${book.value.title}》一起看看吧, path: /pages/detail/detail?id${book.value.id} }))path 必须带参数否则好友点开只会落在首页前面配置的动态标题和详情数据都会落空。卡片的 imageUrl 不传时默认截图当前页面对带封面图的详情页来说效果已经够用。分享出去后如果目标是“让更多人用”就在列表页和详情页都接上这个钩子如果目标是“让管理后台统计转化”记得在 App.vue 的 onLaunch 里读取 query 中的分享来源参数并上报。本文还有配套的精品资源点击获取
返回列表