基于Vue3+TS+Pinia的uniapp全面型快速开发模板实践
2026/9/15 13:40:20 网站建设 项目流程

简介:一套面向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人学习浏览,适合希望快速上手uniapp+Vue3+TypeScript的开发者作为项目基底;模板内置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-radiou-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 undefinedtypescript版本不要盲目升到太新,因为 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.jsonenv.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提供uniwxuniCloud这些全局对象的类型,没有它,uni.getStorageSync这行就会飘红。uview-plus放在types里后,模板中u-button@click事件才能拿到组件实例类型。skipLibCheck必须开,否则依赖包内部的类型报错会刷屏。还要在src/env.d.ts里声明.vue模块,否则 TypeScript 会把.vue文件当做未知模块:

/// <reference types="vite/client" /> declare module '*.vue' { import { DefineComponent } from 'vue' const component: DefineComponent<{}, {}, any> export default component }

环境配置完成后,新页面一律写<script setup lang="ts">,组合式 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只能引用typesutilspages只能引用componentshooksstores,不允许页面直接写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?: Record<string, unknown> auth?: boolean } export function request<T>(options: RequestOptions): Promise<T> { 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?: Record<string, unknown>) => request<T>({ url, data, method: 'GET' }), post: <T>(url: string, data?: Record<string, unknown>) => request<T>({ url, data, method: 'POST' }) }

这里的参数值得说明。auth默认true,登录、验证码这类接口传auth: false即可跳过 token;code === 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 } })

refcomputed在 store 里可以直接用,相当于把组件里的状态逻辑整段搬过来。用uni.setStorageSync做持久化,注意要在App.vueonLaunch里从 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.jsontabBar.list里补充页面路径和图标;第二步确认页面文件已经创建,路径和字符串一致;第三步在页面里做权限判断。uniapp 没有官方全局前置守卫,常见做法是在App.vueonLaunch或每个 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 class="login-page"> <u-form :model="form" :rules="rules" ref="formRef"> <u-form-item label="账号" prop="username"> <u-input v-model="form.username" placeholder="请输入账号" /> </u-form-item> <u-form-item label="密码" prop="password"> <u-input v-model="form.password" type="password" placeholder="请输入密码" /> </u-form-item> <u-button type="primary" text="登录" @click="handleLogin"></u-button> </u-form> </view> </template> <script setup lang="ts"> 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' }) } </script>

u-formrulesvalidate()是 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里的平台配置。下面这几个参数是最容易被忽略的:

配置项微信小程序H5
mp-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节点配置推送厂商参数,至少把离线推送的appidapikey提前准备好,这部分和 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的 easycom
  • methods里用this的逻辑改为组合式函数或普通函数
  • uni.requestsuccess回调替换成封装的http.get/post,避免嵌套回调

迁移的通用顺序是先跑起一个空白页面模板,再逐步搬 store、请求层,最后搬页面。不要一次性把整个项目切过去,否则会把 Vue2 的生命周期和 Vue3 的onMounted混在一个文件里,排查成本比重写还高。热词里“uniapp vue2转vue3方法”的搜索结果很多,但真正见效的往往是先处理this的引用,再处理组件库,顺序反过来很容易产生一堆类型报错。

5. 模板的进阶使用与验证技巧

5.1 用 TypeScript 类型推导把 uview-plus 的组件事件锁死

uview-plus 的组件在模板里写@click,默认参数是 any,但模板里的definePropsdefineEmits建议配合 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) // #endif

5.3 用启动参数做多环境隔离验证

模板在联调阶段最实用的技巧是读取启动参数来切换环境。在App.vueonLaunch里拿到uni.getLaunchOptionsSync().query,如果包含env=test,就切到测试 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 参数env=test,就能在同一台手机上同时调试测试环境和生产环境,不用改代码重新打包。模板验收前也可以跑一遍基础命令:npm run dev:mp-weixinnpm run dev:h5npm run type-check,确认没有类型错误和组件样式错乱,再做页面开发。

本文还有配套的精品资源,点击获取

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

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

立即咨询