小程序项目结构全拆解:从原生到uniapp,避开这些坑
2026/9/14 19:53:22 网站建设 项目流程

做小程序开发这么多年,我接手过不少半途而废的项目,也帮团队重构过好几套结构混乱的代码。我发现一个规律:凡是开发到一半干不下去的,十有八九是项目结构在早期就埋了雷。很多人觉得项目结构就是文件夹怎么建、文件怎么放,是件小事,但真正上了规模、加了需求、换了人接手,结构好坏直接决定你是三天改完一个功能,还是三天改出三个新bug。这篇东西我不想讲虚的,就把小程序项目结构从根目录到页面文件、从原生到uniapp,一层层拆开给你看,把那些文档里不写、但实操中一定会踩的坑一并说出来。

1. 项目结构到底解决什么问题

先说个场景。一个刚学小程序的人,跟着教程敲完一个demo,目录下App.js、app.json、pages、utils、components全混在一起,页面文件命名也随意,比如index2.wxmlindex3.wxml这么往后排。前期确实能跑,等哪天想加个支付、接个地图、上几个运营活动页,你再看这目录——想找某个业务逻辑得全局搜索文件名,想改个公共样式得翻三个页面去复制粘贴,更要命的是你自己写的代码,过了两周再看,你已经想不起来当时为什么要这么写了。

项目结构解决的核心问题有三个:可读性、可维护性、可扩展性。可读性是指任何人拿到你的项目,看目录结构就能大致知道这个项目有哪些模块、每个模块干了什么;可维护性是指改一个功能时,你知道去哪改、影响范围多大,不用全局搜索;可扩展性是指要加新页面、新模块时,你不用推翻现有结构,直接按既定规则往里加就行。

从小程序自身的机制来看,它对项目结构是有硬性约定的。app.json里注册的所有页面路径、pages目录下的四个配套文件、components目录的引用关系,这些规则既是限制,也是一种保护。框架帮你把路由、生命周期、组件通信都规定好了,你要做的就是在这些规则之上,设计出一套清晰的目录组织和代码分层。

我个人的习惯是,在动手写第一个页面之前,先花二十分钟把目录结构和命名规范定下来。这二十分钟不是浪费时间,是给后面几个月的开发省时间。等到项目跑起来再回头改结构,成本和风险都高得多。尤其是当你已经写了一堆页面、各页面之间互相引用了组件和工具函数之后,再动结构,改一次就是一次重构。

2. 原生微信小程序项目结构全拆解

原生小程序的目录结构是整个生态的基础,不管你后面用不用uniapp、Taro这类跨端框架,理解原生结构都能帮你搞清楚底层到底在跑什么。这里我按一个相对标准、也比较规范的项目目录来讲。

2.1 根目录下的核心文件

一个初始化后的原生微信小程序项目,根目录下会出现以下几类核心文件,每个文件承担的责任都不一样。

app.js是小程序逻辑层的入口文件,被称作全局逻辑文件。它里面要做的核心事情包括:调用App()函数注册小程序实例、定义全局数据(globalData)、注册生命周期回调(onLaunchonShowonHide等)。很多新人容易犯的错是把业务逻辑全堆在app.js里,导致这个文件越来越臃肿。我的建议是,app.js只放全局要用的数据和启动时要做的初始化操作,比如获取用户登录态、读取本地缓存做主题恢复等,千万别把页面间的数据通信逻辑也塞进来。

app.json是小程序的全局配置文件,这个文件决定了一个小程序“长什么样”。页面路由、窗口外观、导航栏样式、分包结构、tabBar、网络超时时间、底部栏等,都在这里面定义。所有要在小程序里展示的页面,必须先在pages字段里注册,否则编译能过,但在真机或开发者工具里访问不到对应页面。我遇到过一个很典型的报错场景:页面文件明明已经创建好了,跳转时却提示page not found,查了半天,最后发现是这个页面的路径漏写了或者写错了,就这一个坑,新手能卡一晚上。

app.wxss是全局样式文件,作用于所有页面。它适合放公共样式,比如统一的主色变量、公共按钮样式、通用布局类。但要注意,app.wxss里的样式权重虽然后面加载,但如果页面样式和全局样式产生冲突,依然是靠选择器优先级来决胜负的,不是谁写在后面谁就一定赢。所以公共样式尽量用类名来组织,不要轻易去覆盖组件内部样式,否则多个页面一叠加,样式互相污染,光是排查样式问题就能耗掉半天时间。

