uni-app微信小程序实战:从零搭建树洞笔记本
2026/9/15 20:39:49 网站建设 项目流程

简介:这是一套面向零基础初学者的uniapp微信小程序实战教程资源,以「树洞笔记本」完整项目为载体,系统覆盖页面路由、组件开发、样式控制、响应式数据管理、API接口调用及微信特有功能(如登录授权、分享)等核心开发环节,助力新手快速掌握跨端小程序开发能力。资源包为ZIP格式,共55个文件,包含12个JSON配置文件(如pages.json、manifest.json)、9个JS逻辑脚本、5个Vue单文件组件、6个WXSS样式文件、8个PNG图标资源及1个PHP后端接口脚本,整体仅228KB,轻量易解压,结构清晰便于逐模块研读源码。已有662人学习下载,配套项目目录完整呈现uniapp标准工程结构(含App.vue、main.js、unpackage构建输出等),所有代码均可直接运行调试,并支持基于真实业务场景(笔记增删查改)进行二次开发与功能拓展。

1. 一个能跑通的树洞笔记本,比所有概念讲解都管用

你打开 HBuilderX,双击mynote文件夹里的manifest.json,点“运行到小程序模拟器”,几秒后微信开发者工具里弹出一个带绿色导航栏的笔记列表——这不是 demo,是真实可编辑、可登录、可增删的《树洞笔记本》最小可行版本。它不依赖云开发、不调用第三方 SDK、连wx.login都没封装成 hook,所有逻辑压在uni.request+mysql.txt建表语句 +api.php三件套里。对零基础者来说,这意味着:不用先搞懂 token 刷新机制,就能在pages/edit.vue里改完标题点保存,立刻看到数据库里多了一条INSERT INTO notes (title, content, created_at) VALUES (...);也不用纠结uni-app和原生小程序的生命周期映射关系,因为整个项目只用onLoadonShow两个钩子,其余全靠 Vue 响应式驱动。适合两类人:想用两周做出第一个能上线的小程序的转行者,以及需要快速验证 uniapp 多端能力边界的技术选型者——它不炫技,但每个文件都在回答“这个功能到底要写几行代码”。


2. 页面路由与pages.json的硬核配置逻辑

2.1pages.json不是静态配置表,而是运行时路由编译器

pages.jsonmynote项目中承担了三重角色:页面注册表、 tabBar 渲染指令集、页面级样式注入源。它不是 JSON Schema 校验通过就完事,而是在 HBuilderX 编译阶段被解析为uni-app运行时的路由映射对象。例如pages.json中这段配置:

{ "tabBar": { "color": "#7A7E83", "selectedColor": "#007AFF", "borderStyle": "black", "list": [ { "pagePath": "pages/index/index", "text": "首页", "iconPath": "static/nav1.png", "selectedIconPath": "static/nav1-a.png" }, { "pagePath": "pages/list/list", "text": "笔记", "iconPath": "static/nav2.png", "selectedIconPath": "static/nav2-a.png" } ] }, "subNVue": [], "usingComponents": true }

注意pagePath必须严格匹配pages/目录下的实际路径,且不能带.vue后缀;iconPath路径是相对于项目根目录的,不是相对于pages.json文件位置。若nav1-a.png实际放在static/icons/下却写成"static/nav1-a.png",HBuilderX 不报错但图标显示为空白——这是新手最常卡住的点。

2.1.1 tabBar 图标尺寸与状态切换的底层约束

微信小程序要求 tabBar 图标必须是 81px × 81px 的 PNG,且iconPathselectedIconPath必须同尺寸。mynote提供的nav1.pngnav1-a.png正好满足该约束。但更关键的是:tabBar的激活态切换由微信客户端原生控制,uni-app层无法拦截onTabItemTap事件(除非用uni.hideTabBar()手动隐藏再自定义)。因此pages/index/index.vue中的onLoad逻辑仅用于初始化首页数据,真正的 tab 切换响应必须写在pages/list/list.vueonShow钩子里——因为onShow在 tab 切换时触发,而onLoad只在首次进入时触发。

2.1.2 页面跳转参数传递的两种安全模式

mynoteindex.vue跳转到edit.vue使用的是uni.navigateTo携带 query 参数:

// pages/index/index.vue uni.navigateTo({ url: '/pages/edit/edit?id=' + item.id })

这种写法在edit.vueonLoad中接收:

// pages/edit/edit.vue onLoad(option) { this.noteId = option.id || '' }

