前阵子有个新手朋友问我小程序项目打开之后一堆文件到底先看哪个这个问题我太熟悉了几乎每个从零开始学小程序的人都会卡在这里。不一样的是大多数人第一反应是去看页面代码、看组件怎么用结果代码看懂了项目结构还是一团浆糊——改一个地方另一个地方跟着变全靠感觉和运气。小程序学习和传统网页开发的第一个分水岭就是这个工程结构。网页是HTML、CSS、JS三件套丢进服务器就能跑小程序则有自己的一套文件约束其中最关键的就是小程序开发4个重要文件。把这4个文件搞明白你就能看懂任何一个小程序项目的骨架之后学组件、学API、学框架都会顺畅很多。下面专门拆这4个文件顺带把页面层对应的四件套也一起讲清适合刚装好开发者工具的初学者也适合从网页开发转过来的前端同学。1. 新建项目后先分清哪些文件管全局哪些文件管页面一个全新的原生小程序项目目录结构基本是固定的开发者工具会给你生成默认模板。先别急着改代码花两分钟把目录看清楚比多写两个页面都值钱。1.1 开发者工具默认生成的目录长什么样用开发者工具创建项目之后你会看到类似这样的结构miniprogram/ ├── app.js ├── app.json ├── app.wxss ├── project.config.json ├── sitemap.json └── pages/ ├── index/ │ ├── index.js │ ├── index.json │ ├── index.wxml │ └── index.wxss └── logs/ ├── logs.js ├── logs.json ├── logs.wxml └── logs.wxss注意我标的重点根目录下站着4个文件分别是 app.js、app.json、app.wxss外加一个 project.config.json。这4个文件就是标题里说的4个重要文件它们管理的是整个小程序的全局。而 pages 目录下每一页由4个同名但后缀不同的文件组成也就是 .js、.json、.wxml、.wxss这4个文件管的是页面局部。1.2 一个项目其实只有两层全局层 页面层把这套结构翻译成人话就是全局层决定小程序这个壳子长什么样、启动时先跑什么逻辑、全局的样式基调是什么页面层决定每个页面里到底放什么内容、页面自己的逻辑和样式是什么。这种全局管壳、页面管肉的分层思路和传统网页把公共逻辑放在公共JS里、公共样式放在公共CSS里的做法是一个道理只是小程序把全局层的文件从约定变成了强制——你想让小程序跑起来这四个文件就一个都不能少。这也是为什么说理解4个文件就是拿到了理解小程序工程的钥匙。1.3 为什么是这4个文件而不是3个有人会问app.js、app.json、app.wxss 对应 JS/JSON/CSS一套三件套我懂为什么还要加一个 project.config.json这是因为小程序项目的运行不仅仅依赖运行时代码还依赖开发者工具的工程配置。project.config.json 记录的是这个项目用什么AppID、用什么编译设置、源码目录在哪它不参与线上运行但负责让项目在开发者工具里被正确识别。我见过不少从别人那里拷来的项目打开一堆报错八成是 project.config.json 丢掉了或者AppID被改了。后面我会专门讲它。2. app.json小程序的身份证加地图所有页面的注册入口先看最重要、也最好理解的 app.json。2.1 pages 数组页面注册与首页设定app.json 里的第一个关键配置是 pages它是一个数组里面列的是小程序所有页面的路径。数组的第一项就是这个小程序启动后第一个展示的首页。{ pages: [ pages/index/index, pages/profile/profile, pages/logs/logs ], window: { navigationBarBackgroundColor: #ffffff, navigationBarTitleText: 我的小程序, navigationBarTextStyle: black } }路径里不需要写文件后缀小程序会自动补全同名文件。这里有个容易忽略但很关键的细节如果你新写了一个页面但忘了在 pages 里注册那这个页面就是黑户口——你在开发者工具里通过任何方式都访问不到它只能看到编译报错提示页面文件未找到。反过来的场景也很常见你删掉了一个页面但 pages 数组里还留着它的路径编译时同样会报文件不存在。所以每次增删页面第一件事就是同步 app.json 的 pages 数组。2.2 window全局窗口外观window 配置管的是小程序每一页顶部的导航栏背景色、标题文字、文字颜色。注意这里的标题是全局默认值每个页面自己的 .json 里可以覆盖它。这个全局写在 app.json、局部写在页面 json的覆盖关系是很多新人懵掉的地方后面会有专门段落展开。window: { navigationBarBackgroundColor: #f8f8f8, navigationBarTitleText: 默认标题, navigationBarTextStyle: black, backgroundColor: #ffffff }其中 navigationBarTextStyle 只支持 black 和 white 两个值写成其他颜色不会生效这个坑我当年踩过。2.3 tabBar 与常用配置速查如果你的小程序底部有导航栏还需要配置 tabBar。每个 tab 至少要指定 pagePath 和 text还可以配 iconPath 和 selectedIconPath。我把入门阶段最常用的 app.json 配置项整理了一下配置项作用典型值pages注册所有页面第一个是首页[pages/index/index]window全局导航栏与窗口样式见上面示例tabBar底部或顶部标签导航list 数组networkTimeout各类网络请求超时时间10000permission接口权限申请说明scope.userLocation等sitemapLocation站点地图文件位置sitemap.json初期阶段把 pages 和 window 吃透就够起步了。tabBar 我建议等做到第二个小项目时再深入因为还涉及图标尺寸、选中态配色这些细节一次性铺开反而容易乱。3. app.js小程序的大脑全局逻辑与启动时机app.json 负责静态结构app.js 负责动态逻辑。3.1 App() 与全局生命周期整个小程序只有一个 app.js它必须调用一次 App() 函数传入一个对象对象里可以写小程序启动时的生命周期回调。App({ onLaunch() { console.log(小程序启动); // 这里适合做登录态恢复、版本更新检查等初始化操作 }, onShow() { console.log(小程序从后台回到前台); }, onHide() { console.log(小程序被切到后台); }, globalData: { userInfo: null, token: } });onLaunch 只在小程序冷启动时执行一次onShow 是每次从后台切回前台都会触发onHide 则是切后台时触发。这三个生命周期就是小程序全局层面的出生、回来、离开。很多新手容易漏掉的是 onShow 和 onHide。比如你要在用户每次回到小程序时刷新某个提示如果只写在 onLaunch 里那么用户切出去再切回来时根本不会执行。要理解这些时机的差异最简单的办法是开着控制台手动切几次后台再切回来看打印顺序。3.2 globalData全局共享数据的临时口袋App() 对象里的 globalData 是一个所有页面都能通过getApp().globalData访问到的公共数据区。注意名字叫口袋是因为它只是内存里的普通对象小程序一重启就没了不能当持久化存储用。真正常久的数据请走本地存储接口。我在实际项目里惯用的做法全局登录态放在 globalData页面里的业务数据千万不要往这里堆否则页面多了之后你会发现数据互相污染排查起来非常痛苦。// 页面里访问全局数据 const app getApp(); console.log(app.globalData.token);3.3 app.js 里别写什么新手容易犯的错是把大量页面逻辑塞进 app.js。app.js 的本质是启动器加共享口袋不是业务仓库。页面数据、UI状态、请求封装这些应该留在各自页面或者独立模块里。全局文件臃肿之后每次冷启动的耗时会肉眼可见地变长而且改起来风险极高因为你不知道哪个页面在依赖这段全局逻辑。4. app.wxss 与 project.config.json容易被忽略但早晚要懂的两个文件说完了 app.json 和 app.js剩下两个全局文件一个负责样式一个负责工程配置都是那种不看也能跑但遇到问题就得回头补课的类型。4.1 app.wxss全局样式与 rpx 单位app.wxss 是全局样式表作用范围是所有页面。入门阶段要掌握两个点。第一小程序支持一套独有的响应式单位 rpx。它按 750rpx 等于屏幕宽度来换算你在设计稿上量到多少 px在 750 宽的设计稿下就可以直接写多少 rpx不用自己算屏幕适配。这个机制非常省心比传统网页媒体查询简单太多。第二小程序默认没有网页那种全局 reset 样式不同机型上默认边距并不完全一致。所以很多项目会在 app.wxss 里写一份基础的全局样式比如page { background: #f5f5f5; font-size: 28rpx; color: #333; line-height: 1.6; }注意这里选择器用的是 page 而不是 body这是小程序特有的全局根节点写网页的惯性思维在这里不适用。同样的道理小程序里没有 html 标签你想给根节点设背景色写 page 就对了。4.2 project.config.json开发者工具的项目记忆再来看第四个全局文件 project.config.json。它的内容一般长这样{ description: 项目配置文件, packOptions: { ignore: [] }, setting: { urlCheck: true, es6: true, postcss: true, minified: true }, compileType: miniprogram, libVersion: 3.0.0, appid: your-app-id, projectname: my-mini-program, miniprogramRoot: miniprogram/ }这个文件最容易在团队协作时出问题。很多人把项目压缩打包发出去或者从代码仓库 clone 下来之后发现开发者工具一直提示 AppID 不对、编译配置不对原因就是 project.config.json 里写死的 appid 不是你的。正确做法是拿到别人的项目后要么在工具里重新选择测试号要么把 appid 改成自己的再保存生成新的 project.config.json。另外一个常见场景是 monorepo 或者多端项目源码不在根目录而在某个子目录里。这时候 miniprogramRoot 必须精确指向小程序源码所在目录否则工具找不到 app.json会直接报未找到入口文件。4.3 四个全局文件各自的职责边界我把四个全局文件的职责用一句话串起来app.json 是结构、app.js 是逻辑、app.wxss 是样式、project.config.json 是工程配置。遇到任何全局层面的问题先按这个分类往对应文件里找比在页面里翻半天快得多。5. 页面层的四件套.js、.json、.wxml、.wxss 是怎么组装出一个页面的理解完四个全局文件接下来是每个页面目录里都会重复出现的四个页面文件。对于入门到精通这条学习线来说从全局到页面正好是自然的进阶顺序。5.1 为什么要强制同名同目录在 pages 目录下每个页面是一个文件夹里面放4个文件名相同、后缀不同的文件例如 index.js、index.json、index.wxml、index.wxss。这样设计的目的很朴素开发者工具只需要通过路径 pages/index/index 就能自动关联到这一套文件不需要额外配置。这也意味着你新建页面时不应该手动一个个创建文件而是使用开发者工具的新建 Page功能它会把四件套一起生成。手动创建容易漏文件漏一个编译就报错。5.2 wxml页面骨架类似 HTML 但不完全是wxml 负责页面结构标签是 view、text、button 这类小程序组件。我平时对新人说得最多的一句话是你可以把 wxml 当成 HTML 的儿子基本语义和写法九成相似但不要直接把 HTML 标签往里搬比如 div、span 在小程序里是不存在的得换成 view、text。view classcard text classtitle标题/text button bindtaphandleTap点击/button /viewwxml 里不能直接写 JavaScript 逻辑表达式的那套东西比如if要用wx:if循环要用wx:for这也是从网页思维转过来时需要特别注意的差异点。5.3 wxss页面样式与 app.wxss 有关系也有隔离页面 wxss 作用于当前页面它和全局 app.wxss 的关系是同名选择器下页面样式的优先级更高。全局样式是基调页面样式是做微调。这点跟网页 CSS 的层叠逻辑有相似之处但要注意小程序里自定义组件默认样式隔离你在组件外写的样式是进不去组件内部的这个等学到自定义组件再细说。5.4 js页面逻辑与生命周期页面 js 里调用 Page()传入的对象包含 data、生命周期回调、事件处理函数。data 是页面渲染的初始数据onLoad 是页面第一次加载时执行事件处理函数则负责响应用户点击等交互。Page({ data: { message: 你好 }, onLoad(options) { console.log(页面加载, options); }, onShow() { console.log(页面显示); } });onLoad 里会收到一个 options 参数里面是页面跳转时带过来的参数。比如从列表页跳到详情页详情页的 onLoad 就能拿到options.id这类值。5.5 json页面配置覆盖全局的入口最后一个文件最容易被忽略页面 json。它最常见的用途是覆盖导航栏标题{ navigationBarTitleText: 个人中心, usingComponents: {} }此时 app.json 里 window 设置的导航栏标题就会被覆盖成个人中心。页面 json 里还能配 usingComponents 注册自定义组件这是组件化开发的核心入口学到自定义组件时你会反复见到它。6. 数据是怎么从 .js 走到 .wxml 的数据绑定与 setData四件套分开看都简单但初学者最容易卡住的是它们的协作机制说白了就两个问题数据怎么渲染到页面上用户操作怎么回到逻辑里6.1 插值表达式data 到 wxml页面 js 的 data 对象可以直接在 wxml 里用双大括号取出来view{{ message }}/view对应 jsPage({ data: { message: 你好小程序 } });页面加载时这个 view 就会显示你好小程序。这就是数据驱动的雏形你不需要手动操作 DOM。6.2 setData唯一正确的改数据姿势如果只是初始化渲染上面就够了。真正让新手困惑的是动态更新。在普通网页里你改 JS 变量再手动操作 DOM在小程序里逻辑层和视图层是分开的不能直接操作 DOM必须通过 setData 告诉视图层数据变了请重新渲染。Page({ data: { count: 0 }, onAdd() { this.setData({ count: this.data.count 1 }); } });这里有个新手常走的误区有人直接写this.data.count 1页面纹丝不动因为视图层根本不知道数据变了。记住凡是需要体现到界面上的数据变化一律用 setData。6.3 事件闭环bindtap 到 handler交互闭环就是 wxml 上绑定事件js 里写函数button bindtaponAdd点我加一/buttonbindtap 对应点击事件onAdd 是刚才 js 里定义好的处理函数。于是完整的循环是用户点击bindtap 触发 onAdd函数里 setData视图重新渲染 count。这个数据单向流动的模型是理解小程序运行机制的基石架构感强的人会发现它与现代前端框架的状态管理思路非常接近。6.4 setData 的代价别把整个数据对象往里塞setData 看着方便但它是一次真实通信数据量越大、调用越频繁页面卡顿越明显。我在一个项目里对一个大列表整体 setData在低端手机上明显掉帧。后来改成只更新变化的那一项数据并把高频的实时进度数据用视图层能力分摊掉流畅度立刻上来了。入门阶段记住一个原则setData 只传变化的最小数据单元。比如改列表里的某一行就不要把整个数组再塞进去。7. 新手的头号杀手JSON报错、文件缺失和样式失效的排查链路这部分我直接讲排查而不是罗列一堆注意点因为排查思路比散装知识点更容易迁移。7.1 JSON 文件的三个绝不能JSON 是门非常严格的配置格式小程序里 app.json 和页面 json 只要格式错整个项目直接编译失败。三个最常见的错误第一不能写注释。网页里你习惯了用注释说明配置但在小程序 JSON 里写注释编译器会当场报错即使工具有时显示灰色也不代表它合法。真需要解释配置含义写在外部文档里。第二不能有尾逗号。键值对的最后一个逗号是很多老手都会疏忽的细节修起来又很费眼睛。我见过一个页面 json 末尾多个逗号导致整个页面白屏找了一个上午的案例。第三键名必须用双引号字符串值也必须用双引号单引号会报错。我自己定位 JSON 问题的习惯是写完 JSON 后只看编译器右上角的报错面板它通常能精确指出第几行第几列有问题比人眼扫得快得多。7.2 文件缺失与路径错误编译日志的第一条线索未找到入口 app.json 文件和页面 pages/xxx/xxx 文件不存在这两类报错出现频率极高。前者基本是三种情况项目根目录选错了、project.config.json 里 miniprogramRoot 指错了、app.json 被删了。后者则是路径拼写问题这里尤其要小心大小写比如 pages/index/index 写成 pages/Index/index工具的编译环境会严格区分大小写。遇到这两类报错我的排查顺序是固定的先打开编译日志看具体报错文件路径再对照目录树确认文件真的存在最后检查 app.json 的 pages 数组是否有对应的路径字符串。三步之内基本能定位别一上来就满世界查代码。7.3 样式不生效先分清是没匹配上还是被覆盖了页面样式不生效是进阶阶段的高频问题但原因无外乎三类。一是选择器没匹配上。wxml 里写了 classcontainerwxss 里却写的是 .contianer拼写错一个字开发者工具不会给你任何提示。这种问题最有效的排查方式是在工具的 WXML 面板里点选元素右侧会显示它实际绑定的类名和样式来源。二是被覆盖了。全局 app.wxss 和页面 wxss 同名选择器页面优先生效多个同类选择器靠后写的生效。如果你发现样式怎么改都不变看看是不是有个 !important 或者后写的同名样式在压制你。三是单位不对。rpx 和 px 混用容易产生看起来应该一致却差很多的效果。设计稿量出来 100px直接写 100px 在某些屏幕上会明显偏小应当换算成 100rpx 或按 750 设计稿比例换算。7.4 一个完整的排查案例模拟一个场景某开发者在页面里加了新样式编译通过但预览时按钮没变化。他的排查链路是先在工具里看控制台是否有样式警告没有再到 WXML 面板选中按钮发现类名下有一条样式来源记录指向另一个同名类命中被覆盖了这条路检查 wxss 文件发现在文件末尾确实有一段后写的同名选择器而且加了 !important。删掉多余声明后新样式正常生效。这个例子的价值不在答案而在步骤确认编译层无报错用工具面板定位样式来源检查文件内选择器优先级修复并验证。把这条链路记住以后遇到再诡异的样式问题都能稳住。8. 动手实践从四个全局文件到最小可运行项目前面内容已经足够指导实操。我建议你打开开发者工具按下面的步骤新建一个最小项目把每行代码都亲手敲一遍比看十篇文章都管用。8.1 最小文件清单与内容创建项目时选择不使用模板或空白模板不同工具版本叫法不同总之选最原始的空模板然后清空多余文件保留以下内容。app.json{ pages: [ pages/index/index ], window: { navigationBarTitleText: 我的第一个项目 } }app.jsApp({ onLaunch() { console.log(项目启动); } });app.wxss 可以先留空或者只写一行 page 背景色。pages/index/index.json{ navigationBarTitleText: 首页 }pages/index/index.wxmlview classhello bindtaponTap{{ message }}/viewpages/index/index.wxss.hello { padding: 40rpx; font-size: 32rpx; }pages/index/index.jsPage({ data: { message: 你好小程序 }, onTap() { this.setData({ message: 你点了我一下 }); } });保证页面目录里四个文件齐全、app.json 里 pages 与目录结构一致编译大概率一次通过。点击文字后内容变化说明数据绑定和事件闭环都通了。8.2 两种常见编译失败及处理如果你在实操中没有一次通过多半是下面两种原因。一是 app.json 格式错误。把上面的 JSON 复制进去时务必确认复制时没有夹带注释或全角逗号这类字符肉眼很难识别但编译器会准确报出行列。二是 pages 路径与目录不一致。常见写法是 pages/index/index 但实际文件夹叫 pages/Home或者 index 文件名拼错。修正路径字符串或重命名目录保证两者一致即可。8.3 入门阶段的学习顺序建议最后分享一点我自己的教学思路你可以直接照着走。第一阶段把本文第8节的代码至少从头敲三遍直到不假思索就能描述四个全局文件和四个页面文件各自的职责。第二阶段打开官方组件的文档页从 view、text、button、image 开始每学一个组件就往最小项目里加一个不要光看文档不落地。第三阶段写一个两页的小项目比如首页加个人中心把页面跳转、导航标题覆盖、setData 刷新这三件事串起来跑通。补充一个我反复强调的点入门阶段不要急着上第三方 UI 框架或者复杂脚手架原生工程结构都没彻底理解之前框架只会给你增加黑盒。把原生四件套吃透以后无论遇到什么封装框架都能一眼看懂它到底改写了哪些文件。我在实际调试中还有一个固定习惯每当直觉排查超过十分钟没有进展就强制自己回到文件职责表——全局还是页面配置还是逻辑样式还是结构把问题归类到具体文件上九成以上的入门期问题都能快速解掉。希望这篇文件拆解能帮你省下那些曾经耗死我的排查时间。