
第一次用 uni-app 写页面的时候我按 Vue 单页那套思维一顿操作vue-router配置路由、页面用mounted拉数据、状态全靠ref一把梭。结果跑起来先是白屏后来报Page not found再后来发现alert在小程序端直接 undefined。折腾到大半夜我才明白uni-app 不是一个换皮 Vue而是一套有自己的路由栈、生命周期、样式单位和条件编译体系的跨端运行时。想从菜鸟往大佬走第一步不是急着写业务而是先把它的游戏规则摸清楚。这篇教程面向刚接触 uni-app、或者已经会写一点但总是莫名其妙踩坑的人。我会从 HBuilderX 的环境搭建和第一个项目跑通讲起逐步拆解目录结构、页面路由、生命周期、三端运行流程再重点说清楚 Vue 3 里的ref自动导入怎么配置——这是最近讨论度很高的一个点。整个过程基于我实际跑过的项目和踩过的坑你照着操作一遍基本就能把这个框架的骨架弄明白。1. HBuilderX 上手从安装到第一个项目跑通1.1 为什么推荐用 HBuilderX而不是 VSCodeuni-app 官方支持两种开发方式一种是 HBuilderX一种是 CLI 加 VSCode。很多老手喜欢 CLI因为可以接入自己的工程化体系但我建议新手先从 HBuilderX 入手。原因很现实HBuilderX 把创建项目、编译、运行到各端、打包 App这条链路全部集成好了你不需要自己折腾vue-cli或者vite的构建配置也不用为了跑一个小程序去单独配一堆插件。等你把 uni-app 的机制搞明白了再迁移到 CLI 工程难度会低很多。而且 HBuilderX 内置了对uni系列 API 的语法提示和条件编译支持这对新手友好度极高。1.2 安装与版本选择去官网下载 HBuilderX。它会区分标准版和App 开发版如果你只打算做小程序和 H5标准版够用但只要有一丁点想后面打包成安卓/iOS App 的念头直接下App 开发版省得以后再折腾升级。下载后解压到纯英文路径下避免出现奇怪的编码问题。安装完之后注册并登录账号。不要觉得这一步麻烦uni-app 的很多功能比如云打包、插件市场安装都依赖登录状态。1.3 创建项目的完整流程打开 HBuilderX点菜单栏的文件 - 新建 - 项目左侧选uni-app输入项目名称下面会弹出来项目模板。配置完成后点创建一个标准工程就生成了。这里有一个高频疑问项目创建后目录里文件不多怎么看效果答案是直接用快捷键运行。菜单栏运行里面有很多预览方式比如运行到浏览器运行到小程序模拟器运行到手机或模拟器。我习惯先把电脑上装好微信开发者工具再点运行到小程序模拟器这样能最快看到三端里最真实的表现——因为浏览器端的 H5 和小程序端在某些 API 上行为不一致。1.4 项目模板的差异与选择新建项目时你会看到几个模板默认模板、空模板、Hello uni-app 模板。它们的区别主要是预置内容不同。默认模板带了几个示例页面和基础组件适合刚接触时快速看到页面跳转、列表渲染、请求等常见写法的范例。空模板除了必要的配置文件外基本没有示例代码适合你已经熟悉框架、想从干净工程开始写的时候。Hello uni-app 模板一个类似组件展示柜的示例工程页面很多适合当API 字典翻。我一般自己新建业务项目都会选空模板因为示例代码有时候会干扰目录结构删起来也麻烦。但如果你是第一次接触还是建议先建一个默认模板跑起来把里面的示例页面逐个打开看看能帮你建立原来 uni-app 是这么组织页面的直觉。1.5 第一个项目跑通的验证标准运行成功之后你至少要确认这几件事首页能看到内容并且顶部导航栏标题和pages.json里配置的一致能通过点击事件跳转到下一页返回键也能正常工作控制台没有红色报错。如果这三条都通过说明你的开发环境、编译链路、基础路由都通了。接下来就可以安心往里面加东西了。注意HBuilderX 首次运行小程序项目时如果提示找不到微信开发者工具请先在开发者工具的设置 - 安全设置中开启服务端口。这是 HBuilderX 与微信开发者工具通信的前提也是新手卡住最多的地方之一。2. 目录结构和路由机制别把它当成普通 Vue 项目2.1 目录结构逐项拆解一个标准 uni-app 工程里有几个关键目录和文件我逐个说一下它们的实际用途pages存放页面文件的目录每个页面通常是一个.vue文件。注意 uni-app 的页面是扁平注册的不依赖vue-router那种嵌套路由关系。static放静态资源的目录比如图片、图标等。这个目录下的文件会原样打包不会经过编译处理。components自定义组件的推荐存放位置。uni-app 支持全局注册和局部注册官方更推荐局部引入按需加载。uni_modules从插件市场安装的插件放这里。它有自己的目录规范和components并列通常不需要手改。App.vue整个应用的根组件但不包含页面内容。你可以在这里写全局样式或者通过onLaunch做 App 启动时的初始化逻辑。main.js入口文件负责创建应用实例、安装插件等。manifest.json应用的配置文件包含应用名称、AppID、各平台的 SDK 配置等。pages.json这是 uni-app 最核心的配置文件之一。页面注册、导航栏样式、tabBar 配置全部在这里。unpackage编译输出目录。运行和打包生成的代码都放这里一般不需要手动改也可加入.gitignore。初学者最容易混淆的是pages.json和manifest.json的分工pages.json管的是页面和导航manifest.json管的是应用本身的基础信息和各端配置。记不住的时候就想页面相关的东西去pages.json应用相关的东西去manifest.json。2.2 pages.json 里的路由注册逻辑在 uni-app 中页面路由不是靠代码写的而是靠配置文件注册的。pages.json里的pages数组决定了哪些页面存在以及它们的路径。数组里的第一项就是启动页这个顺序很关键不要乱排。一个常见的页面配置长这样{ pages: [ { path: pages/index/index, style: { navigationBarTitleText: 首页, enablePullDownRefresh: true } }, { path: pages/detail/detail, style: { navigationBarTitleText: 详情页 } } ], globalStyle: { navigationBarTextStyle: black, navigationBarTitleText: uni-app, navigationBarBackgroundColor: #FFFFFF, backgroundColor: #F8F8F8 } }如果页面没有在pages数组里注册直接访问路径会报page not found这是我见过最高频的初学者报错之一。所以新增页面文件后第一件事就是确认它有没有被注册。HBuilderX 提供了右键新建页面的功能会自动帮你写入pages.json但如果是手动新建文件一定要自己补上注册。页面跳转也有自己的接口体系uni.navigateTo跳转新页面并保留当前页入栈uni.redirectTo关闭当前页并跳转替换uni.switchTab只能跳 tabBar 页面uni.navigateBack返回上一页。uni.navigateTo不能跳 tabBar 页面这是很多人第一次写 tab 场景时踩的坑——tabBar 页面必须用uni.switchTab。2.3 生命周期模型页面生命周期和 Vue 生命周期的关系uni-app 的页面有自己的生命周期常用的有onLoad、onShow、onReady、onHide、onUnload。它们和行为的关系是onLoad(options)页面首次加载时触发options里能拿到页面参数。onShow()页面每次显示时触发包括从后台切回来、从别的页面返回时。onReady()页面初次渲染完成可以开始操作渲染相关的内容。onHide()页面从显示变为隐藏时触发比如被新页面覆盖。onUnload()页面卸载时触发。同时你也能用 Vue 组件自己的生命周期onMounted、onBeforeUnmount等。但要注意时序页面级别生命周期先于组件级别生命周期比如onLoad在onMounted之前触发。页面初始化数据的逻辑建议写在onLoad里而不是onMounted这样在页面尚未渲染完成之前就能拿到数据结构上更符合 uni-app 的习惯。一个典型初始化场景是接收上一个页面传来的参数onLoad(options) { if (options.id) { this.initDetail(options.id) } }如果这个页面是从列表页跳进来的参数就是在uni.navigateTo的url里带上的uni.navigateTo({ url: /pages/detail/detail?id1001 })这个模式在所有端上都一致H5、小程序、App 都能跑这也是我觉得 uni-app 生命周期设计最省心的地方——你不需要为每个端单独写一套路由传参。2.4 manifest.json应用的身份证manifest.json里有几个字段很关键name是应用名称appid是 DCloud 分配的应用标识。如果你后面要打包 App需要在这里配置各平台的 SDK 和证书信息做小程序时也要在对应平台节点里填小程序的 AppID。有一个很容易忽略的细节运行到微信开发者工具时如果提示AppID 无效或未匹配可以先把manifest.json里微信小程序的 AppID 留空HBuilderX 会默认使用开发者工具的测试号。但要注意测试号不支持某些需要合法域名的能力比如真实的上传下载接口。所以正式开发时还是尽早换成自己的小程序 AppID。3. 三端联动从浏览器调试到真机预览的完整链路uni-app 的 slogan 是一套代码多端运行但运行这个动作本身是有多条链路的每条链路的调试方式也不一样。很多新手卡在代码写完了不知道怎么看效果或者浏览器上正常小程序上白屏就是因为不清楚三端运行的特点。3.1 运行到浏览器最快速的 H5 预览在 HBuilderX 菜单里选运行 - 运行到浏览器 - Chrome编译完成后会自动打开网页。这是开发中最快的反馈方式改完代码保存浏览器会自动刷新。但浏览器预览有一个局限它模拟的是 H5 端环境不代表小程序端行为。比如某些 uni API 在小程序端有平台限制在 H5 端却表现正常。所以浏览器预览适合看布局和交互不适合作为小程序是否正常的验收标准。3.2 运行到微信小程序模拟器最接近小程序的调试环境点运行 - 运行到小程序模拟器 - 微信开发者工具HBuilderX 会先编译然后尝试拉起微信开发者工具并自动打开对应项目。如果你是第一次做这个操作需要提前确认两件事电脑上已经安装了微信开发者工具微信开发者工具的设置 - 安全设置 - 服务端口已开启。服务端口不开启的话HBuilderX 会一直停在正在启动微信开发者工具的状态坑了很多新手。开启方法很简单打开微信开发者工具点击右上角设置进入安全设置把服务端口按钮打开。跑起来之后你在微信开发者工具里能看到完整的项目模拟器还可以切换 iPhone/安卓机型、调整网络环境、查看小程序特有的报错。这一步是目前 uni-app 开发里最重要的调试环节因为小程序端的坑最多早看早解决。3.3 运行到手机或模拟器App 端的真机表现想验证 App 端效果可以用数据线连接安卓手机并开启 USB 调试然后选运行 - 运行到手机或模拟器。HBuilderX 会自动把调试基座安装到手机上运行效果接近真实 App。这里我要重点提醒调试基座和正式打包的逻辑不一样。调试基座是 DCloud 的公共包适合开发阶段看效果正式发布时必须打正式包否则运行时会弹开发版基座的提示无法上架。如果你在 Windows 上用安卓模拟器需要先让模拟器占用了 ADB 端口再在 HBuilderX 里刷新设备列表。连接失败的时候多半是 ADB 驱动或模拟器兼容问题可以换用一个常见的模拟器试试或者改用真机调试。3.4 各端调试的综合建议我开发时的默认流程是先用浏览器快速验证布局 - 再切到微信开发者工具验证小程序行为 - 需要用到原生能力时再用真机跑 App 端。这样既能保证反馈速度又能尽早发现跨端差异。如果遇到浏览器正常、小程序异常的情况优先检查是不是用了浏览器专属的 API 或变量。比如window、document、localStorage在小程序端都不存在必须用uni提供的 API 代替。这个检查单应该刻在脑子里。4. 响应式数据体系ref、reactive 与自动导入 ref的设置到了这里你已经能跑起项目、做页面跳转了。接下来要解决的是 Vue 3 语法在 uni-app 里的正确用法尤其是ref和自动导入问题。4.1 为什么用 ref 而不是 dataVue 3 之后组合式 API 成为主流ref和reactive替代了原先 Options API 里的data。在 uni-app 里你也可以用 Vue 2 那种data(){ return {} }的写法但新项目我强烈建议直接用 Vue 3 的组合式 API。ref的核心作用是给基本类型或对象创建一个响应式引用。它和reactive的区别可以简化成一句话ref包裹的值在脚本里读取时要加.value在模板里不用reactive直接包对象属性访问不用.value但直接对整个变量重新赋值会失去响应性。使用ref最典型的好处是语义清晰——你一眼就能看出哪些数据是响应式的哪些只是普通变量。这在跨端项目里尤其重要因为编译到小程序时响应式系统的实现和 H5 有所差异ref的行为更可控。一个最简单的计数示例template view classpage text{{ count }}/text button tapincrement加一/button /view /template script setup import { ref } from vue const count ref(0) function increment() { count.value } /script这里count在模板里直接渲染没问题但在increment函数里必须写成count.value。很多刚转 Vue 3 的人会在这里犯迷糊为什么这个.value有时要加、有时不加记住一条规则脚本里操作要加.value模板里统一自动解包。4.2 自动导入 refHBuilderX 和 Vite 的设置方法热搜词里uni-app 设置自动导入 ref指的是不想在每个用到ref的页面顶部都写import { ref } from vue而是让工程帮你自动完成。这在script setup语法下是可以通过工具链做到的。如果你的工程是用 Vite 构建的HBuilderX 新建项目时选择 Vue 3 版本即可推荐用unplugin-auto-import这个插件。先安装再在项目根目录的vite.config.js里配置。安装命令npm install -D unplugin-auto-import配置示例import { defineConfig } from vite import AutoImport from unplugin-auto-import/vite export default defineConfig({ plugins: [ AutoImport({ imports: [vue, uni-app], dts: src/auto-imports.d.ts }) ] })配置完重新运行项目ref、reactive、computed、watch这些 API 就不需要手动引入了。插件会自动生成一个d.ts类型声明文件并在编译时注入对应的 import。这里有一个细节要注意dts生成的auto-imports.d.ts文件建议提交到版本库因为它能让 IDE 正确识别全局类型不然编辑器和编译器可能认为ref is not defined。如果你的工程不是 Vite 构建或者你根本不想引第三方插件也可以退一步老老实实手动写import { ref } from vue。这行代码的成本很低换来的是依赖更少、逻辑更透明。在团队协作中我也见过因为自动导入导致新人困惑很久的情况——他们看到代码里没 import 却能用ref一时间搞不清楚数据从哪来。4.3 自动导入的实际边界与注意事项自动导入不是万能的使用时要清楚几个边界只在script setup中有效。如果你用的是 Options API 的export default { data() {} }写法插件不会帮你自动注入ref。模板中只是使用变量不涉及 API 时不需要导入。自动导入解决的是在脚本里直接调用ref、computed等 API的场景。如果报错报在类型上检查dts路径是否正确以及 IDE 是否重启过让它重新加载自动生成的类型声明。我自己在中小型项目里的习惯是ref、computed这类高频 API 用自动导入业务模块里的自定义工具函数还是显式 import。这样既有便利性又能直观地看出项目里用了哪些自定义依赖。4.4 用组合式 API 重构一个小页面光说不练没用下面这个例子展示了在 uni-app 页面中用ref管理状态和异步数据的常见姿势template view classlist-page view v-foritem in list :keyitem.id classrow text{{ item.name }}/text /view view v-ifloading classloading加载中.../view /view /template script setup // 如果配置了自动导入这两行可以省略 import { ref } from vue import { onLoad } from dcloudio/uni-app const list ref([]) const loading ref(false) async function fetchList() { loading.value true try { const res await uni.request({ url: https://example.com/api/list }) list.value res.data } finally { loading.value false } } onLoad(() { fetchList() }) /script注意这里用了onLoad来做页面初始化。它的作用不同于 Vue 的onMountedonLoad只触发一次并且适合在页面还没渲染完时做数据准备。页面生命周期与ref配合是 uni-app 组合式开发的骨架。5. 新手最容易踩的坑我的排查思路和绕坑方案下面的坑我基本都踩过写出来帮你省几个晚上的排查时间。5.1 页面跳转和 tabBar 的配置冲突坑的症状点击 tabBar 某一项没反应或者uni.navigateTo跳转时报web-view is not defined或找不到页面。排查思路先看pages.json。确认跳转的目标页面是否在pages数组里注册如果目标是 tabBar 页面确认使用的是uni.switchTab而不是uni.navigateTo。tabBar 的list至少需要两个配置项并且pagePath必须和 pages 数组里的路径完全一致多一个斜杠或者少一个斜杠都不行。5.2uni.showToast和alert的选择坑的症状在小程序端调用alert或window.alert页面直接卡住或控制台报错。原因小程序环境没有window.alert。你需要统一用uni.showToast或uni.showModal做提示弹窗。uni.showToast({ title: 操作成功, icon: success })showToast在不同的端上表现有轻微差异H5 端会显示在中间小程序端则倾向于顶部弹出。如果你的团队有统一的设计规范可以考虑在项目里封装一层toast工具函数避免在业务代码里散落各种平台差异。5.3 尺寸单位rpx 的正确打开方式uni-app 提供了响应式单位rpx。它的设计很简单任何机型的屏幕宽度都是 750rpx。如果你拿到的设计稿是 750 宽那量出多少 px代码里直接写多少 rpx几乎不需要换算。时间和经验告诉我页面布局、字体大小、间距这些尽量用rpx但涉及固定的页面边距、阴影尺寸时px也有它存在的场景。需要注意的是如果你同时在做纯 H5 的 PC 端适配rpx会显得非常大需要专门处理或者提供一行代码做转换。移动端优先的 uni-app 项目默认拿rpx做全局单位是最省心的。5.4 图片资源本地图和小程序域名的关系坑的症状小程序里image src本地路径 /可以显示但换成网络图片后一直转圈或失败。原因小程序对网络请求和网络图片有域名白名单限制。开发阶段可以在微信开发者工具里勾选不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书暂时跳过限制正式上线前必须把使用的图片域名配置到小程序后台的downloadFile 合法域名里。这个坑最容易出现在测试环境一切正常生产环境图片挂掉的悲剧里。因为本地开发时不受线上证书限制一旦真机访问线上域名域名校验规则就开始发挥作用。5.5 条件编译处理跨端差异的官方武器uni-app 最强大的一个特性是条件编译。它允许你在代码里按平台写差异逻辑编译时会自动剔除其他平台的内容。语法如下template view !-- #ifdef MP-WEIXIN -- text仅微信小程序显示/text !-- #endif -- !-- #ifndef MP-WEIXIN -- text除微信小程序外的其他端显示/text !-- #endif -- /view /template写法上#ifdef表示只要这个平台存在就保留#ifndef表示只要这个平台不存在就保留。常见的平台标记有H5、MP-WEIXIN、MP-ALIPAY、APP-PLUS。我之前有个项目需要在 App 端调用原生分享而在小程序端使用内置的按钮就是靠条件编译把两套逻辑写在同一个文件里互不干扰。这个特性用好了多端维护的成本能下降一个量级。5.6 底部安全区与 iPhone 的小黑条坑的症状iPhone 上页面底部内容被 Home 指示条遮挡尤其是有固定底栏的页面特别明显。解决思路给底部容器留出安全区边距。CSS 中可以用env(safe-area-inset-bottom).footer { padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom); }constant()是旧版本 iOS 的写法env()是新写法。建议两者都写上做好向下兼容。类似的还有顶部安全区env(safe-area-inset-top)在自定义导航栏时很常用。写在最后的个人体会我从接触 uni-app 到现在做了不下十个跨端项目最大的体会是这个框架的入门门槛确实低但真正提升的关键在于建立多端思维。你不是在写一个网页也不是在写一个原生 App而是在写一套可以被多个平台解释执行的代码。基于这个认知你看待pages.json、rpx、条件编译和 API 选择时就会自然地多问一句这个写法换到别的端上还成立吗如果你顺着这篇教程把环境、工程结构、路由、生命周期、响应式数据和基础排错跑通了那菜鸟阶段的核心障碍就已经扫干净。下一篇文章可以聊聊组件封装、状态管理、请求封装和更复杂的跨端兼容方案——这些才是从能跑走向能上线的分水岭。