Skip to content

附录 C:mpay 迁移指南

本指南帮助开发者理解从 mpay (Angular) 到 BFM Pay (React) 的架构演进和代码迁移策略。


一、架构对比

1.1 技术栈对比

方面mpay (旧)BFM Pay (新)
框架Angular 15React 19
构建Angular CLIVite 7
路由Angular RouterStackflow
状态RxJS + ServicesTanStack Store + Query
表单Reactive FormsTanStack Form
样式Ionic Componentsshadcn/ui + Tailwind
测试Jasmine + KarmaVitest + Playwright
类型TypeScriptTypeScript (Strict)

1.2 项目结构对比

# mpay 结构
src/
├── app/
│   ├── pages/           # 页面模块
│   ├── services/        # 全局服务
│   ├── components/      # 共享组件
│   └── guards/          # 路由守卫

# BFM Pay 结构
src/
├── pages/               # 页面组件
├── components/          # UI 组件
├── services/            # 服务层 (Adapter 模式)
├── stores/              # TanStack Store
├── hooks/               # React Hooks
└── stackflow/           # 导航配置

二、核心模块迁移

2.1 服务层迁移

mpay: Angular Service

typescript
// mpay: wallet.service.ts
@Injectable({ providedIn: 'root' })
export class WalletService {
  private wallets$ = new BehaviorSubject<Wallet[]>([])
  
  getWallets(): Observable<Wallet[]> {
    return this.wallets$.asObservable()
  }
  
  async createWallet(mnemonic: string): Promise<Wallet> {
    // 实现...
  }
}

BFM Pay: TanStack Store + Adapter

typescript
// BFM Pay: stores/wallet.ts
export const walletStore = new Store<WalletState>({
  wallets: [],
  activeWalletId: null
})

// services/wallet/adapter.ts
export class WalletAdapter implements IWalletService {
  async createWallet(params: CreateWalletParams): Promise<Wallet> {
    const wallet = await this.deriveWallet(params)
    walletStore.setState(state => ({
      wallets: [...state.wallets, wallet]
    }))
    return wallet
  }
}

2.2 页面迁移

mpay: Angular Component

typescript
// mpay: home.component.ts
@Component({
  selector: 'app-home',
  templateUrl: './home.component.html'
})
export class HomeComponent implements OnInit {
  wallets$: Observable<Wallet[]>
  
  constructor(private walletService: WalletService) {
    this.wallets$ = this.walletService.getWallets()
  }
  
  ngOnInit() {
    this.loadWallets()
  }
}

BFM Pay: React + Hooks

tsx
// BFM Pay: pages/home/index.tsx
export function HomePage() {
  const wallets = useStore(walletStore, state => state.wallets)
  const { data, isLoading } = useQuery({
    queryKey: ['wallets'],
    queryFn: () => walletAdapter.getWallets()
  })
  
  return (
    <div>
      {wallets.map(wallet => (
        <WalletCard key={wallet.id} wallet={wallet} />
      ))}
    </div>
  )
}

2.3 路由迁移

mpay: Angular Router

typescript
// mpay: app-routing.module.ts
const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'wallet/:id', component: WalletDetailComponent },
  { 
    path: 'settings', 
    loadChildren: () => import('./settings/settings.module')
  }
]

BFM Pay: Stackflow

typescript
// BFM Pay: stackflow/stackflow.ts
const activities = {
  HomeActivity: () => import('./activities/HomeActivity'),
  WalletDetailActivity: () => import('./activities/WalletDetailActivity'),
  SettingsActivity: () => import('./activities/SettingsActivity')
}

// 导航使用
const { push, pop } = useNavigation()
push('WalletDetailActivity', { walletId: '123' })

三、关键文件映射

3.1 页面映射

mpay 页面BFM Pay 页面说明
pages/home/stackflow/activities/HomeActivity首页
pages/mnemonic/pages/home-transfer/pages/transfer/转账
pages/mnemonic/pages/home-receive/pages/receive/收款
pages/authorize/pages/signature/pages/authorize/signatureDWEB 签名授权
pages/setting/stackflow/activities/tabs/SettingsTab设置

3.2 服务映射

mpay 服务BFM Pay 服务说明
wallet-data-storage.tsstores/wallet.ts钱包存储
bfm-chain.service.tsservices/chain/bfm-adapter.tsBFM 链服务
exchange-rate.service.tsservices/currency/汇率服务
biometric.service.tsservices/platform/biometric.ts生物识别

3.3 组件映射

mpay 组件BFM Pay 组件说明
<ion-button><Button>按钮
<ion-input><Input>输入框
<ion-card><Card>卡片
<ion-modal><Sheet> / <Dialog>弹窗
<ion-loading><Skeleton> / <Spinner>加载态

四、RxJS 到 React 迁移

4.1 Observable → useQuery

