飞猪海外酒店 H5 · 双仓合并 + 第三渠道适配方案

合并 h5-hotel-ae · ADCh5-hotel-lazada · WindVane,并预留第三渠道(Daraz / 新国家站)接入能力。

基于双仓真实代码抽样(env.ts / bridgeTool.ts / language.ts / mtopRequest.ts / package.json / pages 目录)+ 仓库 CLAUDE.md 约定产出。

★ 设计心法(给 1~2 人小团队)

每写一行代码只问一句:它有没有直接碰「容器 API / 环境数据(货币·地区·语言·登录态)/ 域名」
没碰 → 公用,一份写两边都生效;碰了 → 丢进对应渠道目录。不用记架构图,就这一句。

代码分三类(按真实占比)

纯公用 · ~90%
core/ + pages/ 详情/下单/列表逻辑、数据加工、hooks、工具函数 —— 改一处两渠道生效
渠道实现 ~8%
channels/<渠道>/ 怎么调容器、怎么取货币、MTOP 头、跳转参数、主题色
独占 2%
pages/channel-only/ AE 的 order_pc、Lazada 的 event

关键:90% 是公用的。双仓现在的痛苦,就是这 90% 被复制成两份——改 bug 改两遍、还容易漏。合并的全部收益就在这里。

代码该放哪 —— 唯一判断规则

写这段代码,它碰到了吗? ├─ 容器 API(跳转/标题/登录/分享) ───→ channels/<渠道>/bridge ├─ 环境数据(货币/地区/语言) ──────→ channels/<渠道>/i18n ├─ MTOP 头 / ttid / cookie ───────→ channels/<渠道>/request ├─ 域名 / URL 必带参(pha_manifest) ──→ channels/<渠道>/router ├─ 颜色 / 品牌 / 字体 ─────────→ channels/<渠道>/theme(css 变量) └─ 以上都没碰 ───────────────→ core/ 或 pages/(公用,一份搞定)

如何保证「各自迭代 + 不互相干扰」

你要改的放哪影响谁
AE 专属逻辑(跳转/容器)channels/ae/只有 AE。构建时 Lazada 包里根本不打进这段,物理隔离,改坏也只坏 AE
公用业务(详情页加字段)pages/detail两渠道同时生效 —— 这正是合并要的收益
渠道独占页(order_pc)pages/channel-only/只有 AE,构建按 __CHANNEL__ 裁剪
主题色 / 文案theme 变量 / i18n改配置,不碰任何逻辑
怕「改公用炸了另一渠道」?双渠道 E2E 对拍当门禁:每次提交两渠道核心链路自动跑一遍,挂了就拦。1~2 人靠机器兜底,不靠人肉记忆。

守住简洁的三条纪律(小团队专属)

小团队的敌人不是「不够优雅」,而是改一处要在五个文件间跳改一处怕炸全局。所以刻意做减法:

① 只抽已分叉的点

适配层只抽现在真实存在的 5~6 个差异,绝不为「未来可能」预留接口。差异冒出来再加。

② 不上框架

就一个 channel 单例对象,getChannel().bridge.open() 直白调用。不要 DI、不要插件机制。

③ 公用层零判断

红线。一旦容忍 if(channel.id==='ae') 进业务代码,就退化回「满地 if」,比双仓更糟。

万一公用代码里真冒出渠道差异?(按顺序消化,别一上来写 if)

只是颜色/文案/开关? 改 theme 变量 / i18n 配置(不碰逻辑) 是行为不同? 塞进对应 adapter 的方法里 是整页不一样? 挪到 channel-only 页 实在是临时的? 允许极少量 channel.id 判断,但留 TODO 记成债务

0 · 现状关键事实(决定方案的硬约束)