除了这三个app开头的文件,还有sitemap.jsonproject.config.jsonsitemap.json是配置小程序页面是否允许被微信索引的,开发调试阶段影响不大,但上线前建议检查一下,避免出索引相关的提示。project.config.json是开发者工具的项目配置文件,里面记录了项目名称、appid、编译设置、本地配置等。这个文件在上传代码时最好检查一下,尤其是团队协作场景,每个人的本地项目配置不一样,如果把这个文件提交上去,很容易互相覆盖掉别人的个性化设置。

2.2 页面目录的四件套

小程序里每个页面都是由四个同名文件组成的,通常称为“四件套”:.js.json.wxml.wxss。比如你有一个首页,那就在pages/index/目录下建index.jsindex.jsonindex.wxmlindex.wxss

这四件套的分工非常清晰。.wxml负责页面结构,相当于HTML,用来描述页面长什么样,由哪些组件、元素构成;.wxss负责页面样式,控制这些元素显示成什么样;.json负责页面级别的配置,比如当前页面的导航栏标题、背景色、组件引用等;.js负责页面逻辑,包括数据、事件处理、生命周期回调等。

关于目录组织,我强烈建议一个页面一个文件夹,文件夹名和页面语义保持一致。早期很多老代码喜欢把页面文件全平铺在pages下面,比如pages/index.jspages/about.js,页面少的时候还行,页面一多,目录就爆炸了。正确的做法是pages/{模块名}/{功能页面}的形式,例如pages/home/index.jspages/cart/index.jspages/user/index.js。这样的好处是,同模块相关的页面放在一起,看目录就知道项目有哪些业务模块,而且后续如果要拆分包,按模块目录来切分也非常顺手。

小程序的页面是支持分包加载的,结构上一般分成主包和分包目录。主包是启动时就要加载的,建议只放启动页、tabBar页和公共资源;分包放其他业务页面,按需加载。这个机制对项目结构影响很大,等后面讲到中大型项目结构时我再细说。

2.3 公共资源目录怎么规划

除了页面目录,项目里一般还会建几个公共目录,这是我从多个项目里总结出来的最省心的规划方式:

  • components/:放自定义组件,每个组件一个文件夹。组件可以按层级再分组,比如components/base/放基础组件(按钮、输入框等),components/business/放业务组件(商品卡片、订单状态条等)。
  • utils/:放公共函数、工具方法,比如格式化日期、请求封装、鉴权相关函数。注意工具函数要写得纯粹一点,不要直接操作页面数据,输入输出要明确,这样才方便复用和测试。
  • api/:放接口请求函数。这个建议单独建目录,把每个业务模块的接口封装成函数,页面里引入调用,这样接口地址变更时能集中修改,不用到页面里一个个找,平时审查网络请求也方便。
  • assets/:放静态资源,图片、图标、音视频文件等。如果项目图标多,建议用字体图标或雪碧图,能显著减小包体积。assets里还可以按资源类型分子目录,比如assets/images/assets/icons/

有些项目还会用到store/models/目录,用来放全局状态管理相关的代码。原生小程序没有内置像Vuex那样的状态管理工具,但可以引入第三方库或者自己封装一个简单的全局状态订阅机制。等页面多了、全局共享数据多了之后,这套东西基本是绕不开的。

上面这套目录规划不是我拍脑袋想的,它是从实际开发里反复折腾出来的结果。一开始我的项目也只有一个pages目录加几个零散文件,后来页面多了、功能复杂了,才一步步把utilsapicomponentsassets拆出来。如果你一开始就能按这个习惯来组织,后面省的事不是一点半点。

3. 三类关键配置文件的参数说明

小程序的配置文件数量不算多,但每一个都极其重要,配置错了会直接影响运行表现。我挨个讲一下实战中必须吃透的参数。

3.1 app.json 配置项逐项讲

app.json是理解小程序结构的一把钥匙。它的最顶层字段包括pageswindowtabBarnetworkTimeout等基础配置,还有subpackagespreloadRule这类进阶配置。

