
接手微信小程序项目第一步不是看代码能不能跑而是先把项目结构吃透。这几天交付了一个2048小游戏的微信小程序源码工程2048-小程序.zip不少朋友拿到压缩包后第一句话就问这些文件夹和文件都是干什么的为什么游戏页面有四个同名文件app.json能不能乱动图片到底该放哪个目录这些问题其实都指向同一件事——微信小程序的目录结构怎么组织。这篇文章就拿2048这个小项目当线索把微信小程序的整体项目结构拆开讲透适合刚转小程序开发、拿到现成工程不知道怎么下手、或者页面写了不少但心里没底的朋友。1. 先从整体看小程序项目到底长什么样1.1 根目录下那几样“定海神针”打开2048-小程序.zip解压之后最显眼的是三个app开头的文件app.js、app.json、app.wxss外加project.config.json和sitemap.json然后是pages文件夹、utils文件夹可能还有一个images目录。很多从网页开发转过来的朋友会下意识找index.html然后发现根本没有这个文件——小程序没有“单个HTML入口”的概念它的入口是app.json里注册的页面列表列表里排第一的那个页面就是启动页。app.js、app.json、app.wxss这三个文件是全局层面的“三件套”。app.js负责创建小程序实例定义全局数据globalData和生命周期钩子app.json是全局配置文件决定页面注册、窗口样式、tabBar、分包等app.wxss是全局样式表里面写的样式对所有页面生效。可以类比网页项目里的入口文件加全局CSS但区别在于这三个文件的名字是写死的改一个字母小程序就不认了新建项目时官方模板会帮你生成好日常维护中基本不用大动。project.config.json是开发者工具的项目配置包含appid、编译设置、代码保护开关等属于“工具向”文件。sitemap.json控制页面能否被微信搜索索引到是个容易被忽略但上线后会咬人的角色。这两者的细节我放到后面专门说先在脑子里记住根目录下这五个文件各有分工前三个管运行后两个管工程化和搜索。1.2 pages目录每个页面一个文件夹的垂直约定小程序把每个页面拆成一个独立文件夹里面放四件套wxml相当于HTML、wxss相当于CSS、js逻辑、json页面级配置。2048项目里pages/index是首页菜单pages/game是游戏主界面。每个页面的文件夹名和文件主名保持一致比如pages/index/index.wxml这种写法这是官方推荐的命名方式虽然不是硬性语法要求但调试时一眼能定位到页面维护成本低很多。页面文件作用网页里的对应物index.wxml页面结构index.htmlindex.js页面逻辑与数据index.jsindex.wxss页面样式index.cssindex.json页面局部配置没有直接对应物很多新手问为什么不能像网页那样把所有页面放根目录、把js和css分开放到不同文件夹里因为小程序的页面是一个“整体”四件套放在同一个目录下复制、删除、迁移页面时直接把整个文件夹搬走就行不会出现改了HTML忘了改CSS的问题。这种“垂直化”组织方式在小项目里看不出优势一旦页面数量超过二十个管理起来比扁平结构省心得多。utils目录放公共模块比如工具函数、常量、请求封装。2048项目的棋盘生成、滑动逻辑就放在utils/board.js里页面js只负责调用。图片资源按惯例放images目录音频和通用组件分别用audio和components目录。这套划分不是微信强制要求的但社区项目基本都按这个套路组织交接和协作成本最低。2. 三个app文件角色定位与配置要点2.1 app.json页面的“户口本”先注册才能用app.json是小程序最核心的配置文件第一项pages数组罗列了所有页面路径第一个路径就是冷启动时的首页。2048项目的app.json大概长这样{ pages: [ pages/index/index, pages/game/game ], window: { navigationBarBackgroundColor: #f8f8f8, navigationBarTitleText: 2048, navigationBarTextStyle: black }, style: v2, sitemapLocation: sitemap.json }有一点必须说透凡是在pages里注册的路径对应文件夹下必须存在同名的wxml、js等文件否则编译直接报错。反过来你新建了页面但忘了在pages里注册程序运行时找不到这个页面跳转一调用就是白屏。我自己的习惯是新增页面永远先改app.json再创建文件这样编译期就能发现路径写没写对而不是运行到一半才报错。window字段用来配置全局导航栏和窗口表现navigationBarTitleText是标题文字navigationBarBackgroundColor是背景色navigationBarTextStyle只支持black和white两个值。深色导航栏配white文字浅色配black配错了文字就藏在背景里看不清。backgroundColor和backgroundTextStyle配的是下拉刷新时露出的背景色别漏了。配置改动后需要重新编译才能在模拟器里生效这个细节也能解释为什么有些人改了半天标题没变。2.2 app.js入口逻辑与globalData的正确用法app.js里只有一个App()调用里面可以写onLaunch、onShow这类全局生命周期也可以放globalData。2048项目里我把最高分存在globalData里这样首页和游戏页都能读。但注意globalData不是响应式的——你在一个页面里改了globalData另一个页面不会自动感知。想实现跨页面响应得配合wx.setStorageSync或者自己封装事件机制这点是新手最容易踩的坑。很多人问app.js里能不能写业务逻辑。我的建议是只放全局初始化、登录态处理、公共数据声明别堆业务代码。小程序有明确的页面生命周期业务留在Page()里最合适全局层一旦膨胀排查问题就像在大杂烩里挑豆子。onLaunch里一般做启动时的全局请求或者本地缓存读取onShow和onHide做主页面切换时的统计上报仅此而已。2.3 app.wxss全局样式的地基app.wxss里定义的样式是所有页面的“地基”。写2048时我把公共颜色变量、通用flex布局、按钮基础样式都放在这里页面里只写差异化样式。需要注意小程序CSS能力是受限的不支持通配符*、不支持属性选择器、很多伪类选择器也不好使。超纲写法在开发者工具里可能正常渲染真机上可能直接失效所以全局样式尽量只用class和标签选择器。rpx是小程序里最推荐的尺寸单位它的设计原理是把屏幕宽度统一分成750份1rpx等于屏幕宽度的750分之一所以不同机型上设置同样数值的rpx能保持视觉比例一致。iPhone 6宽度375px换算1rpx就是0.5pxPro Max这类宽屏上1rpx约等于0.55px元素的物理尺寸会变大但视觉比例稳定。用px写死的布局在不同屏幕上很容易错位除非是边框、阴影这类必须精确到像素的细节。2.4 project.config.json和sitemap.json容易忽略但会咬人的文件project.config.json保存的是开发者工具的项目设置最常见的坑发生在换人或换机器接手工程时。这个文件里绑定的appid如果和当前登录账号不一致预览和上传都会失败。我接手过不止一个项目代码逻辑一点问题没有真机预览一直报“项目配置无效”最后打开project.config.json一看appid还是前一家公司的。解决方法是把appid改成自己小程序账号的或者用测试号开发。sitemap.json控制的是页面能否被微信搜索收录。默认内容允许索引所有页面{ rules: [ { action: allow, page: * } ] }如果某些内部页面不想被收录可以单独列出来配disallow。sitemap.json不影响跑代码但影响上线后的搜索收录很多人交付工程时忽略它等到运营反馈搜不到小程序才追悔莫及。3. 页面四件套一个页面是怎么撑起来的3.1 wxml小程序版的HTML组件才是主角wxml和HTML最大的区别不是语法格式而是组件体系。日常开发中view、text、image、scroll-view都是内置组件分别对应div、span、img和滚动容器。2048游戏页面的棋盘我用view套view通过wx:for循环渲染view classgrid wx:for{{tiles}} wx:keyid view classtile tile-{{item.value}}{{item.value}}/view /viewwx:for是wxml的循环指令等价于前端里的Array.map循环渲染。wx:key必须给否则列表更新时小程序会全量重新渲染性能差不说还会出现数据错位的怪异问题。数据绑定用双花括号{{}}它只支持表达式不支持完整JavaScript{{a b}}可以写{{if (a) { return b }}}就不行。wxml里没有DOM操作API找不到document.querySelector这类东西。想改一个格子的样式或数据唯一的途径是setData。声明式思维最关键的点是“改数据视图跟着变”不要试图去操作节点。很多从jQuery转过来的朋友习惯先找到节点再改属性这套在小程序里行不通需要花点时间切换思维。3.2 页面jsPage()里到底装了什么页面js的核心结构是Page({})里面放data、生命周期函数和自定义方法。data是页面的初始数据渲染层通过{{}}读取。生命周期里最常用的是onLoad它接收页面跳转携带的参数可以在这里初始化数据。2048的game.js在onLoad里读取难度参数然后调用utils里的棋盘初始化方法。setData是页面js里最重要也最危险的API。它负责把新数据送到渲染层并精确更新变更的数据路径但单次setData的数据量不能太大。长列表场景千万别一次性setData塞100条完整记录分页加载才是正路。还有一个细节setData其实是异步的虽然调用后data立即更新但视图层的渲染是分批完成的如果你紧接着想读取渲染结果最好放在回调里。页面自定义方法不需要手动bind在Page对象里定义就行模板里通过bindtap这些事件前缀绑定。方法之间用this调用注意this指向的是页面实例别在回调函数里把this搞丢了。3.3 页面wxss与页面json局部样式就近覆盖页面wxss的写法和app.wxss一致只是作用域限制在当前页面不同页面之间同名class不会互相污染。页面json做局部配置最常用的是navigationBarTitleText用来覆盖全局window里同名字段。2048的game页面标题写“游戏中”index页面写“2048”这种局部配置很灵活。页面json与app.json的window是“就近覆盖”关系页面json写了以页面为准没写的继承全局。利用这个机制可以给不同页面配不同导航栏颜色和标题不需要在全局统一。除了标题useNavigationBar的开启关闭、enablePullDownRefresh的下拉刷新开关、usingComponents的组件注册都在页面json里配置。页面json里还有一个容易忽略的字段disableScroll设为true可以禁止页面整体滚动。做2048这类需要固定视口的游戏页面时这个字段能避免手指滑动时页面跟着跑。3.4 页面跳转与路径约定结构直接决定写码体验页面跳转有两个常用APIwx.navigateTo和wx.redirectTo。前者保留当前页面入栈后者直接替换。2048里从首页进入游戏用navigateTo并带上参数wx.navigateTo({ url: /pages/game/game?level3 })目标页面的onLoad里通过options.level拿到这个3。参数只能是字符串传对象要先JSON.stringify接收端再JSON.parse。路径必须是绝对路径从项目根目录开始写以/开头否则跳转会找不到页面。这就是为什么页面文件夹命名和层级直接影响开发效率——结构起得合理路径就短。navigateTo的页面栈上限是10层超过10层再调用会失败。做商城、题库这类多层跳转的流程时要么用redirectTo替换要么及时用navigateBack回退。2048只跳一次层暂时碰不到这个限制但提前知道能省不少排查时间。4. 从2048实战看结构与功能的对应关系4.1 页面规划首页与游戏页的职责边界2048这个项目我规划了两个页面index是菜单页放游戏标题、最高分、开始按钮game是核心棋盘页负责渲染4x4的格子、处理滑动逻辑、判定胜负。页面职责要单一别把十几个功能塞进一个页面——这是为什么要坚持“一页四件套一文件夹”规范的根本原因。页面之间的公共逻辑比如棋盘初始化的随机数生成、滑动坐标换算放在utils/board.js里用CommonJS风格导出页面里require进来。小程序原生支持这种模块化和Node的写法一致。把公共方法抽到utils最大的好处是页面js变薄、逻辑可以脱离开发者工具单测——本地直接node require这个模块跑测试不用每次打开模拟器点半天。4.2 新增一个页面的完整操作路径新手最需要掌握的其实是“新增页面”的标准动作以给2048加排行榜页为例在app.json的pages数组里加上pages/rank/rank在pages文件夹下新建rank目录在rank目录里新建rank.wxml、rank.wxss、rank.js、rank.json四个空壳文件每个文件写最基础内容wxml放一个viewjs写一个Page({})json里配下标题保存后看编译日志没有报错表示注册成功在入口页面调用wx.navigateTo({url: /pages/rank/rank})核心原则就是注册在前、使用在后顺序反了或者路径写错编译期或运行期都会给你颜色看。如果嫌手动建四个文件麻烦可以在开发者工具左侧资源栏右键选择“新建Page”工具会自动生成四件套模板比自己复制粘贴省事得多。4.3 分包与组件化小项目也要预留的结构边界2048这种小项目用不上分包但有必要打个预防针微信对主包体积有2MB的限制超出后必须把部分页面单独打成“分包”在app.json里用subpackages字段声明。实际开发中主包放首页、登录、公共组件商品列表、订单详情全部扔进分包首屏加载能快一截也从根上绕开2MB红线。组件化方面小程序有原生Component构造器比Page多了properties、observers这些特性。2048的格子动画如果复用得多完全可以把tile抽成组件。组件目录通常叫components一个组件一个文件夹同样是四件套加一个jsonjson里要声明component: true。如果项目还在早期我会建议先把组件边界想清楚不然等页面写大了再回头抠逻辑出来比最开始按组件写费事多了。5. 常见问题与排查技巧实录5.1 路径大小写与真机白屏最经典的问题开发者工具里一切正常一上真机就白屏或者某个页面在模拟器能打开、真机点进去直接报错。绝大多数情况是路径大小写不一致。Windows文件系统不区分大小写但真机的Linux内核区分pages/index和pages/Index在Windows下是同一个路径在真机上就是两个完全不存在的路径。解决方案简单粗暴路径全部小写文件夹名、文件名、app.json注册路径、跳转url全部统一小写养成肌肉记忆。5.2 导航栏高度与安全区适配导航栏是由系统渲染的页面内容如果从顶部开始排版会被盖住。自定义导航栏时需要动态获取右上角胶囊按钮的位置官方API是wx.getMenuButtonBoundingClientRect()能拿到胶囊的坐标和宽高再配合windowHeight计算就能精确定位自定义导航栏的高度。不要硬编码64px或88px不同机型的胶囊位置不一样我见过最惨的案例是开发按iPhone 12写死导航栏高度老板的Android机上按钮直接叠在胶囊上方点都点不了。底部适配同样重要iPhone从底部刘海屏开始就有安全区概念样式里用env(safe-area-inset-bottom)处理底部按钮栏、tabBar都要考虑进去。2048虽然是个全屏游戏底部照样预留安全区边距不然iPhone用户会觉得按键被“吃掉”了一截。5.3 包体超限与加载慢的排查运行项目时如果报“代码包大小超出限制”先检查images目录。无压缩的高清png是触发2MB红线的最常见元凶电商类项目尤其严重。用开发者工具栏的“代码依赖分析”查看各个包的占用一般能一眼抓出大头。做了分包之后还要注意公共代码的引用关系。utils被主包和分包同时引用时不会重复拷贝但如果分包里引用了主包的大模块仍然会拖慢分包加载。还有一个冷门坑wxss里用background-image引用本地图片图片是计入包体的即使这张图只在某个分包页面里用只要被公共样式引用就会被打包到主包体积瞬间失控。5.4 发给别人试用的正确姿势如果要让其他人体验开发中的小程序不要发压缩包让对方自己导入直接在开发者工具里点“预览”按钮会生成一个二维码手机微信扫码就能打开。要是想让一批人同时测试就先点“上传”在公众平台的版本管理里把上传的版本设为体验版再把体验版二维码发给测试人员。收集反馈时建议让测试的人直接录屏加语音不要只发截图和文字。小程序是交互型产品截图看不出动效和手势问题录屏记录滑屏路径排查起来效率高得多。调试接口问题用开发者工具的Network面板和真机调试模式看console日志就够了日常90%的网络排查不用折腾第三方方案。5.5 异常现象速查表现象可能原因排查方向编译报“页面文件不存在”app.json注册了未创建的页面核对pages数组与目录文件真机白屏、模拟器正常路径大小写不一致检查所有路径是否全小写导航栏标题改了没反应app.json修改未重新编译重新编译确认页面json无覆盖setData改了但视图不动数据路径写错或setData被异步覆盖检查this指向和data路径跳转黑屏页面未注册或页面json语法错误先看app.json再看页面json体验版不在列表未在公众平台设为体验版版本管理里切换体验版本这张表是我每次接手项目都会贴在工位边的排查清单大多数新手报上来的问题都能对号入座。最后再分享一个我踩过好几回才长记性的习惯每次新建页面或移动文件后第一时间重编译一次再回app.json看一遍pages顺序和路径最后用真机预览一次。项目文件结构这件事看起来是“文件放哪”的小事实际上是整个开发效率的地基结构一旦乱了后面的跳转、样式、打包、协作都会连坐。2048这个项目用最小的体量把标准结构完整演示了一遍照这套思路去组织你自己的页面后面无论接到多复杂的项目心里都会比较有底。