Skip to content

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 / replaceState

getSystemConfig() 系统配置拉取

应用启动后,首页(或入口页)会并发拉取系统配置:

js
// 首页 onLoad 中
const [systemConfig, userInfo, cartCount] = await Promise.all([
  api.member.getSysConfig(),     // 系统配置(颜色、tabbar、价格设置等)
  Spx.getMemberInfo(),           // 用户信息(已登录时)
  api.cart.getCount()            // 购物车数量
])

系统配置返回后,通过 Redux dispatch 到 sys slice,包含:

  • 主题色colorPrimarycolorMarketingcolorAccent
  • TabBar 配置:tab 页面列表和图标
  • 功能开关openStoreopenRecommendopenScanQrcode
  • 价格设置:购物车页、商品页、订单页的价格显示规则
  • 货币信息:符号、代码

主题色会通过 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 是项目的全局数据访问器,解决两个核心问题:

  1. Token 管理:在 mall 模式和 merchant 模式之间自动切换 Token
  2. 避免循环依赖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'