pages数组的第一项就是小程序的首页。很多人不知道,小程序启动时加载的是pages数组的第一项,而不是某个固定名字的页面。所以如果你想换个启动页,不是改代码,而是调整pages数组的顺序。这个坑我踩过一次,当时想把一个临时活动页恢复成正常首页,结果忘了调整pages数组顺序,启动后还是活动页,排查了半天,最后发现就是数组顺序的问题。

window字段控制的是小程序每个页面的导航栏、背景色、标题等全局默认表现。常用的有navigationBarTitleText(导航栏标题)、navigationBarBackgroundColor(导航栏背景色)、navigationBarTextStyle(导航栏文字颜色,只能取blackwhite)、backgroundColor(窗口背景色)、enablePullDownRefresh(是否开启全局下拉刷新)。页面自己的json可以覆盖这些全局配置,覆盖的优先级是页面配置高于全局配置。

tabBar字段用于配置底部导航栏,这个字段在电商、内容类小程序里非常常见。配置项包括color(文字颜色)、selectedColor(选中态颜色)、backgroundColor(背景色)、list(导航项列表,2到5个)。注意事项是,tabBar页面的路径必须在pages中注册,而且图标文件不能放在远程服务器上,必须使用本地静态资源,否则会报错。还有就是tabBar页面切换时是“切换”而不是“跳转”,所以每个tabBar页面都有自己的独立页面栈,这也会影响wx.navigateTowx.switchTab的使用逻辑。

subpackages是小程序分包配置。分包的初衷是限制主包体积,提升启动速度。微信要求主包不能超过一定大小(具体限制会随平台策略调整,以官方文档为准),超过之后就必须分包。分包内页面可以引用主包内的公共资源,但主包不能反向引用分包内的资源。preloadRule字段可以配置分包预下载规则,在某个页面加载时预下载指定分包,提升后续跳转的体验。

还有其他一些案例,比如usingComponents字段可以声明全局自定义组件,lazyCodeLoading可以开启按需注入,style字段可以声明是否使用新样式特性。看到这里你应该明白了,app.json不只是个配置文件,它直接决定了小程序的架构形态,分包怎么做、tabBar怎么排、全局样式怎么走,都从这一个文件开始。

3.2 页面内 json 的配置能力

页面级别的json文件虽然代码量少,但配置能力一点都不能小看。每个页面的json都覆盖着当前页面的独立表现和行为规则。

页面json常用的配置项包括navigationBarTitleText(当前页面导航栏标题)、enablePullDownRefresh(是否允许下拉刷新)、usingComponents(组件引用列表)、navigationStyle(导航栏样式,可自定义)、backgroundColordisableScroll等。这些配置能覆盖app.json中的对应全局配置,所以你可以给每个页面设置不同的导航栏标题、独立的背景色。

这里有个比较关键的用法:如果你想做自定义导航栏(比如实现了沉浸式头部、导航栏上放自定义按钮),就把页面的navigationStyle设为custom。设置之后,微信提供的默认导航栏就不渲染了,页面内容会延伸到顶部,这时你需要自己处理状态栏高度、胶囊按钮的位置适配。这个功能的坑在于,不同机型的胶囊按钮高度、状态栏高度都不一样,得用wx.getWindowInfo()这类接口动态获取后再做布局,处理不好就会出现顶栏遮挡内容的问题。

usingComponents是页面级组件注册的配置。小程序里使用自定义组件,需先在页面的json里声明,声明方式如下:

{ "usingComponents": { "van-button": "@vant/weapp/button/index", "product-card": "/components/business/product-card/product-card" } }

声明之后,页面wxml里才能直接使用<van-button><product-card>这样的标签。用绝对路径引用组件时,建议以/开头从根目录计算,不容易出错。组件按需注册这个机制也意味着,一个组件如果在多个页面用到,你需要在每个页面的json里都注册一遍,这也是为什么全局组件能抽到app.json里的usingComponents去统一声明。

3.3 project.config.json 与 sitemap.json

