UniApp在现代跨平台开发中的优势
UniApp是一个使用Vue.js语法开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/百度/头条/QQ/快手/钉钉/淘宝)、快应用等多个平台。它解决了多端开发的痛点,大大提高了开发效率和代码复用率。
环境搭建与项目初始化
1. 开发环境配置
确保系统已安装Node.js(版本≥12),然后安装HBuilderX或使用VSCode+uni-app插件
# 通过vue-cli创建uni-app项目
npm install -g @vue/cli
vue create -p dcloudio/uni-preset-vue my-project
# 选择模板(选择默认模板或自定义)
cd my-project
npm run dev:%PLATFORM% # 如:npm run dev:mp-weixin
# 或使用HBuilderX可视化创建
# 文件 → 新建 → 项目 → uni-app → 选择模板
2. 项目目录结构解析
my-project/
├── pages/ # 页面目录
│ ├── index/
│ │ ├── index.vue # 首页页面
│ │ └── index.json # 页面配置文件
├── components/ # 组件目录
├── static/ # 静态资源
├── uni_modules/ # uni模块
├── App.vue # 应用配置
├── main.js # 入口文件
├── manifest.json # 应用配置文件
└── pages.json # 页面路由配置
3. 核心配置文件详解
// pages.json - 全局页面配置
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"enablePullDownRefresh": true
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F8F8F8"
},
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#007AFF",
"list": [{
"pagePath": "pages/index/index",
"text": "首页"
}]
}
}
// manifest.json - 应用配置
{
"name": "my-app",
"appid": "__UNI__XXXXXX",
"description": "我的uni-app应用",
"versionName": "1.0.0",
"mp-weixin": {
"appid": "微信小程序appid",
"setting": {
"urlCheck": false
}
}
}
实战案例:企业级电商多端应用
我们将构建一个完整的电商应用,包含首页、商品列表、商品详情、购物车、个人中心等模块。
1. 项目架构设计
// 项目结构设计
src/
├── api/ # 接口管理
│ ├── index.js # 接口统一导出
│ ├── product.js # 商品相关接口
│ └── user.js # 用户相关接口
├── common/ # 公共资源
│ ├── css/ # 公共样式
│ ├── js/ # 工具函数
│ └── images/ # 公共图片
├── components/ # 公共组件
│ ├── product-card.vue # 商品卡片
│ ├── search-bar.vue # 搜索栏
│ └── tab-bar.vue # 自定义tabbar
├── store/ # 状态管理
│ ├── index.js # store主文件
│ ├── modules/ # 模块化store
│ └── types.js # 类型定义
├── pages/ # 页面目录
└── utils/ # 工具类
├── request.js # 网络请求封装
├── auth.js # 认证相关
└── utils.js # 通用工具函数
2. 网络请求封装
// utils/request.js - 统一请求封装
const BASE_URL = 'https://api.example.com';
class Request {
constructor() {
this.interceptors = {
request: null,
response: null
};
}
// 设置请求拦截器
setRequestInterceptor(interceptor) {
this.interceptors.request = interceptor;
}
// 设置响应拦截器
setResponseInterceptor(interceptor) {
this.interceptors.response = interceptor;
}
async request(url, options = {}) {
const { method = 'GET', data = {}, header = {} } = options;
// 请求拦截
let requestConfig = { url: BASE_URL + url, method, data, header };
if (this.interceptors.request) {
requestConfig = await this.interceptors.request(requestConfig);
}
return new Promise((resolve, reject) => {
uni.request({
...requestConfig,
success: (response) => {
let responseData = response;
// 响应拦截
if (this.interceptors.response) {
responseData = this.interceptors.response(response);
}
resolve(responseData);
},
fail: (error) => {
reject(error);
}
});
});
}
get(url, data = {}, header = {}) {
return this.request(url, { method: 'GET', data, header });
}
post(url, data = {}, header = {}) {
header['Content-Type'] = 'application/json';
return this.request(url, { method: 'POST', data, header });
}
}
// 创建请求实例
const http = new Request();
// 添加请求拦截器 - 添加token
http.setRequestInterceptor(async (config) => {
const token = uni.getStorageSync('token');
if (token) {
config.header.Authorization = `Bearer ${token}`;
}
return config;
});
// 添加响应拦截器 - 统一错误处理
http.setResponseInterceptor((response) => {
const { statusCode, data } = response;
if (statusCode === 200) {
return data;
} else if (statusCode === 401) {
// token过期,跳转到登录页
uni.navigateTo({ url: '/pages/login/login' });
return Promise.reject(new Error('请重新登录'));
} else {
return Promise.reject(new Error(data.message || '请求失败'));
}
});
export default http;
3. 状态管理(Vuex)配置
// store/index.js - Vuex状态管理
import Vue from 'vue';
import Vuex from 'vuex';
import cart from './modules/cart';
import user from './modules/user';
Vue.use(Vuex);
const store = new Vuex.Store({
modules: {
cart,
user
},
state: {
isLoading: false
},
mutations: {
SET_LOADING(state, isLoading) {
state.isLoading = isLoading;
}
},
actions: {
setLoading({ commit }, isLoading) {
commit('SET_LOADING', isLoading);
}
}
});
export default store;
// store/modules/cart.js - 购物车模块
const state = {
cartItems: [],
cartTotal: 0
};
const mutations = {
ADD_TO_CART(state, product) {
const existingItem = state.cartItems.find(item => item.id === product.id);
if (existingItem) {
existingItem.quantity += 1;
} else {
state.cartItems.push({ ...product, quantity: 1 });
}
state.cartTotal = state.cartItems.reduce((total, item) => total + item.price * item.quantity, 0);
},
REMOVE_FROM_CART(state, productId) {
state.cartItems = state.cartItems.filter(item => item.id !== productId);
state.cartTotal = state.cartItems.reduce((total, item) => total + item.price * item.quantity, 0);
},
CLEAR_CART(state) {
state.cartItems = [];
state.cartTotal = 0;
}
};
const actions = {
addToCart({ commit }, product) {
commit('ADD_TO_CART', product);
uni.showToast({
title: '添加成功',
icon: 'success'
});
},
removeFromCart({ commit }, productId) {
commit('REMOVE_FROM_CART', productId);
},
clearCart({ commit }) {
commit('CLEAR_CART');
}
};
export default {
namespaced: true,
state,
mutations,
actions
};
4. 首页组件开发
// pages/index/index.vue - 首页
<template>
<view class="container">
<search-bar @search="handleSearch" />
<swiper class="swiper" indicator-dots autoplay circular>
<swiper-item v-for="(banner, index) in banners" :key="index">
<image :src="banner.image" mode="aspectFill" class="banner-image" @click="navigateTo(banner.url)" />
</swiper-item>
</swiper>
<view class="category-grid">
<view v-for="category in categories" :key="category.id" class="category-item" @click="navigateToCategory(category.id)">
<image :src="category.icon" mode="aspectFit" class="category-icon" />
<text class="category-name">{{ category.name }}</text>
</view>
</view>
<view class="section">
<view class="section-header">
<text class="section-title">热门推荐</text>
<text class="section-more" @click="navigateTo('/pages/product/list')">查看更多 ></text>
</view>
<view class="product-list">
<product-card
v-for="product in recommendedProducts"
:key="product.id"
:product="product"
@add-to-cart="addToCart"
/>
</view>
</view>
</view>
</template>
<script>
import { mapActions } from 'vuex';
import SearchBar from '@/components/search-bar.vue';
import ProductCard from '@/components/product-card.vue';
export default {
components: {
SearchBar,
ProductCard
},
data() {
return {
banners: [],
categories: [],
recommendedProducts: []
};
},
async onLoad() {
await this.loadHomeData();
},
onPullDownRefresh() {
this.loadHomeData().finally(() => {
uni.stopPullDownRefresh();
});
},
methods: {
...mapActions('cart', ['addToCart']),
async loadHomeData() {
try {
const [bannerRes, categoryRes, productRes] = await Promise.all([
this.$http.get('/banners'),
this.$http.get('/categories'),
this.$http.get('/products/recommended')
]);
this.banners = bannerRes.data;
this.categories = categoryRes.data;
this.recommendedProducts = productRes.data;
} catch (error) {
uni.showToast({
title: '加载失败',
icon: 'error'
});
}
},
handleSearch(keyword) {
uni.navigateTo({
url: `/pages/product/list?keyword=${keyword}`
});
},
navigateToCategory(categoryId) {
uni.navigateTo({
url: `/pages/product/list?category_id=${categoryId}`
});
},
navigateTo(url) {
if (url.startsWith('http')) {
// 处理外部链接
uni.navigateTo({
url: `/pages/webview/webview?url=${encodeURIComponent(url)}`
});
} else {
uni.navigateTo({ url });
}
}
}
};
</script>
<style lang="scss">
.container {
padding: 20rpx;
}
.swiper {
height: 300rpx;
margin-bottom: 30rpx;
}
.banner-image {
width: 100%;
height: 100%;
border-radius: 16rpx;
}
.category-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 20rpx;
margin-bottom: 40rpx;
}
.category-item {
display: flex;
flex-direction: column;
align-items: center;
}
.category-icon {
width: 80rpx;
height: 80rpx;
margin-bottom: 10rpx;
}
.category-name {
font-size: 24rpx;
color: #666;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
}
.section-more {
font-size: 24rpx;
color: #999;
}
.product-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
</style>
5. 商品卡片组件
// components/product-card.vue
<template>
<view class="product-card" @click="navigateToDetail">
<image :src="product.image" mode="aspectFill" class="product-image" />
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<text class="product-desc">{{ product.description }}</text>
<view class="product-footer">
<text class="product-price">¥{{ product.price }}</text>
<view class="action-buttons">
<button class="add-cart-btn" @click.stop="handleAddToCart">
<text class="iconfont icon-cart"></text>
</button>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
product: {
type: Object,
required: true
}
},
methods: {
navigateToDetail() {
uni.navigateTo({
url: `/pages/product/detail?id=${this.product.id}`
});
},
handleAddToCart() {
this.$emit('add-to-cart', this.product);
}
}
};
</script>
<style lang="scss">
.product-card {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
}
.product-image {
width: 100%;
height: 300rpx;
}
.product-info {
padding: 20rpx;
}
.product-name {
font-size: 28rpx;
font-weight: bold;
display: block;
margin-bottom: 10rpx;
}
.product-desc {
font-size: 24rpx;
color: #666;
display: block;
margin-bottom: 20rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.product-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.product-price {
font-size: 32rpx;
color: #e64340;
font-weight: bold;
}
.add-cart-btn {
background: #e64340;
width: 60rpx;
height: 60rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
.iconfont {
color: #fff;
font-size: 32rpx;
}
}
</style>
多端适配与优化策略
1. 条件编译处理平台差异
// 使用条件编译处理不同平台逻辑
export default {
methods: {
shareContent() {
// #ifdef MP-WEIXIN
wx.shareAppMessage({
title: '分享标题',
path: '/pages/index/index'
});
// #endif
// #ifdef APP-PLUS
plus.share.sendWithSystem({
content: '分享内容',
href: 'https://example.com'
});
// #endif
// #ifdef H5
if (navigator.share) {
navigator.share({
title: '分享标题',
url: window.location.href
});
}
// #endif
},
// 平台特定的样式处理
getPlatformStyle() {
let style = {};
// #ifdef MP-WEIXIN
style.paddingTop = '44px'; // 微信小程序胶囊按钮高度
// #endif
// #ifdef APP-PLUS
style.paddingTop = 'var(--status-bar-height)';
// #endif
return style;
}
}
};
2. 性能优化策略
// 图片懒加载优化
<image
:src="item.image"
mode="aspectFill"
lazy-load
:fade-show="false"
/>
// 列表渲染优化
<view
v-for="(item, index) in longList"
:key="item.id"
:render-when="index {
this.setData(data);
}, 100);
}
3. 用户体验优化
// 页面加载状态管理
export default {
data() {
return {
isLoading: true,
isError: false
};
},
async onLoad() {
await this.loadData();
},
methods: {
async loadData() {
this.isLoading = true;
this.isError = false;
try {
await this.fetchData();
} catch (error) {
this.isError = true;
console.error('加载失败:', error);
} finally {
this.isLoading = false;
}
},
// 骨架屏渲染
renderSkeleton() {
if (this.isLoading) {
return (
<view class="skeleton">
<view class="skeleton-banner"></view>
<view class="skeleton-item" v-for="i in 6" :key="i"></view>
</view>
);
}
if (this.isError) {
return (
<view class="error-view">
<text>加载失败</text>
<button @click="loadData">重新加载</button>
</view>
);
}
return this.renderContent();
}
}
};
打包发布与部署
1. 多平台打包配置
// package.json 脚本配置
{
"scripts": {
"dev:mp-weixin": "cross-env NODE_ENV=development uni-build --platform mp-weixin",
"build:mp-weixin": "cross-env NODE_ENV=production uni-build --platform mp-weixin",
"build:app": "cross-env NODE_ENV=production uni-build --platform app-plus",
"build:h5": "cross-env NODE_ENV=production uni-build --platform h5"
}
}
// 环境变量配置
const isProduction = process.env.NODE_ENV === 'production';
// API地址配置
export const API_BASE_URL = isProduction
? 'https://api.production.com'
: 'https://api.development.com';
// 微信小程序配置
// manifest.json → mp-weixin
{
"appid": "wx1234567890abcdef",
"setting": {
"urlCheck": false,
"es6": true,
"postcss": true
},
"usingComponents": true
}
2. 自动化部署流程
// GitHub Actions 自动化部署示例
name: Deploy UniApp
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm install
- name: Build for WeChat Mini Program
run: npm run build:mp-weixin
- name: Deploy to WeChat Mini Program
uses: wechat-miniprogram/ci-action@v1
with:
appid: ${{ secrets.WX_APPID }}
privateKey: ${{ secrets.WX_PRIVATE_KEY }}
projectPath: dist/build/mp-weixin
version: ${{ github.sha }}
desc: ${{ github.event.head_commit.message }}
总结
UniApp作为跨平台开发的首选框架,通过一套代码实现多端发布,极大地提高了开发效率和代码复用率。本文通过一个完整的电商应用案例,详细介绍了UniApp的核心概念、项目架构、组件开发、状态管理、性能优化和部署发布的全流程。
关键开发实践:
- 合理的项目结构和代码组织
- 统一的网络请求封装和错误处理
- 有效的状态管理方案
- 组件化开发提高代码复用
- 多端适配和条件编译
- 性能优化和用户体验提升
- 自动化部署流程
掌握UniApp开发技术,能够帮助开发者快速构建高质量的多端应用,适应现代移动开发的多样化需求。