简介:这是一份面向具备一年以上 uni-app 开发经验的前端工程师的开源项目模板,由一位五年经验的 Web 前端开发者整理,旨在提供一套可直接复用的快速开发脚手架,帮助开发者跳过重复搭建环节,把精力集中在业务逻辑与组件设计上。压缩包共 114 个文件,约 274KB,以 34 个 js 脚本、26 个 vue 组件、33 张 png 图片资源为主,另含 scss 与 css 样式、json 配置及少量 md 说明文档,覆盖页面结构、工具函数与表情、消息等聊天场景组件,目录组织清晰,便于按模块查阅与二次改造。目前已有 2430 人学习下载,适合希望参考成熟工程组织方式、提升组件封装与样式管理能力的中高级开发者研读借鉴。
1. 从一次“重装三遍依赖”说起:uni-app-template 到底解决什么问题
如果你做过两个以上的 uni-app 项目,大概率经历过这种循环:新项目初始化,装依赖,配 pages.json,配 manifest.json,封装请求,封装路由拦截,处理登录态,处理多端差异,然后发现上一个项目里踩过的坑,这个项目又踩了一遍。更离谱的是,团队里三个人建了三个模板,目录结构各不相同,新人进来先花两天熟悉“这个项目为什么这么组织”。
uni-app-template 这个词,本质上不是一个具体的开源仓库名,而是一类东西的统称:把 uni-app 项目里那些每次都要重来一遍的工程化配置、目录约定、公共能力,提前固化成一个可复用的起点。它解决的不是业务问题,是“每次从零开始”的问题。适合谁?适合需要同时维护多个 uni-app 项目的前端、需要给团队统一开发规范的技术负责人,以及被多端差异反复折磨、想找一套稳定起手式的人。
我自己的习惯是:模板不是越全越好,而是越“敢删”越好。一个塞了二十个插件的模板,新人根本不敢动;一个只有目录骨架加三五个核心封装的模板,反而能活很久。接下来几章,我按“先立结构、再落代码、最后讲坑”的顺序,把这套东西拆开讲清楚。
2. 目录结构与工程约定:模板的骨架怎么定
2.1 为什么目录结构要按“职责”分而不是按“页面”分
很多人的第一反应是按页面分目录:pages/home、pages/user、pages/order,每个目录里塞自己的 api、组件、store。项目小的时候很舒服,页面一多就崩——公共逻辑没有归属,两个页面都要用的请求封装,放谁目录里都不对,最后复制一份,改的时候漏一个。
我一般会按职责分层,核心就四层:页面层(pages)、公共组件层(components)、能力层(utils / api / store)、静态资源层(static)。页面层只负责“组装和展示”,不写请求细节;能力层只暴露函数,不关心谁调用。这样拆的好处是,当你要换请求库、换状态管理方案时,改动范围被锁死在能力层,页面几乎不用动。
一个我常用的最小目录骨架长这样:
src/ ├── pages/ # 页面,按业务模块再分子目录 ├── components/ # 全局公共组件 ├── api/ # 接口定义,按模块拆分 ├── utils/ # 请求封装、工具函数、常量 ├── store/ # 状态管理 ├── static/ # 图片、字体等不参与编译的资源 ├── styles/ # 全局样式变量与混入 ├── App.vue ├── main.js ├── pages.json └── manifest.json这里有个容易忽略的点:static 和 assets 的区别。static 里的文件不会被编译处理,原样拷贝,适合放第三方 SDK 的静态资源;需要被 webpack/vite 处理、做压缩或哈希的图片,应该放 assets。放错了不会报错,但打包体积会悄悄变大,这个坑后面还会提。
2.2 pages.json 与 manifest.json 的模板化处理
pages.json 是 uni-app 的路由加窗口配置中心,manifest.json 是多端打包配置。这两个文件在模板里最容易被写死,导致换项目时到处改。我的做法是把“会变的部分”抽成变量或注释块,把“不变的部分”固化。
比如 tabBar 的配置,颜色、路径这些每个项目都不同,但结构是固定的。模板里保留完整结构,用注释标出必改项:
{ "pages": [ { "path": "pages/index/index", "style": { "navigationBarTitleText": "首页", "enablePullDownRefresh": true } } ], "tabBar": { "color": "#7A7E83", "selectedColor": "#007AFF", "list": [ { "pagePath": "pages/index/index", "text": "首页" } ] }, "globalStyle": { "navigationBarTextStyle": "black", "navigationBarBackgroundColor": "#FFFFFF", "backgroundColor": "#F5F5F5" } }逻辑说明:pages 数组的第一项就是启动页,顺序不能乱;tabBar 的 list 里每一项的 pagePath 必须在 pages 里存在,否则真机白屏。参数说明:enablePullDownRefresh 开启后,页面要自己实现 onPullDownRefresh 回调,否则下拉动画转一圈就回去了,看起来像卡住。
manifest.json 里我只会固化 appid 占位、版本号命名规则、各端的基础配置结构,具体值留空。因为 appid 一旦写错,打包出来的包名就错了,这个错误在开发阶段完全看不出来,只有提审时才暴露,属于典型的“后悔药没得吃”。
2.3 用条件编译把多端差异关进笼子
uni-app 的多端能力靠条件编译实现,语法是#ifdef/#ifndef/#endif。模板里如果不提前约定条件编译的使用规范,后期会出现“这段代码到底在哪个端生效”的黑匣子。
我的约定是:条件编译只出现在能力层和组件层,页面层尽量不写。原因是页面层写条件编译,会让同一个页面的逻辑在不同端完全分叉,维护成本翻倍。能力层写,相当于给页面提供统一接口,页面无感知。
// utils/platform.js export function getSystemInfo() { // #ifdef APP-PLUS return uni.getSystemInfoSync() // #endif // #ifdef H5 return { platform: 'h5', ...window.__BASE_INFO__ } // #endif }逻辑说明:调用方永远只调 getSystemInfo,不关心当前端。参数说明:APP-PLUS 覆盖 App 端,H5 覆盖浏览器端,小程序端用 MP-WEIXIN 等。注意条件编译的注释必须成对出现,漏一个 #endif 会导致编译报错,而且报错位置经常指向文件末尾,排查起来很费时间。
3. 请求封装与状态管理:模板里最该先写的两块代码
3.1 请求封装:拦截器、错误码、loading 三件事
请求封装是模板里复用率最高的部分。我见过太多项目直接用 uni.request 裸调,结果每个页面都要写一遍 loading、一遍错误提示、一遍 token 拼接。正确的做法是在 utils 里封一层,把这三件事收口。
// utils/request.js const BASE_URL = 'https://api.example.com' // 请求计数器,解决并发请求 loading 闪烁 let loadingCount = 0 function showLoading() { if (loadingCount === 0) { uni.showLoading({ title: '加载中', mask: true }) } loadingCount++ } function hideLoading() { loadingCount-- if (loadingCount <= 0) { loadingCount = 0 uni.hideLoading() } } export function request(options) { const { url, method = 'GET', data = {}, showLoad = true } = options if (showLoad) showLoading() return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + url, method, data, header: { 'Content-Type': 'application/json', // token 从本地存储取,登录后写入 Authorization: uni.getStorageSync('token') || '' }, success: (res) => { const { statusCode, data: body } = res if (statusCode === 200 && body.code === 0) { resolve(body.data) } else if (statusCode === 401) { // 登录态失效,跳登录页 uni.reLaunch({ url: '/pages/login/login' }) reject(new Error('未登录')) } else { uni.showToast({ title: body.message || '请求失败', icon: 'none' }) reject(new Error(body.message)) } }, fail: (err) => { uni.showToast({ title: '网络异常', icon: 'none' }) reject(err) }, complete: () => { if (showLoad) hideLoading() } }) }) }逻辑说明:用计数器管理 loading,是因为并发请求时如果每个请求都 showLoading/hideLoading,会出现“第一个请求结束就关掉 loading,但第二个还在跑”的闪烁。参数说明:showLoad 允许单个请求关闭 loading,适合轮询或静默刷新场景;BASE_URL 在模板里应该抽到环境配置,不要硬编码。
这里有个血泪经验:401 处理里用 reLaunch 而不是 navigateTo,是因为登录页不应该出现在页面栈里,否则用户登录成功后按返回键又回到需要登录的页面,形成死循环。
3.2 状态管理:什么时候该用,什么时候是过度设计
uni-app 支持 Vuex 和 Pinia。模板里要不要内置状态管理,取决于项目复杂度。我的判断标准很简单:如果跨页面共享的状态超过三个,或者有需要持久化的登录信息、购物车、主题配置,就上;否则用简单的模块化 store 就够了。
以 Pinia 为例,模板里我一般只放两个 store:user 和 app。user 管登录态和用户信息,app 管全局配置比如主题、系统信息。
// store/user.js import { defineStore } from 'pinia' import { request } from '@/utils/request' export const useUserStore = defineStore('user', { state: () => ({ token: uni.getStorageSync('token') || '', userInfo: null }), getters: { isLogin: (state) => !!state.token }, actions: { async login(payload) { const data = await request({ url: '/user/login', method: 'POST', data: payload }) this.token = data.token this.userInfo = data.userInfo // 持久化 token,刷新后不丢 uni.setStorageSync('token', data.token) }, logout() { this.token = '' this.userInfo = null uni.removeStorageSync('token') } } })逻辑说明:token 初始化时从本地存储读,保证应用重启后登录态还在。参数说明:getters 里的 isLogin 用于页面做条件渲染,避免每个页面自己判断 token 是否存在。注意 Pinia 在 uni-app 里需要正确注册,main.js 里要 app.use(pinia),漏了会报“getActivePinia”错误。
3.3 环境配置:开发、测试、生产的切换方式
模板里必须解决环境切换问题,否则每次打包前手动改 BASE_URL,迟早出事。常见做法是用不同的 .env 文件配合构建变量,或者用一份 config 文件按 process.env.NODE_ENV 导出不同配置。
// utils/env.js const envMap = { development: { baseUrl: 'https://dev-api.example.com', debug: true }, production: { baseUrl: 'https://api.example.com', debug: false } } const currentEnv = process.env.NODE_ENV || 'development' export default envMap[currentEnv]逻辑说明:request.js 里的 BASE_URL 改成从 env.js 引入,打包时自动切换。参数说明:process.env.NODE_ENV 在 uni-app 的 vite 版本里可用,如果是老版本 cli 需要确认构建工具是否注入。注意不要把小程序的 appid 和密钥写进前端代码,这些应该由后端下发或构建时注入。
4. 避坑与排查:模板落地时最容易翻车的五个点
4.1 现象:真机上样式错乱,模拟器正常
原因:rpx 单位在部分机型上换算基准不同,加上某些 CSS 属性在小程序端不支持。解决:模板里统一用 rpx 做布局,但字体大小用 px 或 rem;避免使用 position: fixed 配合 transform,小程序端渲染层和逻辑层分离,容易出现偏移。排查时用真机调试,不要只信模拟器。
4.2 现象:条件编译代码在 H5 端报错
原因:#ifdef注释被格式化工具删掉或改格式,导致编译时判断失效。解决:在模板里加 eslint 规则或 prettier 配置,保护条件编译注释;提交前用npm run build:h5跑一遍,不要只跑 dev。这个坑我踩过两次,都是因为编辑器自动格式化解开了注释。
4.3 现象:打包体积异常大,超出小程序限制
原因:static 目录放了未压缩的图片,或者引入了整个 UI 库但只用了两个组件。解决:模板里约定 static 只放必要资源,图片走 CDN;UI 库按需引入,配置 easycom 规则自动按需加载。排查用npm run build:mp-weixin后看 dist 目录,哪个文件夹大就查哪个。
4.4 现象:页面返回后数据不刷新
原因:onLoad 只在页面首次加载时触发,返回时走的是 onShow。解决:模板里约定,需要每次进入都刷新的逻辑放 onShow,只初始化一次的放 onLoad。如果用了 keep-alive,还要注意 activated 钩子。这个问题的本质是生命周期理解不清,不是框架 bug。
4.5 现象:登录态失效后接口连环报错
原因:多个并发请求同时收到 401,每个都触发跳登录,导致页面栈混乱。解决:在 request.js 里加一个“正在跳登录”的标志位,第一个 401 触发跳转,后续的直接 reject 不重复跳。
let isRedirecting = false function handle401() { if (isRedirecting) return isRedirecting = true uni.reLaunch({ url: '/pages/login/login', complete: () => { // 跳转完成后重置,避免下次登录后无法再次触发 setTimeout(() => { isRedirecting = false }, 1000) } }) }逻辑说明:标志位加延时重置,既防止并发重复跳转,又保证下次真正失效时还能触发。参数说明:延时时间根据实际跳转速度调整,一般 500 到 1000 毫秒够用。
5. 让模板真正被用起来:校验清单与一个提效技巧
模板写完不是终点,能不能被团队持续使用才是。我的做法是给模板配一份“新建项目校验清单”,每次从模板起新项目,按清单过一遍,避免漏配。
| 检查项 | 检查方式 | 不通过的后果 |
|---|---|---|
| appid 是否替换 | 看 manifest.json | 打包包名错误,提审失败 |
| BASE_URL 是否切换 | 看 env.js | 请求打到测试环境 |
| tabBar 路径是否存在 | 看 pages.json | 真机白屏 |
| 条件编译是否成对 | 全局搜索 #ifdef | 编译报错或逻辑错乱 |
| static 是否含大图 | 看目录体积 | 包体积超标 |
| 登录页路径是否正确 | 搜索 reLaunch | 401 后跳转失败 |
这份清单看起来简单,但能挡掉八成的新项目低级错误。我一般会把它放在模板根目录的 README 里,新人第一次建项目照着勾一遍,比口头交代靠谱得多。
再讲一个提效技巧:把模板里的公共组件用 easycom 规则自动注册,页面里直接用标签,不用 import。配置方式是在 pages.json 里加 easycom 字段:
{ "easycom": { "autoscan": true, "custom": { "^my-(.*)": "@/components/my-$1/my-$1.vue" } } }逻辑说明:autoscan 开启后,components 目录下符合“组件名/组件名.vue”结构的组件自动注册。参数说明:custom 规则里的正则决定匹配方式,^my-(.*)表示以 my- 开头的标签都会去对应目录找。注意组件目录名和文件名必须一致,否则扫描不到,这个规则不匹配时不会报错,只是组件不渲染,排查起来很隐蔽。
最后一个习惯:模板不要一次写完就锁死。我一般会在模板里留一个docs/decisions.md,记录每个封装“为什么这么写”。比如为什么用计数器管 loading、为什么 401 用 reLaunch。半年后回头看,这些记录比代码本身更值钱,因为代码能看懂,但当时的取舍理由只有记录里才有。模板的价值不在于省了多少行代码,而在于让下一个项目少走一遍已经走过的弯路。希望帮到你。
本文还有配套的精品资源,点击获取