
uni-app x 的学习向来有个特点单独看文档里的某个 API感觉都能理解可真到要自己从零搭一个项目时问题会一下子冒出来。系列第一篇把 uvue 的基础写法、编译目标和原生渲染的基本概念捋了一遍到了第二篇我不打算再贴大段的概念说明而是带着你亲手搭一套能跑、能改、能发布预览的最小项目骨架。这个骨架一旦成立后面写页面、接接口、放状态都像是顺着轨道往前走反过来如果一上来就抓一个模板边看边改很多配置项会变成一个个不定时炸弹出问题都不知道去哪找。这篇文章会从项目创建开始一直讲到请求封装、目录约定和几个我实际踩过的坑全程以实操为主。适合刚看完语法、准备动手做自己第一个 uni-app x 小项目的读者也适合已经创建过项目但对工程结构一知半解的人。你不需要多深的背景知识只要电脑上装好了官方 IDE跟着一步步做就能得到一个可复现的开发底座。1. 为什么第二篇就急着搭项目一套骨架的价值边界1.1 先想清楚搭建项目到底在解决什么问题我见过不少人学新框架的习惯是先把文档从头翻到尾再开始写业务代码。这个思路放在 uni-app x 上很容易卡住因为文档内容以 API 为主很少回答一个文件应该放在哪、配置文件改了有什么用、为什么同样一段代码换个平台表现不一样这类工程问题。搭建项目解决的其实是三件事的先后顺序。第一把文件结构定下来让页面、组件、工具函数和配置文件各有归处。第二把编译链路跑通确认代码能变成浏览器和手机上可以看到的东西。第三把扩展点留出来路由怎么加、请求怎么发、状态怎么存一开始就有统一的入口。这三件事的意义比一个页面的 UI 先写出来重要得多。换句话说语法知识是给你一堆零件项目骨架是让你知道零件该装在哪块板上。没有骨架一段样式写错都很难判断是代码问题还是目录问题有了骨架几乎所有问题都能通过配置对不对、文件放没放对、引用路径对不对三个维度快速定位。1.2 骨架价值的三层边界能编译、能预览、能扩展我给这套骨架定的标准只有三条可以实现起来非常有用。第一层是能编译。新建项目之后不需要写任何业务代码先运行一次官方默认模板让编译器把基础文件完整过一遍。这个过程能最早暴露环境问题比如 IDE 版本太低、运行插件没装全、依赖缓存异常。如果默认模板都跑不过后面所有代码都白写。第二层是能预览。最少要在浏览器或者模拟器里看到画面并且每改一个字符保存后画面能刷新。这保证了代码的可反馈性而不是写了一大堆才去验证。预览链路顺了后续调试效率会高很多。第三层是能扩展。项目里要提前留出 pages 目录、组件目录、工具函数目录和状态管理目录同时 pages.json、manifest.json 两个配置文件的位置和语义要清楚。这三层边界可以对应一个表格来理解阶段判断标准常见状态能编译默认模板运行不报错依赖缺失、版本过低能预览改代码后画面可刷新端口占用、缓存未清理能扩展新页面/组件有固定放置规则文件乱放、互相引用混乱大多数人的问题不是不会写代码而是卡在能不能持续扩展这一层。骨架的价值边界到这里就很清楚了它不是让你一步到位设计出最优架构而是保证每加一个新功能都有地方放、有路由可跳、有工具可调。2. 建项目前的三个取舍运行环境、初始化模板和调试基座2.1 编辑器选择官方 IDE 和命令行各适合什么人搭建 uni-app x 项目有两条常见路径一种是用官方 IDEHBuilderX界面化操作另一种是通过命令行工具从模板初始化然后继续用自己习惯的代码编辑器。我在学习阶段强烈建议先走官方 IDE原因很直接它把新建项目、运行到浏览器、运行到手机、查看控制台日志这些高频操作都做成了按钮不需要你去记忆一堆命令。命令行方式不是不好而是它的收益通常体现在团队协作和自动化上。比如后端集成、CI/CD、代码托管检查这些场景下通过命令行初始化项目、执行构建会更方便。但在一开始你还没搞清楚运行到各端到底发生了什么事用命令行只会增加认知负担。我把两种方式的差异列了一下维度官方 IDE 创建命令行创建上手难度低点按钮即可中需要理解命令参数运行调试内置运行菜单一键预览自己执行 dev 脚本团队协作适合个人/小团队快速起步适合标准化工程后续迁移可以转为 CLI 工程一开始就是标准工程我个人的经验是先通过官方 IDE 把项目跑通等你理解清楚了每一步在干什么再根据需要迁移到命令行工程。顺序反了容易在第一天就被环境问题劝退。2.2 初始化模板默认模板、空白模板和官方示例怎么选新建项目时IDE 会让你选择模板。常见的有默认模板、空白模板和官方示例模板。这三者的差异不只是页面上有没有示例代码那么简单。默认模板会生成干净的 pages 目录、App.uvue、main.uvue、pages.json、manifest.json以及一个最简单的首页。它不会往项目里塞太多演示代码适合作为正式项目起点。空白模板更极端连首页都可能不给你写全适合你准备完全自己控骨架的情况。官方示例模板则包含大量演示页面和 API 用法适合翻代码学习但不适合直接在上面改业务因为清理成本非常高。如果是在学习系列里做第二个项目我会建议选默认模板。它给了你一个能直接运行的最小起点又不会干扰你对目录结构的理解。等跑通之后你把首页内容改成自己的东西即可。2.3 调试基座标准基座、自定义基座和正式打包第一次运行到真机时IDE 会提示安装一个调试基座。可以理解为给你包了一层能跑 uni-app x 代码的壳你的代码在这个壳里被解释或编译执行。标准基座是官方提供的日常调试用足够。只有当项目集成了原生插件或本地原生代码时才需要制作自定义基座把原生代码和业务代码一起打进去调试。很多人会把调试基座和正式安装包搞混。调试基座是开发阶段使用的带有调试能力不适合对外发布。正式安装包则需要通过打包生成会有一系列签名、资源配置、权限声明在里面。搭建项目阶段我们只需要标准调试基座能跑起来就好不要在这上面过多纠结。从我实际操作的经验看遇到的基座问题大多不是选择错了而是版本不一致。IDE 更新后手机上的调试基座还停留在旧版本就会出现白屏或者 API 调用没有反应。遇到这种情况先升级调试基座、清除基座缓存再重新运行。这个点后面我还会展开说。3. 完整搭建一条最小闭环创建、跑通、新增页面与路由3.1 新建项目并跑通默认首页下面我们开始动手。打开官方 IDE在菜单栏选择文件 - 新建 - 项目然后在弹出的窗口里选择 uni-app x 的项目类型。项目名称我建议起成uni-app-x-demo不要带中文和空格避免后续路径和编译出错。保存位置可以选一个专门的代码目录比如D:/study/projects或者~/study不要放在系统临时目录下。模板选择默认模板后点创建。等 IDE 右下角提示项目创建完成左侧文件树会出现几个关键文件。还没有写任何代码前直接找到菜单栏的运行 - 运行到浏览器 - Chrome点击运行。首次运行会做编译初始化可能要等一会儿看到浏览器打开一个页面并输出默认欢迎内容就说明编译链路已经通了。这时我建议做一个信号验证把首页文件里的标题文字改掉保存再切到浏览器看是否自动刷新。如果能看到改动说明热更新正常。这一步虽然简单但能帮你区分后面遇到的很多问题到底是代码问题还是环境问题。给一个最简首页代码示例方便理解template view classcontent text classtitle{{ title }}/text /view /template script export default { data() { return { title: 骨架搭建成功 }; } }; /script style scoped .content { flex: 1; align-items: center; justify-content: center; } .title { font-size: 28px; color: #333; } /style这段代码不一定是你项目里的最终样子但通过只改title这个字段你就能验证页面到数据的链路是通的。3.2 拆解项目目录入口、页面、静态资源和全局样式项目创建完成后我们需要认识一下每个文件的位置。以官方 IDE 创建的项目为例目录结构大致是这样uni-app-x-demo/ ├─ pages/ │ └─ index/ │ └─ index.uvue ├─ static/ │ └─ logo.png ├─ App.uvue ├─ main.uvue ├─ pages.json ├─ manifest.json └─ uni.scsspages目录用来放页面文件每个页面一般是一个.uvue后缀的组件文件。static目录放静态资源比如图片、字体引用时不会做特殊编译处理。App.uvue是应用根组件可以在这里写全局生命周期和全局样式。main.uvue是入口文件用来创建应用实例。pages.json是路由和页面窗口配置manifest.json是应用信息和平台配置uni.scss是全局样式变量文件。如果你用的创建方式不同会有多一层src目录本质是一样的只是多套了一层文件夹。关键是有页面归 pages、静态资源归 static、配置归根目录的心智模型。打开pages/index/index.uvue你会看到整个页面文件既包含模板、脚本也包含样式。这是 uni-app x 页面最常见的形态。关于样式我建议在刚搭项目时把全局通用颜色和字号先写到uni.scss页面里能用变量就不要写死后面换主题时你会感谢这个习惯。3.3 新增一个独立页面并把路由表打通首页能跑通之后下一步一定是新增一个页面。我以新增一个关于页面为例整个过程分三步。第一步在pages目录下新建about/about.uvue。你可以先复制首页的模板改一下文案确保这个页面文件本身可以编译。第二步打开pages.json在pages数组中新增一个对象指定path为pages/about/about并设置导航栏标题。{ pages: [ { path: pages/index/index, style: { navigationBarTitleText: 首页 } }, { path: pages/about/about, style: { navigationBarTitleText: 关于 } } ] }这里有一个容易忽略的点pages数组中的第一项是启动首页新增页面只要放在数组后面就行不用调整顺序除非你想改变默认加载的首页。第三步在首页写一个跳转入口通过uni.navigateTo打开关于页。uni.navigateTo({ url: /pages/about/about });保存后重新运行点击按钮如果能看到关于页出现说明路由链路已经打通。这个最小闭环建立以后后续每新增一个页面都是在重复这三步建页面文件、注册路由、调用跳转 API。3.4 把静态资源和全局样式安排妥当页面多了之后静态资源的使用也会频繁起来。一张图片放在static/logo.png在页面模板里一般用image组件引用image src/static/logo.png modeaspectFit/image注意路径最前面的/表示项目根目录很多刚接触的人会漏掉这个斜杠导致图片加载不出来。更稳妥的做法是在脚本里定义相对路径变量或者统一维护一个资源路径常量。全局样式方面除了uni.scss中的设计变量App.uvue的style里也可以写全局样式。比如我想统一所有页面的背景色和默认字体可以写在 App.uvue 中。这样写的好处是页面里不用重复声明坏处是由于作用域规则调试时要意识到这个样式是从全局来的改的时候别不小心影响所有页面。在实际项目中我通常会把通用色板、间距变量放进uni.scss把全局重置样式放进App.uvue页面里的具体布局和局部样式全部写在页面自己的 style 中。这个分层能省掉很多样式冲突的排查时间。4. manifest.json / pages.json / 环境变量三个配置文件如何共同决定项目性格4.1 manifest.json应用身份信息和平台开关很多新手会觉得manifest.json是个不需要动的文件其实它才是项目身份信息的核心。应用名称、唯一标识、版本号、图标、启动图都在这里配置。如果将来要生成正式安装包这些信息都必须正确否则打包环节会报错。以最简单的配置为例你需要把应用名称改成自己项目的名字版本号按自己的发布节奏填写。{ name: uni-app-x-demo, versionName: 1.0.0, versionCode: 100, appid: }appid这个字段比较特殊开发阶段可以为空但使用云打包等功能时通常需要换成自己的标识。在搭建骨架时不必纠结先留空运行到浏览器不影响。每个平台还有自己的配置块比如在 App 端配置最低系统版本、屏幕方向等。这些内容等真正做跨平台适配时再逐项研究。我的建议是搭建阶段把name和versionName配置好就好其它字段保持默认。不要一开始追求所有配置都理解透否则会被配置项淹没。4.2 pages.json页面路由、导航栏与窗口表现pages.json是 uni-app x 项目里我花时间最多的配置文件之一。除了刚才说的页面路由它还控制每个页面的导航栏标题、背景色、是否允许下拉刷新、页面是否全屏等表现。一个完整一点的页面配置可以是这样{ path: pages/index/index, style: { navigationBarTitleText: 首页, navigationBarBackgroundColor: #ffffff, navigationBarTextStyle: black, enablePullDownRefresh: true } }这里有一些容易误解的点。navigationStyle如果不写默认是default也就是系统导航栏如果你想自己画自定义导航栏可以设置成custom但那样你就失去了系统自带返回按钮和标题样式需要自己处理。新手骨架阶段我建议保留默认导航栏减少工作量。还有一个关键理解pages.json里的pages数组顺序不只是路由表还是启动和预加载顺序的依据。第一个页面就是首页。如果你想把一个还没写完的页面放第一个运行时会直接跳到那个页面很容易让自己懵。我通常会在项目初期把首页固定为完成度最高的页面其它页面往后加。4.3 条件编译和环境变量一套代码处理平台差异uni-app x 实际上是要跑在 Web 和原生端的多端框架平台之间天然有差异。比如 iOS 和 Android 在某些 API 上的表现不一样浏览器和原生环境在存储、标题栏样式上也可能有不同。处理这种差异最常用的手段是条件编译。条件编译就是在代码中标记某个片段只属于特定平台。在脚本里这样写// #ifdef H5 console.log(当前是 Web 端); // #endif // #ifdef APP-PLUS console.log(当前是 App 原生端); // #endif在模板和样式中也有对应的注释写法。这个机制的好处是同一套代码可以同时保留多个平台的实现打包时自动裁掉不属于当前平台的代码不会把多余的判断带进产物。环境变量的角色则不太一样。开发环境、测试环境、生产环境通常有不同的接口地址。最简单的方式是用条件编译配合环境判断把 baseURL 写成一组常量。我后面的请求封装里会用到这个思路。现在先记住配置文件的职责边界要清晰manifest.json管身份pages.json管页面表现环境差异用条件编译和常量来管。5. 骨架工程化请求封装、轻量状态容器和目录约定5.1 先封装一个统一的请求函数省下后面所有接口维护成本项目要真正用于业务几乎离不开网络请求。新手最容易犯的错误是在每个页面里直接调用uni.request接口地址散落得到处都是。这样开发时很快但后端一换域名或者要统一处理登录过期你就会体验到处翻代码的绝望。我建议在骨架阶段就建立一个utils/request.uvue文件把请求逻辑收拢到一起。一个足够起步的封装至少要包含基础地址、超时时间、成功失败的判断以及一个可以统一处理错误的地方。下面是一个简化的示例可以按自己的实际需求调整const BASE_URL https://your-api.example.com/api; function request(path, method, data) { return new Promise((resolve, reject) { uni.request({ url: ${BASE_URL}${path}, method: method || GET, data: data || {}, timeout: 10000, success: (res) { // 根据业务约定判断状态码 if (res.statusCode 200 res.statusCode 300) { resolve(res.data); } else { reject(res); } }, fail: (err) { reject(err); } }); }); } export function get(path, data) { return request(path, GET, data); } export function post(path, data) { return request(path, POST, data); }注意示例中只是最基础的封装。真实的项目还可以在success或fail里统一处理登录过期、弹出错误提示、上报日志。封装的价值不在于代码写得华丽而在于所有请求的出入口是唯一的以后加一层拦截器不用改上百个调用点。5.2 一个轻量状态容器不需要重型状态管理也能管理登录态很多项目一上来就引入整套状态管理方案但对于一个骨架阶段的小项目来说可能并不需要那么复杂的依赖。我通常会先做一个轻量的状态容器用模块级变量加简单的订阅通知来做跨页面数据共享。下面这个例子演示了如何管理登录态const state { token: , userInfo: null }; const listeners []; function setState(partial) { Object.assign(state, partial); listeners.forEach((listener) listener(partial)); } function subscribe(listener) { listeners.push(listener); return () { const index listeners.indexOf(listener); if (index -1) { listeners.splice(index, 1); } }; } export default { state, setState, subscribe };这样登录成功后调用setState({ token })任何页面通过subscribe订阅状态变化或者直接读state里的内容。好处是结构非常轻心智负担小项目成长后再替换成更完整的状态管理也不难。5.3 用目录约定把需求钉死在固定位置目录约定看起来是小事但它直接影响项目能不能长期维护。我在骨架项目里会固定一套简单的目录分类pages/ 页面文件按模块分子目录 components/ 可复用组件按功能命名 utils/ 普通工具函数 apis/ 接口定义一个模块一个文件 store/ 状态容器 styles/ 公共样式或样式变量规则要简单到看一眼就知道一个文件该放哪里。比如页面放在pages/order/list.uvue接口定义放在apis/order.js组件放在components/order-card/order-card.uvue状态逻辑放在store/order.js。这样后续增加人手或者自己隔几周回来看不会出现不知道该去哪找这段逻辑的问题。目录约定的价值是把不确定性变成确定性和习惯。哪怕骨架初期所有模块都只有一个文件这种习惯也会让你后面的项目受益。6. 搭建现场踩过的四个坑现象、排查链路与修复方案6.1 第一坑页面里的样式改动后真机运行时表现不一致我在搭建骨架时遇到过在浏览器里样式正常但一发布到真机预览某些间距和颜色就不生效。第一次遇到时我以为是代码写错了反复检查页面文件最后才发现问题出在样式的作用域策略和基础样式差异上。排查链路是这样的先复制同一个页面到浏览器和真机分别预览确认差异只在真机出现。然后把页面里的样式逐步注释掉再用二分法定位到出问题的那个 class。最后查下去发现是某些 reset 样式在真机的解析顺序和浏览器不同导致继承关系错乱。修复方案也很基础把公共色板和基础间距统一放在uni.scss的变量中页面里避免写裸的颜色值同时尽量在页面根部显式设置背景色不依赖全局继承。这个坑的教训不是某个具体样式而是提醒你跨端差异不能靠浏览器看着没问题来推断要有真机预览的习惯。6.2 第二坑import 路径大小写不一致编译报错但不提示具体文件这是非常诡异的一个坑。我在components目录下新建了一个组件文件叫UserCard.uvue在页面里 import 时写成了./components/user-card开发环境下跑了一两天都没事某次整理代码后突然报编译错误而且报错信息里只提示某个入口文件找不到没有直接指出大小写问题。排查链路很清晰先把报错信息里的文件路径复制出来对照真实目录一个一个看发现文件名是UserCard.uvue引用路径却是小写。当场改成一致后再编译错误消失且开发过程中再也没有出现过同类报错。这个坑的核心在于开发环境可能做了大小写兼容但构建或真机环境不一定做。为了避免我给自己定了一条规矩所有文件名统一用小写加中划线例如user-card.uvue这样 import 路径和文件名永远不会出现大小写差异。6.3 第三坑调试基座版本和编译器版本不一致导致二次打开白屏项目跑到第三天时我改了首页文案保存后页面一直刷新不出来。浏览器预览正常但真机上白屏。第一反应以为是代码调用了某个不存在的 API翻了半天都没找到问题。后来想起昨天的官方 IDE 好像提示过组件库更新于是去检查调试基座版本果然手机上装的基座还是旧的。处理办法很简单在 IDE 里重新安装或升级标准调试基座然后断开手机连接、杀掉基座进程重新运行。整个过程不到两分钟但排查花了半个多小时。这个经验让我养成了一个习惯真机调试出问题时第一件事不是回看代码而是确认调试基座版本和 IDE 版本是否匹配。白屏这种问题最怕你按业务代码的方向去查。先把环境变量收敛成版本一致、缓存干净、端口没占再去看代码逻辑效率会高很多。6.4 第四坑自定义组件明明写了页面却提示 component not found在小项目里新增一个自定义组件我在components目录里放好文件也在页面里加了 import 和components注册但运行时一直提示找不到组件。报错时间有长有短有时候刷新一下又好了容易让人误以为随机问题。后来检查发现我同时用了两种组件引用方式一种走目录约定自动引入另一种手动 import两种规则在同一个页面里发生了冲突。框架的组件查找规则是优先按约定路径去匹配手动注册的写法如果在路径上不符合约定就会被自动匹配遮住。修复方式是统一约定小项目尽量全部走目录约定的思路把组件文件放在components/组件名/组件名.uvue的固定结构里避免手动 import 和自动查找规则混乱。如果你发现组件报找不到先看是不是路径结构和命名不匹配再看是不是注册方式混用。这个问题排查起来不复杂但对刚接触项目结构的人来说会卡很久。7. 项目骨架搭好之后留给第三篇的功课7.1 这个骨架能支撑怎样的后续迭代如果你完整走完前面的步骤项目现在应该具备一个可靠的目录结构、可运行的页面路由、统一的请求封装入口、轻量状态容器以及一套踩过坑之后沉淀下来的注意事项。这个骨架已经有能力支撑很多常规场景了。比如加一个列表页你只需要在pages下建目录、在pages.json注册路由、在apis里定义接口列表然后用封装好的get方法去拉数据再把结果渲染到页面。比如做用户登录可以把登录接口放进apis/user.js登录成功后把 token 写入状态容器其它页面订阅状态变化就能更新 UI。骨架的价值就在这里它不帮你写业务但让业务有地方生长。7.2 第三篇的练习题给骨架加一个带参数跳转的列表页这里我想留一个明确的动手任务也算是对这一篇成果的验证。请在现有骨架基础上新增一个list页面从首页跳转到list页面时带上一个type参数比如uni.navigateTo({ url: /pages/list/list?typenews })。在list页面读取这个参数并用它去请求一个模拟接口最后把返回的数据渲染成一个简单的列表。完成这个任务时你会用到页面生命周期、路由参数、请求封装和状态容器中的至少三个部分。如果中间发现某个环节不顺畅大概率是前面某一节没有完全理解可以回头重看。这个练习做好了框架的骨架才算真正长在了你身上。7.3 两个值得长期养成的习惯最后分享两个我在搭建项目过程中形成的习惯。第一个习惯是每次改动一个配置文件或升级一次工具立刻做一次运行验证。不要等代码写了很多再运行因为问题叠加时你很难判断是哪一步引入的。我通常会把运行一次验证一下当成焊接时的固定点固定点越密后面越不容易脱轨。第二个习惯是报错先看控制台和编译输出再去看业务代码。很多问题看起来玄学实际上输出日志里已经把方向指得很清楚了。我踩过的那些坑几乎都能通过日志缩小查错范围。拿到报错信息先复制、再对照目录结构和版本信息往往比盲读代码快得多。搭建项目这一步不必追求一步到位。能跑通、能扩展、能控制变量就已经是一个很扎实的起点。接下来再往里面填业务你会发现自己比想象中更知道自己要干什么了。