ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Vue3+TS+Pinia的uniapp全面型快速开发模板实践

基于Vue3+TS+Pinia的uniapp全面型快速开发模板实践 简介一套面向uniapp生态与Vue3技术栈的快速开发模板集成uview-plus、TypeScript与Pinia适合需要在多端快速落地的开发者模板基于Scss组织样式并配有readme说明可有效降低从零搭建与二次改造成本。资源包共29个文件、约478KB涵盖TypeScript、JavaScript、JSON、Vue、Scss/CSS等类型覆盖组件封装、状态管理、页面路由、类型声明、构建配置与全局样式且.gitignore、package.json、tsconfig.json、vite.config.ts等工程文件一应俱全目录结构清晰。目前已有638人学习浏览适合希望快速上手uniappVue3TypeScript的开发者作为项目基底模板内置custom-navbar等自定义组件、统一请求封装、用户状态模块与样式变量能省去环境配置与目录规划的时间是一套高效、可扩展的跨端应用起步方案。1. 为什么需要一个全面型快速开发模版而不是从零搭 uniapp 项目做 uniapp 项目时团队很快会面临一个选择是继续守着 Vue2 uView 的老方案还是一步到位切到 Vue3 TypeScript Pinia。前者资料多但维护成本高组件库早就停更后者组合新却要同时处理组件库兼容、类型声明、状态管理、跨端配置四件事任何一个环节卡住项目热更新都会停摆。所谓“全面型快速开发模版”就是把这几件事提前组装好目录结构、请求封装、权限路由、主题变量、打包参数都是现成的新页面只需要往里面填业务逻辑。这套基于 uniapp uview-plus Vue3 TypeScript Pinia 的模板体系解决的正是“从零初始化”和“业务无序膨胀”之间的断裂。适合正在评估跨端方案的技术负责人也适合被 uniapp 微信小程序和 H5 双端差异反复折磨的开发者。2. 从选型到工程骨架uview-plus 与 Vue3、TypeScript、Pinia 的适配关系2.1 为什么是 uview-plus 而不是原版 uView原版 uView 1.0 是基于 Vue2 的组件库作者停更后Vue3 项目再选组件库就得换方向。uview-plus 是这个生态里比较完整的社区主线组件命名保留u-前缀配置方式延续 easycom 规则。选它最直接的理由是迁移成本低老项目的组件标签能继续用改 import 方式和部分属性写法就能跑起来。另一个原因是 uniapp 官方没有提供像 Vant 那样在每个端都一致的组件库uview-plus 通过编译期处理让表单、弹出层、日期选择这些组件在小程序和 H5 上的行为尽量对齐省掉大量自己适配的时间。选型时还要看依赖树uview-plus 的 release 版本和 uni 编译器版本绑定得比较紧。我一般会先在空项目里装一遍跑一次npm run dev:mp-weixin确认没有makeStyle之类的报错再锁版本。如果是从 uView 1.0 迁移要注意 uview-plus 对u-radio、u-checkbox这类组件的v-model绑定做了调整不能简单全量替换。还有一点uview-plus 的文档里标注了 Vue3 和 Vue2 两套分支下载时别选错 tag否则会在运行时报this.$u找不到。2.2 依赖版本怎么锁package.json 中容易踩的版本坑模板要能“直接能用”依赖版本是关键。下面这份是当前 Vue3 线常见的组合{ dependencies: { vue: ^3.4.21, pinia: ^2.1.7, uview-plus: ^3.3.20, dcloudio/uni-app: 3.0.0-4020920240930001 }, devDependencies: { typescript: ^5.4.0, vite: ^5.0.0, vue-tsc: ^2.0.0, dcloudio/types: ^3.4.8 } }这里有几个容易踩的坑。uview-plus必须和dcloudio/uni-app的编译期对齐否则会报Cannot read property xxx of undefined。typescript版本不要盲目升到太新因为 uni 生成带编译器版本的类型声明可能还没跟上建议保持在 5.4 附近。vue-tsc用于npm run type-check很多模板会漏掉它结果是编辑器不报错CI 里却跑不过。锁版本建议把package-lock.json提交进仓库而不是只靠package.json里的^因为同一个^在不同时间安装可能拉出不同补丁版本跨端构建时很容易出现“昨天能跑今天不能跑”的情况。2.3 Pinia 与 Vuex 在 uniapp 里的选择Vue3 项目里 Pinia 已经是默认选项这套模板用 Pinia 也符合直觉。Pinia 比 Vuex 少了 mutations 层store 里直接写 action 就能改 state配合 TypeScript 时类型推导更顺。在 uniapp 场景里还有个实际好处Pinia 可以在 App 启动时注入实例每个页面通过useUserStore()共享登录态、购物车、设置项不需要像 Vuex 那样定义一堆 module 再套 namespace。下面是两者对比对比项PiniaVuex 4类型推导store 即类型state 和 getter 自动推导需要手动声明 module 类型变更状态action 内直接赋值commit mutation调试体验devtools 里 action 栈更直观相对传统uniapp 支持Vue3 项目可直接使用需要额外维护 Vuex 4 依赖在模板里每个业务模块一个 store 文件没有 module 嵌套也没有 namespace 概念跨 store 调用时直接useOtherStore()就行。这是从 vuex 迁移过来的团队最容易上手的地方。需要注意的是Pinia 的 store 必须在 Vue 组件初始化后才能调用如果放在请求工具模块里使用要确保工具函数只在页面生命周期里执行避免在模块顶部直接useUserStore()。2.4 TypeScript 在 uniapp 中的开启方式uniapp 官方 CLI 模板默认支持 TypeScript但很多项目是从 JS 模板改过来的经常缺少两个文件tsconfig.json和env.d.ts。下面这份 tsconfig 可以直接放进模板根目录{ compilerOptions: { target: esnext, module: esnext, moduleResolution: node, strict: true, jsx: preserve, sourceMap: true, esModuleInterop: true, skipLibCheck: true, allowJs: true, types: [dcloudio/types, uview-plus] }, include: [src/**/*.ts, src/**/*.d.ts, src/**/*.tsx, src/**/*.vue], exclude: [node_modules, dist] }dcloudio/types提供uni、wx、uniCloud这些全局对象的类型没有它uni.getStorageSync这行就会飘红。uview-plus放在types里后模板中u-button的click事件才能拿到组件实例类型。skipLibCheck必须开否则依赖包内部的类型报错会刷屏。还要在src/env.d.ts里声明.vue模块否则 TypeScript 会把.vue文件当做未知模块/// reference typesvite/client / declare module *.vue { import { DefineComponent } from vue const component: DefineComponent{}, {}, any export default component }环境配置完成后新页面一律写script setup langts组合式 API 配合类型标注能在编译前拦住字段错拼、缺失参数这类低级错误。3. 模板的目录设计与核心模块实现3.1 目录结构一个能支撑业务“直接能用”的分层全面型模板的目录不是越花哨越好我见过拆到八个层级的项目最后没人知道文件该放哪。下面这套结构是跨端项目里通用性较高的方式src/ ├── api/ # 接口定义按业务模块拆文件 │ └── user.ts ├── components/ # 业务通用组件 ├── hooks/ # 组合式函数usePermission、usePagination ├── pages/ # 页面按业务域放子目录 │ ├── login/ │ ├── home/ │ └── mine/ ├── stores/ # Pinia stores │ ├── index.ts # pinia 实例 │ ├── user.ts │ └── app.ts ├── styles/ # uni.scss、主题变量 ├── types/ # 全局类型声明 ├── utils/ # request、auth、tools │ └── request.ts ├── manifest.json ├── pages.json └── main.ts关键不是目录名字而是依赖方向api只能引用types和utilspages只能引用components、hooks、stores不允许页面直接写uni.request。这样后端接口变化时只动api目录业务代码不用跟着改。hooks可以替换掉很多写在onLoad里的逻辑比如把下拉刷新、分页请求都封装成usePagination页面里只剩十几行调用。模板里components不要放 uview-plus 的组件那些由 easycom 自动引入业务组件才需要放在这个目录。3.2 请求封装让小程序、H5、App 共用一层网络层直接使用uni.request会面临三个问题token 注入、错误码统一处理、登录失效后清理状态。模板里常见做法是封装一个request函数核心代码如下// src/utils/request.ts import { useUserStore } from /stores/user interface RequestOptions { url: string method?: GET | POST | PUT | DELETE data?: Recordstring, unknown auth?: boolean } export function requestT(options: RequestOptions): PromiseT { const userStore useUserStore() return new Promise((resolve, reject) { uni.request({ url: ${import.meta.env.VITE_API_BASE_URL}${options.url}, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, ...(options.auth ! false userStore.token ? { Authorization: Bearer ${userStore.token} } : {}) }, success: (res) { const data res.data as { code: number; message: string; data: T } if (data.code 0) { resolve(data.data) } else if (data.code 401) { userStore.logout() uni.reLaunch({ url: /pages/login/index }) reject(new Error(data.message)) } else { uni.showToast({ title: data.message, icon: none }) reject(new Error(data.message)) } }, fail: (err) { uni.showToast({ title: 网络异常, icon: none }) reject(err) } }) }) } export const http { get: T(url: string, data?: Recordstring, unknown) requestT({ url, data, method: GET }), post: T(url: string, data?: Recordstring, unknown) requestT({ url, data, method: POST }) }这里的参数值得说明。auth默认true登录、验证码这类接口传auth: false即可跳过 tokencode 0是后端统一成功标记实际项目中按接口文档调整401 时直接调用 store 的logout清掉 token 和用户信息再reLaunch到登录页。这样每个接口调用者不用自己写错误弹窗和跳转模板里的页面代码会干净很多。封装里没有做请求缓存和重试这些属于业务层能力需要时再加到拦截器里避免模板过度设计。3.3 Pinia store 的轻量封装比每个页面写 useState 更可控Pinia 的 store 定义有 options 和 setup 两种写法模板里我更推荐 setup 写法因为它可以把多个状态和动作收敛在一起类型推断也直接。下面是一个常见的用户 store// src/stores/user.ts import { defineStore } from pinia export const useUserStore defineStore(user, () { const token ref() const profile ref{ nickname: string; avatar: string } | null(null) const isLoggedIn computed(() !!token.value) function setToken(value: string) { token.value value uni.setStorageSync(token, value) } function setProfile(value: typeof profile.value) { profile.value value } async function login(payload: { username: string; password: string }) { const data await http.post{ token: string }(/login, payload) setToken(data.token) } function logout() { token.value profile.value null uni.removeStorageSync(token) } return { token, profile, isLoggedIn, login, logout } })ref和computed在 store 里可以直接用相当于把组件里的状态逻辑整段搬过来。用uni.setStorageSync做持久化注意要在App.vue的onLaunch里从 storage 恢复 token否则用户进入小程序时 store 是空的会出现“已登录却跳回登录页”的体验问题。模板里一般会在main.ts创建 Pinia 后立即从 storage 同步一份初始状态到 store这个步骤别漏。3.4 uview-plus 的按需引入与主题变量接入uview-plus 官方建议使用 easycom 自动按需引入这样小程序端打包时不会把整个组件库塞进去。需要在pages.json里配置{ easycom: { autoscan: true, custom: { ^u-(.*): uview-plus/components/u-$1/u-$1.vue } }, pages: [ { path: pages/index/index } ], globalStyle: { navigationBarBackgroundColor: #FFFFFF, navigationBarTextStyle: black, navigationBarTitleText: 模板 } }然后在main.ts里像这样接入import { createSSRApp } from vue import * as Pinia from pinia import uviewPlus from uview-plus import App from ./App.vue export function createApp() { const app createSSRApp(App) app.use(Pinia.createPinia()) app.use(uviewPlus) return { app, Pinia } }app.use(uviewPlus)加载的是全局能力easycom 保证单独组件按需编译。主题变量写在src/styles/uni.scss里比如$u-primary: #2979ff;uview-plus 的默认样式会读取这些变量。注意uni.scss在 uniapp 中是自动注入每个组件样式文件的不能改成别的文件名否则所有$u-*颜色变量都会失效。如果自定义主题色只需要覆盖这一份文件不需要去改 node_modules 里的源码。4. 从模板到业务页面、路由、权限与打包参数怎么落地4.1 新增一个带权限的 tabBar 页面要动哪些文件tabBar 页面在 uniapp 里不像普通页面那样只加pages数组就行。第一步在pages.json的tabBar.list里补充页面路径和图标第二步确认页面文件已经创建路径和字符串一致第三步在页面里做权限判断。uniapp 没有官方全局前置守卫常见做法是在App.vue的onLaunch或每个 tabBar 页面的onShow里检查登录态。模板一般会封装一个usePermissionhook// src/hooks/usePermission.ts export function usePermission() { const userStore useUserStore() function ensureLogin() { if (!userStore.isLoggedIn) { uni.reLaunch({ url: /pages/login/index }) return false } return true } return { ensureLogin } }页面onShow里调用ensureLogin()没登录就 redirect。这里要注意不要用uni.navigateTo跳登录页因为 tabBar 页面跳转后返回栈会乱用reLaunch清空所有页面再进登录页更安全。另外tabBar.list里最多配置 5 个页面图标大小和文本不能为空否则小程序编译会报错。新增 tabBar 页面后微信开发者工具可能需要清缓存才能看到变化这是编辑器缓存导致的问题不是模板代码问题。4.2 用 uview-plus 表单组件搭一个带校验的登录页下面是一个最小可用的登录页片段直接用模板里现成的组件搭起来template view classlogin-page u-form :modelform :rulesrules refformRef u-form-item label账号 propusername u-input v-modelform.username placeholder请输入账号 / /u-form-item u-form-item label密码 proppassword u-input v-modelform.password typepassword placeholder请输入密码 / /u-form-item u-button typeprimary text登录 clickhandleLogin/u-button /u-form /view /template script setup langts import { reactive, ref } from vue import { useUserStore } from /stores/user const userStore useUserStore() const formRef ref() const form reactive({ username: , password: }) const rules { username: [{ required: true, message: 请输入账号, trigger: [blur] }], password: [{ required: true, message: 请输入密码, trigger: [blur] }] } async function handleLogin() { await formRef.value.validate() await userStore.login(form) uni.switchTab({ url: /pages/index/index }) } /scriptu-form的rules和validate()是 uview-plus 提供的校验规则写法沿用 async-validator。点击登录后先validate()通过后再调用 store 的login。这里的formRef.value.validate()在 uview-plus 3.x 里返回 Promise直接用await即可不需要回调。userStore.login内部已经处理 token 持久化所以页面不用做任何 storage 操作。登录成功后用uni.switchTab回到首页因为如果首页是 tabBar 页面navigateTo无法跳转过去。这个页面是模板内最基础的一部分直接拿过来改字段就能塞进业务里。4.3 微信小程序与 H5 的差异化配置manifest.json 里最常改的几个参数模板要同时支撑“uniapp 微信小程序”和“H5 嵌入公众号”需要区分manifest.json里的平台配置。下面这几个参数是最容易被忽略的配置项微信小程序H5mp-weixin.appid必填没填无法预览不生效h5.router.base不生效部署子路径时设置例如/h5/h5.title不生效浏览器标签页标题mp-weixin.permission需要配置定位权限描述不生效app-plus.distributeApp 打包证书相关不生效模板里的manifest.json一般已经分好了注释但很多人会漏掉h5.router.base。部署到服务器子目录时首页能打开刷新后却 404原因就是这个 base 没配。微信小程序还需要在mp-weixin节点下配置permission否则调用地理位置接口会被拒绝。如果要上架安卓应用市场还要在app-plus节点配置推送厂商参数至少把离线推送的appid和apikey提前准备好这部分和 uniapp 官方云打包最容易卡住。热词里搜“uniapp 上架安卓应用市场”出现那么多问题多数是卡在证书别名和包名不一致上。4.4 从 Vue2 项目迁移到这套模板的快速转换清单如果你手头是旧的 uView 1.0 Vue2 项目想迁移到这套模板直接照下面这份清单走把main.js改成main.ts并替换为createSSRApp写法Vue.use改成app.usenew Vue改成createApp所有this.$store改成useStore()并且只在setup里调用uView1.0 的组件安装方式替换成uview-plus的 easycommethods里用this的逻辑改为组合式函数或普通函数uni.request的success回调替换成封装的http.get/post避免嵌套回调迁移的通用顺序是先跑起一个空白页面模板再逐步搬 store、请求层最后搬页面。不要一次性把整个项目切过去否则会把 Vue2 的生命周期和 Vue3 的onMounted混在一个文件里排查成本比重写还高。热词里“uniapp vue2转vue3方法”的搜索结果很多但真正见效的往往是先处理this的引用再处理组件库顺序反过来很容易产生一堆类型报错。5. 模板的进阶使用与验证技巧5.1 用 TypeScript 类型推导把 uview-plus 的组件事件锁死uview-plus 的组件在模板里写click默认参数是 any但模板里的defineProps和defineEmits建议配合 TS 泛型使用这样业务代码才能获得完整提示。例如定义页面的标题栏属性const props defineProps{ title: string showBack?: boolean }() const emit defineEmits{ (e: back): void }()还需要在src/types里给业务模型建类型API 接口返回的数据不要直接填any这样后续改字段时编辑器能同时标出所有调用点。5.2 编译优化按平台裁剪代码与条件编译uniapp 的条件编译在模板和 script 里都能用// #ifdef MP-WEIXIN和// #ifdef H5可以把平台差异代码留在源码中但打包时按平台裁剪。模板里不建议用太多只在真机表现不一致的地方使用比如支付、分享和地图。例如微信支付和公众号支付在同一个方法里分平台处理// #ifdef MP-WEIXIN uni.requestPayment({ provider: wxpay, ...options }) // #endif // #ifdef H5 window.WeixinJSBridge.invoke(getBrandWCPayRequest, options) // #endif5.3 用启动参数做多环境隔离验证模板在联调阶段最实用的技巧是读取启动参数来切换环境。在App.vue的onLaunch里拿到uni.getLaunchOptionsSync().query如果包含envtest就切到测试 API 地址const launchOptions uni.getLaunchOptionsSync() const isTestEnv launchOptions.query?.env test const baseURL isTestEnv ? https://test-api.example.com : https://api.example.com uni.setStorageSync(baseURL, baseURL)真机调试时在微信开发者工具里点“编译模式”输入 query 参数envtest就能在同一台手机上同时调试测试环境和生产环境不用改代码重新打包。模板验收前也可以跑一遍基础命令npm run dev:mp-weixin、npm run dev:h5、npm run type-check确认没有类型错误和组件样式错乱再做页面开发。本文还有配套的精品资源点击获取
返回列表