project.config.json是项目级的配置文件,由微信开发者工具生成和维护。它里面记录了appid、项目名称、编译选项、自定义编译条件等。团队协作时,我一般建议把除了本地开发路径之外的公共配置提交到代码仓库,这样团队成员拉下来代码后,工具能自动识别项目设置,不用每个人手动配置一遍。不过要注意,project.private.config.json这类本地私有配置不应该提交到版本库,它存放的是开发者个人偏好,比如本地调试的编译模式、自定义的本地代码片段等,提交上去会造成冲突。

sitemap.json配置的是小程序页面的索引规则。简单理解就是哪些页面允许被微信搜索收录,哪些不允许。它的rules数组里可以配置actionallowdisallow,配合page*通配符来匹配页面路径。虽然大多数业务小程序对这个配置不太敏感,但如果你做了SEO相关的事情,打开开发者工具菜单第一行“详情 —— 本地设置”中的“自动是否打开索引”选项后,就会看到sitemap的提示在模拟器里出现。上线前建议把无关的调试页面配置成disallow,避免不必要的索引提示。

4. 页面生命周期与四文件联动机制

理解结构只是第一步,真正要上手开发,还得搞清楚页面里这四件套是怎么联动运行的。很多人把页面的jswxmlwxss当成三块独立的代码,写完就完事,但到了复杂交互时才发现,很多事情不是“写对代码”就行的,而是要顺着生命周期去安排逻辑。

4.1 四个文件的职责边界

用小白的视角来类比:.wxml是人的骨架,决定了身体结构;.wxss是皮囊和衣服,决定了外观;.js是大脑,决定遇到事情怎么反应;.json是说明书,告诉别人这个页面允许怎么做、不允许怎么做。

具体到开发习惯上,wxml里尽量只写结构,不要在标签属性里堆一长串三元表达式。以前我见过有人把一段复杂的a ? b : (c ? d : e)直接塞进wxml的属性里,渲染是一点问题没有,但后期维护时,阅读成本直线上升。复杂逻辑放到jscomputed或在setData之前处理好,wxml只负责展示结果。

wxss除了写样式,还要处理好单位换算。小程序里常用的rpx单位是响应式像素,750rpx等于屏幕宽度,这是从小程序框架层面就定好的适配方案。写页面时推荐全部用rpx做响应式,但字体大小、边框宽这种细节,我有时候也会直接用px,避免过度缩放时出现模糊或失真,这个看团队规范,关键是统一,不要一半页面用rpx一半用px,那样会在不同屏幕上疯掉。

js是四件套里最复杂的,它管理页面数据data、生命周期函数、事件处理逻辑、以及和其他模块的数据交互。data里的数据会在页面初始化时通过setData渲染到wxml上,setData是主要的数据到视图的通道,它的性能直接影响页面流畅度。

json是页面级的配置和注册中心,前面已经讲过,重点记得两件事:一是页面级json能覆盖全局配置;二是组件必须在json里注册后才能用。

4.2 生命周期函数执行顺序

生命周期是小程序运行机制的底层逻辑。一个普通的页面从加载到销毁,会依次经历几个关键时间点:onLoadonShowonReadyonHideonUnload

onLoad在页面首次加载时触发,通常在这里做页面初始化,比如读取options参数、请求首屏数据。onShow在页面每次显示时触发,不仅首次会触发,从后台切回前台、从其他页面返回时也会触发。onReady在页面初次渲染完成之后触发,这时可以开始做一些依赖界面已生成的操作,比如操作canvas组件。onHide在页面被隐藏时触发,比如跳转到下一个页面,当前页就会先经历onHideonUnload在页面被销毁时触发,比如执行navigateBack返回操作后,页面实例被卸载。

理解了这套执行顺序,很多问题就好解释了。举个例子,为什么从二级页面返回时,需要刷新列表数据?因为二级页面可能修改了数据,返回后触发的是onShow,不是onLoad,所以如果你只在onLoad里请求数据,返回时数据是不会自动更新的。这时候正确的做法是,把“每次进入页面都要刷新”的逻辑写在onShow里,把“只在首次加载时执行一次”的逻辑放在onLoad里。

还有一个坑是setData的时机。在onLoad里直接调用this.setData虽然没问题,但如果数据量很大,或者你在onLoad里做了复杂计算后再setData,首屏时间会被拖长。比较稳妥的做法是缩小首屏渲染的数据范围,把非关键数据放到触发onReady或页面加载完之后再补拉。

