尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

小程序项目结构设计实战:从目录规范到分包加载

小程序项目结构设计实战:从目录规范到分包加载 接手过不少乱七八糟的小程序项目之后我对一个结论越来越笃定项目结构这东西看着不起眼却是决定小程序后期好不好维护、能不能多人协作、上线之后还敢不敢改代码的关键。很多人拿到一个小程序项目第一件事是跑起来看页面结果跑是跑起来了真到要加个功能、换套UI、切个分包的时候才发现结构一团乱组件散落、公共方法重复、全局变量满天飞改一行代码要翻三个目录。这篇文章想跟你认真聊一聊小程序的项目结构——从目录设计的底层逻辑到app.json、app.js这些核心文件的职责再到页面结构、组件化、分包加载、状态管理这些日常开发绕不开的内容最后把我踩过的坑一并整理出来。不管你是刚接触小程序的新人还是已经开始接手完整项目的同学这篇内容都能帮你把项目的骨架搞清楚少走不少弯路。1. 项目结构的核心思路先想清楚再动手1.1 框架选型原生、uni-app 还是 Taro聊项目结构之前必须先定技术栈。不同技术栈的小程序项目结构差异很大这个决定了你整个目录怎么搭。目前主流选择基本是三种原生微信小程序、uni-app、Taro。原生小程序的好处是直接、干净官方文档完整运行的性能和底层 API 访问也最直接适合纯微信端的项目。uni-app 和 Taro 则主打多端复用一套代码编译到微信小程序、支付宝小程序、H5 甚至 App。代价就是多了编译层和一套自己的目录约定遇到跨端兼容问题时需要查框架文档。我个人的建议是如果业务只做微信端没有明显的多端诉求直接用原生小程序项目结构最简单、排查问题最直接。如果公司明确说以后要覆盖多端再考虑 uni-app 或 Taro。最怕的是项目做到一半想从原生迁过去那基本等于重写别问我怎么知道的。1.2 目录设计从小项目到大项目的演进不管用哪种框架目录设计的原则都差不多按“业务功能”和“基础能力”两条线来组织而不是把文件乱扔。我来展示一个我在实际项目中比较推荐的原生小程序目录结构miniprogram/ ├── app.js ├── app.json ├── app.wxss ├── project.config.json ├── sitemap.json ├── pages/ │ ├── index/ │ │ ├── index.wxml │ │ ├── index.wxss │ │ ├── index.js │ │ └── index.json │ ├── order/ │ │ ├── order.wxml │ │ ├── order.wxss │ │ ├── order.js │ │ └── order.json │ └── mine/ ├── components/ │ ├── custom-nav/ │ ├── empty-view/ │ └── product-card/ ├── utils/ │ ├── request.js │ ├── format.js │ └── auth.js ├── apis/ │ ├── home.js │ ├── order.js │ └── user.js ├── store/ │ ├── index.js │ └── user.js ├── static/ │ ├── images/ │ ├── icons/ │ └── tabbar/ └── styles/ ├── variables.wxss └── common.wxsspages 放页面一个页面一个文件夹四件套放一起components 放公共组件utils 放无业务逻辑的纯函数工具apis 放接口请求按业务模块拆分store 放全局状态static 放静态资源styles 放公共样式变量。项目规模变大以后光靠这一层还不够可以考虑加一个modules或features层把某些业务域涉及的页面、组件、API、store 放在一起形成业务闭环。这个思路在大型项目里很有用避免一个人改了订单模块另一个人的页面受影响。2. 核心文件逐项拆解读懂项目的内在脉络2.1 app.json小程序的全局路由器与配置中心app.json是小程序的全局配置相当于整个应用的“总控台”微信开发者工具一打开项目就读这个文件。核心配置项我挑重要的说pages数组是最关键的它是小程序的页面路由表。数组第一项就是首页也就是启动时加载的页面。比如下面这样{ pages: [ pages/index/index, pages/order/order, pages/mine/mine, pages/detail/detail ], window: { navigationBarTitleText: 我的小程序, navigationBarBackgroundColor: #ffffff, navigationBarTextStyle: black, backgroundColor: #f5f5f5 }, tabBar: { list: [ { pagePath: pages/index/index, text: 首页, iconPath: static/tabbar/home.png, selectedIconPath: static/tabbar/home-active.png }, { pagePath: pages/order/order, text: 订单, iconPath: static/tabbar/order.png, selectedIconPath: static/tabbar/order-active.png } ] } }很多新手刚开始会把window里的navigationBarTitleText拿来当成页面标题全局设置结果发现每个页面标题都长得一样。其实全局配置只是一个默认值页面自己的.json文件可以覆盖它。这里有一个很隐蔽的点pages数组的顺序影响打包顺序也可以间接影响启动性能。主页面、高频访问页尽量往前放冷门页面可以往后放。小程序的主包代码体积限制是 2MB如果超出就需要把部分页面挪进分包。2.2 app.js全局逻辑和启动过程的“门卫”app.js是小程序的入口逻辑文件小程序启动时执行。它里面最重要的方法就是App({})包含onLaunch、onShow、onHide等生命周期函数以及globalData全局数据。onLaunch会先于所有页面加载执行适合在这里做登录态初始化、全局配置拉取、版本更新检测等操作。下面是一个常见示例App({ globalData: { userInfo: null, token: , systemInfo: {} }, onLaunch() { this.initSystemInfo() this.initLogin() }, initSystemInfo() { const systemInfo wx.getSystemInfoSync() this.globalData.systemInfo systemInfo if (systemInfo.statusBarHeight) { this.globalData.statusBarHeight systemInfo.statusBarHeight } }, initLogin() { const token wx.getStorageSync(token) if (token) { this.globalData.token token } else { this.login() } }, login() { // 登录逻辑拿到 code 后传给后端换取 token } })有一点请特别注意globalData只存在于内存中小程序切到后台再回来数据可能会被系统回收。所以像 token、用户信息这种重要数据一定要同步写到 storage 里。只依赖globalData等用户杀进程再打开小程序会踩“登录状态丢失”的坑。app.wxss是全局样式文件里面定义的类名在任意页面的 WXML 中都生效。注意这里有副作用全局样式会影响所有页面因此像.container这种通用类名尽量少写全局样式避免页面之间互相干扰。我在实际项目中会把全局app.wxss只放基础 reset、通用布局类和 CSS 变量。2.3 project.config.json 与 sitemap.json容易被忽略的基础配置project.config.json是开发者工具的项目配置文件保存了 appid、项目名称、编译设置、代码上传配置等。多人协作时这个文件很容易产生冲突尤其是每个人本地工具版本不一样字段会自动变化。经验之谈.gitignore里不要整个忽略这个文件但可以让团队成员统一工具版本减少差异。另外project.private.config.json是个人本地配置不该提交到仓库。sitemap.json控制小程序的页面是否可以被微信索引普通业务保持默认就好。如果不想某页面被索引需要单独配置disallow规则。3. 页面结构、组件化与状态管理3.1 页面四件套wxml、wxss、js、json 是怎么协作的一个小程序页面由四个文件构成.wxml负责结构.wxss负责样式.js负责逻辑和数据.json负责页面级配置。这个结构跟 HTML CSS JS 很像但有几个差异要特别留意。WXML 不能直接调用 JS 里定义的任意函数只能用 WXS 或小程序内置的表达式。页面 JS 的数据绑定遵循单向数据流数据在data里定义通过setData修改WXML 里用{{ }}引用。一个典型的页面 JS 结构Page({ data: { list: [], loading: false, pageNum: 1, hasMore: true }, onLoad(options) { this.loadList() }, async loadList() { if (this.data.loading || !this.data.hasMore) return this.setData({ loading: true }) try { const res await request(/api/list, { pageNum: this.data.pageNum }) this.setData({ list: [...this.data.list, ...res.list], pageNum: this.data.pageNum 1, hasMore: res.list.length 0 }) } finally { this.setData({ loading: false }) } }, onPullDownRefresh() { this.setData({ list: [], pageNum: 1, hasMore: true }, () { this.loadList() wx.stopPullDownRefresh() }) } })页面.json文件的配置会覆盖app.json的全局window配置。比如你想让某个页面单独设置导航栏标题就在页面 json 里写{ navigationBarTitleText: 订单详情, usingComponents: { product-card: /components/product-card/product-card } }usingComponents是组件的注册入口只有在这里注册过的组件才能在当前页面的 WXML 中使用。这也是项目结构里比较重要的一环——组件注册关系直接体现了页面与组件之间的依赖。3.2 组件化拆分的实战建议很多新手做项目所有页面都自己写一套 UI公共模块靠复制粘贴项目越到后期越难维护。组件化是解决这个问题的根本手段。判断一个模块该不该拆成组件我的标准很简单一个 UI 模块如果在两个以上页面出现且有一定复杂度就值得拆。比如商品卡片、空状态视图、自定义导航栏、订单状态标签这些都是典型的公共组件。拆分之后组件之间的通信方式是重点。父传子用属性// 子组件 properties properties: { title: { type: String, value: } }子传父用自定义事件// 子组件内部 this.triggerEvent(clickitem, { id: item.id })!-- 父组件 WXML -- product-card >const userStore { userInfo: null, listeners: [], setUserInfo(userInfo) { this.userInfo userInfo this.listeners.forEach(listener listener(userInfo)) }, subscribe(listener) { this.listeners.push(listener) return () { this.listeners this.listeners.filter(item item ! listener) } } } module.exports userStore页面可以这样使用const userStore require(../../store/user) Page({ onLoad() { this.unsubscribe userStore.subscribe(userInfo { this.setData({ userInfo }) }) }, onUnload() { this.unsubscribe() } })状态管理不要一步到位引入重型方案先看业务复杂度。只有两三个页面共享数据用globalData storage 就能解决。等业务规模大了再升级也不迟。4. 分包加载与性能优化4.1 主包分包设计把代码体积控制住微信小程序主包体积限制 2MB超过就没有办法上传发布。如果一个普通的商城项目把所有页面都堆在主包里很容易就超限。分包是最好的解决办法。分包的设计思路是主包只放启动页、tabBar 页面、公共组件和公共工具其他业务页面按模块拆到分包里。比如{ pages: [ pages/index/index, pages/mine/mine ], subpackages: [ { root: packageOrder, pages: [ pages/order-list/index, pages/order-detail/index ] }, { root: packageGoods, pages: [ pages/goods-list/index, pages/goods-detail/index ] } ] }分包不是绕过体积限制的“偷懒”手段它是一种代码组织方式需要配合页面职能来划分。我常用的划分维度是用户完成一条完整业务链路所涉及的那些页面放到同一个分包。比如下单流程购物车、确认订单、支付结果、订单详情放一个包用户信息个人资料、地址管理、设置放一个包。4.2 独立分包与分包预下载把启动速度提上去独立分包是小程序一个很好用的特性。独立分包可以不依赖主包的其他页面而独立运行尤其适合分享页、扫码进入的页面。举个例子用户通过分享卡片进入商品详情页如果详情页在分包里传统分包需要下载完主包才能进入。但独立分包可以做到只下载这个分包就能直接打开页面加载速度明显快很多。配置方式跟普通分包类似多一个independent: true字段{ subpackages: [ { root: packageShare, pages: [ pages/goods-share/index ], independent: true } ] }分包预下载则适合用户进入小程序后有很高概率访问的分包。比如用户进首页后大概率进入商品列表就可以在首页对应的分包配置里预下载商品包{ preloadRule: { pages/index/index: { network: all, packages: [packageGoods] } } }预下载也不是越多越好。包体积太大、下载消耗流量多实际访问率又低反而拖慢首屏。我的习惯是只对真实点击率超过 50% 的分包做预下载。4.3 体积优化和首屏加载的实操经验代码体积优化有几个点图片压缩放在静态资源里,不要直接把设计稿的大图塞进去组件库按需引入公共代码尽量复用不要到处都是冗余的 JSON 配置文件。首屏加载方面建议把首屏页面依赖的几个图片资源做压缩和尺寸裁剪接口返回速度也要关注。小程序启动时onLaunch里的同步逻辑越多首屏感觉越慢。凡是能延后执行的逻辑比如版本更新检查、非关键埋点都尽量放到页面加载之后再做。另外有个常被忽略的点CSS 文件也会影响首屏。全局app.wxss不要写大量无用样式因为它是跟随主包一起加载的。我自己会把全局样式严格控制在 reset、CSS 变量和通用工具类三层业务样式全部写到页面和组件内部。5. 常见工程问题与排查经验5.1 修改刚进入的加载页面很多初学者搞不清楚“刚进入小程序显示的是哪个页面”其实规则很简单app.json里pages数组的第一项就是启动页。想换成别的页面把那个页面的路径移到第一项就行。但要注意如果这个页面是 tabBar 页面修改tabBar.list里的顺序也可能影响启动时的默认 tab。通常我会把首页保持在 pages 第一项也保持 tabBar 的 list 第一项一致避免出现认知混乱。5.2 动态设置导航栏标题做页面的时候经常会遇到“进入页面后才拿到数据再根据数据改标题”的场景。原生小程序里可以通过wx.setNavigationBarTitle动态设置标题// 页面 JS 中 wx.setNavigationBarTitle({ title: 商品详情 - this.data.goodsName })这个方法只对当前页面生效并且要在onReady之后调用才稳妥。如果你用了自定义导航栏组件就需要通过组件内部的方法修改标题不会自动同步。5.3 顶部导航栏高度适配自定义导航栏在小程序里非常常见。做的时候最难搞的是顶部导航栏高度不同设备状态栏高度、导航栏高度都不一样。常规做法是利用wx.getSystemInfoSync获取statusBarHeight导航栏默认高度是 44pxiOS或 48pxAndroid但具体还是要以实际设备为准。稳定一点的方案是动态计算const systemInfo wx.getSystemInfoSync() const statusBarHeight systemInfo.statusBarHeight const menuButton wx.getMenuButtonBoundingClientRect() // 导航栏高度 (menuButton.top - statusBarHeight) * 2 menuButton.height const navBarHeight (menuButton.top - statusBarHeight) * 2 menuButton.heightwx.getMenuButtonBoundingClientRect()获取胶囊按钮的位置信息用胶囊的位置反推导航栏高度是我在项目中验证过比较可靠的方式适配不同手机的差异很小。5.4 长按拖拽滚动和单选框这几个细节做过商城类小程序的人应该都遇到过商品列表需要长按拖拽排序或者自定义单选样式。长按拖拽在小程序里没有现成的原生组件通常要借助movable-area、movable-view或者通过监听touchstart、touchmove事件自己实现排序逻辑。这种功能一定要多设备测试不同手机的触摸响应差异很大。单选框也一样原生radio组件样式很难看很多人会用view模拟单选。模拟的时候需要注意无障碍、点击区域大小、选中态的切换状态避免出现“点了没反应”的反馈问题。5.5 地址栏参数被转义的坑有段时间我调试页面跳转发现 URL 参数里含有号时onLoad(options)拿到的参数会被转成百分号编码页面里取不到原始值。这是因为路径参数在跳转时会经过 encodeURIComponent 编码。解决办法很直接跳转前用encodeURIComponent对参数统一编码在onLoad里用decodeURIComponent解码。如果你在webview页面传 URL 给 H5也要注意先编码再拼接否则 H5 拿到的是被截断的地址。// 跳转前 const query encodeURIComponent(type1codeA) wx.navigateTo({ url: /pages/detail/index?data${query} }) // onLoad 内 onLoad(options) { const data decodeURIComponent(options.data) }5.6 多人协作和代码管理小程序项目多人协作最容易出的问题project.config.json互相覆盖、页面文件没有按模块划分导致各自开发互相碰到同一个文件、公共样式被别人随便改导致全局崩掉。我的经验是定好项目结构的规范写在 README 里从创建项目第一天就按规范建目录。公共组件和工具模块的改动必须走 code review禁止不经讨论就改公共接口。页面之间尽量独立通过组件和 API 层交互而不是互相引用对方的页面文件。微信开发者工具的代码管理功能可以配合 git 使用建议每个功能分支独立开发合并前确保主分支可以直接编译运行。项目结构看似是老生常谈但每次排查到“复制粘贴三处、改漏一处”或者“因为主包超限凌晨赶工拆包”的现场我都会更佩服那些把结构当成工程规范来对待的团队。一套好的项目结构不是为了让目录看起来整洁而是让团队里最不了解这段代码的人也能在五分钟内找到它需要改的那个文件。这也是我写下这篇文章的初衷别等项目堆到十万行再来重构从第一个页面开始就把骨架搭对。
返回列表