维度LazadaAE
容器WindVane(isWindVanewindow.adc.environment / isAeADC
i18n 来源@ali/rxpi-i18n-utils 同步取环境x-i18n-* header + hng cookie(原 SDK 逻辑被注释)
MTOP标准 MTop@ali/pocketI18nMTopttid:'12AE0000002',手写 hng
跳转桥@ali/alitrip-bridge同库 + adc_manifest:'fliggy' / pha_manifest=default / 沉浸式
组件库3 个 @ali/ihotel-material-*6 个(多 calendar/city-sug/occupancy-selector;mds-pro-span 版本不一致)
独占页event / rule-guideorder_pc / hotel-domestic-map / hotel-domestic-translate-detail
核心判断:业务页面(detail / booking / order / searchlist)高度对称,差异 90% 集中在 容器能力、i18n 环境、MTOP 头、跳转参数、主题 五个「环境接缝」——这就是渠道适配层要收口的边界。

1 · 总体架构选型

维度A · 单仓多渠道
适配层 + 多入口构建
B · Monorepo
核心包 + 渠道包
C · 双仓 + 公共包
代码复用高(共享业务页)高(核心包)中(仅 utils/组件)
新渠道接入成本高(再开仓)
双侧同步成本 (当前痛点)几乎消失
构建/发布复杂度中(多产物)高(turbo+版本联动)低(各自独立)
改造风险中(一次性合并)高(拆包+版本治理)
回归爆炸半径大(共享改动影响全渠道)中(按包灰度)小(仓间隔离)
路线 A ✓ 推荐

业务代码共享一份,环境差异沉到 channel 层,按渠道多入口出包。

路线 B

收益/成本不划算;可作为 A 的演进终点。

路线 C

维持现状,双侧同步成本无解。

选 A 的理由

① 当前最大成本是「双侧同步」,AE 本就是 Lazada 的镜像分叉,A 直接消灭分叉。
② 差异是可枚举的环境接缝、非业务分歧,用接口收口即可,不必物理分包(B 太重)也别容忍复制(C 的痛)。
③ B 把业务页面发 npm 包会拖慢高频迭代(组件库已是此痛点)。
④ A 的 core/ 未来可平滑抽成 B,反向不可逆——先 A 后 B 风险可控。

一句话:第三渠道 = 加一个 channel 实现 + 一个构建入口,不碰业务页面

2 · 核心抽象:渠道适配层(Channel Adapter)

业务层零渠道判断,统一通过 getChannel() 拿适配器。isAE / window.adc / WindVane / UA 正则只允许出现在 channels/

// core/channel/types.ts —— 渠道适配层总接口
interface ChannelAdapter {
  id: 'lazada' | 'ae' | string;
  bridge:  BridgeAdapter;   // 容器能力: open / setTitle / call / login
  env:     EnvAdapter;      // stage / 平台 / 域名 / cookie domain
  i18n:    I18nAdapter;     // getEnv()→{language,region,currency}; decorateRequest 注 x-i18n-*/hng
  router:  RouterAdapter;   // toDetail / toBooking ...; buildUrl 拼域名 + pha_manifest
  theme:   ThemeAdapter;    // tokens + 组件变体 + 根 class
  request: RequestAdapter;  // MTOP ttid / 头 / cookie 装配
}
差异最大 = I18nAdapter。 Lazada:getEnv 走 SDK 同步取、decorateRequest 基本空操作; AE:getEnvwindow.adc.environmentdecorateRequesthng 并设 x-i18n-language/regionID/currency

装配入口(构建时注入)

// entry/ae.tsx —— AE 构建入口
import { setChannel } from '@/core/channel';
import { aeChannel } from '@/channels/ae';

setChannel(aeChannel);     // 先装配渠道
import('./app');            // 业务入口,完全渠道无关

3 · 目录结构草案(路线 A)

src/
├── core/        # ✅ 渠道无关: channel 接口 / service / hooks / utils / 共享组件
│   └── channel/  # types.ts + getChannel/setChannel
├── channels/    # ⚠ 唯一允许 window.adc / WindVane / UA 的地方
│   ├── lazada/  {bridge,env,i18n,router,theme,request}.ts
│   ├── ae/      {... 含 ADC / pha_manifest / x-i18n-*}
│   └── <third>/ # 第三渠道,新增即接入
├── pages/       # ✅ 共享一份业务页
│   └── channel-only/  # 独占页(order_pc→ae / event→lazada),按构建 define 裁剪
├── i18n/  styles/  # 主题 css vars + data-channel="ae"
└── entry/       # ✅ 多入口: lazada.tsx / ae.tsx / <third>.tsx

core/pages/ 零渠道判断;独占页通过路由表 + 构建 __CHANNEL__ define 做 tree-shaking,避免把 AE 的 order_pc 打进 Lazada 包。

4 · 迁移步骤

P0 · 抽象就位,不合代码 1~2 周 · 低风险

两仓各自原地引入 channel 接口,把 env / bridgeTool / mtopRequest / language / jump 改成 channels/<self> 实现,业务层收敛散落的 isAE 判断。只重构不改业务,用现有 playwright E2E 做回归基线。

P1 · 物理合仓,共享业务代码 2~4 周 · 中风险

新建合并仓,以 Lazada 为基,搬入 AE channel + 独占页;对称页面逐页 diff 合成一份(差异下沉 channel/theme),配多入口构建。
风险缓解:隐性差异被合丢 → 逐页 diff + 双渠道 E2E 对拍 + 预发灰度;依赖并集冲突 → 动态 import 不进主 bundle;i18n key 冲突 → 并集去重。

P2 · 第三渠道接入 + 向 B 演进 按需

按 §5 清单接第三渠道验证抽象闭环;若 core/ 需被外部复用,再抽 npm 包演进到路线 B。

红线纪律:每阶段「完成」前必须在预发真实容器(WindVane 端 + AE ADC pha_manifest=default)各跑一遍 列表→详情→下单→订单。构建成功 ≠ 迁移生效。

5 · 第三渠道接入清单

接入一个新渠道 = 在 channels/<new>/ 实现一套适配 + 一个入口,不改 core / pages

  • EnvAdapter:域名 / stage 判定 / cookie domain
  • BridgeAdapter:容器类型(WindVane/ADC/PHA/H5)+ 降级
  • I18nAdapter:环境来源 / hng 写法 / header / medusaApp
  • RequestAdapter:MTOP ttid / 必带头
  • RouterAdapter:域名 + 渠道必带参数(pha_manifest)
  • ThemeAdapter:主色 / tokens / 组件变体 / 根 class
  • 构建入口entry/<new>.tsx + 一条 channel 产物
  • 页面裁剪:声明启用 / 独占页路由表
  • 组件库依赖:所需 @ali/ihotel-material-* 子集
  • i18n 文案:走 formatjs extract → Medusa
  • 埋点监控:ttid / SPM / 渠道维度
  • E2E:核心链路用例挂该渠道跑预发

6 · 风险与权衡

领域风险等级要点与缓解
i18n最高两侧环境机制不同,必须由 I18nAdapter 完全吸收;共用 Medusa lazada_hotel 合仓前做 key 并集去重 + 冲突检测;货币/地区禁止从 URL 推断
构建发布AE 独占依赖不能打进 Lazada 包(动态 import + tree-shaking,监控 bundle size);clam prepub/publish 扩展为按渠道发布,发 aone 用 aone MCP。
组件库依赖版本不同(mds-pro-span AE ^1.0.9 vs Lazada ^1.0.15),先拉齐版本再合代码,分两步降低排查难度;禁止软链覆盖。
回归测试A 路线代价是爆炸半径变大 → 双渠道 E2E 对拍做合并门禁 + 预发灰度。
守住一条红线:业务页面零渠道判断,不允许出现 if (channel.id==='ae')。守住则方案成立且可演进到 B;守不住则等于把双仓的复制粘贴换成单仓的满地 if,得不偿失。
抽样验证文件:h5-hotel-{ae,lazada}/src/utils/{env,bridgeTool,language}.tssrc/service/mtopRequest.tspackage.json
本方案由 workflow 综合 agent 基于双仓真实代码 + CLAUDE.md 约定产出,供技术方案评审使用。