微信小程序小说阅读器源码拆解:从路由到阅读进度恢复

发布时间:2026/9/16 8:35:37
微信小程序小说阅读器源码拆解:从路由到阅读进度恢复 简介一套基于微信小程序开发的小说阅读器源代码工程面向小程序初学者和有阅读类产品开发需求的开发者覆盖小说列表、章节列表、章节内容阅读的完整链路并包含日志、关于等辅助页面。压缩包为zip格式共30个文件以js逻辑脚本、wxml页面结构、wxss样式表、json配置为主搭配png/jpg封面图与md说明文档整体仅93KB结构紧凑适合直接导入微信开发者工具逐行研读。已有1650人浏览学习是快速上手小程序网络请求与页面跳转的实用样例。项目中的小说列表通过wx.request获取元数据并用滚动视图渲染章节内容页支持阅读进度缓存与本地存储源码还涉及登录授权、错误处理与日志记录等工程化细节可掌握从页面布局、事件绑定到数据缓存、模块拆分的完整开发路径也便于二次开发成个人作品。1. 一套能跑通阅读闭环的小程序源码长什么样一个小说阅读器小程序对外是列表、章节、正文三个页面对内却牵扯着网络请求、路由传参、本地缓存和阅读进度恢复。这套源代码的 pages 目录正好覆盖 index、list、content 三个核心界面app.js 挂全局数据utils/util.js 收敛纯函数assets 存放静态资源目录结构没有冗余装饰适合先通读再按自己的书源改造。拆它最有价值的地方在于你能同时看到微信小程序的数据流、页面生命周期与组件交互如何在同一个项目里相互配合而不是零散地看某个 API 的用法。下面的内容按源码的文件组织顺序展开每个模块都会说明设计原因、参数选择和容易出错的边界。2. app.json 与 app.js先定骨架再谈数据流2.1 页面注册顺序与 window 配置的影响范围app.json 是整个小程序的配置文件pages 数组决定了哪些页面被打包也决定了冷启动后第一个进入哪个页面。源码里 index 排在第一位用户打开小程序先看到小说列表list 和 content 是二级、三级页面通过 wx.navigateTo 压栈进入返回时由页面栈逐级弹栈。window 节点里的配置是全局默认导航栏标题、背景色、文本颜色会被所有页面继承但每个页面目录下都可以有自己的 .json 文件覆盖这些值。一个常见的坑是把 enablePullDownRefresh 在全局打开。下拉刷新对列表页有用但对阅读页来说是灾难——正文页误触下拉会整个页面拖动阅读位置直接丢失。所以这个开关应该放在 index.json 里单独控制{ navigationBarTitleText: 书架, enablePullDownRefresh: true }这段配置只对 index 页面生效app.json 的 window 节点里保持关闭其他页面的 json 也不需要再做额外处理继承全局默认值即可。下面是源码里 app.json 的关键配置{ pages: [ pages/index/index, pages/list/list, pages/content/content, pages/logs/logs ], window: { navigationBarBackgroundColor: #2c2c2c, navigationBarTitleText: 小说阅读器, navigationBarTextStyle: white, backgroundColor: #f5f5f5, backgroundTextStyle: dark }, style: v2, sitemapLocation: sitemap.json }navigationBarTextStyle 只接受 white 或 black设置其他颜色会被微信直接忽略。backgroundColor 是下拉动作露出区域的背景色阅读器应用建议调成和正文底色接近否则下拉时会出现明显的色块断层。backgroundTextStyle 控制下拉 loading 三个点的颜色深色导航栏配 dark浅色导航栏配 light。style 字段声明为 v2 后基础库版本不够时部分组件的圆角和阴影会有差异开发阶段就要在详情面板确认最低基础库版本。配置字段可选值影响范围注意事项navigationBarTextStylewhite / black全局导航栏其他值无效enablePullDownRefreshtrue / false按页面覆盖阅读页务必关闭backgroundColor任意颜色值下拉露出区域与正文底色保持一致backgroundTextStylelight / dark下拉 loading 点深色导航栏配 dark2.2 globalData 与阅读进度的持久化app.js 里 App() 注册整个应用实例globalData 是跨页面共享运行期数据的标准方案。阅读器场景需要用 globalData 承载三类数据当前选中的书、用户调过的字号、阅读历史列表。需要注意的是globalData 只存活在内存中冷启动后全部还原为初始值。无条件地把数据挂在 globalData 上页面刷新后拿到的还是旧值所以 onLaunch 阶段要从 Storage 里恢复。App({ globalData: { currentBook: null, fontSize: 16, readingHistory: [] }, onLaunch() { const history wx.getStorageSync(reading_history) const fontSize Number(wx.getStorageSync(reader_font_size)) || 16 if (history) { this.globalData.readingHistory history } this.globalData.fontSize fontSize }, setReadingProgress(bookId, chapterId, scrollTop) { const key progress_ bookId const data { chapterId, scrollTop, updateAt: Date.now() } wx.setStorageSync(key, data) this.globalData.currentBook { bookId, chapterId, scrollTop } } })onLaunch 在页面 onLoad 之前执行所以页面里读取 globalData 不会遇到空值。setReadingProgress 由阅读页调用key 用书籍 ID 隔离scrollTop 记录当前章内滚动偏移量updateAt 时间戳用于“最近阅读”排序恢复阅读位置时三个字段缺一不可。数据写入 Storage 后再更新 globalData保证两次读取结果一致。提示setStorageSync 是同步方法高频调用会阻塞 JS 线程onPageScroll 里直接调用会出现明显卡顿必须做节流处理。2.3 app.wxss 的主题类与全局样式边界app.wxss 里的样式会被所有页面继承适合放两类内容基础布局类和主题状态类。阅读器场景建议在这里定义 .theme-light 和 .theme-dark 两个容器类而不是散落在各个页面 wxss 里定义相同的颜色值。切换夜间模式时内容页最外层 view 只改一个 class内部所有子节点的配色通过后代选择器覆盖。关于 pages/logs 页面它通常展示启动日志或调试信息不能继承阅读主题。如果 logs 页也套用深色背景后续排查问题时日志里的时间戳会看不清。全局样式按“通用布局”和“阅读主题”两个层级组织logs 和 about 这类功能性页面保持默认浅色样式只在 content 页面里动态切换主容器类名。2.4 utils/util.js 的纯函数边界utils 目录在源码里只有 util.js 一个文件承担日期格式化、文本清洗、URL 参数解析等无状态函数。一个明显的设计特征是 util.js 里不出现任何 wx 调用。页面把 wx.setStorageSync、wx.request 这类 API 作为参数传入工具函数只做数据转换。这样任何页面 require 它都不会产生副作用后续做单元测试也不需要 mock 微信环境。function formatTime(date) { const d date || new Date() const pad (n) (n 10 ? 0 n : n) return d.getFullYear() - pad(d.getMonth() 1) - pad(d.getDate()) } function cleanContent(text) { if (!text) return return text .replace(/nbsp;/g, ) .replace(/br\s*\/?/gi, \n) .replace(/\r\n/g, \n) .replace(/[ \t]\n/g, \n) .trim() } function parseQuery(url) { const query url.split(?)[1] || const result {} query.split().forEach((pair) { const [key, value] pair.split() if (key) result[key] decodeURIComponent(value || ) }) return result } module.exports { formatTime, cleanContent, parseQuery }formatTime 缺省参数时取当前时间调用方不用每次 new Date()。cleanContent 处理接口返回的 HTML 片段把 还原为空格、br 标签替换为换行再统一换行符。parseQuery 用于分享链接和 webview 场景它和 Page onLoad 的 options 有重叠但 options 只能解析当前页面路径上的参数完整 URL 场景必须单独处理。三个函数都没有访问 this导出后直接解构引用即可。3. 小说列表页请求封装、列表渲染与分页参数3.1 用 Promise 收敛 wx.request 回调wx.request 走的是回调风格success 和 fail 各接一个函数。页面里如果连续请求两三个接口回调嵌套会迅速失去可读性。常见做法是先用 Promise 包一层把网络请求的细节收敛到统一封装里页面只关心 resolve 出来的数据。function request(options) { return new Promise((resolve, reject) { wx.request({ url: options.url, method: options.method || GET, data: options.data || {}, header: Object.assign({ content-type: application/json }, options.header), timeout: options.timeout || 10000, success(res) { if (res.statusCode 200 res.statusCode 300) { resolve(res.data) } else { reject(new Error(HTTP res.statusCode)) } }, fail(err) { reject(err) } }) }) } module.exports { request }statusCode 的判断收敛在这一处页面调用时不再关心 200 和 302 的区别。timeout 默认 10 秒阅读类应用对首屏速度敏感超过 10 秒的请求直接判定失败并提示用户检查网络。resolve 出去的是 res.data 而不是整个 response 对象避免页面里出现 res.data.data.list 这种长链路。header 里的 content-type 需要按接口格式调整后端要求表单格式时就改成 application/x-www-form-urlencoded。3.2 WXML 列表渲染与封面懒加载列表页使用页面级滚动而不是 scroll-viewonReachBottom 是 Page 自带生命周期不需要手动监听 scroll 事件。WXML 里用 wx:for 遍历 bookList每条数据绑定书名、作者、封面和简介。view classbook-list view classbook-item wx:for{{bookList}} wx:keyid bindtaponBookTap >Page({ data: { bookList: [], page: 1, pageSize: 10, hasMore: true, loadingMore: false }, onLoad() { this.fetchBooks(1, true) }, onReachBottom() { if (this.data.hasMore !this.data.loadingMore) { this.fetchBooks(this.data.page 1, false) } }, fetchBooks(page, reset) { if (this.data.loadingMore) return this.setData({ loadingMore: true }) request({ url: /api/books, data: { page, pageSize: this.data.pageSize } }) .then((res) { const list reset ? res.list : this.data.bookList.concat(res.list) this.setData({ bookList: list, page: page, hasMore: list.length res.total, loadingMore: false }) }) .catch(() { this.setData({ loadingMore: false }) wx.showToast({ title: 加载失败, icon: none }) }) } })fetchBooks 的 reset 参数控制覆盖还是追加onLoad 首次进入传 true触底加载传 false。concat 生成新数组而不是直接 pushsetData 需要传入完整的新值直接 push 后再 setData 拿不到变更后的数组。hasMore 的判等条件用 list.length res.total只要累计数量小于总数就认为还有下一页不依赖后端额外返回 hasMore 字段。page 自增放在请求参数里而不是成功后修改防止失败重试时页号已经加过头。参数初始值修改时机作用page1请求成功后更新当前页码pageSize10固定不变每页条数hasMoretrue每次请求后重新判断是否还有下一页loadingMorefalse请求期间置 true防止重复请求提示下拉刷新触发时要重置 page 为 1并且用 reset 参数覆盖列表否则会重复追加第一页之后的所有数据。3.4 点击卡片跳转并传递参数列表项点击后跳转到章节列表页使用 wx.navigateTo。navigateTo 会保留当前页面在页面栈中返回时回到列表原位置不会重新加载。url 里的 query 参数在目标页面 onLoad 的 options 里解析。onBookTap(e) { const { id, title } e.currentTarget.dataset wx.navigateTo({ url: /pages/list/list?bookId id title encodeURIComponent(title), fail(err) { console.error(navigate failed, err) } }) }title 做 encodeURIComponent 是必须的书名可能包含中文和特殊符号不编码的话 URL 解析会错位。目标页面拿到 options.title 后如果还要拼进 URL 传给 content 页记得用 decodeURIComponent 还原否则会出现双重编码导致的中文乱码。4. 章节列表与阅读页路由参数、正文渲染与阅读体验4.1 章节列表的初始化与增量渲染章节列表页接收 bookId 和 title 两个参数onLoad 里把 title 设置到导航栏同时发起章节列表请求。这里的关键问题是章节列表可能几百章一次性渲染所有节点会有明显卡顿。页面级滚动下先渲染前 50 章后续通过“加载更多”逐步展开。Page({ data: { bookId: , chapterList: [], visibleCount: 50, chapterIndex: 0 }, onLoad(options) { const bookId options.bookId || const title decodeURIComponent(options.title || ) this.setData({ bookId }) wx.setNavigationBarTitle({ title: title }) request({ url: /api/chapters, data: { bookId } }) .then((res) { this.setData({ chapterList: res.chapters }) }) .catch(() { wx.showToast({ title: 章节加载失败, icon: none }) }) }, loadMoreChapters() { this.setData({ visibleCount: this.data.visibleCount 50 }) } })visibleCount 控制展示长度WXML 里用 slice 截断当前数组。setNavigationBarTitle 可以直接修改导航栏标题不需要页面级 json 写死。bookId 为空时要提前拦截避免接口请求发出无效参数。章节列表的每一项绑定>onChapterTap(e) { const index e.currentTarget.dataset.index const chapter this.data.chapterList[index] const url /pages/content/content?bookId this.data.bookId chapterId chapter.id chapterIndex index wx.navigateTo({ url }) }章节对象里的 id 可能是数字也可能是字符串拼接 URL 时统一转成字符串避免某些场景下把 0 当空值过滤掉。内容页 onLoad 拿到参数后优先从缓存读正文缓存没有再请求接口。参数传递方消费方作用bookId列表页 → 章节页 → 阅读页章节页、阅读页区分书籍title列表页 → 章节页章节页导航栏标题chapterId章节页 → 阅读页阅读页请求正文chapterIndex章节页 → 阅读页阅读页上下章切换4.3 正文渲染rich-text 与纯文本分支章节正文要兼容两种形态后端返回 HTML 片段时用 rich-text 渲染返回纯文本时用 text 加 white-space 样式控制换行。源码的 content 页面应该两个分支都保留因为不同书源的接口格式不一致。按 content-type 字段区分渲染时判断一次即可。view classreader-content stylefont-size: {{fontSize}}rpx; rich-text nodes{{contentNodes}} wx:if{{contentType html}} / text classplain-text wx:elif{{contentType text}} user-select{{plainText}}/text /viewrich-text 的 nodes 支持 HTML 字符串和节点数组两种形式但内部不能嵌套自定义组件图片长按预览这类交互需要另外绑定事件代理。纯文本场景加 user-select 属性后长按可以选择文字做笔记。字体大小用 rpx 会跟随屏幕宽度缩放阅读器场景比 px 更合适大屏手机上字不会显得过小。4.4 字号、夜间模式与阅读设置落盘阅读设置包含字号、行距、背景色三组参数。源码里把 fontSize 放在 globalData但用户调节后必须落盘。缓存键用 reader_font_size 这种固定名称读出来转数字取不到时用默认值 16。adjustFontSize(delta) { const current Number(wx.getStorageSync(reader_font_size)) || 16 const next Math.min(24, Math.max(12, current delta)) wx.setStorageSync(reader_font_size, next) this.setData({ fontSize: next }) }Math.min 和 Math.max 把字号限制在 12 到 24 之间防止用户连续点击把字号调到无法阅读的程度。夜间模式不需要引入额外 UI 组件在 page 根节点切换 isNight 类名用 WXSS 覆盖正文背景和文字颜色即可。.page-night .reader-content { background-color: #1e1e1e; color: #9e9e9e; } .page-night .plain-text { color: #9e9e9e; }这种切换方式没有用 CSS 变量对小程序来说更稳定WXSS 对 CSS 自定义属性的支持存在基础库版本差异。切换类名只要在根节点加一个 class子节点通过后代选择器覆盖兼容性最好。about 页面如果要展示书籍简介和版权信息同样套用这套主题类保证风格一致。4.5 阅读进度记录与续读恢复返回列表时保留阅读位置是阅读器类小程序和普通内容展示页最核心的区别。滚动位置用 onPageScroll 监听并节流写入缓存章节切换时存 chapterId 和 scrollTop。onPageScroll(e) { if (this._throttled) return this._throttled true setTimeout(() { const app getApp() app.setReadingProgress(this.data.bookId, this.data.chapterId, e.scrollTop) this._throttled false }, 500) }setTimeout 做了 500ms 节流onPageScroll 触发频率很高每次都写 Storage 会造成频繁 IO。setReadingProgress 是第 2 章定义的全局方法所有写入逻辑收敛在一处。回列表页时如果检测到该书有未完成进度用 wx.showModal 询问是否续读再跳回对应章节并执行 wx.pageScrollTo 定位到 scrollTop。5. 真机调试、缓存隔离与发布前检查5.1 用 vConsole 定位请求链路开发者工具的 Network 面板能看请求但真机上请求失败时看不到具体错误。小程序基础库自带 vConsole真机调试打开后右下角出现绿色按钮点进去能看到 console 输出、网络请求和系统日志。定位请求问题时优先看 response 的状态码和耗时而不是直接改代码。常见的三类问题是接口域名没配到 request 合法域名真机报 url not in domain list本地调试时没开“不校验合法域名”后端返回非 JSON 格式success 回调里 res.data 解析失败。前两类改开发者工具设置第三类在 request 封装的 success 里检查 Content-Type 并做兜底处理。5.2 章节缓存与 TTL 策略wx.setStorageSync 单个 key 上限 1MB总容量 10MB。阅读器场景最占空间的是章节正文每章都缓存的话读几十章就把额度耗尽。合理的策略是只缓存最近阅读的章节每本书的缓存 key 附带时间戳过期即清。const CACHE_PREFIX chapter_ const CACHE_TTL 7 * 24 * 60 * 60 * 1000 function getChapterCache(bookId, chapterId) { const key CACHE_PREFIX bookId _ chapterId const data wx.getStorageSync(key) if (!data) return null if (Date.now() - data.timestamp CACHE_TTL) { wx.removeStorageSync(key) return null } return data.content } function setChapterCache(bookId, chapterId, content) { const key CACHE_PREFIX bookId _ chapterId wx.setStorageSync(key, { content, timestamp: Date.now() }) }TTL 设为 7 天超过直接移除避免缓存无限膨胀。内容页每次进入先 getChapterCache命中就不发请求用户翻前一章时体验明显更好。logs 页面里也可以增加一个清理入口展示当前 Storage 占用情况这个值可以通过 wx.getStorageInfoSync 拿到。5.3 发布前对照检查提交审核前逐条过一遍检查项检查项操作方式常见问题接口域名小程序后台配置 request 合法域名漏配导致真机请求失败隐私协议app.json 配置 privacy 相关声明审核退回缺少用户隐私保护指引缓存占用页面加载时清理过期缓存存储超限使 setStorageSync 静默失败导航栏标题每个页面 json 单独确认从列表跳到详情后标题未更新README 说明补充 mock 数据启动方式和接口文档位置接手项目的人无法本地跑通README.md 里至少写清三件事接口服务怎么启动、mock 数据放哪个目录、打包时 assets 里的封面图是否需要替换。最后确认 onUnload 时是否把当前章节和 scrollTop 落盘避免用户直接手势返回导致进度丢失。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询