4.3 自定义组件结构

自定义组件让小程序的复用能力大大增强。一个自定义组件目录和页面很像,也有四个文件:jsjsonwxmlwxss。组件和页面的区别在于,组件有自己的数据管理方式,父页面通过组件的properties给组件传数据,组件通过触发事件(triggerEvent)把内部逻辑结果抛回给父页面。

组件里有两个生命周期需要特别注意:attached是在组件实例被插入页面节点时执行,类似页面里的onLoaddetached是在组件被移除时执行,类似onUnload。组件里还有一个lifetimes配置,推荐把生命周期函数写在这里,而不是直接写在顶层与自定义方法并列,这样代码组织和执行顺序都会清晰很多。

组件通信方式常用的有三种:父传子用properties,子传父用triggerEvent,跨层级访问用this.selectComponent或全局状态。我个人习惯是:能用properties和事件解决的,绝不用全局状态,因为全局状态一旦多了,代码就很难追踪数据流向,出了问题你根本不知道是哪个环节改了数据。

5. 从原生到 uniapp:跨端项目的结构对比

现实开发中,很多人不是从原生小程序起步的,而是直接上手uniappTaro这类跨端框架。这类框架的项目结构和原生小程序差别很大,理解它们的差异,能帮你在选型和项目初始化时少走弯路。

5.1 uniapp 项目目录结构

uniapp的项目根目录下,常见的有pages.jsonmanifest.jsonmain.jsApp.vueuni.scssstatic/pages/以及可选的components/store/utils/api/等目录。

pages.jsonuniapp的全局页面配置文件,它对应原生小程序里的app.json。但pages.json的功能更强大,它不只是页面注册和窗口外观配置,还包含了tabBareasycom规则、条件编译、原生插件等配置。manifest.json配置的是应用级的元信息,包括appid、应用名称、版本号、各平台(微信小程序、App、H5等)的SDK信息。main.js是入口文件,负责创建Vue实例、注册插件、挂载全局属性。App.vue是应用根组件,里面可以写全局生命周期(onLaunchonShow)和全局样式。

pages目录下存放的是.vue单文件组件,每个页面就是一个个.vue文件。.vue文件里把模板、脚本、样式都写在一个文件里,和原生的四件套相比,页面整体性更强,开发单个页面时不用在多个文件之间跳来跳去。但单文件组件也有个结构上要注意的点:如果多个页面复用了公共布局,建议把公共布局抽成components里的组件,不要在页面里重复写大段模板结构。

5.2 原生小程序与 uniapp 的结构选型考量

做跨端项目前,先问自己一句话:产品真的需要运行在多个端上吗?如果确定只做微信小程序,原生开发其实已经很成熟,而且从性能、调试便利性、原生能力覆盖度来说,原生绝不差。但如果产品需要同时上线微信、支付宝、百度等多个小程序,或者还要出H5、App端,那用uniapp这类跨端框架就是降低成本的选择。

从项目结构的角度看,原生小程序的结构更直接,页面四件套、组件四件套、全局配置一目了然,但也正因为如此,原生项目在代码组织上更依赖团队自己的规范,没有框架层帮你做模块约定,很容易写出“一堆文件堆在pages里”的代码。

uniapp的优势是它基于Vue单文件组件模式,组件化能力和工程化方案(比如Vite、TypeScript、ESLint)比原生更容易接入。但uniapp的结构也有一个明显的“坑”:跨端条件编译。同一个.vue文件里可能需要写#ifdef MP-WEIXIN#ifndef H5这类条件注释,文件里到处都是平台分支,结构清晰度会下降。所以用uniapp时,我建议把跨端的差异逻辑尽量封装到utils或独立组件里,不要在页面模板里散落大量条件编译块。

选型的另一个考量是团队技术栈。团队对Vue比较熟,uniapp上手成本低;团队对小程序原生API更熟,或者项目里要大量使用小程序独有组件和插件,那原生更合适。结构本身没有绝对的好坏,只有适不适合当前团队、当前产品。

6. 项目结构成长路线:从小项目到复杂项目

最后聊一下项目结构在不同阶段的演进方式。很多人拿到的项目需求是从小起步的,比如先做一个产品展示加联系方式,后面再加商城、加订单、加售后。如果没有提前为这种成长留出结构余量,后面每加一个模块都是一次伤筋动骨的重构。

