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 项目有两条常见路径:一种是用官方 IDE(HBuilderX)界面化操作,另一种是通过命令行工具从模板初始化,然后继续用自己习惯的代码编辑器。我在学习阶段强烈建议先走官方 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 class="content"> <text class="title">{{ 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" mode="aspectFit"></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?type=news" })。在list页面读取这个参数,并用它去请求一个模拟接口,最后把返回的数据渲染成一个简单的列表。
完成这个任务时,你会用到页面生命周期、路由参数、请求封装和状态容器中的至少三个部分。如果中间发现某个环节不顺畅,大概率是前面某一节没有完全理解,可以回头重看。这个练习做好了,框架的"骨架"才算真正长在了你身上。
7.3 两个值得长期养成的习惯
最后分享两个我在搭建项目过程中形成的习惯。
第一个习惯是:每次改动一个配置文件或升级一次工具,立刻做一次运行验证。不要等代码写了很多再运行,因为问题叠加时,你很难判断是哪一步引入的。我通常会把"运行一次验证一下"当成焊接时的固定点,固定点越密,后面越不容易脱轨。
第二个习惯是:报错先看控制台和编译输出,再去看业务代码。很多问题看起来玄学,实际上输出日志里已经把方向指得很清楚了。我踩过的那些坑,几乎都能通过日志缩小查错范围。拿到报错信息,先复制、再对照目录结构和版本信息,往往比盲读代码快得多。
搭建项目这一步,不必追求一步到位。能跑通、能扩展、能控制变量,就已经是一个很扎实的起点。接下来再往里面填业务,你会发现自己比想象中更知道自己要干什么了。