在跨端开发领域,Uniapp 凭借其 "一套代码、多端部署" 的核心优势,已成为开发者首选的跨端解决方案。而 Vue3 作为当前最流行的前端框架之一,不仅带来了更优秀的性能表现,其推出的 <script setup> 语法糖更是彻底简化了组件开发流程,让代码结构更简洁、逻辑更清晰。
当 Uniapp 遇上 Vue3 语法糖,不仅能充分发挥跨端开发的高效性,还能享受 Vue3 带来的开发体验升级 ------ 更少的模板代码、更直观的 Composition API 调用、更便捷的组件通信方式,让跨端应用开发变得事半功倍。
一、项目初始化与技术栈选型
1.1 项目创建与基础配置
首先需要确保已安装最新版的 HBuilderX(Uniapp 官方推荐开发工具),或使用 Vue CLI 搭建 Uniapp 项目。本文采用 HBuilderX 可视化创建,步骤如下:
- 打开 HBuilderX → 新建 → 项目 → 选择 "Uniapp 项目";
- 填写项目名称(如
uniapp-vue3-dashboard),选择 "Vue3 + TypeScript" 模板,勾选 "启用<script setup>语法糖"; - 勾选需要适配的端(本文选择 H5、微信小程序、App 端);
- 点击创建,等待项目初始化完成。
1.2 核心技术栈选型
| 技术 / 工具 | 说明 | 核心作用 |
|---|---|---|
| Uniapp | 跨端开发框架 | 一套代码适配多端,统一页面路由、组件体系 |
Vue3 + <script setup> |
前端框架与语法糖 | 简化组件开发,优化逻辑组织,提升开发效率 |
| TypeScript | 类型检查工具 | 提供类型约束,减少运行时错误,提升代码可维护性 |
| Pinia | 状态管理工具 | 替代 Vuex,轻量简洁,原生支持 Vue3 |
| uView Plus | UI 组件库 | 适配 Uniapp 的 Vue3 组件库,提供丰富 UI 组件 |
| ECharts UniApp 版 | 图表可视化库 | 实现折线图、柱状图、饼图等数据可视化展示 |
| PostCSS | 样式处理工具 | 配合 px2rpx 插件实现多端自适应布局 |
1.3 项目目录结构说明
XML
uniapp-vue3-dashboard/
├── api/ // 接口请求封装
│ ├── index.ts // 请求拦截、响应拦截配置
│ └── dashboard.ts // 数据看板相关接口
├── components/ // 公共组件
│ ├── ChartCard.vue // 图表卡片组件
│ ├── StatisticCard.vue // 统计数字卡片组件
│ └── NavBar.vue // 自定义导航栏组件
├── pages/ // 页面目录
│ ├── index/ // 首页(数据看板)
│ ├── detail/ // 详情页
│ └── mine/ // 个人中心
├── pinia/ // Pinia 状态管理
│ ├── index.ts // Pinia 实例创建
│ └── modules/ // 模块划分
│ ├── user.ts // 用户状态
│ └── dashboard.ts // 看板数据状态
├── static/ // 静态资源(图片、图标)
├── styles/ // 全局样式
│ ├── common.scss // 公共样式变量
│ └── main.scss // 全局样式入口
├── types/ // TypeScript 类型定义
│ └── index.ts // 公共类型、接口定义
├── utils/ // 工具函数
│ ├── format.ts // 数据格式化工具
│ └── adapter.ts // 多端适配工具
├── App.vue // 应用入口组件
├── main.ts // 入口文件(初始化 Pinia、路由等)
├── manifest.json // Uniapp 配置文件(多端配置、权限等)
├── pages.json // 路由配置文件
└── tsconfig.json // TypeScript 配置文件
1.4 基础配置实现
1.4.1 全局样式配置(styles/main.scss)
css
// 引入公共样式变量
@import "./common.scss";
// 全局样式重置
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
// 全局字体设置(适配多端)
body,
uni-page-body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: $font-size-base; // 从 common.scss 引入的变量
color: $text-color-primary;
background-color: $bg-color-primary;
}
// 自定义滚动条样式(H5 端)
::-webkit-scrollbar {
width: 4px;
height: 4px;
}
::-webkit-scrollbar-thumb {
background-color: $border-color-light;
border-radius: 2px;
}
// 适配小程序、App 端的安全区域
.page-container {
padding-bottom: env(safe-area-inset-bottom);
min-height: 100vh;
}
1.4.2 Pinia 初始化(pinia/index.ts)
TypeScript
import { createPinia } from 'pinia'
import { App } from 'vue'
// 创建 Pinia 实例
const pinia = createPinia()
// 导出安装函数,在 main.ts 中使用
export function installPinia(app: App) {
app.use(pinia)
}
export default pinia
1.4.3 接口请求封装(api/index.ts)
TypeScript
import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
import { showToast } from '@/utils/uni-api' // 封装的 uni.showToast
// 创建 axios 实例
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, // 从环境变量获取基础地址
timeout: 10000, // 请求超时时间
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
})
// 请求拦截器:添加 token、设置请求头等
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
// 从 Pinia 获取用户 token(后续会实现 user 模块)
const token = uni.getStorageSync('token')
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error: AxiosError) => {
return Promise.reject(error)
}
)
// 响应拦截器:统一处理响应结果、错误提示等
service.interceptors.response.use(
(response: AxiosResponse) => {
const res = response.data
// 假设后端约定:code=200 为成功,其他为错误
if (res.code !== 200) {
showToast(res.message || '请求失败', 'error')
return Promise.reject(res)
}
return res.data // 直接返回响应体中的 data 字段
},
(error: AxiosError) => {
// 网络错误、超时等处理
const message = error.message || '网络异常,请稍后重试'
showToast(message, 'error')
return Promise.reject(error)
}
)
// 封装请求方法:get、post、put、delete
export const request = {
get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
return service.get(url, config)
},
post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return service.post(url, data, config)
},
put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return service.put(url, data, config)
},
delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
return service.delete(url, config)
}
}
export default service
1.4.4 TypeScript 类型定义(types/index.ts)
TypeScript
// 响应数据通用类型
export interface ApiResponse<T = any> {
code: number
message: string
data: T
}
// 统计卡片数据类型
export interface StatisticItem {
id: string
title: string
value: number
unit: string
icon: string
color: string
trend: 'up' | 'down' | 'flat' // 趋势:上升、下降、平稳
trendValue: number // 趋势值(百分比)
}
// 折线图数据类型
export interface LineChartData {
dates: string[] // X轴日期
series: {
name: string
data: number[] // Y轴数据
color: string
}[]
}
// 饼图数据类型
export interface PieChartData {
name: string
value: number
color: string
}[]
// 柱状图数据类型
export interface BarChartData {
categories: string[] // X轴分类
series: {
name: string
data: number[] // Y轴数据
color: string
}[]
}
二、核心功能实现:响应式数据看板
2.1 状态管理设计(pinia/modules/dashboard.ts)
使用 Pinia 管理看板数据,实现数据缓存、全局共享,避免重复请求:
TypeScript
import { defineStore } from 'pinia'
import { request } from '@/api'
import {
StatisticItem,
LineChartData,
PieChartData,
BarChartData
} from '@/types'
// 定义看板数据存储
export const useDashboardStore = defineStore('dashboard', {
state: () => ({
// 统计卡片数据
statisticData: [] as StatisticItem[],
// 折线图数据(访问量趋势)
visitLineData: {} as LineChartData,
// 饼图数据(用户来源分布)
sourcePieData: [] as PieChartData,
// 柱状图数据(各模块使用占比)
moduleBarData: {} as BarChartData,
// 加载状态
loading: false
}),
getters: {
// 计算总访问量(从折线图数据汇总)
totalVisits(): number {
return this.visitLineData.series?.[0]?.data.reduce((sum, curr) => sum + curr, 0) || 0
}
},
actions: {
// 重置所有数据
resetData() {
this.statisticData = []
this.visitLineData = {} as LineChartData
this.sourcePieData = []
this.moduleBarData = {} as BarChartData
},
// 获取所有看板数据(批量请求,优化性能)
async fetchAllDashboardData() {
try {
this.loading = true
// 并行请求多个接口,提升加载速度
const [statisticRes, lineRes, pieRes, barRes] = await Promise.all([
request.get<StatisticItem[]>('/dashboard/statistic'),
request.get<LineChartData>('/dashboard/visit-line'),
request.get<PieChartData>('/dashboard/source-pie'),
request.get<BarChartData>('/dashboard/module-bar')
])
// 赋值到状态
this.statisticData = statisticRes
this.visitLineData = lineRes
this.sourcePieData = pieRes
this.moduleBarData = barRes
} catch (error) {
console.error('获取看板数据失败:', error)
this.resetData() // 失败时重置数据
} finally {
this.loading = false
}
}
},
// 持久化配置:将数据缓存到本地存储,页面刷新不丢失
persist: {
enabled: true,
strategies: [
{
key: 'dashboard_data',
storage: uni.getStorageSync('isH5') ? localStorage : uni.getStorageSync, // 适配 H5 和小程序/App
paths: ['statisticData', 'visitLineData', 'sourcePieData', 'moduleBarData'] // 需要持久化的字段
}
]
}
})
2.2 公共组件开发
2.2.1 统计数字卡片(components/StatisticCard.vue)
实现数据展示、趋势指示、多端适配的统计卡片:
html
<template>
<view class="statistic-card" :style="{ backgroundColor: bgColor }">
<!-- 图标区域 -->
<view class="card-icon" :style="{ backgroundColor: item.color + '20' }">
<uni-icons :type="item.icon" :color="item.color" size="24"></uni-icons>
</view>
<!-- 数据区域 -->
<view class="card-content">
<view class="card-title">{{ item.title }}</view>
<view class="card-value">
<span class="value-text">{{ formatNumber(item.value) }}</span>
<span class="value-unit">{{ item.unit }}</span>
</view>
<!-- 趋势区域 -->
<view class="card-trend" :class="['trend-' + item.trend]">
<uni-icons
:type="item.trend === 'up' ? 'arrowup' : item.trend === 'down' ? 'arrowdown' : 'minus'"
:color="getTrendColor(item.trend)"
size="14"
></uni-icons>
<span class="trend-value">{{ item.trendValue }}%</span>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { defineProps, computed } from 'vue'
import { StatisticItem } from '@/types'
import { formatNumber } from '@/utils/format'
// 定义组件 props
const props = defineProps<{
item: StatisticItem // 统计数据项
bgColor?: string // 卡片背景色(默认白色)
}>()
// 计算趋势颜色
const getTrendColor = computed(() => {
switch (props.item.trend) {
case 'up':
return '#00b42a' // 上升-绿色
case 'down':
return '#f53f3f' // 下降-红色
case 'flat':
return '#86909c' // 平稳-灰色
default:
return '#86909c'
}
})
// 默认背景色
const bgColor = computed(() => props.bgColor || '#ffffff')
</script>
<style scoped lang="scss">
.statistic-card {
display: flex;
align-items: center;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
margin-bottom: 12px;
.card-icon {
width: 48px;
height: 48px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
}
.card-content {
flex: 1;
.card-title {
font-size: $font-size-sm;
color: $text-color-secondary;
margin-bottom: 4px;
}
.card-value {
display: flex;
align-items: baseline;
margin-bottom: 4px;
.value-text {
font-size: $font-size-lg;
font-weight: 600;
color: $text-color-primary;
}
.value-unit {
font-size: $font-size-base;
color: $text-color-secondary;
margin-left: 4px;
}
}
.card-trend {
display: flex;
align-items: center;
font-size: $font-size-xs;
.trend-value {
margin-left: 4px;
}
&.trend-up {
color: #00b42a;
}
&.trend-down {
color: #f53f3f;
}
&.trend-flat {
color: #86909c;
}
}
}
}
</style>
2.2.2 图表卡片组件(components/ChartCard.vue)
统一图表容器样式,适配不同图表类型(折线图、饼图、柱状图):
html
<template>
<view class="chart-card">
<!-- 卡片头部 -->
<view class="card-header">
<view class="header-title">{{ title }}</view>
<view class="header-action" @click="handleRefresh">
<uni-icons type="refresh" color="#86909c" size="16"></uni-icons>
</view>
</view>
<!-- 图表容器(适配多端高度) -->
<view class="chart-container" :style="{ height: chartHeight + 'px' }">
<!-- 插槽:传入具体图表组件 -->
<slot name="chart"></slot>
</view>
</view>
</template>
<script setup lang="ts">
import { defineProps, emit, defineEmits } from 'vue'
// 定义组件 props
const props = defineProps<{
title: string // 卡片标题
chartHeight?: number // 图表高度(默认 240px)
}>()
// 定义组件事件
const emit = defineEmits<{
(e: 'refresh'): void // 刷新事件
}>()
// 默认图表高度
const chartHeight = props.chartHeight || 240
// 处理刷新点击
const handleRefresh = () => {
emit('refresh')
}
</script>
<style scoped lang="scss">
.chart-card {
background-color: #ffffff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
padding: 16px;
margin-bottom: 16px;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.header-title {
font-size: $font-size-base;
font-weight: 500;
color: $text-color-primary;
}
.header-action {
cursor: pointer;
}
}
.chart-container {
width: 100%;
overflow: hidden;
}
</style>
2.3 首页数据看板实现(pages/index/index.vue)
整合所有组件,实现响应式布局、数据加载、多端适配:
html
<template>
<view class="page-container">
<!-- 自定义导航栏 -->
<nav-bar title="数据看板" :show-back="false"></nav-bar>
<!-- 加载中提示 -->
<uni-loading-mask :show="dashboardStore.loading" text="加载中..."></uni-loading-mask>
<!-- 统计卡片区域(2列布局,适配多端) -->
<view class="statistic-list">
<view class="statistic-row" v-for="(row, rowIndex) in statisticRows" :key="rowIndex">
<statistic-card
v-for="(item, index) in row"
:key="item.id"
:item="item"
:bgColor="index % 2 === 0 ? '#f0f9ff' : '#fef7fb'"
></statistic-card>
</view>
</view>
<!-- 图表区域 -->
<view class="chart-list">
<!-- 访问量趋势折线图 -->
<chart-card title="访问量趋势" @refresh="fetchDashboardData">
<template #chart>
<line-chart :data="dashboardStore.visitLineData"></line-chart>
</template>
</chart-card>
<!-- 双列图表布局(饼图 + 柱状图) -->
<view class="chart-row">
<!-- 用户来源分布饼图 -->
<chart-card title="用户来源分布" :chartHeight="200" @refresh="fetchDashboardData">
<template #chart>
<pie-chart :data="dashboardStore.sourcePieData"></pie-chart>
</template>
</chart-card>
<!-- 模块使用占比柱状图 -->
<chart-card title="模块使用占比" :chartHeight="200" @refresh="fetchDashboardData">
<template #chart>
<bar-chart :data="dashboardStore.moduleBarData"></bar-chart>
</template>
</chart-card>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { onMounted, computed } from 'vue'
import { useDashboardStore } from '@/pinia/modules/dashboard'
import NavBar from '@/components/NavBar.vue'
import StatisticCard from '@/components/StatisticCard.vue'
import ChartCard from '@/components/ChartCard.vue'
import LineChart from '@/components/charts/LineChart.vue'
import PieChart from '@/components/charts/PieChart.vue'
import BarChart from '@/components/charts/BarChart.vue'
// 获取 Pinia 实例
const dashboardStore = useDashboardStore()
// 初始化:获取看板数据
onMounted(() => {
// 优先使用缓存数据,无缓存则请求接口
if (dashboardStore.statisticData.length === 0) {
fetchDashboardData()
}
})
// 重新获取看板数据
const fetchDashboardData = () => {
dashboardStore.fetchAllDashboardData()
}
// 统计卡片分两行显示(每行2个)
const statisticRows = computed(() => {
const rows = []
// 将一维数组转为二维数组(每行2个元素)
for (let i = 0; i < dashboardStore.statisticData.length; i += 2) {
rows.push(dashboardStore.statisticData.slice(i, i + 2))
}
return rows
})
</script>
<style scoped lang="scss">
// 统计卡片布局:H5/PC 端2列,小程序/App 端自适应
.statistic-list {
padding: 16px;
.statistic-row {
display: flex;
gap: 12px;
margin-bottom: 12px;
// 响应式:屏幕宽度小于768px时,单列布局
@media (max-width: 767px) {
flex-direction: column;
}
statistic-card {
flex: 1; // 平分宽度
}
}
}
// 图表区域布局
.chart-list {
padding: 0 16px 16px;
.chart-row {
display: flex;
gap: 16px;
margin-top: 16px;
// 响应式:屏幕宽度小于768px时,单列布局
@media (max-width: 767px) {
flex-direction: column;
}
chart-card {
flex: 1; // 平分宽度
}
}
}
</style>
2.4 图表组件实现(以折线图为例)
html
<template>
<view class="line-chart">
<!-- ECharts 容器 -->
<view ref="chartRef" class="chart-dom"></view>
</view>
</template>
<script setup lang="ts">
import { defineProps, onMounted, onUnmounted, watch, ref } from 'vue'
import * as echarts from 'echarts'
import { LineChartData } from '@/types'
import { debounce } from '@/utils/format'
// 定义组件 props
const props = defineProps<{
data: LineChartData // 折线图数据
}>()
// ECharts 实例引用
const chartRef = ref<HTMLElement | null>(null)
let chartInstance: echarts.ECharts | null = null
// 初始化图表
const initChart = () => {
if (!chartRef.value || !props.data.dates || props.data.series.length === 0) return
// 创建 ECharts 实例
chartInstance = echarts.init(chartRef.value)
// 图表配置项
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
textStyle: {
fontSize: 12
},
padding: 10
},
grid: {
left: '10%',
right: '5%',
top: '15%',
bottom: '15%',
containLabel: true
},
xAxis: {
type: 'category',
data: props.data.dates,
axisLine: {
lineStyle: {
color: '#e5e6eb'
}
},
axisLabel: {
fontSize: 11,
color: '#86909c',
rotate: 30 // X轴标签旋转30度,避免重叠
},
splitLine: {
show: false
}
},
yAxis: {
type: 'value',
axisLine: {
show: false
},
axisLabel: {
fontSize: 11,
color: '#86909c',
formatter: (value: number) => {
// 数值格式化:大于1000时显示为千分位
return value >= 1000 ? (value / 1000).toFixed(1) + 'k' : value
}
},
splitLine: {
lineStyle: {
color: '#f2f3f5'
}
},
boundaryGap: [0, '10%']
},
series: props.data.series.map(series => ({
name: series.name,
type: 'line',
data: series.data,
smooth: true, // 平滑曲线
symbol: 'circle', // 标记点样式
symbolSize: 4,
lineStyle: {
width: 2,
color: series.color
},
itemStyle: {
color: series.color,
borderWidth: 1,
borderColor: '#ffffff'
},
areaStyle: {
// 填充区域渐变
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: series.color + '80'
},
{
offset: 1,
color: series.color + '10'
}
])
},
emphasis: {
itemStyle: {
symbolSize: 6
}
}
}))
}
// 设置配置项并渲染图表
chartInstance.setOption(option)
// 监听窗口大小变化,自适应图表(防抖处理)
const resizeHandler = debounce(() => {
chartInstance?.resize()
}, 300)
window.addEventListener('resize', resizeHandler)
// 组件卸载时移除监听
onUnmounted(() => {
window.removeEventListener('resize', resizeHandler)
})
}
// 监听数据变化,重新渲染图表
watch(
() => props.data,
(newVal) => {
if (newVal.dates && newVal.series.length > 0) {
if (chartInstance) {
// 已有实例,更新配置项
chartInstance.setOption({
xAxis: { data: newVal.dates },
series: newVal.series.map(series => ({
name: series.name,
data: series.data,
lineStyle: { color: series.color },
itemStyle: { color: series.color },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: series.color + '80' },
{ offset: 1, color: series.color + '10' }
])
}
}))
})
} else {
// 无实例,初始化图表
initChart()
}
}
},
{ deep: true } // 深度监听对象变化
)
// 组件挂载时初始化图表
onMounted(() => {
initChart()
})
// 组件卸载时销毁图表实例
onUnmounted(() => {
chartInstance?.dispose()
chartInstance = null
})
</script>
<style scoped lang="scss">
.line-chart {
width: 100%;
height: 100%;
.chart-dom {
width: 100%;
height: 100%;
}
}
</style>
三、多端适配与性能优化
3.1 多端适配方案
Uniapp 本身提供了完善的多端适配能力,结合 Vue3 语法糖,我们可以从以下几个维度实现精细化适配:
3.1.1 样式适配
-
rpx 自动转换 :Uniapp 支持将
rpx自动转换为对应端的单位(H5 转px,小程序转rpx,App 转dp),开发时优先使用rpx作为长度单位; -
媒体查询 :针对不同屏幕尺寸(如 H5 端的 PC 大屏和移动端小屏),使用
@media查询编写差异化样式,如首页的统计卡片在大屏时 2 列布局,小屏时单列布局; -
条件编译 :针对特定端的样式差异,使用 Uniapp 条件编译语法:
css/* #ifdef H5 */ // H5 端专属样式(如滚动条、导航栏高度) .header { height: 60px; } /* #endif */ /* #ifdef MP-WEIXIN */ // 微信小程序专属样式(如胶囊按钮适配) .header { height: 48px; padding-top: 10px; } /* #endif */3.1.2 功能适配
-
API 适配 :Uniapp 提供了
uni前缀的跨端 API,但部分 API 存在端差异,需使用条件编译处理:TypeScript// 保存图片到本地(适配 H5 和小程序) const saveImage = async (url: string) => { try { /* #ifdef H5 */ // H5 端:创建 a 标签下载 const link = document.createElement('a') link.href = url link.download = '图表.png' link.click() /* #endif */ /* #ifdef MP-WEIXIN || APP-PLUS */ // 小程序/App 端:使用 uni API await uni.downloadFile({ url }) await uni.saveImageToPhotosAlbum({ filePath: res.tempFilePath }) uni.showToast({ title: '保存成功' }) /* #endif */ } catch (error) { uni.showToast({ title: '保存失败', icon: 'none' }) } } -
组件适配 :部分组件在不同端的表现不一致,可通过
uni.createSelectorQuery()等 API 获取设备信息,动态调整组件属性:TypeScript// 动态设置图表高度(根据屏幕宽度适配) const setChartHeight = () => { uni.createSelectorQuery().in(getCurrentInstance()).select('.chart-container').boundingClientRect(rect => { if (rect) { // 图表高度 = 屏幕宽度 * 0.6(保持宽高比) chartHeight.value = rect.width * 0.6 } }).exec() }3.2 性能优化策略
3.2.1 数据层面优化
-
Pinia 持久化:将看板数据持久化到本地存储,页面刷新后无需重新请求接口,提升加载速度;
-
并行请求 :使用
Promise.all并行请求多个接口,减少请求等待时间(如 2.1 中fetchAllDashboardData方法); -
数据缓存与过期策略 :对高频访问但不常变化的数据(如统计卡片数据),设置缓存过期时间,避免过度请求:
TypeScript// 在 Pinia actions 中添加缓存过期逻辑 async fetchAllDashboardData() { // 检查缓存是否过期(假设缓存有效期为10分钟) const cacheTime = uni.getStorageSync('dashboard_cache_time') const now = Date.now() if (cacheTime && now - cacheTime < 10 * 60 * 1000) { // 缓存未过期,直接使用本地数据 return } // 缓存过期,重新请求接口 try { this.loading = true const [statisticRes, lineRes, pieRes, barRes] = await Promise.all([/* 接口请求 */]) // 更新状态 this.statisticData = statisticRes // ...其他数据赋值 // 记录缓存时间 uni.setStorageSync('dashboard_cache_time', now) } catch (error) { // 错误处理 } finally { this.loading = false } }3.2.2 渲染层面优化
-
组件懒加载:对非首屏组件(如详情页、个人中心),使用 Uniapp 的路由懒加载:
TypeScript
// pages.json
{
"pages": [
{
"path": "pages/index/index",
"style": { "navigationBarTitleText": "数据看板" }
},
{
"path": "pages/detail/detail",
"style": { "navigationBarTitleText": "详情页" },
"lazyCodeLoading": "requiredComponents" // 懒加载
}
]
}
-
虚拟列表 :如果看板数据量较大(如统计卡片超过 20 个),使用
uni-virtual-list组件实现虚拟滚动,减少 DOM 节点数量:html<uni-virtual-list :height="500" :item-height="100" :items="dashboardStore.statisticData" > <template #default="{ item }"> <statistic-card :item="item"></statistic-card> </template> </uni-virtual-list> -
ECharts 优化 :
- 使用
debounce处理窗口 resize 事件,避免频繁触发图表重绘; - 组件卸载时销毁 ECharts 实例,释放内存;
- 减少图表动画复杂度,提升渲染性能。
- 使用
-
按需引入组件:uView Plus、ECharts 等库支持按需引入,避免全量打包导致体积过大:
3.2.3 打包优化
TypeScript
// 按需引入 ECharts 模块(减少打包体积)
import * as echarts from 'echarts/core'
import { LineChart, PieChart, BarChart } from 'echarts/charts'
import { CanvasRenderer } from 'echarts/renderers'
// 注册所需模块
echarts.use([LineChart, PieChart, BarChart, CanvasRenderer])
-
代码压缩与 tree-shaking :在
manifest.json中开启代码压缩:TypeScript// manifest.json { "h5": { "optimization": { "treeShaking": true, "minify": true, "splitChunks": true } } }四、常见问题与解决方案
4.1 Vue3 语法糖适配问题
问题 1:
<script setup>中无法使用this解决方案 :Vue3 组合式 API 中无需
this,直接通过导入的模块、props、ref 等访问数据:TypeScript// 错误写法(Vue2 语法) this.fetchData() // 正确写法(Vue3 语法糖) import { fetchData } from '@/api' fetchData() // 访问 Pinia 状态 const store = useDashboardStore() console.log(store.statisticData)4.3 性能问题
问题:页面加载缓慢、卡顿
排查方向:
-
检查是否有大量同步请求,改为并行请求;
-
检查图表是否过多或数据量过大,考虑分页加载或简化图表;
-
检查是否有内存泄漏(如未销毁 ECharts 实例、未移除事件监听);
-
使用 Uniapp 开发者工具的 "性能分析" 功能,定位耗时操作。