uniapp + Vue3 + Pinia 跨端待办应用开发实战:从零到上线的完整教程

2026-08-23 0 136

上个月给公司做内部工具,需求是一个产品要同时上微信小程序、H5 和 App。时间紧任务重,我直接选了 uni-app,用 Vue3 + Vite 模板把项目搭起来,状态管理没有用 Vuex,直接用 Pinia。一天时间跑通了核心流程,三端同时上线。

最近 uni-app 社区里关于 Vue3 + Vite 编译速度的讨论挺热的,我今天也把这一套完整方案从项目初始化到跨端适配全部拆开讲。文章所有代码都能跑通,跟着敲一遍,你也能做出自己的跨端待办应用。

一、技术选型,为什么是这套组合

如果你的项目需要同时覆盖小程序、H5、App,团队主要技术栈又是 Vue,那 uni-app 可能是目前最靠谱的选择。这是业务倒逼出来的结论,不是吹。

Vue3 的 Composition API 写起来比 Options API 舒服太多,代码逻辑可以按功能聚合,而不是分散在 data、methods、computed 各个地方。Pinia 更是把状态管理简化到了一个相当轻量的程度,没有 mutation 的冗余步骤,store 就是一个普通的响应式对象,怎么写怎么顺手。

二、创建项目

用官方 Vite 预设模板来创建,可以减少一堆手工配置的麻烦。

npx degit dcloudio/uni-preset-vue#vite my-todo
cd my-todo
npm install
npm install pinia

安装完成后,项目目录结构大体是这样的:

my-todo/
├── src/
│   ├── pages/
│   │   └── index/
│   │       └── index.vue
│   ├── static/
│   ├── App.vue
│   ├── main.js
│   ├── manifest.json
│   └── pages.json
├── index.html
└── package.json

三、注册 Pinia:main.js 里的关键细节

打开 src/main.js,这里有一个需要注意的细节。

import { createSSRApp } from 'vue'
import * as Pinia from 'pinia'
import App from './App.vue'

export function createApp() {
    const app = createSSRApp(App)
    app.use(Pinia.createPinia())
    return {
        app,
        Pinia
    }
}

看到没有,我用的是 import * as Pinia from 'pinia',而不是 import { createPinia } from 'pinia'。这个坑折腾了我一下午——uni-app 编译到某些平台时,会对具名导入做 tree-shaking,把 Pinia 的核心逻辑摇掉了,导致运行时直接报错。用命名空间导入就稳了,建议大家都这么写。

四、定义 Todo Store:数据管理的核心

在 src 目录下新建 stores 文件夹,创建 todo.js。

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useTodoStore = defineStore('todo', () => {
    const todos = ref([])

    const completedCount = computed(() => {
        return todos.value.filter(item => item.done).length
    })

    const activeCount = computed(() => {
        return todos.value.filter(item => !item.done).length
    })

    function addTodo(text) {
        if (!text || !text.trim()) return
        todos.value.unshift({
            id: Date.now().toString(36) + Math.random().toString(36).slice(2, 8),
            text: text.trim(),
            done: false
        })
        save()
    }

    function toggleTodo(id) {
        const todo = todos.value.find(item => item.id === id)
        if (todo) {
            todo.done = !todo.done
            save()
        }
    }

    function removeTodo(id) {
        todos.value = todos.value.filter(item => item.id !== id)
        save()
    }

    function load() {
        const data = uni.getStorageSync('todos')
        if (data) {
            todos.value = data
        }
    }

    function save() {
        uni.setStorageSync('todos', todos.value)
    }

    return { todos, completedCount, activeCount, addTodo, toggleTodo, removeTodo, load }
})

这个 store 用的 setup 语法,非常直观。每次修改数据后调用 save(),把最新数组同步到本地存储。load() 在页面加载时调用,从本地存储恢复之前的数据。

id 我用了时间戳的 36 进制加上随机字符串,避免快速操作时出现重复 id,这个细节在列表渲染中很重要,key 一旦重复,渲染就会出错。

五、编写待办页面:组合式 API 的完整实践

核心页面在 src/pages/index/index.vue,上完整代码。

<template>
    <view class="page">
        <view class="header">
            <text class="title">我的待办</text>
            <text class="date">{{ today }}</text>
        </view>

        <view class="input-wrap">
            <input
                v-model="inputText"
                placeholder="有什么要做的?"
                confirm-type="done"
                @confirm="handleAdd"
            />
            <button class="btn" @click="handleAdd">添加</button>
        </view>

        <view class="stats">
            <text>全部 {{ todoStore.todos.length }}</text>
            <text>待办 {{ todoStore.activeCount }}</text>
            <text>完成 {{ todoStore.completedCount }}</text>
        </view>

        <view class="list">
            <view
                v-for="todo in todoStore.todos"
                :key="todo.id"
                class="item"
                :class="{ done: todo.done }"
            >
                <view class="circle" @click="todoStore.toggleTodo(todo.id)">
                    <text v-if="todo.done">✓</text>
                </view>
                <text class="text">{{ todo.text }}</text>
                <view class="del" @click="todoStore.removeTodo(todo.id)">
                    <text>删除</text>
                </view>
            </view>
        </view>

        <view v-if="todoStore.todos.length === 0" class="empty">
            <text>暂无待办,给自己加一个目标吧</text>
        </view>
    </view>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import { useTodoStore } from '@/stores/todo'