但需警惕:option.id是字符串类型,若后端返回的是数字 ID,此处必须手动parseInt(option.id)。另一种更健壮的方式是使用uni.setStorageSync+uni.getStorageSync在跳转前存临时状态:

// index.vue uni.setStorageSync('editNoteId', item.id) uni.navigateTo({ url: '/pages/edit/edit' })
// edit.vue onLoad() { this.noteId = uni.getStorageSync('editNoteId') || '' uni.removeStorageSync('editNoteId') // 清理避免污染 }

提示uni.navigateTo的 URL 长度限制为 1024 字符,若需传递长文本或复杂对象,必须走 storage 中转,否则在 iOS 微信中会静默失败。

2.2pages.jsonApp.vue的样式协同机制

App.vue中的<style>块定义全局样式,但pages.json中的style字段可覆盖特定页面的导航栏和下拉刷新行为。例如pages.json中未显式声明navigationStyle: 'custom',则index.vue顶部会默认显示微信原生导航栏;若改为:

{ "pages": [ { "path": "pages/index/index", "style": { "navigationStyle": "custom", "backgroundColor": "#f8f8f8" } } ] }

index.vue需自行实现<view class="custom-nav">...</view>,且必须用uni.getSystemInfoSync().statusBarHeight动态计算状态栏高度。mynote项目选择保留默认导航栏,是因为其index.vue顶部无复杂交互,避免引入safe-area-inset-top兼容性问题。


3. 数据流闭环:从uni.request到 MySQL 的完整链路

3.1api.php接口设计的最小化原则

mynote的后端接口全部集中在单个api.php文件中,通过$_GET['action']分发请求。这种设计牺牲了 RESTful 规范性,但极大降低了初学者理解成本。核心逻辑如下:

<?php header('Content-Type: application/json; charset=utf-8'); $action = $_GET['action'] ?? ''; $mysqli = new mysqli('localhost', 'root', '', 'mynote'); switch($action) { case 'getNotes': $result = $mysqli->query("SELECT * FROM notes ORDER BY created_at DESC"); echo json_encode(['code'=>0, 'data'=>$result->fetch_all(MYSQLI_ASSOC)]); break; case 'saveNote': $title = $_POST['title'] ?? ''; $content = $_POST['content'] ?? ''; $id = $_POST['id'] ?? 0; if ($id) { $mysqli->query("UPDATE notes SET title='$title', content='$content' WHERE id=$id"); } else { $mysqli->query("INSERT INTO notes (title, content, created_at) VALUES ('$title', '$content', NOW())"); } echo json_encode(['code'=>0, 'msg'=>'success']); break; default: echo json_encode(['code'=>1, 'msg'=>'invalid action']); } ?>

注意:此代码存在 SQL 注入风险,仅用于教学演示。生产环境必须使用mysqli->prepare()绑定参数,如$stmt = $mysqli->prepare("INSERT INTO notes (title, content) VALUES (?, ?)"); $stmt->bind_param("ss", $title, $content);

3.1.1uni.request的超时与错误捕获标准写法

前端调用api.php时,mynotemain.js中封装了基础请求方法:

// main.js const request = (url, data = {}, method = 'GET') => { return new Promise((resolve, reject) => { uni.request({ url: 'http://localhost/api.php?' + url, data: method === 'GET' ? {} : data, method: method, timeout: 10000, success: (res) => { if (res.data.code === 0) { resolve(res.data) } else { reject(new Error(res.data.msg || '请求失败')) } }, fail: (err) => { reject(new Error('网络错误:' + err.errMsg)) } }) }) } export { request }

关键点在于:

  • timeout设为 10000ms(而非默认 6000ms),避免本地测试时因 MySQL 连接慢导致请求中断;
  • fail回调必须处理errMsg,常见值如request:fail net::ERR_CONNECTION_REFUSED表示后端服务未启动;
  • success中二次校验res.data.code,防止后端返回非 JSON 内容(如 PHP 错误信息)导致前端解析崩溃。
3.1.2mysql.txt建表语句的字段设计意图

mysql.txt中的建表语句:

CREATE TABLE `notes` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL DEFAULT '', `content` text NOT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

字段设计直指业务本质:

  • title限定 100 字符,对应微信小程序输入框的maxlength="100"约束,避免前端截断与后端存储不一致;
  • contenttext类型而非varchar(5000),因笔记内容长度不可预估,且text在 MySQL 中支持全文索引;
  • updated_atON UPDATE CURRENT_TIMESTAMP确保每次UPDATE自动更新时间,省去前端传参;
  • 引擎指定InnoDB而非MyISAM,因前者支持事务和外键(后续扩展用户表时必需)。

