03 - 架构设计
应用启动流程
app.js 入口
src/app.js 是 Taro 应用入口,负责初始化整个应用:
js
import { configStore } from '@/store'
import Spx from '@/spx'
import { requestIntercept, routeIntercept } from '@/plugin'
const { store } = configStore()
// 初始化 Spx 全局数据层
Spx.init({ store })
// 安装请求拦截器(注入导购员信息到结算请求)
requestIntercept()
// 安装路由拦截器(内购/版本路由重定向)
routeIntercept()启动时序
app.js 入口
│
├─→ configStore() # 创建 Redux Store(单例)
│ ├─ configureStore()
│ ├─ persistReducer() # 包装持久化 reducer
│ └─ persistStore() # 初始化持久化
│
├─→ Spx.init({ store }) # 初始化全局数据层
│ └─ 挂载到 global.__SPX__
│
├─→ requestIntercept() # 安装请求拦截器
│ └─ Taro.addInterceptor()
│
└─→ routeIntercept() # 安装路由拦截器
├─ weapp: 重写 Taro.navigateTo / redirectTo
└─ h5: 重写 window.history.pushState / replaceStategetSystemConfig() 系统配置拉取
应用启动后,首页(或入口页)会并发拉取系统配置:
js
// 首页 onLoad 中
const [systemConfig, userInfo, cartCount] = await Promise.all([
api.member.getSysConfig(), // 系统配置(颜色、tabbar、价格设置等)
Spx.getMemberInfo(), // 用户信息(已登录时)
api.cart.getCount() // 购物车数量
])系统配置返回后,通过 Redux dispatch 到 sys slice,包含:
- 主题色:
colorPrimary、colorMarketing、colorAccent - TabBar 配置:tab 页面列表和图标
- 功能开关:
openStore、openRecommend、openScanQrcode - 价格设置:购物车页、商品页、订单页的价格显示规则
- 货币信息:符号、代码
主题色会通过 getThemeStyle() 转为 CSS 变量,注入到页面根节点:
js
function getThemeStyle() {
const { colorPrimary, colorMarketing, colorAccent, rgb } = sysConfig
return {
'--color-primary': colorPrimary,
'--color-marketing': colorMarketing,
'--color-accent': colorAccent,
'--color-rgb': rgb
}
}Spx 全局数据层
设计目的
Spx 是项目的全局数据访问器,解决两个核心问题:
- Token 管理:在 mall 模式和 merchant 模式之间自动切换 Token
- 避免循环依赖:
req.js需要 Token → Token 由 Spx 管理 → Spx 需要 api → 形成循环
循环依赖解决方案
req.js ──getS()──→ global.__SPX__ (运行时动态获取)
│
Spx ──getApi()──→ require('@/api').default (惰性加载)- Spx 启动时将实例挂载到
global.__SPX__和globalThis.__SPX__ req.js通过getS()函数获取 Spx 实例,不直接 import- Spx 需要 API 时通过
require('@/api').default惰性加载
Spx 核心方法
| 方法 | 参数 | 说明 |
|---|---|---|
get(key, forceLocal) | key, 是否从 Storage 重读 | 获取缓存值 |
set(key, val, forceLocal) | key, val, 是否同步 Storage | 设置缓存值 |
getAuthToken() | - | 根据 isMerchantModule() 返回对应 Token |
setAuthToken(token) | token | 设置当前模块对应的 Token |
logout() | - | 清除 Token + dispatch clearUserInfo |
bind(name, fn) | 名称, 回调 | 绑定钩子 |
trigger(name, ...args) | 名称, 参数 | 触发钩子(支持 async) |
autoLogin(ctx, next) | 上下文 | 自动登录(微信 code) |
login(ctx, isRedirect) | 上下文 | 主动登录 |
getMemberInfo() | - | 获取会员信息并 dispatch |
formatMoney(num) | 数字 | 格式化金额(千分位) |
Mall Token vs Merchant Token
js
getAuthToken() {
if (isMerchantModule()) {
return this.get(MERCHANT_TOKEN) || Taro.getStorageSync(MERCHANT_TOKEN)
}
return this.get(SG_TOKEN) || Taro.getStorageSync(SG_TOKEN)
}- SG_TOKEN(
'token'):商城会员 Token,C 端购物场景使用 - MERCHANT_TOKEN(
'merchant_token'):商户入驻 Token,商家管理场景使用 isMerchantModule()根据当前路由判断是否在商户模块(/subpages/merchant/)
Redux Store
Store 创建
src/store/index.js 使用单例模式创建 Store:
js
export default function configStore(preloadedState = {}) {
if (!store) {
store = configureStore({
reducer, // persistReducer 包装的 rootReducer
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: { ignoredActions: ['persist/PERSIST'] }
}).concat(logger),
preloadedState
})
persistor = persistStore(store)
}
return { store, persistor }
}Reducer 组合
js
const rootReducer = combineReducers({
guide: guideReducer, // 导购
user: userReducer, // 用户
colors: colorsReducer, // 颜色
sys: sysReducer, // 系统配置
cart: cartReducer, // 购物车
merchant: merchantReducer, // 商户入驻
shop: shopReducer, // 店铺
tabBar: tabBar, // TabBar
community: communityReducer, // 社区团购
dianwu: dianwuReducer, // 店务
purchase: purchaseReducer, // 内购
member: memberReducer // 会员
})redux-persist 持久化
js
const reducer = persistReducer({
key: 'root',
storage, // weapp: 自定义 Taro Storage 适配器; h5: localStorage
blacklist: ['merchant', 'select', 'sys'], // 这三个不持久化
throttle: 20 // 20ms 节流
}, rootReducer)不持久化的 Slice:
merchant:商户入驻状态(敏感数据)select:社区团购选品(临时状态)sys:系统配置(每次启动从服务端拉取)
Storage 适配器(weapp 使用自定义适配器):
js
export default {
getItem(key) { return new Promise(resolve => resolve(Taro.getStorageSync(key))) },
setItem(key, data) { return Taro.setStorage({ key, data }) },
removeItem(key) { return Taro.removeStorage({ key }) },
clear() { return Taro.clearStorage() }
}Spx 与 Redux 的关系
用户登录成功
│
├─→ Spx.setAuthToken(token) # Spx 存储 Token
│
├─→ Spx.getMemberInfo() # Spx 调用 API 获取用户信息
│ └─→ dispatch(updateUserInfo(data)) # dispatch 到 Redux
│
└─→ dispatch(fetchCartList()) # Redux thunk 获取购物车- Spx 管理运行时缓存数据(Token、用户信息、UV 时间戳等),同步读写 Taro Storage
- Redux 管理页面级状态(购物车列表、选中地址、店铺信息等),部分持久化
- 两者通过 dispatch 协同:Spx 获取数据后 dispatch 到 Redux,组件从 Redux 读取
拦截器体系
请求拦截器(requestIntercept)
src/plugin/requestIntercept.js 拦截结算页面的订单请求,注入导购员信息:
js
export function requestIntercept() {
const interceptor = (chain) => {
const { method, data, url } = chain.requestParams
const { path } = getCurrentInstance()?.router || {}
// 在 espier-checkout 页面,对 order_new 和 getFreightFee 请求注入参数
if (path === '/pages/cart/espier-checkout' &&
[`${API_BASE}/order_new`, `${API_BASE}/getFreightFee`].includes(url)) {
_data['work_userid'] = work_userid // 企微用户ID
if (smid) _data['salesman_id'] = smid // 导购员ID
}
return chain.proceed(requestParams)
}
Taro.addInterceptor(interceptor)
}路由拦截器(routeIntercept)
src/plugin/routeIntercept.js 根据产品版本,自动重定向路由:
js
class RouteIntercept {
constructor() {
this.routes = {
'standard': { /* ... */ },
'platform': { /* ... */ }
}
}
}当前项目只使用
standard(云店 B2C)和platform(ECShopX BBC)两个产品版本。
H5 实现:重写 window.history.pushState / replaceState
小程序实现:重写 Taro.navigateTo / redirectTo,额外检查页面栈深度 > 8 时自动 redirectTo
重定向规则:
- URL 带
_original=1参数时不重定向 - 根据 URL 中的
activity_id/enterprise_id/invite_code/type=passcode判断是否在内购场景
APP 导航拦截
src/plugin/app/Mapp.js 在 H5+ 环境下完全接管多 webview 导航,详见 09-平台适配。
页面生命周期
Class Component 页面
js
export default class MyPage extends Component {
componentDidMount() {
// 页面首次加载
}
componentDidShow() {
// 页面显示(包括从其他页面返回)
}
componentDidHide() {
// 页面隐藏
}
componentWillUnmount() {
// 页面卸载
}
onReachBottom() {
// 上拉触底
}
onPullDownRefresh() {
// 下拉刷新
}
}Function Component 页面
使用 Taro Hooks:
js
import { useDidShow, useDidHide, useReady, usePullDownRefresh, useReachBottom } from '@tarojs/taro'
function MyPage() {
const [data, setData] = useState([])
useDidShow(() => { /* 页面显示 */ })
useDidHide(() => { /* 页面隐藏 */ })
useReady(() => { /* 页面首次渲染完成 */ })
useReachBottom(() => { /* 上拉触底 */ })
usePullDownRefresh(() => { /* 下拉刷新 */ })
}路径别名
项目配置了 @ 路径别名,指向 src/ 目录:
js
// config/index.js alias 配置
alias: {
'@': path.join(__dirname, '../src'),
'taro-ui$': 'taro-ui/lib/index'
}
// jsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": { "@/*": ["./src/*"] }
}
}使用示例:
js
import Spx from '@/spx'
import { useLogin } from '@/hooks'
import SpButton from '@/components/sp-button'