const todoStore = useTodoStore()
const inputText = ref('')

const today = computed(() => {
    const d = new Date()
    return `${d.getMonth() + 1}月${d.getDate()}日`
})

function handleAdd() {
    todoStore.addTodo(inputText.value)
    inputText.value = ''
}

onMounted(() => {
    todoStore.load()
})
</script>

页面加载时用 onMounted 调用 load(),把上次的数据恢复回来。输入框的 confirm-type=”done” 会把键盘右下角变成”完成”按钮,配合 @confirm 事件,用户在手机上不需要去点”添加”按钮也能直接录入,这个交互细节对移动端体验很重要。

记住,代码里没有展示样式部分,实际项目里样式需要自己补上,建议用 scoped。

六、跨端适配,这五个坑必须说

同样一段代码,在不同端跑起来行为可能会有细微差异。我在微信小程序、H5、iOS App 三个端都跑了一遍,这几个坑是优先需要处理的。

坑一:安全区域没有适配

iPhone 底部的 Home Indicator 会遮挡页面内容。在自定义导航栏的场景下,必须通过 uni.getSystemInfoSync() 获取状态栏高度,然后动态设置页面的 padding-top。

const info = uni.getSystemInfoSync()
const statusBarHeight = info.statusBarHeight || 20

H5 端没有状态栏,需要用条件编译单独处理这段逻辑。

坑二:点击态不一致

H5 上点击元素自带 :hover 效果,小程序上啥都没有。为了让两端体验一致,建议所有可点击元素都绑定 hover-class,然后设置 hover-stay-time=”70″。如果不想要点击态,在 H5 端页面上加上 -webkit-tap-highlight-color: transparent

坑三:本地存储的容量限制

微信小程序的 setStorageSync 单条数据上限是 1MB,H5 的 localStorage 上限是 5MB。待办数据少的时候没啥感觉,但如果数据量很大,建议压缩存储或者使用异步版本。我这边本地存储只是缓存,后端还有接口,所以直接用同步版本没毛病。

坑四:flex 布局的兼容性

部分旧版本 iOS 上 justify-content: space-between 会表现异常,这就是为什么有些小程序页面要求使用 padding 和 margin 来布局。建议尽早约定好布局方案,避免临时改样式。

坑五:pages.json 的导航栏自定义

把 navigationStyle 设为 custom 后,默认导航栏消失了,但状态栏的占位区域也需要自行处理。如果不做处理,页面顶部会顶到状态栏,非常难看。

七、扩展:加一个分组筛选功能

为了演示 Pinia 的灵活性,我再加一个筛选功能:全部 / 待办 / 已完成 三个 tab 切换。

在 store 中加一个筛选状态和过滤后的列表:

const filter = ref('all') // all | active | done

const filteredTodos = computed(() => {
    if (filter.value === 'active') {
        return todos.value.filter(item => !item.done)
    }
    if (filter.value === 'done') {
        return todos.value.filter(item => item.done)
    }
    return todos.value
})

然后页面上放三个 tab,点击切换 filter 的值。这个逻辑如果在 Options API 里,又要写三个 methods 三个 data 一个 computed,现在用 setup 风格就是一个 ref 加一个 computed,清爽得不行。

八、打包上线

不同端的打包命令 package.json 里都配好了,常用的是下面几个:

npm run dev:mp-weixin   # 微信小程序
npm run dev:h5          # H5
npm run build:app       # App 打包

小程序端把 dist/dev/mp-weixin 目录导入微信开发者工具就能预览。H5 端打包后丢到服务器上就行。App 端注意了,不能用 npm 命令直接生成安装包,还是得用 HBuilderX 打开项目,然后走云打包或者本地打包。

总结

这套组合用下来,我的感受是 Pinia 把状态管理变轻了,Composition API 把业务逻辑理顺了,uni-app 把多端打包的麻烦事扛了。三个配合起来,开发效率确实高。

不过 uni-app 的坑也是真的多,尤其是跨端兼容这块,建议项目一开始就统一好代码规范,条件编译的地方写清楚注释,不然后面维护代码的人真的会骂人。

今天这篇文章是用一个线上在跑的案例拆解的。代码都在上面,自己动手敲一遍,比看十遍文章有用。有问题评论区聊,看到会回。

uniapp + Vue3 + Pinia 跨端待办应用开发实战:从零到上线的完整教程
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 uniapp uniapp + Vue3 + Pinia 跨端待办应用开发实战:从零到上线的完整教程 https://www.taomawang.com/web/uniapp/2590.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务