去年接手公司电商App重构项目时,我面临一个艰难抉择:继续维护老旧的Vue2代码,还是全面升级到Vue3技术栈。最终我们选择了后者,结果开发效率提升了40%,包体积缩小了25%。本文将分享这个实战过程中的完整技术方案,特别针对移动端电商场景下的Vue3最佳实践。
在移动端电商项目中,我们需要平衡开发效率、性能表现和用户体验。Vue3的Composition API带来了更好的代码组织方式,而Vant4作为轻量级移动组件库,提供了电商项目所需的大部分UI组件。
关键优势对比:
| 特性 | Vue2 + Vant3 | Vue3 + Vant4 |
|---|---|---|
| 代码组织 | Options API | Composition API |
| 性能 | 中等 | 更快的渲染速度 |
| 包体积 | 较大 | Tree-shaking优化 |
| 类型支持 | 有限 | 完善的TypeScript支持 |
| 组件复用 | Mixins | 组合式函数 |
使用Vite创建项目能获得更快的开发体验:
bash复制npm create vite@latest ecommerce-app --template vue-ts
cd ecommerce-app
npm install vant@next
配置按需引入可以显著减小包体积。创建vant-import.ts文件:
typescript复制import {
Search, Tabbar, TabbarItem,
Swipe, SwipeItem, Grid, GridItem
} from 'vant'
const components = [
Search,
Tabbar,
TabbarItem,
Swipe,
SwipeItem,
Grid,
GridItem
]
export function setupVant(app) {
components.forEach(component => {
app.use(component)
})
}
然后在main.ts中使用:
typescript复制import { createApp } from 'vue'
import { setupVant } from './utils/vant-import'
import App from './App.vue'
const app = createApp(App)
setupVant(app)
app.mount('#app')
电商首页通常包含轮播图、商品分类导航和商品列表。使用Vant4组件可以快速搭建这些UI元素。
性能优化技巧:
<van-lazyload>实现图片懒加载vue复制<template>
<div class="home">
<van-swipe :autoplay="3000" indicator-color="white">
<van-swipe-item v-for="image in banners" :key="image.id">
<van-image :src="image.url" fit="cover" />
</van-swipe-item>
</van-swipe>
<van-grid :column-num="5" :border="false">
<van-grid-item
v-for="category in categories"
:key="category.id"
:icon="category.icon"
:text="category.name"
/>
</van-grid>
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<product-card
v-for="item in productList"
:key="item.id"
:product="item"
/>
</van-list>
</div>
</template>
购物车功能需要考虑多种交互场景:
typescript复制// 使用Composition API组织购物车逻辑
export function useCart() {
const cartItems = ref<CartItem[]>([])
const selectedIds = ref<number[]>([])
const totalPrice = computed(() => {
return cartItems.value
.filter(item => selectedIds.value.includes(item.id))
.reduce((sum, item) => sum + item.price * item.quantity, 0)
})
function toggleSelect(id: number) {
const index = selectedIds.value.indexOf(id)
if (index > -1) {
selectedIds.value.splice(index, 1)
} else {
selectedIds.value.push(id)
}
}
return {
cartItems,
selectedIds,
totalPrice,
toggleSelect
}
}
移动端需要特别关注手势操作和交互动画:
vue复制<template>
<van-swipe-cell
:before-close="beforeClose"
:name="item.id"
>
<van-card
:price="item.price"
:desc="item.spec"
:title="item.name"
/>
<template #right>
<van-button
square
type="danger"
text="删除"
@click="deleteItem(item.id)"
/>
</template>
</van-swipe-cell>
</template>
<script setup>
const beforeClose = ({ position, instance, name }) => {
if (position === 'right') {
return new Promise(resolve => {
Dialog.confirm({
message: '确定删除吗?',
}).then(() => {
resolve(true)
}).catch(() => {
instance.close()
resolve(false)
})
})
}
}
</script>
使用Chrome Lighthouse进行性能分析后,我们实施了以下优化:
javascript复制// vite.config.js 优化配置
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return 'vendor'
}
if (id.includes('src/views')) {
return id.split('/').slice(-1)[0].replace('.vue', '')
}
}
}
}
}
})
我们最终选择了Capacitor作为混合应用打包方案,原因如下:
| 特性 | Cordova | Capacitor |
|---|---|---|
| 与现代框架集成 | 需要插件 | 原生支持 |
| 性能 | 中等 | 更好 |
| 维护状态 | 逐渐淘汰 | 活跃维护 |
| 配置复杂度 | 高 | 低 |
| 热更新支持 | 有限 | 完善 |
bash复制npm run build
bash复制npx cap add android
配置应用信息:
修改android/app/src/main/AndroidManifest.xml中的包名和应用名称
处理常见问题:
bash复制cd android
./gradlew assembleRelease
java复制// 在MainActivity.java中添加
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
WebView webView = new WebView(this);
setContentView(webView);
// 预加载关键资源
webView.loadUrl("file:///android_asset/index.html");
}
typescript复制// 定义原生插件
import { registerPlugin } from '@capacitor/core'
const NativeBridge = registerPlugin('NativeBridge')
export function useNative() {
const shareProduct = async (productId: number) => {
try {
await NativeBridge.share({ productId })
} catch (err) {
Toast('分享失败')
}
}
return { shareProduct }
}
我们使用GitHub Actions实现了自动化构建和部署:
yaml复制name: Build and Deploy
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- run: npm install
- run: npm run build
- run: npx cap sync android
- uses: actions/upload-artifact@v2
with:
name: android-build
path: android/app/build/outputs/apk/release
前端监控我们选择了Sentry,配置如下:
javascript复制// 在主入口文件初始化
import * as Sentry from '@sentry/vue'
import { BrowserTracing } from '@sentry/tracing'
Sentry.init({
app,
dsn: 'your-dsn',
integrations: [
new BrowserTracing({
routingInstrumentation: Sentry.vueRouterInstrumentation(router),
tracingOrigins: ['your-domain.com'],
}),
],
tracesSampleRate: 0.2,
})
在电商项目中,特别需要监控以下指标:
我们采用渐进式迁移策略:
@vue/compat桥接版本迁移前后对比:
| 指标 | 迁移前 | 迁移后 |
|---|---|---|
| 构建时间 | 45s | 28s |
| 首屏加载 | 1.8s | 1.2s |
| 代码行数 | 12k | 9.5k |
| 内存占用 | 85MB | 62MB |
通过babel插件实现更精细的按需引入:
javascript复制// babel.config.js
module.exports = {
plugins: [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
style: true
}, 'vant']
]
}
然后在组件中可以直接引入:
javascript复制import { Button } from 'vant'
这种方式的优势是:
使用WebPageTest进行性能测试后,我们发现并解决了几个关键问题:
html复制<van-image
lazy
:src="imageUrl"
:loading-icon-size="20"
:error-icon-size="20"
:lazy-load="true"
:preview-size="100"
/>
typescript复制let lastRequestId = 0
async function fetchProducts(categoryId) {
const currentId = ++lastRequestId
const res = await api.getProducts(categoryId)
if (currentId === lastRequestId) {
products.value = res.data
}
}
javascript复制// 在开发环境添加内存泄漏检查
if (process.env.NODE_ENV === 'development') {
window.addEventListener('beforeunload', () => {
console.log('Active event listeners:', getEventListeners(document))
})
}
电商项目需要特别关注无障碍访问:
vue复制<template>
<van-button
type="primary"
aria-label="加入购物车"
@click="addToCart"
>
<template #icon>
<van-icon
name="cart-o"
aria-hidden="true"
/>
</template>
加入购物车
</van-button>
</template>
对于中型电商项目,我们推荐使用Pinia:
typescript复制// stores/cart.ts
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] as CartItem[],
selected: [] as number[]
}),
getters: {
totalPrice(state) {
return state.items
.filter(item => state.selected.includes(item.id))
.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
},
actions: {
async loadCartItems() {
this.items = await api.getCartItems()
},
toggleSelect(id: number) {
const index = this.selected.indexOf(id)
if (index > -1) {
this.selected.splice(index, 1)
} else {
this.selected.push(id)
}
}
}
})
统一的API层封装能提高代码可维护性:
typescript复制// api/index.ts
import axios from 'axios'
const instance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000
})
// 请求拦截器
instance.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器
instance.interceptors.response.use(
response => {
if (response.data.code !== 0) {
return Promise.reject(response.data.message)
}
return response.data.data
},
error => {
if (error.response?.status === 401) {
// 处理未授权
}
return Promise.reject(error)
}
)
export const getProducts = (params) =>
instance.get('/products', { params })
export const addToCart = (data) =>
instance.post('/cart/items', data)
使用Vitest进行组件测试:
javascript复制// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'happy-dom'
}
})
示例测试用例:
typescript复制import { mount } from '@vue/test-utils'
import ProductCard from '../ProductCard.vue'
describe('ProductCard', () => {
it('renders product info correctly', () => {
const product = {
id: 1,
name: '测试商品',
price: 100,
image: 'test.jpg'
}
const wrapper = mount(ProductCard, {
props: { product }
})
expect(wrapper.text()).toContain('测试商品')
expect(wrapper.text()).toContain('¥100')
})
})
使用Cypress进行端到端测试:
javascript复制// cypress/integration/cart.spec.js
describe('购物车流程', () => {
beforeEach(() => {
cy.login()
cy.visit('/products')
})
it('可以添加商品到购物车', () => {
cy.get('[data-testid="product-card"]').first().click()
cy.get('[data-testid="add-to-cart"]').click()
cy.get('[data-testid="cart-badge"]').should('contain', '1')
})
})
在完成这个电商项目后,我们总结了几个关键经验:
组件设计原则:
性能优化要点:
团队协作规范:
错误处理策略:
typescript复制// 全局错误处理
app.config.errorHandler = (err, instance, info) => {
console.error('Vue error:', err)
Sentry.captureException(err, {
extra: { component: instance?.$options.name, info }
})
if (isNetworkError(err)) {
showToast('网络异常,请检查连接')
}
}