3.2 页面级数据管理:datamethods的精准耦合

pages/list/list.vue中的数据流体现uni-app的 Vue 2 响应式本质:

<template> <view class="container"> <view v-for="item in notes" :key="item.id" @tap="goToEdit(item)"> <view class="note-item"> <text class="title">{{ item.title }}</text> <text class="content">{{ item.content.substring(0, 30) }}...</text> </view> </view> </view> </template> <script> import { request } from '@/utils/request.js' export default { data() { return { notes: [] } }, methods: { async loadNotes() { try { const res = await request('action=getNotes') this.notes = res.data } catch (err) { uni.showToast({ title: err.message, icon: 'none' }) } }, goToEdit(note) { uni.navigateTo({ url: `/pages/edit/edit?id=${note.id}` }) } }, onShow() { this.loadNotes() } } </script>

这里的关键实践是:

  • notes数组直接绑定v-for,无需this.$setObject.assign,因uni-app的 Vue 2 版本已代理数组变异方法;
  • loadNotes使用async/await而非then/catch,提升可读性,且try/catch能捕获await中的任何异常;
  • onShow而非onLoad触发加载,确保每次切换到该 tab 都刷新数据,符合小程序 tab 页生命周期。

4. HBuilderX 工程配置与launch.json的调试真相

4.1launch.json不是 IDE 配置,而是运行时环境描述符

mynote项目根目录下的launch.json并非 VS Code 的调试配置,而是 HBuilderX 识别项目类型、启动参数和平台目标的核心文件。其内容:

{ "name": "mynote", "platform": "mp-weixin", "version": "3.0.0", "env": { "NODE_ENV": "development" }, "build": { "minify": false, "sourceMap": true } }

其中platform: "mp-weixin"是决定性字段——它告诉 HBuilderX:编译目标为微信小程序,生成unpackage/dist/build/mp-weixin目录,并自动注入wx全局对象。若误写为"h5",则uni.request会调用XMLHttpRequest而非wx.request,导致在微信开发者工具中报错wx is not defined

4.1.1manifest.jsonappid的双重作用

manifest.jsonnameappid字段影响两个层面:

  • name决定小程序在微信开发者工具中的项目名称,也作为uni-app编译时的包名前缀;
  • appid在开发阶段可为空(微信开发者工具允许使用测试号),但一旦填写,HBuilderX 会在编译时校验该appid是否已在微信开放平台注册,未注册则编译失败并提示appid 未备案mynote默认留空,正是为降低入门门槛。
4.1.2uni.scss的变量注入机制

uni.scssuni-app的全局样式入口,其内容:

$uni-primary-color: #007AFF; $uni-border-color: #e5e5e5; $uni-text-color: #333; @import './common/uni.css';

这些变量会被uni-app编译器提取,并注入到所有.vue文件的<style scoped>中。例如index.vue中的.btn { background-color: $uni-primary-color; }能直接生效,无需@import。但需注意:$uni-primary-color仅在scss语法下可用,若用cssbackground-color: var(--uni-primary-color)则无效——uni-app的 CSS 变量注入需配合@supports检测,mynote未启用该特性。

4.2unpackage/dist目录结构的逆向工程价值

unpackage/dist是 HBuilderX 编译后的产物目录,mynotemp-weixin子目录结构揭示了uni-app的小程序适配原理:

unpackage/dist/build/mp-weixin/ ├── app.js // 入口 JS,包含 App 构造器和路由注册 ├── app.json // 微信小程序原生配置,由 pages.json 编译生成 ├── project.config.json // 微信开发者工具配置,含 appid、projectname ├── pages/ │ ├── index/ │ │ ├── index.js // 对应 index.vue 编译后的逻辑 │ │ ├── index.wxml // 模板编译结果 │ │ └── index.wxss // 样式编译结果 │ └── list/ │ ├── list.js │ ├── list.wxml │ └── list.wxss └── static/ // 原样复制的资源文件

提示:若修改pages/index/index.vueunpackage/dist/build/mp-weixin/pages/index/index.js未更新,说明 HBuilderX 的热编译未触发,需手动点击菜单栏「运行」→「重新编译」,或检查launch.jsonplatform是否正确。


5. 实战排错:五个高频崩溃点与现场修复指令

5.1 “空白页”问题的三层诊断法

pages/index/index.vue在微信开发者工具中显示空白,按以下顺序排查:

5.1.1 检查pages.json页面注册路径

执行命令查看编译日志:

