ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

小程序代码构成剖析:全局与页面四件套协作机制

小程序代码构成剖析:全局与页面四件套协作机制 如果你第一次拿到一个小程序项目的源代码第一反应大概率是这个目录跳来跳去到底哪些文件才算代码我当年刚开始接触小程序时面对的正是这种困惑——wxml、wxss、js、json 四个后缀来回交替加上根目录的 app.js、app.json、app.wxss完全不知道先看哪个文件。等到把这些文件按职责拆开梳理才发现小程序代码的构成其实非常固定所有逻辑就是全局三件套 页面四件套 工程配置文件。这篇文章的核心不是把官方文档重新抄一遍而是把小程序代码到底由哪些部分组成、每个部分之间怎么协作这条主线讲清楚。如果你正准备接手别人的小程序项目、从 Web 开发转向小程序或者正在纠结原生开发和多端框架顺着下面这条线走几乎所有常见问题都能在代码层面找到原因。1. 小程序代码的整体骨架从全局三件套开始看1.1 app.json页面路由、窗口表现、底部 tabBar 的配置中心正式打开仓库的第一步我习惯先看根目录的 app.json因为它决定了一个小程序的入口在哪。pages字段是一个字符串数组每一项都是一个页面的路径路径不需要写后缀而且数组的第一项就是小程序冷启动时展示的页面。我实际部署过一个「小程序商城」项目当时顶层页面有首页、分类、购物车、个人中心四个 tabjson 配置看起来像这样{ pages: [ pages/index/index, pages/category/category, pages/cart/cart, pages/mine/mine ], window: { navigationBarBackgroundColor: #f8f8f8, navigationBarTitleText: 商城, navigationBarTextStyle: black }, tabBar: { color: #999999, selectedColor: #ff6b35, list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/category/category, text: 分类 }, { pagePath: pages/cart/cart, text: 购物车 }, { pagePath: pages/mine/mine, text: 我的 } ] }, sitemapLocation: sitemap.json }很多第一次接触的开发者会问为什么 tabBar 里已经配置了首页pages 数组里的第一项还是首页其实 tabBar 只是表示底部导航要展示哪些页面跟启动页无关。pagePath必须和pages数组中的路径完全一致否则运行时 tab 会显示不出来。这里要提醒两个实际踩过的坑。第一app.json 不允许写注释不可能像 JS 那样用//临时跳过某个页面只能整段删除再改回。第二配置window时navigationBarTextStyle只有 black 和 white 两个值这跟微信顶部文字颜色的设计限制有关如果你传了灰色小程序不会在编译期报错但线上表现会变成黑白色视觉效果和预期完全不一样。除了这些app.json 里还有subPackages分包、permission权限说明、usingComponents全局组件声明等字段。分包配置在大型项目中尤其关键。微信对主包体积有 2MB 的限制超过这个阈值就塞不进正式包了所以商品详情这类低频页面从一开始就应该规划到分包里去。真实项目里我见过团队做到第 3 期需求时才发现主包超限最后只能加班拆包非常痛苦。1.2 app.js全局生命周期和 globalData 的真相根目录第二个必须看的文件是 app.js。小程序代码的构成里app.js 是全局逻辑的唯一入口它的作用是调用App()注册小程序的顶层实例。生命周期方法有三个onLaunch在代码包初始化完成后触发onShow在小程序从后台进入前台时触发onHide在小程序进入后台时触发。我在不少项目里遇到过同一个问题团队把登录鉴权、获取用户信息、初始化第三方 SDK 全堆在 onLaunch 里然后在页面里同步判断取状态。这会引发一个很隐蔽的隐患——Page 里的 onLoad 可能先于某个异步初始化结束就执行导致首屏数据请求带上一个空 token。接口如果没有做重试用户看到的白屏就成了定局。更稳妥的做法是把初始化逻辑封装成 Promise在页面里等状态就绪再发请求或者把所有请求统一封装在 session 过期时自动跳转登录页。globalData是挂在 App 实例上的普通对象页面里通过getApp().globalData访问。它适合存进程级的信息比如登录 token、设备信息、版本号但不适合当数据库用。举个真实例子你在首页塞了一个购物车数组订单页改了它结算页又改了它最后你根本分不清哪里是唯一数据源代码很快就难以维护。跨页面共享业务数据我习惯用一个轻量的 store 来管理这部分放到后面工程化章节展开。// app.js App({ onLaunch() { this.globalData.version 1.0.0 this.initUser() }, globalData: { userInfo: null, token: } })1.3 app.wxss 与工程配置文件公共样式和项目元信息app.wxss 是全局样式表所有页面都会应用这里定义的样式。我一般在这里放三类内容通用颜色变量、页面级的初始化样式、常用工具类。由于小程序里的组件有样式隔离app.wxss 默认不会穿透到自定义组件内部但页面 wxml 直接写的标签是可以被全局样式覆盖的所以很多团队把 button、input 的 reset 样式放这里。page { background: #f5f5f5; font-size: 28rpx; color: #333; }project.config.json存放编译相关的项目配置比如 appid、编译模式、代码压缩等。这里有一个真实的协作隐患开发工具在你本机改动 project.config.json 后如果没有约定统一 appid提交到 git 的配置文件就可能带着你个人的测试号。别人拉下来直接编译发现无效的 appid或预览失败排查半天才发现是配置文件的锅。我的建议是project.config.json 由团队固定一份模板提交project.private.config.json 通过 gitignore 排除本地私有的编译配置不要进入仓库。2. 页面级代码一个页面是怎么跑起来的2.1 WXML结构层的模板语法每个页面在 pages 下都有四个同名文件其中.wxml负责描述页面结构。很多人说 WXML 长得像 HTML确实像但它的底层逻辑更接近 Vue 的模板。最核心的是数据绑定JS 里 data 的数据会同步渲染到 WXML 的{{ }}插值中。比如 index 页面的 js 里写了data: { userName: 张三 }wxml 里写text{{userName}}/text页面就能显示出来。除了插值WXML 还有几个高频语法。wx:for用于列表渲染wx:if、wx:elif、wx:else用于条件渲染事件绑定用的是bindtap、catchtap这类属性。下面是一个简单的商品列表片断view classgoods-list view wx:for{{goodsList}} wx:keyid classgoods-item bindtaponGoodsTap >const newList this.data.list.concat(newItem) this.setData({ list: newList })小程序是数据驱动视图但不代表你可以直接改 data 里的对象属性而不调用 setData视图不会自己刷新。还有一点setData的数据量有个经验上限每次尽量控制在 1MB 以内。频繁把大体积字段塞进 setData页面会在低端安卓机上出现肉眼可见的白屏和掉帧。2.4 页面 json导航栏标题、窗口表现与组件引用页面.json是页面级配置优先级高于 app.json 里的 window 配置。最常见的用途是把导航栏标题改成当前页面专属的标题比如商品详情页写商品详情、支付结果页写支付结果。配置很简单{ navigationBarTitleText: 商品详情, usingComponents: {} }看到usingComponents字段很多人会问它和 app.json 里的 usingComponents 是什么关系。其实页面级优先全局声明了公共组件后页面里可以复用但如果某个组件只有少数页面用我倾向放在页面级引用这样能减少主包体积并降低全局命名冲突。要注意的是在页面 json 中引用的组件路径和实际目录必须一致否则开发工具虽然不报 js 错误页面渲染时组件就是空白。3. 从源码到运行编译、生命周期与多端框架3.1 小程序代码是怎么被编译执行的小程序最终并不是直接在微信里跑源码而是逻辑代码会被打包成一个 wxapkg 包开发工具和手机微信都基于这个包运行。从代码构成的角度看wxml 会被编译成渲染层可执行的虚拟节点树js 跑在逻辑层两层之间通过事件和数据协议通信。理解这一点对排查问题特别有用。比如你在 wxml 里直接写Math.random()这类表达式会发现有些基础库上运行结果不稳定因为模板表达式的能力是受限的复杂业务还是要提前计算好放进 data。再比如某些 CSS 属性在预览时不生效但又没报错很可能是编译后渲染层版本不支持开发工具和真机基础库版本不同表现自然不同。3.2 页面生命周期里代码怎么放页面生命周期有 onLoad、onShow、onReady、onHide、onUnload。onLoad 适合做参数解析和初始化请求onShow 适合做每次进入页面的数据刷新onReady 通常意味着页面初次渲染完成可以安全地操作一些跟渲染相关的 APIonHide 和 onUnload 则适合清理定时器、中断请求。我遇到过一个典型的 bug用户在 A 页发起支付跳到微信支付再回到 A 页如果数据刷新只写在 onLoad 里支付回来后的订单状态不会立刻变化。后来把刷新动作挪到 onShow 里问题才解决。这个分工模式几乎适用于所有从外部返回页面需要刷新的场景。3.3 原生开发还是 uni-app/Taro小程序代码的构成方式直接决定了技术选型。原生开发的好处是接口跟手、调试方便、新功能支持最快代价是你只能处理微信一个平台。uni-app 和 Taro 这类多端框架会把源码编译成小程序代码以及其他平台代码如果团队既要小程序又要 App同时写两套原生会很浪费用多端框架能节省成本。我的经验是只有微信小程序需求的特别是对稳定性要求较高的项目优先原生强调多端复用、有 Vue/React 团队基础、需要快速迭代业务的活动页用 uni-app 或 Taro 更划算。多端框架的坑也明确平台差异始终存在。比如在 uni-app 里写死了某个 css 属性iOS 和 Android 表现可能不同跨平台代码不等于零成本替换。3.4 请求域名、服务器部署和自己电脑当服务器小程序对网络请求有明确限制request 的合法域名需要配置成 HTTPS开发工具里可以勾选不校验合法域名来临时调试但真机预览时必须使用已在小程序后台配置的域名。很多人会问家里电脑当服务器可以部署小程序吗本质上可以只要你有一个公网能访问到的服务器并且绑定了备案域名、配置了 HTTPS 证书。但家庭宽带的公网 IP、端口开放、稳定性都会成为现实瓶颈所以实际项目几乎都选择云服务器而不是本地电脑。这也算是一条代码之外的基础设施常识做项目规划时越早了解越好。4. 工程化组织目录、组件、状态和实用技巧4.1 目录结构怎么划分讲完每个文件做什么剩下的就是怎么把小项目组织成能长期维护的工程。我常用的目录划分是├── app.js ├── app.json ├── app.wxss ├── pages/ # 页面 │ ├── index/ │ └── goods-detail/ ├── components/ # 自定义组件 ├── utils/ # 工具函数 ├── services/ # 接口封装 ├── assets/ # 静态资源 └── store/ # 状态管理pages 目录下每个页面一个文件夹services 里按业务域拆接口文件比如 user.js、goods.js、order.jsutils 放时间和字符串处理等纯函数。接口封装不要直接写在 Page 里因为多个页面复用同一个接口时改一处路径就要全局搜索麻烦得很。4.2 自定义组件把 UI 和逻辑一起抽出去自定义组件是小程序代码构成里很值得用的一项。通过Component()注册props 用properties定义组件自己的状态放data方法放methods。一个简单的删除确认弹窗组件可能是这样Component({ properties: { visible: Boolean, title: String }, data: {}, methods: { onCancel() { this.triggerEvent(cancel) }, onConfirm() { this.triggerEvent(confirm) } } })triggerEvent可以把组件内部事件抛给页面处理这是父子组件通信的正确姿势。有些新手习惯在组件内部直接改 properties虽然能跑但数据流会变成一团乱后续排查问题特别痛苦。properties 应该视作只读要改就通过事件通知父级改。4.3 全局状态和页面间通信小程序本身没有 Vuex 那种官方库但项目大了之后全局状态还是得管理起来。我常用的方案很简单写一个 store 模块模块内部持有数据同时暴露修改数据的方法页面引入后通过 subscribe 机制感知变化。核心代码其实很少const listeners [] const state { cartCount: 0 } export function getState() { return state } export function setState(partial) { Object.assign(state, partial) listeners.forEach(fn fn(state)) } export function subscribe(fn) { listeners.push(fn) return () { const i listeners.indexOf(fn) if (i -1) listeners.splice(i, 1) } }这样比到处getApp().globalData.xxx 1清晰也比引入一个大库更轻量。页面在 onLoad 里 subscribe在 onUnload 里取消订阅避免内存泄漏。项目规模到几十个页面时这种极简方案往往够用。4.4 动态设置标题、缓存时间和顶部导航栏高度这三个是搜索词里非常高频的小问题也是实际开发中几乎必遇到的。动态设置标题小程序提供wx.setNavigationBarTitle。我常用的写法是在页面 onShow 里根据业务数据调用比如订单详情页订单加载成功后把标题改成订单详情如果没加载完就保持默认标题。注意这个 API 对字符长度有上限10 个字符之内体验最好。缓存时间更偏工程策略。微信的wx.setStorageSync只有设置、读取、清除三个动作本身不提供过期机制。想要缓存 5 分钟需要自己包一层function setCache(key, value, ttl) { wx.setStorageSync(key, { value, expire: Date.now() ttl }) } function getCache(key) { const data wx.getStorageSync(key) if (!data) return null if (Date.now() data.expire) { wx.removeStorageSync(key) return null } return data.value }这样首页接口数据、用户操作记录这类内容就能很稳妥地做本地缓存不至于出现永不过期的脏数据。顶部导航栏高度一般出现在自定义导航栏的场景。微信会给小程序页面默认加一个系统导航如果你想让头部更个性化就需要隐藏默认导航自己用 view 模拟。胶囊按钮的位置可以通过wx.getMenuButtonBoundingClientRect()拿到再结合wx.getWindowInfo()拿状态栏高度去做换算。网上很多代码片段过时正是因为基础库版本差异建议抽一个工具函数统一维护不要让每个页面都粘贴一份。5. 常见问题与排查技巧实录5.1 开发版小程序已过期请重新扫码这个报错几乎每个用开发者工具的人都会遇到。微信开发版和体验版都有时间限制过期后再次预览就会出现这个提示。解决办法很简单回到微信开发者工具重新点击预览用手机扫码后调起一个新的开发版即可。这个报错不代表代码有问题只要不是刚发布的版本都正常。这里有一类更隐蔽的情况你在开发者工具里改了代码后、没有重新上传手机上却发现旧代码还在运行。这是因为手机上的开发版预览包在你扫码那一刻就固定下来了后续工具改动不会自动同步到真机必须重新点预览或上传再扫码才能拿到新包。5.2 动态设置标题为什么没生效如果你在页面 json 里已经写了navigationBarTitleText又在 JS 里通过wx.setNavigationBarTitle动态改偶尔会遇到标题没变。我把这类问题的排查顺序固定为先确认调用时机不要在 onLoad 里过于靠前的位置调用这个 API最好放在 onShow 或数据回调完成之后再检查是不是页面被复用了比如同一个页面处理多个订单详情但标题却写到了旧的变量上最后看基础库版本太旧的版本对 API 支持不一致。5.3 页面数据缓存没有更新的真相很多人把wx.setStorageSync当成 HTTP 缓存用结果发现明明删掉了缓存页面还在显示旧数据。查下来多数情况不是 storage 的问题而是接口请求自身的响应头缓存。微信小程序的 request 默认会遵循一些缓存策略如果你服务器返回的 header 里带上了强缓存标记或者你在代码里对请求 URL 写了固定参数数据就很容易被中间层缓存住。解决办法是给 URL 加时间戳参数或者在服务端把响应头的 Cache-Control 设为 no-cache。另外wx.setStorageSync是同步方法数据量很大时会在逻辑层阻塞渲染超过一定体量的存储建议拆分成多个 key 管理而不是塞进一个大对象里。5.4 关于反编译和代码保护我一直想说实话微信小程序的线上代码包是 wxapkg 文件存放在手机本地确实存在一些工具可以解开它看到 wxml 还原结构和部分 js 逻辑。这是技术上的现实。所以如果你把核心算法、密钥、业务流程全都写在源码里别人拿到包就等于拿到了实现。我的真实建议是高价值逻辑放到服务端小程序端只做展示与交互校验重要的配置和密钥放在后端通过接口下发。代码保护永远是概率事件安全设计不要建立在别人反编译不了的假设上。5.5 几个容易被忽略的小坑单选框和复选框这类控件在小程序里坑最多。radio-group 里的 radio 一定要保证同一个组内 name 唯一否则多个 radio 的状态会互相影响checkbox 的 value 值不要重复。另一个高频问题是点击事件嵌套如果列表项里同时用了 catchtap 和 bindtap事件冒泡会非常混乱。建议先在开发者工具的 wxml 面板里查节点层级再决定用 bind 还是 catch。视频类内容则是另一个高频需求。比如微信小程序中的视频下载这类搜索词我要明确一句业务里不要试图用代码去扒别人的视频源平台对视频下载有明确的接口边界。你在自己的项目中接入 video 组件、渲染自己的视频地址是合法且正常的但把爬取或下载逻辑写进小程序代码既不稳定也可能违规。这类灰色做法不建议碰更不值得为短期功能赌上一个正式号的稳定性。最后说一个我自己的习惯。每次接手一个新项目我不会急着看业务函数而是先花半小时把根目录和页面目录扫一遍在纸上记下每个文件承担的角色标出生命周期里的关键动作。小程序代码的构成比大型后端服务清爽得多它没有隐藏的魔术只有固定分工的几个文件。把每个文件到底负责什么这个问题彻底想清楚之后再复杂的页面你也能做到心里有底。
返回列表