钟看图掌握核心观点图 1 VS 图 2您更倾向于哪张图来辅助理解全文呢欢迎在评论区留言一、目标悟空系统多地区化共线改造用一套代码、一套架构实现多地区部署后续的增量功能一次开发全量复用已有机房实现100%复用新增机房节约90%开发成本开发者开发组件的方式不需要做任何改变公共npm依赖包无需迁移开发者低成本完成组件迁移本文将深度解析悟空系统多地区共线改造的架构设计从页面多语言、站点的编译、npm私服、开发者等环节进行解析让读者能够有所收获。二、整体方案设计开发之前我们进行了整体的梳理涉及的范围如下图所示业务分层图从用户层、服务层、调度层进行拆解可以进一步分析出需要实施的要点如图所示整体功能点梳理图进行整体的分析之后我们可以从平台侧开始一层层的进行拆解从用户能直观看到的web层到用户感知比较弱的编译服务层再到私服和底层库的处理上核心主要分为三个模块进行改造平台改造、编译服务、npm私服底层库。三、模块拆解3.1 平台改造这部分介绍平台web侧的改造主要分为三个方向中英文改造、平台登录改造、国家码存储改造。平台中英文国际化改造背景 平台本身是用的vue进行开发所以这里我们采用Vue.js vue-i18n的国际化解决方案支持中文(zh)和英文(en)双语切换。我们来看一段核心代码示例// i18n.js 核心配置 // 语言包配置方便扩展 const messages { zh: { ...zh, ...zhLocale }, // 中文语言包 Element UI中文 en: { ...en, ...enLocale } // 英文语言包 Element UI英文 } // 基于域名的地区检测 // 不同地区自动读取对应语言包 const domainConfigMap new Map([ [****.vivo.com.cn, { region: 01, local: zh }], // 01地区 读取zhLocale语言包 [****.vivo.com, { region: 02, local: en}], // 示例02地区 读取enLocale语言包 [in-****.vivo.com, { region: 03, local: en }] // 示例03地区 读取enLocale语言包 ])使用vue-i18n有以下几点优势成熟稳定 vue-i18n是Vue.js生态中最成熟的国际化库与Vue 2.x完美兼容功能完善 支持复数形式、日期时间格式化、数字格式化等高级特性性能优异 采用懒加载机制按需加载语言包减少初始加载时间开发友好 提供丰富的API和插值语法支持嵌套翻译和动态参数平台登录改造悟空平台会存在多个机房场景如何使用同一个域名做为入口简化多地区登录场景链路。我们设计了一个多地区统一域名入口进入之后运营可以根据需求切换不同地区不需要在单独保存各个地区的独立链接登录链路也会整合到入口域名整体链路如下图所示代码示例如下getUucLogin (key, region) { ... const locationUrl getLocationUrl(region, env) // 回跳的链接根据地区信息来区分 return ${originMap[region][env]}/#/login?orgfrom${locationUrl}/project${key} // uuc登录地址融合地区信息和环境信息 }通过上述的方案我们将机房的匹配集成在系统内部进行减少用户感知降低用户使用成本这样可以做到统一域名入口运营不需要本地记录多个地址同时平台还提供便捷的地区切换能力极大的提升跨地区运营的便利程度。国家码存储方案用户选择国家之后悟空需要存储当前地区的地区码、语言码、时区信息并且对应地区的站点语言和生效时间都要和地区时区匹配而平台本身除了新开tab需要携带地区信息外还有开发者组件、iframe嵌套需要获取地区信息的场景针对这些场景悟空平台采用三层级的国家码存储策略① 新开tab时地区信息通过URL参数携带// 项目跳转时携带地区参数 goList(projectId) { const params { projectId: projectId } const wkCountryInfo Utils.tools.getCountryInfoParams() // 获取地区参数 const query {...params, ...wkCountryInfo} // 合并参数 this.$router.push({ path: /main, query: query }) } // 例如 getCountryInfoParams 返回格式{ loc: AA, lan: th_AA, tz: REGION/aa }② Vuex Store存储地区信息在应用状态中持久化存储开发者可以在组件内通过store读取国家码信息。// Store中的地区信息存储 state: { siteConfig: { wkCountryInfo: { loc: AA, // 地区信息 lan: th_AA, // 语言码 tz: REGION/aa // 时区 } } } // 获取Store中的地区信息 const storeCountryInfo store.getters[edit/snapInfo].wkCountryInfo || store.getters[interactive/snapInfo].wkCountryInfo③ LocalStorage缓存用户地区选择持久化到本地存储适用于iframe嵌套场景。如果父子iframe是同源策略可以直接读取LocalStorage如果非同源策略可以通过postMessage获取地区信息。// 地区切换时的存储逻辑 changeRegion(value) { const item this.headerRegion.list.find(v v.countryCode value) const wkCountryInfo { loc: item.countryCode, // 如AA, BB, CC lan: item.languageCode, // 如th_AA, en_BB, zh_CC tz: item.timezone// 如REGION/aa, REGION/bb } localStorage.setItem(__wk_platform_region_info_, JSON.stringify(wkCountryInfo)) }三级存储有以下几点优势状态一致性 三层存储机制确保地区信息在不同场景下的一致性用户体验 地区切换后新开Tab自动继承地区设置容错机制 多级地区码存储策略避免地区信息丢失合规保障 满足不同地区的数据本地化存储法规3.2 编译服务改造介绍完平台web侧的改造内容后接下来会详细介绍悟空系统编译服务多地区化改造方案。该方案通过统一的配置管理、多机房部署策略、差异化构建流程等技术手段实现了01地区、02地区、03地区 的全面支持。整体架构设计整体架构图该架构图展示了悟空互动平台多地区改造的整体设计思路地区识别根据环境变量或请求参数识别目标地区机房分发将请求路由到对应地区的服务器配置管理每个地区使用独立的配置文件代码分离不同地区使用专门的前端代码目录依赖隔离DLL文件和API包按地区分离核心技术方案整体方案设计完毕之后我们接下来一层层解析每个环节的改造。① 统一环境配置管理配置入口统一化通过对context.ts文件进行改造实现不同机房部署的统一入口处理。// 示例代码 // server/src/app/extend/context.ts get env(): Ienv { return env[process.env.REGION || AA][(this as any).app.config.env] }通过上述代码我们可以发现环境配置读取从只有一级的环境区分改造为机房信息环境的二级目录结构这样修改后服务启动时代码内部通过env方法可以获取当前机房信息下对应环境的全部配置信息方便全局调用。核心特性通过 process.env.REGION 环境变量动态选择地区配置支持 01、02、03 三个地区结合 app.config.env 实现环境级别的配置分发分层配置结构统一配置入口改造完毕之后接下来我们介绍下入口文件往下一层去查询每个机房各个环境具体配置信息的改造。配置文件信息按地区和环境进行分层管理改造后整体目录结构如下所示// 目录结构 server/src/app/util/env/ ├── index.ts # 配置入口 ├── 01 # 01地区配置 │ ├── index.ts │ ├── local.ts │ ├── test.ts │ ├── prod.ts │ └── ... ├── 02 # 02地区配置 │ ├── index.ts │ ├── test.ts │ ├── prod.ts │ └── ... └── 03 # 03地区配置 ├── index.ts ├── test.ts ├── prod.ts └── ...通过上述目录结构可以看出不同机房的配置信息一目了然相互独立方便维护和定位问题后续新增机房信息时只需要按照当前规则添加即可不需要额外关心内部业务逻辑。整体流程分为以下四个步骤应用启动时读取region地区信息根据region值选择对应地区配置目录01/02/03结合egg_server_env选择具体环境配置文件local/test/prev/prod返回最终配置信息供各个目录使用② ZooKeeper服务发现与调度改造环境隔离策略由于测试环境和预发环境都部署在01和02机房通过模拟的方式支持01、02地区而线上环境才是真正的物理隔离机房因此在ZooKeeper服务发现中需要特殊处理01、02为示例地区信息核心调度逻辑改造如下// server/src/app.js const isTestOrPreEnv process.env.EGG_SERVER_ENV.includes(test) || process.env.EGG_SERVER_ENV.includes(pre); // 添加机房信息 let group isTestOrPreEnv ? ${process.env.REGION || AA}-${process.env.EGG_SERVER_ENV}: process.env.EGG_SERVER_ENV const serviceClient new BeehiveService({ zkhost: ctx.env.zkHost, pong: true, services: { siteService: ctx.service.site, dspService: ctx.service.genDsp }, config: c.Config(c.group(group), c.maxTimeout(3 * 60 * 1000)) })通过上述代码我们可以发现服务注册和发现都需要按照机房信息环境信息作改造这样可以有效避免测试环境和预发环境站点编译时调度的机房出现异常线上环境由于物理机房隔离服务注册和发现可以不做调整。不同环境的ZooKeeper配置介绍完环境隔离策略后接下来我们介绍下不同环境的zk配置信息的改造。测试环境模拟多地区// 所有地区测试环境都使用同一个ZK集群 zkHost: zookeeper-*****.vivo.xyz:2183 // 但通过group区分01-test, 02-test, 03-test预发环境模拟多地区// 所有地区预发环境使用同一个ZK集群 zkHost: common-zk-****.vivo.lan:2181 // 通过group区分01-prev, 02-prev, 03-prev生产环境真实隔离机房// 机房1示例名称 zkHost: common-*****-zk.vivo.lan:2181 // 机房2示例名称 zkHost: in-common-*****-zk.vivo.lan:2181 // 机房3示例名称 zkHost: app.*****.zk.prd.****.vivo.lan:2181通过上述两个地方改造服务发现分组策略如下所示本地开发跳过ZooKeeper连接避免开发环境干扰测试/预发环境使用{region}-{env} 格式进行分组如01-test、02-prev生产环境直接使用环境名prod_wk依靠物理机房隔离③ 多机房构建策略编译服务在生成站点时还会对每个站点的主js文件做dll拆包处理将公共依赖打包成独立的dll基座文件降低页面的主资源体积提升加载速度那么针对多地区改造场景我们会做哪些处理呢差异化DLL构建针对不同地区我们需要使用不同的API包实现差异化的DLL构建01地区DLL配置// webpack.dll.config.js const vendors [ .... vivo/wk-api, // 01地区专用API vue-lazyload, ] module.exports { output: { path: path.join(__dirname, ./dll), // 输出到dll目录 filename: [name].[hash].js, }, // ... }02、03地区DLL配置// webpack.dll.02.config.js //webpack.dll.03.config.js const vendors [ .... vivo/asia-wk-api, // 02、03地区专用API vue-lazyload, ] module.exports { output: { path: path.join(__dirname, ./dll-02), // 输出到dll-02目录或者dll-03 filename: [name].[hash].js, }, // ... }在基座dll文件构建时由于多地区的登录、分享、埋点合规等存在较大差异我们对多地区场景做了单独的底层库封装dll文件生成需要根据不同地区分别构建并输出到不同目录。构建脚本配置修改dll配置文件时同时也需要在 package.json 中配置不同地区的构建命令01、02由于保密均为地区示例信息//代码示例 { scripts: { // DLL构建 dll: npx webpack --config webpack.dll.config.js, dll:01: npx webpack --config webpack.dll.01.config.js, dll:02: npx webpack --config webpack.dll.02.config.js, // 服务启动 start_test_01: EGG_SERVER_ENVtest REGION01 yarn dock_start, start_prev_01: EGG_SERVER_ENVprev REGION01 yarn dock_start, start_prod_01: EGG_SERVER_ENVprod_wk REGION01 yarn dock_start, start_test_02: EGG_SERVER_ENVtest REGION02 yarn dock_start, start_prev_02: EGG_SERVER_ENVprev REGION02 yarn dock_start, start_prod_02: EGG_SERVER_ENVprod_wk REGION02 yarn dock_start } }通过上述指令的改造我们可以很清晰的看到不同地区的dll构建指令都根据region信息做了区分方便后续的机房扩充和维护。④ Webpack多机房配置改造这里主要介绍webpack打包时如何实现不同地区的dll动态引入以及将国家码等信息编译到站点内。动态DLL引用不同地区的dll文件构建完毕之后我们需要在 webpack.pkg.config.js 中实现基于地区的动态DLL引用// 代码示例 // webpack.pkg.config.js const dllReferencePlugin config.plugins.find(plugin { const name plugin.constructor.name if ([DllReferencePlugin].includes(name)) { returntrue } }) if (dllReferencePlugin amp;amp; dllReferencePlugin.options) { dllReferencePlugin.options.context pageTempPath // 动态dll文件引用改造根据wukong.region01,02,03 来设置manifest路径 const DLL_DIR wukong.region 02 ? dll-02 : wukong.region 03 ? dll-03 : dll dllReferencePlugin.options.manifest require(./${DLL_DIR}/manifest.json) .... }通过上述示例代码我们需要将不同机房构建dll文件时生成的manifest.json进行不同动态引入改造避免站点运行时相同依赖通过chunkid进行匹配时出现错乱导致页面异常。全局配置注入通过编译服务能够拿到平台web用户在哪个地区编译发布的站点但是这些信息如何编译到用户可访问的每个站点里呢我们通过wk_siteInfo将地区信息注入到前端// webpack.pkg.config.js const site_option { host: ip.address(), port: 8080, stPath: ****, loginPath: ****, wk_siteInfo: { siteId, .... wkCountryInfo, // 地区信息、时区、语言码信息 region, // 地区信息 }, ...wukong } // 完成地区信息的注入 // index.html script // 示例代码 window.wk_siteInfo JSON.stringify(htmlWebpackPlugin.options.wk_siteInfo) /script通过修改webpack.config.js将站点的地区信息、时区、语言码等信息注入到wk_siteInfo然后通过htmlWebpackPlugin将打包后的地区码信息注入到html中这样就能实现页面对地区信息读取。⑤多地区部署流程构建流程图该流程图详细展示了多地区构建部署的完整流程地区选择根据目标部署地区选择相应的构建分支DLL构建为不同地区构建专用的DLL文件使用对应的API包Web构建使用地区特定的前端代码目录进行构建CDN部署将构建产物部署到对应地区的CDN服务服务启动使用正确的环境变量启动对应地区的服务环境变量配置编译服务改造技术亮点① 统一入口设计通过context.ts实现配置分发的统一入口运行时动态选择地区配置无需重新编译支持环境变量覆盖便于容器化部署② 差异化API包管理01地区使用vivo/wk-api02、03地区使用vivo/asia-wk-apiDLL构建时自动选择对应API包避免冗余③ 服务发现机制测试/预发环境通过group区分地区生产环境依靠物理机房隔离统一使用prod_wk分组自动识别环境类型动态选择ZooKeeper集群和分组策略本地开发环境自动跳过服务发现避免干扰④ 智能构建策略Webpack配置根据region参数动态调整自动扩展DLL manifest内容路径dll基座互相独立⑤ 前端代码隔离不同地区使用独立的前端代码目录保持核心业务逻辑复用的同时实现地区定制SDK初始化时自动注入地区信息通过上述的整套共线方案设计后续新增国家机房时新增地区只需配置相应环境文件无需修改核心代码配置结构清晰各地区配置完全隔离节约90%重复建设成本提升了系统的灵活性和可扩展性。通过统一的技术架构和清晰的配置管理成功实现了一套代码多地部署的目标为悟空互动平台的多地区化业务发展提供了坚实的技术保障。3.3 npm私服底层库介绍完编译服务后接下来介绍私服的代理策略和公共底层库的外销化改造。npm私服悟空除了服务业务运营还有开发者部分基于目前开发者整体的开发习惯我们需要做到开发者零感知实现02地区npm包的部署。基于此我们有以下三点目标开发者在办公网络可以直接开发、发布各机房环境的组件一套PaaS服务为多个机房提供组件物料的管理和发放确保物料传输合规为了实现上述目标我们做了一套完整的02地区私服方案设计具体如下图所示开发者开发组件上传维持01机房npm私服开发上传习惯无需新增02机房源npm物料仍然托管在01npm私服。同时在02机房建设代理npm私服通过ip白名单与悟空通信私服本身通过verdaccioss服务配置代理uplinks: zhan-npm: url: http://****.vivo.lan:8080 packages: **: ... # allow all known users to publish packages # (anyone can register by default, remember?) # if package is not available locally, proxy requests to npmjs registry proxy: zhan-npm通过02机房代理私服的方式既能减少了悟空开发需要额外多维护02机房源同时也降低了开发者组件包维护成本实现本地不需要新增任何npm源即可实现02机房包的开发上线流程。底层库外销改造wk-api是悟空平台封装的底层npm库为了实现多机房场景下业务组件多地区场景请求接口域名不变我们通过fetch统一拦截器header国家码信息直接将业务组件的请求转发到不同国家的接口服务。具体实现代码如下// 请求拦截器 - 动态URL路由 axios.interceptors.request.use(config { const { region } app; // 根据region动态获取prodUrl if (region 01) { config.baseURL https://****.vivo.com.cn; } elseif (region 02||region 03) { config.baseURL https://****.vivo.com; } // 请求头注入 if (wkCountryInfo.loc; wkCountryInfo.lan; wkCountryInfo.tz) { code loc${wkCountryInfo.loc};lan${wkCountryInfo.lan};tz${wkCountryInfo.tz}; loc wkCountryInfo.loc; } config.headers Object.assign(config.headers, { X-I8n-Code: code, X-Wukong-Loc: loc }); return config; });统一拦截器策略有以下优点零侵入性业务组件无需关心当前部署环境统一使用相同的API调用方式智能路由根据 region 参数自动选择对应机房的服务器地址请求头增强自动注入地理位置、来源页面等关键信息支持后端的精细化处理四、总结悟空系统的整体改造从上到下可以分为用户能直观看到的平台改造然后到用户感知不到的编译服务改造最后是开发者也无需感知的外销私服部署从前期梳理到后续一个个模块的拆解出方案进行开发落地最终实现了以下目标一种架构、一套代码实现多地区部署