# 在 HBuilderX 终端中(项目根目录) grep -r "pages/index/index" unpackage/dist/build/mp-weixin/app.json

若无输出,说明pages.json中路径错误或未保存。正确路径应为"pages/index/index",而非"pages/index""pages/index/index.vue"

5.1.2 验证App.vuerender函数是否执行

App.vueonLaunch中添加调试语句:

onLaunch() { console.log('[App] onLaunch triggered') uni.showToast({ title: 'App 启动', icon: 'none' }) }

若微信开发者工具控制台无日志,且无 toast 提示,说明App.vue未被加载——此时检查main.jscreateApp(App)是否被正确调用,以及App.vue是否有语法错误(如<template>标签未闭合)。

5.1.3 抓取uni.request网络请求

在微信开发者工具「Network」面板中筛选api.php,观察:

  • 若请求状态为failed,检查api.php是否可被浏览器直接访问(http://localhost/api.php?action=getNotes);
  • 若状态为200但响应体为空,检查api.php是否有header()调用前的输出(如echo或空格);
  • 若响应体为{"code":1,"msg":"invalid action"},说明action参数未正确传递,检查uni.requesturl拼接逻辑。

5.2nav1-a.png图标不显示的像素级修复

tabBar图标不显示的根源常是图像元数据问题。用命令行批量检查 PNG 尺寸:

# Linux/macOS identify -format "%w x %h %m %f\n" static/nav*.png # 输出示例:81 x 81 PNG static/nav1-a.png

若尺寸不符,用 ImageMagick 重置:

convert static/nav1.png -resize 81x81! -background white -gravity center -extent 81x81 static/nav1-fixed.png

注意-resize 81x81!中的!表示强制缩放到精确尺寸,忽略原始宽高比;-extent确保画布为正方形,避免微信客户端裁剪。

5.3uni.navigateTo跳转失败的 URL 编码验证

uni.navigateTo({ url: '/pages/edit/edit?id=你好' })失败,需对参数编码:

// 正确写法 const encodedId = encodeURIComponent('你好') uni.navigateTo({ url: `/pages/edit/edit?id=${encodedId}` })

edit.vue中解码:

onLoad(option) { this.noteId = decodeURIComponent(option.id || '') }

否则option.id会是乱码,MySQL 查询时WHERE id='ȸ'无匹配结果。

5.4mysql.txt导入失败的字符集强制指定

mysql.txt执行source mysql.txt报错ERROR 1064 (42000),通常因文件编码为 UTF-8 BOM。用 Vim 清除 BOM:

vim mysql.txt :set nobomb :wq

或用命令行:

sed -i '1s/^\xEF\xBB\xBF//' mysql.txt

5.5launch.json修改后不生效的强制刷新

HBuilderX 缓存launch.json配置,修改后需:

  1. 关闭 HBuilderX;
  2. 删除项目根目录下.hbuilderx隐藏文件夹;
  3. 重启 HBuilderX 并重新打开项目。
    否则platform字段变更不会触发重新编译。

6. 进阶技巧:用uni.preload实现页面间数据预加载

mynotelist.vue加载笔记列表后跳转到edit.vue,传统做法是onLoad中再请求单条数据,造成两次网络往返。uni-app提供uni.preloadAPI 实现预加载:

6.1 在list.vue中预加载笔记详情
// pages/list/list.vue goToEdit(note) { // 预加载数据到页面栈 uni.preload({ url: '/pages/edit/edit', data: { note: note } // 注意:data 必须是可序列化的 plain object }) uni.navigateTo({ url: '/pages/edit/edit' }) }
6.2 在edit.vue中获取预加载数据
// pages/edit/edit.vue onLoad(option) { // 优先取预加载数据, fallback 到 query 参数 const preloadData = uni.getPreloadData() if (preloadData && preloadData.note) { this.note = preloadData.note } else if (option.id) { this.loadNoteById(option.id) } }

优势uni.preload将数据存于内存而非 URL,规避了encodeURIComponent长度限制和编码开销;且getPreloadData()返回的是原始对象,无需JSON.parse。但需注意:预加载数据仅在当前页面栈生命周期内有效,页面销毁后自动清除。

6.3uni.preloaduni.setStorageSync的性能对比
方案传输方式最大容量生命周期适用场景
uni.preload内存引用~10MB页面栈存在期间同步传递复杂对象(如富文本编辑器状态)
uni.setStorageSync本地存储10MB持久化跨进程传递(如从 App 启动小程序)

mynote这类单用户笔记应用,uni.preload足够覆盖所有页面跳转场景,且避免了 storage I/O 开销。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询