typescript
// mpay: RxJS
this.balance$ = this.chainService.getBalance(address).pipe(
  switchMap(balance => this.convertToCurrency(balance)),
  shareReplay(1)
)

// BFM Pay: TanStack Query
const { data: balance } = useQuery({
  queryKey: ['balance', address],
  queryFn: () => chainAdapter.getBalance(address),
  select: data => convertToCurrency(data)
})

4.2 BehaviorSubject → Store

typescript
// mpay: BehaviorSubject
private settings$ = new BehaviorSubject<Settings>(defaultSettings)

updateSettings(partial: Partial<Settings>) {
  this.settings$.next({ ...this.settings$.value, ...partial })
}

// BFM Pay: TanStack Store
const settingsStore = new Store<Settings>(defaultSettings)

settingsStore.setState(state => ({ ...state, ...partial }))

4.3 Subject → Event Emitter

typescript
// mpay: Subject
private txEvent$ = new Subject<TransactionEvent>()

emitTransaction(event: TransactionEvent) {
  this.txEvent$.next(event)
}

// BFM Pay: Subscribable
class TransactionEvents implements Subscribable<TransactionEvent> {
  private listeners = new Set<(event: TransactionEvent) => void>()
  
  subscribe(callback: (event: TransactionEvent) => void) {
    this.listeners.add(callback)
    return () => this.listeners.delete(callback)
  }
  
  emit(event: TransactionEvent) {
    this.listeners.forEach(cb => cb(event))
  }
}

五、DWEB/Plaoc 迁移

typescript
// mpay: Angular
this.deepLinks.route({
  '/authorize/address': AddressAuthPage,
  '/authorize/signature': SignatureAuthPage
})

// BFM Pay: React
// src/services/authorize/deep-link.ts
export function handleDeepLink(url: string) {
  const parsed = parseDeepLink(url)
  
  switch (parsed.action) {
    case 'authorize-address':
      navigation.push('AuthorizeAddressActivity', parsed.params)
      break
    case 'authorize-signature':
      navigation.push('AuthorizeSignatureActivity', parsed.params)
      break
  }
}

5.2 Plaoc API 调用

typescript
// mpay 与 BFM Pay 相同 (Plaoc API 不变)
import { dwebServiceWorker } from '@aspect/aspect'

// 返回授权结果
await dwebServiceWorker.postMessage({
  type: 'authorize-response',
  data: { address, signature }
})

六、数据迁移

6.1 存储格式兼容

typescript
// 读取 mpay 格式数据
interface MpayWalletData {
  wallets: Array<{
    name: string
    mnemonic: string  // 加密存储
    addresses: string[]
  }>
}

// 转换为 BFM Pay 格式
function migrateMpayData(mpayData: MpayWalletData): WalletState {
  return {
    wallets: mpayData.wallets.map(w => ({
      id: generateId(),
      name: w.name,
      encryptedMnemonic: w.mnemonic,
      accounts: w.addresses.map(addr => ({ address: addr }))
    })),
    activeWalletId: null
  }
}

6.2 迁移检测

typescript
// 应用启动时检测
async function checkMigration() {
  const mpayData = await storage.get('mpay-wallets')
  
  if (mpayData && !await storage.get('bfmpay-migrated')) {
    const migrated = migrateMpayData(mpayData)
    await storage.set('bfmpay-wallets', migrated)
    await storage.set('bfmpay-migrated', true)
  }
}

七、常见迁移问题

Q1: Angular DI 如何迁移?

使用 React Context 或直接导入单例:

typescript
// 方案 1: Context
const ChainContext = createContext<IChainAdapter>(null!)
export const useChainAdapter = () => useContext(ChainContext)

// 方案 2: 单例导入 (推荐简单场景)
import { chainAdapter } from '@/services/chain'

Q2: ngOnInit / ngOnDestroy 如何迁移?

typescript
// Angular
ngOnInit() { this.loadData() }
ngOnDestroy() { this.subscription.unsubscribe() }

// React
useEffect(() => {
  loadData()
  return () => cleanup()
}, [])

Q3: 模板语法如何迁移?

html
<!-- Angular -->
<div *ngIf="loading">Loading...</div>
<div *ngFor="let item of items">{{ item.name }}</div>

<!-- React -->
{loading && <div>Loading...</div>}
{items.map(item => <div key={item.id}>{item.name}</div>)}

Q4: Ionic 组件如何替换?

参考组件映射表 (3.3),使用 shadcn/ui 对应组件。


八、迁移检查清单

  • [ ] 服务层迁移到 Adapter 模式
  • [ ] RxJS 迁移到 TanStack Query/Store
  • [ ] 路由迁移到 Stackflow
  • [ ] 表单迁移到 TanStack Form
  • [ ] Ionic 组件迁移到 shadcn/ui
  • [ ] DWEB/Plaoc 功能验证
  • [ ] 数据格式兼容性测试
  • [ ] E2E 测试覆盖核心流程

Released under the MIT License.