想象一下,你需要开发一个移动应用,但是你的老板说:“我们需要同时上线 Android、iOS 还有华为的鸿蒙系统”。传统的做法是什么?雇三个团队,写三套代码,维护三个项目… 想想都头疼对吧?
这个框架就是来拯救你的!一套代码,三个平台同时搞定。不用学 Java、Swift、ArkTS,只要你会 Vue,就能轻松上手。
简单来说,这个框架就像是一个"翻译官",你用熟悉的 Vue 语法写代码,它帮你自动翻译成各个平台能理解的语言。
还记得小时候用万能遥控器吗?一个遥控器能控制所有家电。这个框架就是代码界的万能遥控器:
我们选择的技术栈就像买车一样,都挑最可靠的品牌:
就像买房子带精装修一样,常用的功能我们都给你准备好了:
把这个项目想象成一个公司,每个文件夹就是一个部门,各司其职:
uniapp-multi-platform/ # 公司总部
├── src/ # 核心业务部门
│ ├── api/ # 对外联络部(负责和服务器打交道)
│ │ └── index.ts # 统一管理所有接口
│ ├── assets/ # 资源管理部
│ │ ├── fonts/ # 字体库(各种好看的字体)
│ │ ├── icons/ # 图标库(小图标们的家)
│ │ ├── images/ # 图片库(大图片住这里)
│ │ └── styles/ # 设计部(统一的样式规范)
│ ├── components/ # 零件制造部
│ │ └── common/ # 通用零件车间
│ │ ├── EmptyState.vue # "暂无数据"提示器
│ │ └── LoadingSpinner.vue # 转圈圈加载器
│ ├── pages/ # 产品展示部(用户看到的页面)
│ │ ├── index/ # 首页
│ │ └── user/ # 用户中心
│ ├── store/ # ️ 数据仓库部
│ │ ├── app.ts # 应用全局数据
│ │ └── user.ts # 用户相关数据
│ ├── types/ # 规则制定部(TypeScript 类型定义)
│ │ └── uni.d.ts # uniapp 的使用说明书
│ └── utils/ # 工具维修部
│ ├── common.ts # 万能工具箱
│ ├── request.ts # 网络通信工具
│ └── storage.ts # 本地存储工具
├── App.vue # 公司大门(应用入口)
├── main.ts # ⚡ 电源总开关
├── pages.json # ️ 导航地图(页面路由配置)
├── manifest.json # 营业执照(应用信息配置)
└── uni.scss # 公司VI标准(全局样式)
这样的结构有什么好处?找东西超级方便! 想找网络请求相关的代码?直接去 utils/request.ts
。想添加新页面?去 pages/
文件夹。想修改全局样式?uni.scss
等着你。
想象一下你的应用是一个商店,状态管理就像是这个商店的"收银台",所有重要信息都在这里集中管理:用户信息、购物车内容、商品库存等等。
没有状态管理的应用就像没有大脑的人 - 页面 A 记住的用户信息,页面 B 就忘了;用户登录状态在这个组件有效,那个组件就不认识你了。
// store/app.ts - 这是应用的"大脑"
export const useAppStore = defineStore('app', () => {
// 这些是应用要记住的重要信息
const appVersion = ref('1.0.0') // 应用版本号
const systemInfo = ref(null) // 手机系统信息
const networkType = ref('unknown') // 网络状态
const currentUser = ref(null) // 当前登录用户
// 初始化应用 - 就像开机启动时要做的事情
const initApp = async () => {
await getSystemInfo() // 获取手机信息
await getNetworkType() // 检查网络状态
checkLoginStatus() // 检查是否已登录
}
// 用户登录
const login = async (username, password) => {
try {
const user = await api.login(username, password)
currentUser.value = user
showToast('登录成功!')
} catch (error) {
showToast('登录失败,请检查用户名和密码')
}
}
// 用户退出
const logout = () => {
currentUser.value = null
removeStorage('token') // 清除本地存储的登录凭证
showToast('已退出登录')
}
return {
appVersion,
systemInfo,
networkType,
currentUser,
initApp,
login,
logout
}
})
<template>
<view>
<text>当前版本:{{ appStore.appVersion }}</text>
<text v-if="appStore.currentUser">
欢迎,{{ appStore.currentUser.name }}!
</text>
<button v-else @click="showLogin">点击登录</button>
</view>
</template>
<script setup>
import { useAppStore } from '@/store/app'
const appStore = useAppStore()
const showLogin = () => {
// 调用登录功能
appStore.login('用户名', '密码')
}
</script>
好处是什么?
你的应用需要和服务器"聊天"对吧?比如登录、获取数据、上传图片等。网络请求就是这个"快递员",负责在你的应用和服务器之间传递消息。
原生的网络请求就像让你自己去邮局寄快递 - 每次都要填一大堆单子,很麻烦。我们的封装就像有了个专属快递员,你只要说"帮我寄个包裹到这个地址",其他的他都帮你搞定。
// utils/request.ts - 这是我们的"快递公司"
const request = axios.create({
baseURL: 'https://api.yourapp.com', // 服务器地址
timeout: 10000, // 10秒没响应就算超时
headers: {
'Content-Type': 'application/json' // 告诉服务器我们发的是JSON
}
})
// 请求拦截器 - 快递出发前的检查
request.interceptors.request.use(config => {
// 自动添加用户令牌(就像寄快递要写寄件人信息)
const token = getStorage('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// 添加时间戳防止缓存
if (config.method === 'get') {
config.params = { ...config.params, _t: Date.now() }
}
console.log('发送请求:', config.url)
return config
})
// 响应拦截器 - 快递到了的处理
request.interceptors.response.use(
response => {
console.log('请求成功:', response.config.url)
// 统一处理服务器返回的数据格式
const { code, data, message } = response.data
if (code === 200) {
return data // 只返回真正的数据
} else if (code === 401) {
// 登录过期,自动跳转到登录页
showToast('登录过期,请重新登录')
uni.navigateTo({ url: '/pages/login/login' })
return Promise.reject(new Error('登录过期'))
} else {
// 其他错误,显示错误信息
showToast(message || '请求失败')
return Promise.reject(new Error(message))
}
},
error => {
console.log('请求失败:', error)
// 网络错误的友好提示
if (error.code === 'NETWORK_ERROR') {
showToast('网络连接失败,请检查网络')
} else if (error.code === 'TIMEOUT') {
showToast('请求超时,请稍后重试')
} else {
showToast('服务器开小差了,请稍后重试')
}
return Promise.reject(error)
}
)
// api/index.ts - 定义具体的接口
export const userApi = {
// 用户登录
login: (username: string, password: string) => {
return request.post('/auth/login', { username, password })
},
// 获取用户信息
getUserInfo: () => {
return request.get('/user/info')
},
// 更新用户头像
updateAvatar: (avatarFile: File) => {
const formData = new FormData()
formData.append('avatar', avatarFile)
return request.post('/user/avatar', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
},
// 获取商品列表
getProductList: (page: number, size: number) => {
return request.get('/products', { params: { page, size } })
}
}
// 在页面中使用
const handleLogin = async () => {
try {
showLoading('登录中...')
const userInfo = await userApi.login(username.value, password.value)
console.log('登录成功,用户信息:', userInfo)
// 保存用户信息到状态管理
const appStore = useAppStore()
appStore.setUserInfo(userInfo)
// 跳转到首页
uni.switchTab({ url: '/pages/index/index' })
} catch (error) {
console.log('登录失败:', error.message)
// 错误信息已经在拦截器中显示了,这里不用再处理
} finally {
hideLoading()
}
}
这样封装的好处:
想象一下你的手机有个小盒子,可以把重要的东西放进去,下次打开应用还能找到。这就是本地存储 - 把数据保存在用户手机里,应用关闭了再打开,数据还在。
支持过期时间! 就像超市的食品有保质期一样,我们可以给数据设定"保质期",过期自动删除。
// utils/storage.ts - 智能存储工具
export const setStorage = (key: string, value: any, expire?: number) => {
const data = {
value, // 真正要存的数据
expire: expire ? Date.now() + expire : null, // 过期时间戳
createTime: Date.now() // 创建时间
}
try {
uni.setStorageSync(key, JSON.stringify(data))
console.log(`✅ 数据已保存: ${key}`)
} catch (error) {
console.error('❌ 存储失败:', error)
}
}
export const getStorage = (key: string) => {
try {
const jsonData = uni.getStorageSync(key)
if (!jsonData) return null
const data = JSON.parse(jsonData)
// 检查是否过期
if (data.expire && Date.now() > data.expire) {
console.log(`⏰ 数据已过期,自动清除: ${key}`)
removeStorage(key)
return null
}
return data.value
} catch (error) {
console.error('❌ 读取存储失败:', error)
return null
}
}
export const removeStorage = (key: string) => {
try {
uni.removeStorageSync(key)
console.log(`️ 数据已删除: ${key}`)
} catch (error) {
console.error('❌ 删除失败:', error)
}
}
export const clearStorage = () => {
try {
uni.clearStorageSync()
console.log(' 所有存储数据已清除')
} catch (error) {
console.error('❌ 清除失败:', error)
}
}
// 检查存储使用情况
export const getStorageInfo = () => {
try {
const info = uni.getStorageInfoSync()
console.log(' 存储使用情况:', {
已使用: `${info.currentSize}KB`,
总容量: `${info.limitSize}KB`,
所有key: info.keys
})
return info
} catch (error) {
console.error('❌ 获取存储信息失败:', error)
return null
}
}
// 实际应用中怎么用
// 1. 保存用户登录信息(7天后过期)
const saveUserLogin = (userInfo, token) => {
setStorage('userInfo', userInfo, 7 * 24 * 60 * 60 * 1000) // 7天
setStorage('token', token, 7 * 24 * 60 * 60 * 1000)
}
// 2. 保存用户设置(永不过期)
const saveUserSettings = (settings) => {
setStorage('userSettings', settings) // 不传过期时间就是永久保存
}
// 3. 缓存商品列表(30分钟后过期)
const cacheProductList = (products) => {
setStorage('productList', products, 30 * 60 * 1000) // 30分钟
}
// 4. 保存表单草稿(1小时后过期)
const saveDraft = (formData) => {
setStorage('formDraft', formData, 60 * 60 * 1000) // 1小时
}
// 读取数据
const userInfo = getStorage('userInfo') // 可能返回用户信息或null(如果过期)
const settings = getStorage('userSettings') // 用户设置
const products = getStorage('productList') // 可能返回商品列表或null(如果过期)
// 在页面中使用
const loadUserData = () => {
const userInfo = getStorage('userInfo')
const token = getStorage('token')
if (userInfo && token) {
// 用户已登录且未过期
console.log('用户自动登录成功')
appStore.setUserInfo(userInfo)
} else {
// 用户未登录或登录已过期
console.log('需要重新登录')
uni.navigateTo({ url: '/pages/login/login' })
}
}
// 主题设置示例
const toggleTheme = () => {
const currentTheme = getStorage('theme') || 'light'
const newTheme = currentTheme === 'light' ? 'dark' : 'light'
setStorage('theme', newTheme) // 永久保存主题设置
applyTheme(newTheme)
}
// 智能缓存管理
export const CacheManager = {
// 缓存用户数据
cacheUserData: (userId: string, userData: any) => {
setStorage(`user_${userId}`, userData, 24 * 60 * 60 * 1000) // 24小时缓存
},
// 获取用户数据缓存
getUserDataCache: (userId: string) => {
return getStorage(`user_${userId}`)
},
// 清除所有用户相关缓存
clearUserCache: (userId: string) => {
const keys = ['userInfo', 'token', `user_${userId}`, 'userSettings']
keys.forEach(key => removeStorage(key))
},
// 检查并清理过期缓存
cleanExpiredCache: () => {
const info = getStorageInfo()
if (info && info.keys) {
info.keys.forEach(key => {
// 触发一次读取,自动清理过期数据
getStorage(key)
})
}
}
}
为什么要用这个工具?
TypeScript 就像给 JavaScript 加了个"智能语法检查器",写代码时会提示你哪里写错了,什么类型不匹配,避免很多低级错误。
举个例子:
// 普通JavaScript - 容易出错
function getUserAge(user) {
return user.age + 1 // 如果user是null会报错,如果age是字符串会出现'251'这种奇怪结果
}
// TypeScript - 智能提示和错误检查
function getUserAge(user: User | null): number {
if (!user) return 0 // 编辑器会提示user可能为null
return Number(user.age) + 1 // 编辑器会提示age的类型
}
// types/uni.d.ts - uniapp的"说明书"
declare global {
const uni: UniNamespace.Uni
namespace UniNamespace {
interface Uni {
// 消息提示
showToast(options: ShowToastOptions): void
showModal(options: ShowModalOptions): void
showLoading(options?: ShowLoadingOptions): void
hideLoading(): void
// 页面跳转
navigateTo(options: NavigateToOptions): void
redirectTo(options: RedirectToOptions): void
switchTab(options: SwitchTabOptions): void
navigateBack(options?: NavigateBackOptions): void
// 网络请求
request(options: RequestOptions): RequestTask
uploadFile(options: UploadFileOptions): UploadTask
downloadFile(options: DownloadFileOptions): DownloadTask
// 本地存储
setStorageSync(key: string, data: any): void
getStorageSync(key: string): any
removeStorageSync(key: string): void
clearStorageSync(): void
// 设备信息
getSystemInfo(options?: GetSystemInfoOptions): void
getNetworkType(options?: GetNetworkTypeOptions): void
// 更多 API...
}
// 详细的参数类型定义
interface ShowToastOptions {
title: string // 提示文字,必填
icon?: 'success' | 'error' | 'loading' | 'none' // 图标类型
duration?: number // 显示时长,默认1500ms
mask?: boolean // 是否显示透明蒙层,防止触摸穿透
success?: () => void // 成功回调
fail?: () => void // 失败回调
complete?: () => void // 完成回调
}
interface RequestOptions {
url: string // 请求地址,必填
data?: any // 请求参数
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' // 请求方法
header?: Record<string, string> // 请求头
timeout?: number // 超时时间
success?: (res: RequestSuccessCallbackResult) => void
fail?: (err: any) => void
complete?: () => void
}
// 更多类型定义...
}
}
export {} // 使这个文件成为一个模块
// 1. 智能提示 - 编辑器会自动提示可用的属性和方法
uni.showToast({
title: '操作成功',
icon: 'success', // 编辑器会提示可选值:'success' | 'error' | 'loading' | 'none'
duration: 2000
})
// 2. 错误检查 - 类型不匹配会立即报错
uni.showToast({
title: '操作成功',
icon: 'ok' // ❌ 报错:'ok' 不是有效的图标类型
})
// 3. 函数参数检查
const navigateToPage = (url: string, params?: Record<string, any>) => {
const query = params ? '?' + new URLSearchParams(params).toString() : ''
uni.navigateTo({
url: url + query,
success: () => console.log('跳转成功'),
fail: (err) => console.log('跳转失败', err)
})
}
// 使用时有智能提示
navigateToPage('/pages/detail/detail', {
id: 123, // ✅ 正确
name: 'product' // ✅ 正确
})
// 4. 接口数据类型定义
interface User {
id: number
name: string
email: string
avatar?: string // 可选属性
createdAt: Date
}
interface Product {
id: number
title: string
price: number
images: string[]
category: {
id: number
name: string
}
}
// API 返回数据类型定义
interface ApiResponse<T = any> {
code: number
message: string
data: T
}
// 使用时有完整的类型提示
const getUserInfo = async (userId: number): Promise<User> => {
const response: ApiResponse<User> = await request.get(`/users/${userId}`)
return response.data // 这里会有 User 类型的智能提示
}
// 5. 组件属性类型定义
interface ButtonProps {
type?: 'primary' | 'secondary' | 'danger'
size?: 'small' | 'medium' | 'large'
disabled?: boolean
loading?: boolean
onClick?: () => void
}
// Vue组件中使用
const props = defineProps<ButtonProps>()
// 现在所有属性都有类型检查和智能提示
// tsconfig.json - TypeScript 配置文件
{
"compilerOptions": {
"target": "es2020",
"module": "es2020",
"moduleResolution": "node",
"strict": true, // 开启严格模式
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"], // 路径别名
"@/components/*": ["src/components/*"],
"@/utils/*": ["src/utils/*"],
"@/api/*": ["src/api/*"]
},
"types": [
"@dcloudio/types", // uniapp 官方类型
"./src/types/uni.d.ts" // 我们扩展的类型
]
},
"include": [
"src/**/*",
"*.vue",
"*.ts"
],
"exclude": [
"node_modules",
"dist",
"unpackage"
]
}
TypeScript 的实际好处:
想象一下乐高积木,每个小零件都有特定的功能,可以组合成各种复杂的作品。我们的组件就是代码世界的"乐高积木"。
我们已经为你准备好了最常用的组件,拿来就能用!
什么时候用?
怎么用?
<template>
<view>
<!-- 简单用法 -->
<LoadingSpinner :visible="isLoading" />
<!-- 完整用法 -->
<LoadingSpinner
:visible="isLoading"
text="正在登录中..."
size="large"
:overlay="true"
/>
</view>
</template>
<script setup>
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'
const isLoading = ref(false)
const handleLogin = async () => {
isLoading.value = true // 显示加载动画
try {
await api.login(username, password)
console.log('登录成功')
} finally {
isLoading.value = false // 隐藏加载动画
}
}
</script>
参数说明:
visible
: 是否显示(true/false)text
: 显示的文字,比如"加载中…"size
: 大小(‘small’ | ‘medium’ | ‘large’)overlay
: 是否显示遮罩层,防止用户乱点什么时候用?
怎么用?
<template>
<view>
<!-- 基础用法 -->
<EmptyState v-if="productList.length === 0" />
<!-- 自定义用法 -->
<EmptyState
v-if="searchResults.length === 0"
title="没有找到相关商品"
description="试试其他关键词,或者浏览推荐商品"
icon=""
button-text="查看推荐"
@button-click="showRecommended"
/>
<!-- 购物车为空 -->
<EmptyState
v-if="cartItems.length === 0"
title="购物车是空的"
description="快去挑选心仪的商品吧"
icon=""
button-text="去购物"
@button-click="goShopping"
/>
</view>
</template>
<script setup>
import EmptyState from '@/components/common/EmptyState.vue'
const productList = ref([])
const searchResults = ref([])
const cartItems = ref([])
const showRecommended = () => {
// 显示推荐商品
uni.navigateTo({ url: '/pages/recommend/recommend' })
}
const goShopping = () => {
// 跳转到商品页面
uni.switchTab({ url: '/pages/products/products' })
}
</script>
参数说明:
title
: 主标题,比如"暂无数据"description
: 描述文字(可选)icon
: 显示的图标(可选),可以用 emoji 或图片button-text
: 按钮文字(可选)@button-click
: 按钮点击事件(可选)组件文件命名:
PascalCase.vue // 每个单词首字母大写,比如 UserProfile.vue
组件基本结构:
<template>
<view class="my-component">
<!-- 组件内容 -->
</view>
</template>
<script setup lang="ts">
// 1. 定义属性类型
interface Props {
title: string // 必填属性
subtitle?: string // 可选属性
type?: 'primary' | 'secondary' // 限定值
onClick?: () => void // 事件回调
}
// 2. 定义默认值
const props = withDefaults(defineProps<Props>(), {
type: 'primary',
subtitle: ''
})
// 3. 定义事件
const emit = defineEmits<{
click: [] // 无参数事件
change: [value: string] // 有参数事件
}>()
// 4. 组件逻辑
const handleClick = () => {
emit('click')
props.onClick?.()
}
</script>
<style lang="scss" scoped>
.my-component {
// 组件样式
}
</style>
使用自定义组件:
<template>
<MyComponent
title="标题"
:type="buttonType"
@click="handleComponentClick"
/>
</template>
<script setup>
import MyComponent from '@/components/MyComponent.vue'
const buttonType = ref('primary')
const handleComponentClick = () => {
console.log('组件被点击了')
}
</script>
这个页面展示了框架的核心能力:
展示用户相关功能:
<!-- 布局相关 -->
<view class="flex">横向排列</view>
<view class="flex-center">居中对齐</view>
<view class="flex-between">两端对齐</view>
<!-- 间距相关 -->
<view class="m-2 p-3">外边距16px,内边距24px</view>
<view class="mt-1 mb-2">上边距8px,下边距16px</view>
<!-- 文字相关 -->
<text class="text-center text-primary">居中的蓝色文字</text>
<text class="text-lg">大号文字</text>
<!-- 颜色相关 -->
<view class="text-success">成功绿色</view>
<view class="text-warning">警告橙色</view>
<view class="text-danger">危险红色</view>
数字规律:
m-1
= 8px,m-2
= 16px,m-3
= 24pxmt
(上) mr
(右) mb
(下) ml
(左)m
(margin外边距) p
(padding内边距)npm install
# 在项目根目录运行
npm install # 安装依赖
然后在 HBuilderX 中:
src/pages/demo/demo.vue
<template>
<view class="demo-page">
<text class="title">这是我的新页面</text>
<button @click="handleClick">点击我</button>
</view>
</template>
<script setup lang="ts">
import { showToast } from '@/utils/common'
const handleClick = () => {
showToast('Hello World!')
}
</script>
<style lang="scss" scoped>
.demo-page {
padding: 20px;
text-align: center;
}
.title {
font-size: 18px;
margin-bottom: 20px;
}
</style>
pages.json
中添加{
"pages": [
// ... 其他页面
{
"path": "src/pages/demo/demo",
"style": {
"navigationBarTitleText": "演示页面"
}
}
]
}
<button @click="goToDemo">去新页面</button>
<script setup>
const goToDemo = () => {
uni.navigateTo({ url: '/src/pages/demo/demo' })
}
</script>
<script setup lang="ts">
import { request } from '@/utils/request'
const userList = ref([])
const loading = ref(false)
const loadUsers = async () => {
loading.value = true
try {
const data = await request.get('/api/users')
userList.value = data
} catch (error) {
console.log('获取用户列表失败:', error)
} finally {
loading.value = false
}
}
// 页面加载时获取数据
onMounted(() => {
loadUsers()
})
</script>
<script setup lang="ts">
import { setStorage, getStorage } from '@/utils/storage'
// 保存用户偏好
const savePreference = () => {
setStorage('userTheme', 'dark') // 永久保存
setStorage('tempData', { id: 1 }, 60 * 60 * 1000) // 1小时后过期
}
// 读取用户偏好
const loadPreference = () => {
const theme = getStorage('userTheme')
if (theme) {
console.log('用户喜欢的主题:', theme)
}
}
</script>
// 在真机上查看日志
console.log('调试信息:', data)
// 显示调试信息
uni.showModal({
title: '调试',
content: JSON.stringify(data, null, 2)
})
<style lang="scss" scoped>
/* 使用 rpx 单位自动适配屏幕 */
.container {
width: 750rpx; /* 等于屏幕宽度 */
height: 200rpx; /* 在任何设备上都是合适的高度 */
}
/* 安全区域适配 */
.bottom-bar {
padding-bottom: env(safe-area-inset-bottom); /* 适配刘海屏 */
}
</style>
<template>
<!-- 长列表优化 -->
<scroll-view scroll-y class="list">
<view v-for="item in visibleItems" :key="item.id">
{{ item.name }}
</view>
</scroll-view>
</template>
<script setup>
// 只渲染可见的列表项
const visibleItems = computed(() => {
return allItems.value.slice(0, 50) // 只显示前50项
})
</script>
这个框架就像一个"开发工具箱",里面有你需要的各种工具:
一句话总结: 学会Vue,就能开发多平台应用!
有问题?看代码注释,或者直接在现有功能基础上修改,这是学习最快的方式。祝你开发愉快!
完整源码地址: https://gitcode.com/codeCao/uniapp-multi-platform.git
欢迎 Star ⭐ 和 Fork ,一起完善这个多平台开发框架!
如果这个项目对你有帮助,别忘了给个 Star 支持一下~