
简介《uniapp从入门到精通》是一份面向 uni-app 初学者的系统学习资料适合希望掌握 Vue.js 跨平台开发、快速构建小程序与 App 的开发者。内容由环境搭建入手逐步讲解 HBuilderX 与微信开发者工具的使用、项目目录结构、开发规范、全局配置文件 pages.json 中的 globalStyle、页面路由、tabBar 与条件编译以及 text、icon 等常用组件随后深入页面样式与布局涵盖尺寸单位、样式导入、选择器、全局与局部样式、sass 用法并系统复习 Vue 基本语法与生命周期。在此基础上延伸至下拉刷新、上拉加载、网络请求及二次封装、数据缓存、图片上传和预览等 16 个主题覆盖从基础到进阶的完整知识链路。资源为 1 个 docx 文档压缩包约 5.98MB文本形式便于按章节检索、复制代码示例或做学习笔记。已有 573 人浏览学习内容中融入作者实际操作中的常见问题与排查思路如微信开发者工具服务端口设置目录结构清晰适合边看边练快速建立 uni-app 整体知识框架。1. uniapp 的“一套代码”到底省在哪先看清跨端开发的真实成本如果你同时被小程序、H5、App 三端需求压着大概率听过这句话用 uniapp 写一套三端都有。这句话对但只对一半。真正跑过 uniapp 项目的人都知道一套代码省的是业务层重复开发不是把三端差异也抹平。导航栏、存储、支付、路由这些按平台分开处理的细节才是决定项目能不能顺利上线的那另一半。下面按一个真实落地方案的顺序往下走从脚手架怎么选、页面和状态怎么管到条件编译怎么写才不乱最后把我在真机和审核阶段反复踩过的坑一条条讲清楚。适合已经会 Vue 基础、正准备用 uniapp 接第一个跨端项目的开发者也适合正在排查线上问题的半熟手。2. 本地跑通 uniapp 最小项目从 CLI 脚手架到三端预览2.1 为什么我选 CLI 创建而不是图形界面工具很多教程上来就是打开图形界面工具点按钮新建项目。这条路对单人开发最省事编译器内置、真机预览一键到位但一旦项目进入多人协作问题就来了图形界面创建的项目本质上是编辑器替你管理编译环境换个电脑、接进 CI、和现有 git flow 融合时环境差异会成为第一个隐形炸弹。我一般建议用 CLI 方式创建项目核心原因是它能得到一个干净的 Node 工程依赖、脚本、构建产物全部落在 package.json 里团队成员克隆下来 npm install 就能跑和前端团队现有的代码规范、eslint、git 钩子可以无缝衔接。选型时还要定 Vue 版本。uniapp 同时支持 Vue2 和 Vue3Vue2 走 webpack 构建生态成熟但维护成本逐年上升Vue3 走 Vite 构建启动快、组合式 API 写起来舒服而且现在官方主推的也是这条线。新项目我无脑选 Vue3除非团队里有人对 Vue2 特别熟且项目必须兼容一套老组件库。Vue3 Vite 的代价是部分老插件需要换替代方案但对比跨端开发本身的复杂度这个代价值得付。2.2 创建项目的最小命令与 src 目录结构拆解用 CLI 拉模板不需要图形界面核心就三条命令# 拉取官方 vue3 vite 预设模板my-uniapp-app 是项目名 npx degit dcloudio/uni-preset-vue#vite my-uniapp-app cd my-uniapp-app npm install这里 degit 的作用是拉取模板但不携带 git 历史比直接 git clone 更干净。项目名注意用小写字母和连字符不要带中文和大写npm 包名规范会限制这一点。npm install 如果卡在网络可以临时切 registry但根因通常是依赖数量多耐心等完即可。依赖装完后核心代码都在 src 目录下它和普通 Vue 项目最大的区别是多了两个文件src/ pages.json # 路由、窗口样式、tabBar 配置三端统一读它 manifest.json # AppID、各平台权限、SDK 配置 App.vue # 应用入口组件onLaunch 在这里 main.js # 入口导出 createApp 工厂函数pages.json 是 uniapp 项目的地基后面会单独讲。manifest.json 里最常改的是 AppID、小程序 AppID 和各平台模块权限这些配置忘掉的后果在第五章会看到。2.3 跑进小程序模拟器和浏览器两条必会命令项目装完先别急着写代码把最小链路跑通再说。两条命令是日常开发主力# 运行到微信小程序产物输出到 dist/dev/mp-weixin npm run dev:mp-weixin # 运行到 H5默认起本地服务端口 5173 npm run dev:h5跑小程序那步产物会输出到 dist/dev/mp-weixin你需要打开微信开发者工具选择“导入项目”并指向这个目录。首次导入时开发者工具会要求填 AppID没有正式 AppID 可以选测试号并不影响本地开发。注意微信开发者工具里要开启“服务端口”否则 CLI 的构建结果不会自动被识别。跑 H5 那条命令更直接浏览器打开终端提示的地址就能看到。这里有一个容易误解的点uniapp 的 H5 端并不是把小程序代码翻译成网页而是用同一套 Vue 页面编译成标准 Web 应用所以浏览器里能看到完整页面。两条命令都跑通之后你手里就同时有了小程序和 H5 两套可调试产物后面所有代码改动都会实时编译过去这就是跨端开发第一个直观体感。3. 页面、路由与状态管理搭好单页骨架再去碰多端差异3.1 pages.json 不只是路由表窗口、tabBar 与首屏都在这用 uniapp 的人千万不要去找 vue-router它就是不用 vue-router。页面路由、窗口样式、导航栏、tabBar 全部集中在 pages.json 一个文件里。这个设计初看别扭实际用起来反而省心三端共用一个配置不需要为小程序维护一套路由、H5 再维护一套路由。{ pages: [ { path: pages/index/index, style: { navigationBarTitleText: 首页, navigationBarBackgroundColor: #ffffff } }, { path: pages/detail/detail, style: { navigationBarTitleText: 详情 } } ], tabBar: { list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/detail/detail, text: 详情 } ] } }pages 数组里的第一个页面就是启动页这决定了 App 冷启动后看到的第一个界面常见误用是页面顺序写得随意导致打开 App 先进了一个不该进的内页。tabBar 的 list 每一项的 pagePath 必须在 pages 数组里存在漏配会直接编译报错。这里还有一个隐藏规则tabBar 页面之间跳转要用 uni.switchTab不能用 uni.navigateTo后者在微信小程序端会因为找不到页面而静默失败很折磨人。style 里 navigationBarTitleText 是导航栏标题在 H5 端映射成浏览器标签页标题在小程序端是原生导航栏文字在 App 端则要看是否启用了自定义导航栏。如果你要做一个沉浸式头部就全局关闭原生导航栏这套配置三端生效但坑在于不同端的关闭方式不完全一样这个后面单独展开。3.2 页面生命周期和组件生命周期的触发顺序跨端项目最常见的运行问题是把页面生命周期和小程序自定义组件的生命周期混为一谈。uniapp 的页面不是普通 Vue 组件它额外提供了一套生命周期onLoad、onShow、onReady、onHide、onUnload。这些钩子的触发顺序决定了你该在哪一步初始化数据。export default { onLoad(options) { // 页面创建时触发只触发一次options 携带上一个页面传来的参数 console.log(页面加载, options) }, onShow() { // 每次页面切换到前台都触发包括从后台返回 console.log(页面显示) }, onReady() { // 页面首次渲染完成适合操作 canvas 或获取 dom 信息 console.log(页面就绪) } }onLoad 只会在页面实例创建时走一次适合放初始化请求onShow 每次进入前台都会走适合做需要实时刷新的逻辑比如从详情页返回列表页后刷新当前筛选条件。常见翻车是开发者把请求全放在 onLoad 里结果从二级页面返回时列表数据不更新或者把请求放在 onShow 里导致每次切换都重复拉取。这里没有银弹正确做法是首次渲染的数据放 onLoad依赖页面焦点变化的数据放 onShow。另一个容易踩的点是组件生命周期。页面里的子组件走的是标准 Vue 生命周期 created、mounted它们和页面的 onLoad 是平行关系不是嵌套关系。实测下来页面 onLoad 触发时子组件 created 可能还没执行完所以在 onLoad 里拿 this.$refs 经常拿到 undefined。要操作子组件方法放在 onReady 里更稳。3.3 vue3 项目统一用 Piniamain.js 的导出方式先改对Vue3 项目状态管理直接用 PiniaVue2 才继续用 Vuex。Pinia 对 TypeScript 支持和组合式 API 的融合度更好而且 uniapp 官方在 Vue3 这条线上已经适配了 Pinia不必担心跨端兼容性问题。接入方式和普通 Vue 项目基本一致唯一的区别是 main.js 的导出结构。// src/main.js import { createSSRApp } from vue import { createPinia } from pinia import App from ./App.vue export function createApp() { const app createSSRApp(App) app.use(createPinia()) return { app } }注意这里的 createApp 导出的是工厂函数不是直接挂载这是 uniapp 的 Vue3 模板特有问题每个端在启动时都会调用一次 createApp 来生成实例。如果你按普通 Vue 项目习惯写成const app createApp(App); app.use(createPinia()); app.mount(#app)在小程序端会直接挂载失败或导致状态不共享。初次迁移的人在这个文件上卡住的不在少数但照着上面这个结构抄就不会有事。状态定义本身和 Web 端完全相同页面里通过useCounterStore()取实例、调用 action 更新状态。我实际用下来的体感是跨页面同步的数据比如用户登录态、全局筛选条件、购物车放 Pinia 没问题一次性页面间跳转传参别放 Pinia直接用 onLoad 的参数通道更直观否则会积累大量“只被一个页面用过但挂在全局”的无效状态项目越到后期越难维护。4. 多端差异处理条件编译与平台 API 封装的正确姿势4.1 条件编译是注释不是 if平台分支代码的写法边界uniapp 跨端最核心的机制是条件编译。它的写法看起来像注释但编译阶段会被真实解析符合平台条件的代码进入产物不符合的直接丢弃。这个机制和运行时判断有本质区别很多人初学时不理解把条件编译写成普通 if 判断导致所有平台的代码都被打进包里体积变大不说还会出现在 A 端引用 B 端专属 API 导致的运行时报错。// #ifdef MP-WEIXIN // 只有微信小程序会编译进这行代码 uni.showShareMenu({ withShareTicket: true }) // #endif // #ifdef H5 // 只有 H5 端会编译进这行代码 console.log(当前是浏览器环境) // #endif // #ifndef MP-WEIXIN // 非微信小程序平台都会编译进这行代码 console.log(不是微信小程序) // #endif平台标识里MP-WEIXIN 是微信小程序MP-ALIPAY 是支付宝小程序H5 是浏览器端APP-PLUS 是 App 端。实际开发中分享、支付、登录这三个功能几乎每个平台都有差异条件编译是唯一能让代码保持干净的写法。注意这个语法在 JS、CSS、template 模板里都能用CSS 里用注释包裹/* #ifdef H5 */ ... /* #endif */。条件编译还有一个容易被忽略的用法用#ifndef做反向编译即“除了某个平台都执行”。这个在写统一降级逻辑时很好用比如支付在 App 端走原生 SDK其他端统一走 H5 跳转。写条件编译的核心纪律是分支不要嵌套太深能拆成独立函数就拆不要在一个函数里铺满三四个平台分支否则后期同事改起来根本不知道哪个分支是有效的。4.2 跨端 API 差异清单与统一封装从 storage 到支付登录uniapp 提供了很多 uni.* 前缀的跨端 API但这个“跨端”是相对的API 名称统一不代表行为统一。以 storage 为例uni.setStorageSync 在三端都有但 App 端这个同步方法底层是异步实现调用后立刻 getStorageSync 可能拿不到刚写的值这个细节放到第五章展开。支付和登录则是另一个极端API 长得完全不一样不封装就会把业务代码搅成一锅粥。我常用的方式是做一个轻量封装层只在差异明显的地方包一层// src/utils/platform.js export function login() { // #ifdef MP-WEIXIN return new Promise((resolve, reject) { uni.login({ provider: weixin, success: (res) resolve(res.code), fail: reject }) }) // #endif // #ifdef APP-PLUS return new Promise((resolve, reject) { uni.login({ provider: apple, success: (res) resolve(res.code), fail: reject }) }) // #endif // #ifdef H5 return Promise.reject(new Error(H5 端请走扫码登录)) // #endif }这个封装解决的痛点很具体业务层永远只调 login()不需要关心自己在哪个端。注意里面用到的是 Promise 包装因为不同平台登录成功后的回调结构不统一统一转成 Promise 后调用方可以用同一套 async/await 处理。类似的还有支付App 端的支付需要先向服务端下单拿订单编号再调 uni.requestPayment小程序端的流程类似但参数不同。把这些逻辑收进一个 createPayment(order) 函数里切换平台时才不会改得满屏都是。还有一个容易忽略的差异是 URL 跳转。H5 端可以用 window.location小程序端没有这个概念需要用 uni.navigateTo 或 web-view 组件。如果项目里有落地页跳转外链的需求这一块必须要做平台分化否则 H5 能跑的代码在小程序端压崩溃。4.3 自定义组件样式隔离H5 与小程序的两套逻辑组件化在 uniapp 里逃不掉但样式隔离机制在三端表现不一样这是页面“本地正常、真机错乱”的高发区。H5 端天然有 CSS 作用域Vue 的 scoped 属性就能把样式锁在组件内小程序端则默认开启样式隔离父组件的样式传不进子组件。template view classcard view classcard__title标题/view /view /template style scoped .card { border-radius: 8px; } /* H5 端穿透子组件内部样式小程序端写法略有差异 */ :deep(.card__title) { font-size: 28rpx; } /style这里的关键词是 :deep()它能把 scoped 样式穿透到子组件内部。H5 端这个写法完全兼容小程序端在较新版本的基础库上也支持但如果你遇到穿透无效就要在小程序端手动处理组件样式隔离给子组件增加options: { styleIsolation: apply-shared }。这是小程序原生 API 提供的样式共享配置允许页面样式渗入组件内部。实践里我一般遵循三条纪律第一样式尽量写在组件自己身上不要依赖父组件穿透这是最省心的第二全局样式字体、主题色放 App.vue 里的公共样式表并显式确认各端都能读到第三当遇到某个组件在小程序端样式怎么都不对时先检查是不是样式隔离机制在作怪再去怀疑 css 写法排查顺序反了会浪费大量时间。5. uniapp 避坑实录五个反复出现的翻车场景跨端开发的特点就是每个端单独跑都正常一合起来就出问题。下面五条是项目里反复出现的典型翻车每一条都按现象、原因、解决三步拆开照做能省不少排查时间。5.1 跳转参数变成 undefined 字符串onLoad 解析要防一手现象列表页跳详情页onLoad 里打印 options.id 是 undefined但跳转代码里明明传了 id。原因页面路径如果是pages/detail/detail?id123name测试在小程序端参数会原样传递但如果某个参数值本身含中文或特殊字符部分端不会自动解码导致取值错位。解决跳转前对参数做 encodeURIComponent接收后用 decodeURIComponent 还原。// 跳转前 uni.navigateTo({ url: /pages/detail/detail?id${encodeURIComponent(id)} }) // 接收处 onLoad(options) { const id decodeURIComponent(options.id || ) }这个坑在 H5 端通常不明显因为浏览器会自动处理编码但小程序端不总这么智能。另一个隐藏点是onLoad 的 options 里所有值都是字符串即使你传的是数字 123拿到手也是字符串 123直接拿去做布尔判断会踩到 “0 为真” 的经典坑。养成习惯onLoad 收到的参数一律先转类型再使用。5.2 弹层出现后页面跟着滚动滚动穿透的三种解法现象页面弹出底部弹层或筛选面板手指在弹层上滑动底下的页面也跟着滚。原因小程序端的 web-view 和页面滚动是独立体系弹层没有阻止事件冒泡滚动事件穿透到了 page 层。解决按平台分三套写法。!-- 弹层最外层小程序和 H5 通用拦截 -- view classmask touchmove.stop.prevent !-- 弹层内容 -- /view第一小程序端在弹层根节点加touchmove.stop.prevent多数场景能直接解决。第二处理不了时在页面级引入page-meta组件设置disable-scroll动态控制页面滚动。第三H5 端可以直接给 body 加overflow: hidden类名小程序端没有 document.body所以这条路不通。我的建议是优先用 stop.prevent少动页面级滚动开关因为 disable-scroll 在部分安卓机上有概率导致弹层关闭后页面滚动位置丢失。5.3 App 端 setStorageSync 后立刻读不到同步 API 的异步实现现象App 端登录成功后 uni.setStorageSync 写入 token紧接着 uni.getStorageSync 读取返回 null但过 100ms 再读又能拿到。原因uni 官方文档里明确写过App 端的同步存储 API 底层实现是异步的setStorageSync 只是把数据交给系统不保证写入落盘完成。解决App 端敏感数据写入改用异步版 setStorage在回调里再处理后续逻辑。// App 端这个写法更稳妥 uni.setStorage({ key: token, data: token, success() { // 写入完成的回调里再跳转或读值 } })这里最坑的是同样的代码在微信小程序端和 H5 端完全正常一跑到 App 真机就偶发复现很容易怀疑是时序问题。正规做法是把存储封装成统一函数在 App 端走异步、其他端走同步业务层不感知差异。同时登录态非要立刻同步校验的话可以把 token 同时放在内存变量里不依赖 storage 的即时一致性。5.4 vue3 的 setup 里 onLoad 未定义组合式 API 导入路径现象用 Vue3 组合式 API 写页面import { onLoad } from vue编译报错 onLoad is not defined。原因uniapp 的页面生命周期不是 Vue 自带的它由 dcloudio/uni-app 提供从 vue 里根本导不出来。解决换导入来源。import { onLoad, onShow } from dcloudio/uni-app onLoad((options) { // 这里能拿到跳转参数 })这个 API 在 uniapp 的 Vue3 项目里是组合式 API 风格的生命周期钩子用法和 Vue 的 onMounted 一致传入回调即可。注意它的回调接收的参数和选项式写法一样onLoad 接收 options、onShow 不接收。另外别把它和 Vue 的 onMounted 混用页面级别的数据初始化用 onLoad组件内部用 onMounted两者触发时机完全不同。5.5 真机白屏但模拟器正常manifest 权限与合法域名现象在开发者工具里预览一切正常一上真机就白屏或所有请求失败。原因真机环境比模拟器严格得多。微信小程序真机要求所有请求域名在公众平台配置合法域名且必须是 HTTPSApp 端则要求 manifest.json 里勾选对应的模块权限比如定位、支付漏勾了模块 API 调用直接无反应。解决两个地方逐一核对。第一小程序后台的“开发管理-服务器域名”加好 request 合法域名开发模式下可在开发者工具里临时关闭域名校验但真机预览不受这个开关控制。第二App 端打开 manifest.json在 App 模块配置里勾选实际用到的原生插件再重新打包基座只改代码不重打基座权限配置不会生效。第三如果是 H5 端真机预览白屏多半是局域网 ip 访问被浏览器拦了或者是部署路径的 base 配置不对。白屏问题我排查时固定顺序是先看控制台有没有请求失败再看 manifest 权限最后看是不是代码在某个端编译报错这个顺序能过滤掉八成问题。6. 发布前的最后一个习惯三端核查清单跑一遍项目走到发版这一步功能代码通常已经不是主要风险配置遗漏才是。我现在的习惯是每次发版前花十分钟过一遍下面这张清单能拦住大部分发版后才发现的问题。检查项位置常见错误小程序 AppID 与 request 合法域名manifest.json / 平台后台只配了开发域名线上请求全被拦截H5 路由基路径 router.basemanifest.json - h5部署到子目录没配 base刷新后 404App 端模块权限manifest.json - App 模块配置漏勾定位/支付模块真机调用无响应隐私协议与用户隐私保护指引平台后台收集用户信息未声明审核被打回主包体积与分包策略微信开发者工具主包超 2MB 无法上传这套清单的验证方法不复杂H5 端发布到测试环境后用浏览器无痕模式完整走一遍注册登录流程小程序端用真机预览模式跑同样的流程重点看请求是否带上了 token、支付回调是否正常App 端打自定义基座把每个涉及原生能力的页面点一遍。三端跑完同一套业务冒烟用例比看多少文档都管用。冒烟用例不需要多覆盖登录、首页加载、详情跳转、支付、个人中心这五条主链路就够了每条链路在三个端各走一遍总共十五次操作基本能把 80% 的发版问题拦下来。我自己的血泪教训是有次急着发版只测了小程序端结果 H5 端因为 router.base 没配线上所有页面刷新后直接 404用户反馈才意识到。从那以后发版前跑一遍三端清单就成了固定动作虽然每次要多花十分钟但比起线上事故后的紧急修复这十分钟实在太便宜。希望这份笔记能帮你在 uniapp 这条路上少踩几个坑。本文还有配套的精品资源点击获取