6.1 小项目起步阶段的结构设计

小项目阶段,我建议不要过度设计,目录层级不用追求一步到位。一个首页、一个列表页、一个详情页、一个联系页,就正常用pages分组目录,utils放一个请求封装,components放一两个高频复用的组件,够用就行。

但这并不代表可以乱写。即使小项目,命名规范、页面分组、组件命名也要从一开始就确认好。我见过很多项目从小变大后,光是改文件夹名和组件名就花了两天时间,期间还伴随着各种路径报错。为了避免这种情况,建议从一开始就按“模块化目录 + 语义化命名”的方式组织,哪怕项目里只有三个页面,也保持pages/homepages/about这种形式,而不是pages/apages/b

6.2 中大型项目的结构演进攻略

当项目发展成一个有商城、订单、个人中心、售后咨询等多模块的中大型应用,结构上基本就要进入“分包 + 模块化 + 状态管理”的阶段了。

首先,一定要做分包。把启动页、tabBar页和公共组件库放在主包,把每个业务模块的页面放到独立分包里。例如packageA对应商城模块,packageB对应订单模块,packageC对应售后模块。分包按模块划分后,路由管理、代码更新、团队协作边界都会清晰很多。

其次,业务逻辑要抽层。页面做的只是“取数据和渲染数据”,接口请求放api目录,复杂的数据加工放utils或独立的services层。页面里不直接写wx.request,要用封装好的请求方法,这样统一处理token、错误码、超时重试的逻辑就不会散落在各个页面。

再就是状态管理。当多个页面共享用户信息、购物车数据、全局配置时,建议引入全局状态管理。原生小程序可以自己封装一套store,也可以引入mobx-miniprogram这类库;uniapp项目则直接用vuexpinia。不管用哪种,核心原则是:只有跨页面、跨组件共享的数据才放到全局状态里,页面私有的数据,老老实实留在页面自己的data里,不要什么都往全局塞。

最后是目录规划的“防腐层”。我建议把第三方SDK、工具库、请求封装都包在自己的壳里,不要直接在页面里用原生API满天飞。比如将来要换地图SDK,只需要在utils/map.js里改适配层,所有调用地图的页面都不需要动。如果每个页面都直接调第三方SDK的API,将来换SDK时就是一场噩梦。

6.3 项目结构调整时的注意事项

如果项目已经烂到一定程度,想要重构结构,有几个实操建议。

第一步,先冻结新需求,把当前项目跑起来,梳理清楚现有页面、组件、接口的依赖关系。可以用微信开发者工具的“依赖分析”功能,也可以手动把pagecomponentutil的引用关系列个清单。第二步,按“页面 -> 公共组件 -> 工具函数 -> 接口”的顺序逐步迁移,每迁完一个模块就跑一遍测试和回归,不要一次性把所有文件都移动完再改路径,那样报错时根本定位不了问题。第三步,移动文件后要全局搜索旧路径引用,因为小程序里很多路径是字符串写死的,编译不报错,但真机运行时会找不到资源。我在重构时吃过这个亏——把components/business/order-card移到了components/order/order-card,搜索order-card时只改了页面json里的引用,漏了一个老页面的wxml里直接用了标签,导致线上出现了渲染异常。

还有一个细节:分包路径和主包路径要避免重叠。比如主包里有个pages/order/index,分包里又建了个packageA/pages/order/index,看起来都叫order,但运行时常常会因为路由路径混淆出现“页面找不到”的怪问题。分包里的页面路径建议带分包名前缀,从设计上就避免这种“同名页面”的歧义。

结语:结构这事,值得多花心思

我做小程序这些年,最大的体会是:项目结构这个东西,前期省的那点时间,后期会加倍还回来。与其等代码烂到不敢动再找“救火方案”,不如在项目刚开始时,多花一点时间把目录、命名、组件边界、分包策略这些基础问题想清楚。你不需要一开始就搭出一套完美的架构,但要留出可以平滑演进的空间。每当你觉得“现在改还麻烦,先这样吧”的时候,记得这句话——小程序项目的技术债,很大一部分是从糟糕的项目结构开始欠下的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询