去年我接手了一个反向代购平台的前端重构工作。原有项目基于React Hooks,但是状态管理写得像意大利面------用户选完商品、计算代购转运运费时,页面经常卡死。后来我参考了taocarts的前端架构,用Vue3 + Pinia 重写了整个代购系统的用户端。这篇文章记录一下我在状态设计上的思考,希望能给做跨境独立站或代购源码的同学一些启发。
一、代购前端的特殊状态
普通电商只需要管购物车和订单。但反向海淘的前端还要维护:
多个平台的商品实时价格(淘宝、1688、拼多多)
实时汇率(人民币转美元、加元等)
国际运费试算(代购集运合箱计费)
包裹的物流轨迹聚合
taocarts 的前端用的是 React + Redux Toolkit,它的 store 设计得特别细:一个 productSlice 存商品,一个 cartSlice 存本地购物车,还有一个 consolidationSlice 专门处理合箱。但我觉得 Redux 的样板代码太多了,所以 Vue3 这边我选了 Pinia。
二、模仿taocarts但简化:一个典型的商品模块Store
先看看我写的 useProductStore,用于管理淘宝1688代购系统抓取到的商品数据:
javascript
// stores/productStore.js
import { defineStore } from 'pinia'
import { fetchTaobaoItem, fetch1688Item } from '@/api/crawler'
export const useProductStore = defineStore('product', {
state: () => ({
items: new Map(), // id -> 商品详情
loadingSet: new Set(),
priceCache: new Map() // 临时缓存价格,避免重复请求
}),
actions: {
async loadItem(source, id) {
if (this.items.has(id)) return this.items.get(id)
if (this.loadingSet.has(id)) return // 防止并发重复请求
this.loadingSet.add(id)
try {
let data
if (source === 'taobao') {
data = await fetchTaobaoItem(id)
} else if (source === '1688') {
data = await fetch1688Item(id)
}
// 添加**跨境代购**必需的字段:预估国际运费、关税
data.estimatedFreight = this.calcEstimateFreight(data.weight, data.destCountry)
this.items.set(id, data)
return data
} finally {
this.loadingSet.delete(id)
}
},
calcEstimateFreight(weight, country) {
// 简化逻辑:调用后端运费引擎
return this.freightStore.getRate(country) * weight
}
},
getters: {
// 实时价格比较(用于降价提醒)
priceDiff: (state) => (id) => {
const current = state.items.get(id)?.price
const cached = state.priceCache.get(id)
if (!cached) return 0
return ((current - cached) / cached) * 100
}
}
})
这个 store 被我放在了一个独立的包里,后来被公司其他三个代购系统项目复用。taocarts 的优点是模块化,但缺点是把很多业务逻辑塞进了组件。我们用 Pinia 之后,组件只负责渲染,测试也方便多了。
三、踩坑:代购集运的合箱计算在客户端做还是服务端做?
一开始我把代购集运的合箱费用计算完全放在前端,结果用户勾选多个包裹时,页面直接卡死。因为每个包裹都要递归计算体积重,还要调用汇率接口。
后来我参考 taocarts 的做法:前端只负责展示待选包裹列表,用户点击"合箱预览"时,后端返回合箱后的总费用和推荐渠道。前端 Store 里只存结果:
javascript
// consolidationStore.js
async previewCombine(packageIds) {
const res = await api.post('/consolidation/preview', { packageIds })
this.combineResult = res.data
// 结果包含: totalWeight, totalVolWeight, channelName, fee
return res.data
}
四、与taocarts的React方案对比
taocarts 使用 useSelector 和 useDispatch,我个人觉得有点繁琐。Vue3 的组合式 API 配合 Pinia 的 storeToRefs 更直观:
javascript
<template>
<div>
<div v-for="item in productList" :key="item.id">
{{ item.title }} - ¥{{ item.price }}
<button @click="addToCart(item)">代购</button>
</div>
</div>
</template>
<script setup>
import { useProductStore } from '@/stores/productStore'
import { storeToRefs } from 'pinia'
const productStore = useProductStore()
const { productList, loading } = storeToRefs(productStore)
const { addToCart } = productStore
</script>
没有 mapState 之类的模板代码,新手也能很快上手。如果你是做代购源码二次开发,我强烈建议放弃 Redux 换 Pinia,开发效率能提升 30% 以上。
五、总结
前端状态管理对反向代购这种交互复杂的业务太重要了。taocarts 给了我很多模块划分的思路,但实现上我选了更简洁的 Vue3 + Pinia。最终页面首屏加载时间从 2.1 秒降到了 0.9 秒(主要靠 store 缓存)。下一篇我再讲讲这套代购系统背后的数据库设计,欢迎交流。