发布日期:2024年1月20日
阅读时长:15分钟
UniApp开发新范式:一次开发,多端部署的工程化实践
随着移动互联网的快速发展,跨平台开发已成为企业降本增效的关键技术。UniApp基于Vue.js生态,为开发者提供了一套完整的跨端解决方案。本文将深入探讨UniApp在企业级项目中的最佳实践。
一、UniApp项目架构设计与工程化配置
1.1 现代化项目目录结构
project-root/
├── src/
│ ├── components/ # 公共组件
│ ├── pages/ # 页面文件
│ ├── static/ # 静态资源
│ ├── store/ # 状态管理
│ ├── utils/ # 工具函数
│ ├── api/ # 接口管理
│ ├── config/ # 配置文件
│ └── styles/ # 样式文件
├── platforms/ # 平台特定代码
├── uni_modules/ # 插件市场模块
├── manifest.json # 应用配置
├── pages.json # 页面路由
└── vue.config.js # 构建配置
1.2 智能环境配置管理
// config/environment.js
const environments = {
development: {
baseURL: 'https://dev-api.example.com',
appId: 'dev_20240120',
debug: true
},
production: {
baseURL: 'https://api.example.com',
appId: 'prod_20240120',
debug: false
}
}
const getCurrentEnv = () => {
// 根据编译条件自动切换环境
#ifdef MP-WEIXIN
return __wxConfig.envVersion === 'release' ?
'production' : 'development';
#endif
#ifdef H5
return process.env.NODE_ENV === 'production' ?
'production' : 'development';
#endif
return 'development';
}
export const envConfig = environments[getCurrentEnv()];
1.3 自动化构建配置
// vue.config.js
const path = require('path');
module.exports = {
transpileDependencies: ['@dcloudio/uni-ui'],
configureWebpack: {
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils')
}
}
},
chainWebpack: (config) => {
// 自定义打包优化
config.optimization.minimize(true);
}
};
二、Vue3组合式API在UniApp中的深度应用
2.1 组合式函数封装与复用
// composables/usePageLogic.js
import { ref, onLoad, onShow } from '@dcloudio/uni-app';
import { useRequest } from './useRequest';
export function usePageLogic(apiFunction, options = {}) {
const { autoLoad = true, immediate = true } = options;
const dataList = ref([]);
const loading = ref(false);
const finished = ref(false);
const pagination = ref({
page: 1,
pageSize: 10
});
const { run: fetchData } = useRequest(apiFunction, {
manual: !autoLoad,
onSuccess: (result) => {
if (pagination.value.page === 1) {
dataList.value = result.list;
} else {
dataList.value = [...dataList.value, ...result.list];
}
finished.value = !result.hasNext;
}
});
const loadData = async (isRefresh = false) => {
if (loading.value) return;
loading.value = true;
if (isRefresh) {
pagination.value.page = 1;
finished.value = false;
}
try {
await fetchData({
page: pagination.value.page,
pageSize: pagination.value.pageSize
});
if (!isRefresh) {
pagination.value.page++;
}
} finally {
loading.value = false;
}
};
const refreshData = () => loadData(true);
// 生命周期集成
onLoad(() => {
if (immediate) {
loadData();
}
});
return {
dataList,
loading,
finished,
loadData,
refreshData
};
}
2.2 页面组件实战示例
<template>
<view class="product-list">
<scroll-view
scroll-y
@scrolltolower="loadMore"
class="scroll-container"
>
<product-card
v-for="product in productList"
:key="product.id"
:product="product"
@click="navigateToDetail(product.id)"
/>
<load-more
:loading="loading"
:finished="finished"
/>
</scroll-view>
<floating-action-button @click="showFilter" />
</view>
</template>
<script setup>
import { usePageLogic } from '@/composables/usePageLogic';
import { productApi } from '@/api/product';
import { onPullDownRefresh } from '@dcloudio/uni-app';
const {
dataList: productList,
loading,
finished,
loadData,
refreshData
} = usePageLogic(productApi.getProductList);
// 下拉刷新
onPullDownRefresh(async () => {
await refreshData();
uni.stopPullDownRefresh();
});
// 加载更多
const loadMore = () => {
if (!loading.value && !finished.value) {
loadData();
}
};
// 页面跳转
const navigateToDetail = (productId) => {
uni.navigateTo({
url: `/pages/product/detail?id=${productId}`
});
};
// 显示筛选
const showFilter = () => {
uni.showActionSheet({
itemList: ['价格排序', '销量排序', '筛选条件'],
success: (res) => {
handleFilterSelect(res.tapIndex);
}
});
};
</script>
三、Pinia状态管理在企业级应用中的实践
3.1 模块化状态管理设计
// store/user.js
import { defineStore } from 'pinia';
import { userApi } from '@/api/user';
export const useUserStore = defineStore('user', {
state: () => ({
userInfo: null,
token: uni.getStorageSync('token') || null,
permissions: [],
loginStatus: 'unlogin' // unlogin, logging, logged
}),
getters: {
isLogged: (state) => !!state.token,
userName: (state) => state.userInfo?.name || '未登录',
hasPermission: (state) => (permission) => {
return state.permissions.includes(permission);
}
},
actions: {
async login(loginData) {
this.loginStatus = 'logging';
try {
const result = await userApi.login(loginData);
this.token = result.token;
this.userInfo = result.userInfo;
this.loginStatus = 'logged';
// 持久化存储
uni.setStorageSync('token', result.token);
uni.setStorageSync('userInfo', result.userInfo);
return result;
} catch (error) {
this.loginStatus = 'unlogin';
throw error;
}
},
async logout() {
try {
await userApi.logout();
} finally {
this.clearAuth();
}
},
clearAuth() {
this.token = null;
this.userInfo = null;
this.loginStatus = 'unlogin';
uni.removeStorageSync('token');
uni.removeStorageSync('userInfo');
},
async checkAuth() {
if (!this.token) {
throw new Error('未登录');
}
try {
const userInfo = await userApi.getUserInfo();
this.userInfo = userInfo;
return userInfo;
} catch (error) {
this.clearAuth();
throw error;
}
}
}
});
3.2 购物车状态管理实战
// store/cart.js
export const useCartStore = defineStore('cart', {
state: () => ({
items: uni.getStorageSync('cart_items') || [],
selectedIds: new Set()
}),
getters: {
totalCount: (state) => {
return state.items.reduce((total, item) =>
total + item.quantity, 0);
},
totalPrice: (state) => {
return state.items.reduce((total, item) =>
total + (item.price * item.quantity), 0);
},
selectedItems: (state) => {
return state.items.filter(item =>
state.selectedIds.has(item.id));
},
selectedTotal: (state, getters) => {
return getters.selectedItems.reduce((total, item) =>
total + (item.price * item.quantity), 0);
}
},
actions: {
addItem(product, quantity = 1) {
const existingItem = this.items.find(item =>
item.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.items.push({
...product,
quantity,
selected: false
});
}
this.persistCart();
},
removeItem(productId) {
this.items = this.items.filter(item => item.id !== productId);
this.selectedIds.delete(productId);
this.persistCart();
},
updateQuantity(productId, quantity) {
const item = this.items.find(item => item.id === productId);
if (item) {
item.quantity = Math.max(1, quantity);
this.persistCart();
}
},
toggleSelect(productId) {
if (this.selectedIds.has(productId)) {
this.selectedIds.delete(productId);
} else {
this.selectedIds.add(productId);
}
},
selectAll() {
this.selectedIds = new Set(this.items.map(item => item.id));
},
unselectAll() {
this.selectedIds.clear();
},
clearCart() {
this.items = [];
this.selectedIds.clear();
uni.removeStorageSync('cart_items');
},
persistCart() {
uni.setStorageSync('cart_items', this.items);
}
}
});
四、UniApp性能优化深度实战
4.1 图片加载与懒加载优化
// components/optimized-image.vue
<template>
<view class="image-container">
<image
:src="actualSrc"
:lazy-load="lazyLoad"
:mode="mode"
@load="handleLoad"
@error="handleError"
class="optimized-image"
:class="{ loaded: imageLoaded }"
/>
<view v-if="showLoading" class="image-placeholder">
<uni-loading></uni-loading>
</view>
</view>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
const props = defineProps({
src: String,
lazyLoad: {
type: Boolean,
default: true
},
mode: {
type: String,
default: 'aspectFill'
},
placeholder: {
type: String,
default: '/static/images/placeholder.png'
}
});
const imageLoaded = ref(false);
const loadError = ref(false);
const actualSrc = computed(() => {
if (loadError.value) {
return props.placeholder;
}
// 根据网络环境返回不同质量的图片
const quality = uni.getSystemInfoSync().pixelRatio > 2 ? 'high' : 'normal';
return `${props.src}?quality=${quality}`;
});
const showLoading = computed(() => !imageLoaded.value && !loadError.value);
const handleLoad = () => {
imageLoaded.value = true;
loadError.value = false;
};
const handleError = () => {
loadError.value = true;
imageLoaded.value = false;
};
watch(() => props.src, () => {
imageLoaded.value = false;
loadError.value = false;
});
</script>
4.2 虚拟列表优化长列表性能
// composables/useVirtualList.js
import { ref, computed, onMounted } from 'vue';
export function useVirtualList(options) {
const {
data,
itemHeight = 100,
containerHeight = 400,
bufferSize = 5
} = options;
const scrollTop = ref(0);
const containerRef = ref(null);
const visibleCount = computed(() =>
Math.ceil(containerHeight / itemHeight) + bufferSize * 2
);
const startIndex = computed(() =>
Math.max(0, Math.floor(scrollTop.value / itemHeight) - bufferSize)
);
const endIndex = computed(() =>
Math.min(data.value.length, startIndex.value + visibleCount.value)
);
const visibleData = computed(() =>
data.value.slice(startIndex.value, endIndex.value)
);
const totalHeight = computed(() =>
data.value.length * itemHeight
);
const offsetY = computed(() =>
startIndex.value * itemHeight
);
const onScroll = (event) => {
scrollTop.value = event.detail.scrollTop;
};
return {
scrollTop,
containerRef,
visibleData,
totalHeight,
offsetY,
onScroll
};
}
五、多端发布与平台差异化适配
5.1 条件编译与平台特定代码
// utils/platform-adapter.js
export class PlatformAdapter {
static navigateTo(options) {
#ifdef H5
if (options.url.startsWith('/')) {
window.location.href = options.url;
return;
}
#endif
#ifdef MP-WEIXIN
wx.navigateTo(options);
#endif
#ifdef APP-PLUS
uni.navigateTo(options);
#endif
}
static showToast(message) {
#ifdef MP-WEIXIN
wx.showToast({ title: message, icon: 'none' });
#else
uni.showToast({ title: message, icon: 'none' });
#endif
}
static getPlatformInfo() {
#ifdef H5
return { platform: 'h5', env: 'browser' };
#endif
#ifdef MP-WEIXIN
return {
platform: 'weixin',
env: wx.getAccountInfoSync().miniProgram.envVersion
};
#endif
#ifdef APP-PLUS
return { platform: 'app', env: 'production' };
#endif
}
}
// 平台特定样式处理
export const platformStyle = {
getStatusBarHeight() {
#ifdef APP-PLUS
return plus.navigator.getStatusbarHeight();
#else
return 0;
#endif
},
getSafeArea() {
#ifdef MP-WEIXIN
const systemInfo = wx.getSystemInfoSync();
return systemInfo.safeArea;
#else
return { bottom: 0 };
#endif
}
};
六、实战案例:企业级电商应用开发
6.1 商品详情页完整实现
<template>
<view class="product-detail">
<scroll-view
scroll-y
class="detail-scroll"
@scroll="handleScroll"
>
<!-- 商品图片轮播 -->
<product-gallery :images="product.images" />
<!-- 商品基本信息 -->
<product-info
:product="product"
@share="handleShare"
/>
<!-- 商品规格选择 -->
<sku-selector
:skus="product.skus"
@change="handleSkuChange"
/>
<!-- 商品详情 -->
<rich-text :nodes="product.detail" />
<!-- 用户评价 -->
<product-reviews :reviews="reviews" />
</scroll-view>
<!-- 底部操作栏 -->
<product-action-bar
:product="product"
@add-to-cart="addToCart"
@buy-now="buyNow"
/>
</view>
</template>
<script setup>
import { ref, onLoad } from 'vue';
import { useProductDetail } from '@/composables/useProductDetail';
import { useCartStore } from '@/store/cart';
const { product, reviews, loading, loadProductDetail } = useProductDetail();
const cartStore = useCartStore();
onLoad((options) => {
if (options.id) {
loadProductDetail(options.id);
}
});
const handleSkuChange = (selectedSku) => {
product.value.selectedSku = selectedSku;
};
const addToCart = async () => {
if (!product.value.selectedSku) {
uni.showToast({ title: '请选择商品规格', icon: 'none' });
return;
}
try {
await cartStore.addItem({
...product.value,
...product.value.selectedSku
});
uni.showToast({ title: '添加成功' });
} catch (error) {
uni.showToast({ title: '添加失败', icon: 'none' });
}
};
const buyNow = () => {
// 立即购买逻辑
};
</script>
// 页面交互增强
document.addEventListener(‘DOMContentLoaded’, function() {
// 代码块语法高亮和复制功能
const codeBlocks = document.querySelectorAll(‘pre code’);
codeBlocks.forEach((block, index) => {
// 添加复制按钮
const copyButton = document.createElement(‘button’);
copyButton.textContent = ‘复制’;
copyButton.className = ‘copy-btn’;
copyButton.onclick = function() {
navigator.clipboard.writeText(block.textContent).then(() => {
const originalText = copyButton.textContent;
copyButton.textContent = ‘已复制!’;
setTimeout(() => {
copyButton.textContent = originalText;
}, 2000);
});
};
block.parentNode.insertBefore(copyButton, block);
});
// 目录导航高亮
const sections = document.querySelectorAll(‘article’);
const navLinks = document.querySelectorAll(‘nav a’);
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const id = entry.target.getAttribute(‘id’);
navLinks.forEach(link => {
link.classList.remove(‘active’);
if (link.getAttribute(‘href’) === `#${id}`) {
link.classList.add(‘active’);
}
});
}
});
}, { threshold: 0.5 });
sections.forEach(section => {
observer.observe(section);
});
});