引言
2023-12-06 是我在酷家乐的最后一天,想把我在酷家乐这 4 年主导落地的项目做个总结,聊聊每个项目的创立背景、结果成败,以及反思。
为了防止业务敏感信息泄漏,文中不会涉及到任何业务情况,项目结果数据,项目截图等内容。
19 相遇
时间来到 19 年 8 月,那是我加入酷家乐的日子。作为 IC 投入到酷家乐用户增长团队,当时团队主要在做激励体系、积分抽奖、酷签到、勋章等 To C 促活业务。
新业务
19 年 10 月,用户平台线成立 "设计圈" 新项目,是个 To B 的 SaaS 业务,目的是打通企业内部设计孤岛,让企业内部设计师共享、共建、共成长。后被大家戏称 "小主站",即主站的子功能 SaaS 化。
过程中我统一所有前台页面启动逻辑,增加启动的中间件机制,中间件机制也是首次被引入到页面启动流程中,对于多页、统一的场景至关重要;对于管理后台,引入当时比较前沿的 UForm(即现在的 Formily),并进行业务定制的封装,目的是简化表单、表格等场景的开发工作,而此动作也提效明显。在此感谢阿里对开源的贡献。
反思:
- 多页应用,需要有入口做全局逻辑的管控。而落地做法很多:html 入口/JS 统一使用固定的 boot 逻辑等等
- 垂直领域能做一定的技术轮子。例如:表单表格的管理后台场景,需要有垂直领域的组件来做提效,基础组件还不够
- 过程中担任 SM ,反推自己以全局视角考虑问题,并且关注团队成员的任务与过程
20 回归
20 年 2 月一半精力回归用户增长团队,直到 6 月份完全回归。
小程序平台
20 年 1 月,公司内部小程序业务增多,需要做一定的基础设施,以提升整体的开发效率。前端团队老大,推动成立"小程序平台"专项虚拟小组,由几个小程序的业务团队同学(设计圈也有小程序业务),以及基架组转岗过来的同学组成。
我主动负责其中的 CI/CD 部分,接入 Def(公司前端统一 CLI 工具),完成套件、构建、部署等功能。当时微信小程序还不支持 CLI 部署,只能借助 "微信小程序开发者" 工具,在 Windows/Mac 上使用,而公司已有工程化 Linux 相关的基建,完全用不了,故在 Windows 虚拟机上安装"微信小程序开发者"工具,并且本地启动 proxy server 与开发者工具互通,CLI 再调用 Windows 本地的 proxy server 完成互通。
反思:
- 微信开发者工具客户端等以 UI 的形式提供给使用方,对于小团队很友好,对于想集成到大团队自有工作流的系统中,很差(好在微信现在已提供 SDK 跨平台发布,以便于集成到现有系统中)
- 我只参与了"小程序平台"不到半年,随后平台越发庞大:包括微信公众号管理、用户管理,甚至域名管理、人员管理、运维中心、营销工具等微信本身已经提供的能力包装。投入了非常多的人力,但是我个人认为过于超前。主要原因是:
- 酷家乐本身各业务小程序并没有太多增长
- 边缘功能太多,绝大多数场景根本用不到。我理解仅需要这些核心能力:模板分发多商家小程序、CI/CD、组件库、脚手架。
- 基建不应太过超前,优先满足最核心、提效最高的能力。
TMS
对于 To C 的产品,用户增长当时主要靠运营驱动,借助一些营销获客、促留存的手段,而产品需求绝大部分来自于运营同学。而面向运营同学的工具,有 2 款:
- TMS:仿淘宝店铺装修的页面搭建平台,主要可以完成产品介绍页、营销页等功能的搭建;
- 云台:运营平台,面向 To C 用户的营销推送(短信、公众号、邮箱、站内信等场景)、广告位管理等核心应用场景。JS 全栈开发,包括对 MySQL、Redis 等持久层的直接调用。
对于 TMS 页面搭建平台,有个极大地痛点:所有的模块(前端开发的定制模块)很难开发与发布,所有模块都杂糅在一个 NPM 包内,所以开发一个模块的流程是这样:
- TMS 管理后台创建一个模块,就是元信息了拿到模块 ID
- Mod Package 里开发一个模块,包含展示场景和编辑场景的组件
- 将 NPM 包 Link 到 TMS 管理后台的仓库
- 启动仓库本地 debug mode (体验很差)
-
开发阶段结束,开始发布阶段
- 发布 NPM 包
- 分别安装到对外渲染、对内后台管理的 Repo 上
- 分别发布对外、对内系统
整体流程很长,导致业务开发同学更愿意 0-1 写一个静态页,而不是开发一个 TMS 模块,进而造成了业务模块并不是很多,生态不丰富。
我在开发 TMS 模块时也深感痛苦,故过程中对 "新开发一个模块" 的流程进行了改进。
整体原则就是将模块的安装、加载从主体中剥离,从 NPM 包转变为浏览器端运行时注入的模块。当时已经有了 SEED,它比较基础,且全局都有安装,它是一个运行时模块加载、管理器,可以简单类比 SeaJS。通过维护一个 Alias ,模块 key 与 JS/CSS CDN 列表的映射关系,来决定如何加载模块,而这个 Alias 本身也是通过一个大 JSON 进行保存。
那么解决思路很直观了,只需要保存一个 TMS 模块 Key 与模块打包后的 JS/CSS 产物,即可做到将模块的安装由 buildtime -> runtime,进而能做到模块的调试、打包、发布与 TMS 主系统完全隔离。
优化后的流程是这样的:
- TMS 管理后台创建一个模块,就是元信息了拿到模块 ID
- 各自业务仓库开发一个符合一定 Interface 的模块
- 业务仓库本地 debug (打开 TMS 测试环境,直接把模块请求代理到本地即可)
-
开发阶段结束,开始发布阶段
- 各业务仓库构建模块,产出 JS/CSS,并自动上传 CDN,修改 Alias JSON 完成发布
本地调试由于仅需要构建当前模块,所以开发体验很棒。
SEED 与微应用
但是 SEED 也有它自己的问题,Alias JSON 独立于现有其他发布平台维护,且无灰度、无回滚,是个很大的稳定性隐患。
当时公司基建(Pub)已初步成型,比较超前,核心是以最小可发布粒度的一站式解决方案,而首推的就是页面单元,能将传统的以 Repo 多页为发布单元,转为以独立页面为发布单元,且秒级发布、秒级回滚。前端微应用当时也初步成型,主要目的是拆分酷家乐工具的巨石应用,提升开发效能。
当时主站还在继续使用 SEED,前端微应用和 SEED 其实目标非常类似,核心都是独立开发、独立发布。这时产生了一个想法 "能否让前端微应用支持浏览器端运行时加载,以替代掉 SEED 模块管理部分的能力 ",达到 "All in micro " 的效果。
此时,前端微应用的输出模式是 html 片段,此片段可以注入到 page 中,最终输出完整的 page html 给到浏览器,即拼接形式。页面与微应用可独立发布,在统一的 Node.js Page Render 层进行服务端拼接,以组装成一个可以由多团队共建的完整应用。
那么,做法很清晰了,需要将仅支持在服务端 html 拼接形式使用的微应用,扩展为支持浏览器端运行时动态获取微应用 html 片段,并注入到 DOM 中去,并解决 Script 等标签无法执行与如何同步有序执行的问题,这就诞生了"Pub 微应用加载器 "。
此时已存在 Single SPA 或乾坤等库,独立发布的功能是大家共有的,沙箱&路由联动等特性是不需要的,所以也没有参考这些开源库实现。
此阶段之后就顺势推动 SEED 历史模块全量迁移 Pub 微应用,相对的好处是:
- 拥抱同样的基建(CI/CD),灰发&回滚等机制
- 无需页面预置环境
- 去中心化,微应用加载器分布式安装在各个微应用 or 页面 bundle 内,不到 8K (未压缩)
而 TMS 的新模块开发方式也由 SEED 模块过渡到使用 Pub 微应用模块。
公共包
基建相对比较成熟了,但是主站业务的公共包却一直比较混乱,质量也不高。"磨刀不误砍柴工",工具库、业务组件库的重要性不言而喻,这半年也开启了公共库的创建和规范:
- types:以业务域划分,定义业务通用的类型单元,例如方案等
- utils:工具函数
- rc:业务特定的组件库
- etc...
这部分内容大多数公司做的事情类似,不细讲了。
反思
- HTML 是组成页面的基本单元,以它为切入点,相对以 JS Entry 能做更多事;
- 跨团队协作,独立发布,低耦合是效能王道
- 开源产品能解决部分通用问题,工作流的串联,整体架构还需独立设计
- 秒级发布&回滚,能解决绝大多数稳定性问题
- 发布卡点 or 审批对于新手是保护,对于老手是枷锁
H2 开始,也带来一些新的挑战:
- 如何快速搭建新站点
- 类似的区块如何复用,是否复制是个更好的选择?
21 创新
Star
基于 20 下半年业务上各种新站点搭建带来的效率以及质量的综合挑战,21 年初我在思考"是否要造一个全司共建共享的物料共享平台 ",以打破团队间信息壁垒。
在此阶段我已经是敏捷组 TO,并且有一定的影响力,所以大家愿意跟着我的想法一起干,包括隔壁组同学。此时恰好 UED 团队同学有"设计物料共享"的想法,所以一拍即合,前端 5 人 + 设计 2 人,自建组成虚拟小组,利用业余时间创建:Star 物料平台 。
平台设想大而全:
- 开发物料:Web 端、VSCode 插件、物料开发 CLI;分为 2 大类:区块、页面模板
- 设计物料:Web 端、Sketch 插件
这里主要讨论下开发物料,区块和页面模板都是参考自"飞冰"的设计,利用"复制"的手段,达到复用的目的。好处就是可以任意修改,不会因为 Interface 不满足而无法使用或扩展,相似的视觉效果都能直接拿来用。
而 VSCode 和 Sketch 插件的代码分别 Fork 自开源项目 IceWork、Kitchen(好像是),进行自有系统以及物料库的集成。
整个系统全栈 TS 开发,包括 Sketch 插件,服务端采用 NestJS+MySQL+Serverless 完成。
反思:
- 现在看来,区块的复用方式不如组件的形式,而且也没有用起来
- 页面模板倒是用来做初始化页面 or 微应用的规范了,也是一种将各业务线规范落地的平台
- 设计物料和 Sketch 插件使用量可观,相对于原始 File 下载分发,借助 Sketch 插件自动享受最新的设计物料比较高效
所以就区块来说,Star 是失败的,所以后来又逐步优化,新增了微应用文档的接入,因为微应用的使用方必然是需要阅读文档的,Star 就是一个比较好的集成微应用使用文档的平台,直接关联微应用的唯一 Key。
登录注册
在这之前我也兼账号体系(UIC)的前端负责人。酷家乐的账号体系也许是互联网行业最复杂的系统之一,它的复杂性来源于:
- 面向多种产品:To C、To B
- 面向多种身份:设计师、业主、从业主,在这之下又有很多细分行业
登录注册链路也有一定的复杂性:
- 注册链路极长,三方绑定 -> 手机验证 -> 选择身份 -> 推荐设计师/业主 -> 发放奖励
- 登录的形式:三方、扫码,弹窗登录、登录页面
- 登录过程中的风控拦截,图片验证,
- 登录过程中的 C & B 多账号绑定
- etc.. 还有很多没有列出来的
面临的挑战:
- 整体偏过程式的写法:你可以想想一个回调函数内部写了非常长的逻辑,且牵一发动全身
- 数据流与执行流的混乱:Promise 可能存在一直 Pending 的状态,例如某个 callback(resolve) 一直不执行,流程中的数据传递混乱,没有一条主线
- 以上带来的结果就是,涉及登录注册的任务估时 x2
- 美间、模袋等业务的加入,需要打通账号体系,并且复用同一套登录注册能力(但是不接受走同一个页面完成 SSO,这决定了后续的架构模式)
基于此,对登录注册组件进行了彻底的重构:
- 更合理的分层:基础包(通用 UIC 逻辑)、核心能力(支持配置化的形式确定外部需要何种登录注册方式)、酷家乐业务场景下的微应用 以及 其他业务场景下的页面
- 插件化的架构模式:借助 Tapable 完成异步串行的执行场景,增加登录注册前后等超 10 个 Hook,为后续扩展奠定了基础,并解决执行流问题
- 全局 Store:解决数据流问题
- 将原有非核心链路的逻辑拆分出接近 10 个插件,完成业务逻辑
结果:
- 扩展性:最初设想就是未来至少 3 年不需要重构登录注册模块,目前我认为至少 5 年是没有问题的
- 研发效率的提升:统一群核之下的几乎所有业务线的登录注册;后续几年的实战中,对于登录注册业务上的各种大需求,都没有对核心部分造成影响,通过插件都能满足需求
- 整体的 ROI 还是很高的
反思:
- 对核心业务的架构优化是值得投入的
- 插件化不仅用于工程化领域,也可用于业务,需要一定复用性、扩展性的场景都可考虑
- 架构是为了不让复杂度指数级爆炸
开发效率与规范
21 年的以上 2 个偏全司基建或特定业务,开发效率与规范也在持续进行:
- 为了多仓库共享代码,造了 Rocket CLI,定位是基于 subtree 的业务线级别的代码共享
- 规范了业务域为单位的 Owner 机制,并且不分端(PC、H5、小程序)
- 规范了 lint/babel/postcss/ts config,并且基于 Rocket 可以做到及时的共享更新
- 规范了所有 page 的启动方式,也基于 Rocket 进行共享
- 规范了全局弹窗的管理器,支持优先级队列机制
- etc...
基于 Rocket 的基建能力,做了到所有业务仓库共享同一套 xxx config,共享同一套业务启动逻辑。
但是也带来了一些棘手的问题:
- Git subtree 的机制,会让 Repo 历史记录混乱,掺杂很多不相干的 commit
- 高版本 Git subtree 提交时,部分同学总是无法 push 上去
- 随着时间推移,2 年后的今天,commit 已达 2k 多条(中间应是某些同学误操作带上去的),导致后期又增加了 reset 的机制,并且把 shared Repo 给重置了,进而又导致 shared Repo 与业务 Repo history 对不上...
这些问题只能通过比较懂的同学人肉操作下,以达到可以正常 push pull。所以后期会弃用 Rocket,改回 Npm Package,但是增加一些功能让他能保持定期更新。
反思:
- Subtree 有其局限性,最好的协作模式我认为一个业务线采用单一的 monorepo,通过基建去直面单 Repo 的构建性能问题,部署效率问题;
- 对于业务线的开发团队,最优先的是制定规范、落地规范到代码里、及时更新规范,以达到开发者同一套开发思路,对于协同开发效率是极好的;
- 不要分端,其他端的开发成本相对团队多人的沟通成本低很多;
22 再创新
22 年底有写过《2022 年终总结》,所以这里尽量谈的更宽泛一些, 有一定的相似处。
职位的变化,21 年中开始担任 Action Mgr,22 年初转为正式 Mgr。也会有一些管理思考,但是本文不会涉及。
客户端打包平台
没错,又开始造平台了。背景是酷家乐的主要用户在 PC 端,且绝大多数都使用客户端(基于 Electron),而且其他业务线也会开发自己的客户端(例如美间)。
所以除了一些基础 Electron 扩展能力的复用之外,长远来看最好能有个工程化平台,集成端侧的构建、打包、发布、分发等一系列的能力。这就是"客户端打包平台" 也可以称之为"客户端 DevOps 平台"。
做了如下事情:
- 首先需要一个打包环境,不仅要打包 Windows/Mac 上的 Electron 应用,后续还支持了 Android App 的打包
- 其次需要一个打包管理后台,包括:应用管理、构建管理、版本管理、发布管理以及权限
- 最后定义一套接入规范,以 Node.js 脚本形式接入,脚本接收一些入参,根据参数构建、打包、签名产出最终的安装包(固定目录),平台进行上传并回调更新 CDN URL、版本等信息
整体逻辑并不复杂,说一些它和 Web 页面发布的区别:
- 存在版本,线上版本碎片
- 存在复杂的更新机制,也有灰发机制
- 存在不同渠道分发不同安装包,便于后续的安装来源统计
- 多种打包目标:Windows/Mac/Android,不同目标会有提供不同的打包环境
对于平台,还有很多事没做,例如:数据看板,版本分布等,但是对于近几年足够了。
有些同学可能会不理解,和 Gitlab CI 有啥区别?
借助 CI 仅能完成任务的触发,而任务是需要特定的运行环境的,除此之外:版本的管理、灰发、渠道分发都是平台特有的能力。
反思:
- 针对核心业务做基建更不易出错
SSR
过去几年,随着基建升级,老的 FreeMarker(JAVA) + JQuery ,慢慢转变为 Nunjucks(Node.js)+ JQuery,再转变为 Nunjucks(Node.js)+ React。而到 React 阶段,服务端直出页面关键 HTML(SSR)已不存在。产生的结果就是来自搜索引擎的流量逐渐下滑,而 SEO 对于酷家乐来说至关重要,是个非常重要的流量窗口。为了拯救 SEO,22 年上半年开始了一些 SSR 的尝试。
但是,要做 SSR ,会和业界常用方案有所不同:不会采用 Next.js 类似全栈框架,因为此类全栈框架带来的问题是每个业务都需要独立的 Node.js 服务,还需要持续的观测稳定性,出问题对于业务开发者来说是非常棘手的,对开发者的要求极高。
所以 SSR 服务需要做到的效果:
- 每个 SSR 页面都可以独立发布,即使他们在一个 Repo
- 创建 SSR 容器服务,由 SSR 服务的开发者管理服务的稳定性,业务开发者无需关心
- 所有 SSR 页面都运行在这个容器内
- 所有 SSR 页面需要有沙箱,运行上下文隔离
- 需要有降级到 CSR 的策略
除了 SSR 服务本身之外,也需要有其周边的工具链:
- 针对每个 SSR 页面构建打包为独立的 Server Entry
- Server Entry 需要符合约定的 Interface,输入请求上下文,输出渲染结果
- TS type 包,便于接入
方案详情页是第一个接入的页面,上线前借助 Apache Benchmark 工具做了一定的压测,上线结果也是很好的,此阶段 22 年中上线。
到此阶段,还有一些工程化问题需要联合基架组一起解决,深入集成到 Pub 系统内:
- 本地开发:支持 SSR 和 CSR 的同步开发,以及规范的本地开发调用链
- 构建:自动识别哪些页面需要走 SSR ,完成 Server Entry 构建
- 发布:发布后,对于页面信息的变更,秒级同步到 PR 与 SSR,完成应用的自动更新
- 运行:集成请求链路 浏览器 -> PR -> SSR,自动降级能力;以及 Pub 上配置包、语言包等能力的打通
此阶段在 22 年下半年完成,完成后对于业务开发者来说,开发一个 SSR 页面和 CSR 一样简单,不仅是 SEO 的提升,对于首屏优化也有效。
过程中也遇到了各种问题:
- 一些二方包实现时没有考虑 Node 端运行场景,例如使用了很多 window/navigator 等浏览器端全局变量,使用 jsdom 注入到每个页面上下文里解决(但也带了问预料之外问题,见 3)
- OOM 问题:随着 SSR 流量增多,有一天触发了一端代码的临界点,即运行几天后内存溢出,服务被 K8s Pod 自动重启,反复;排查下来是一个非常基础的监控模块,在并发的 HTTP 链接达到一定数量后进入另一个分支,这个分支对缓存的清理有问题,导致持续增长的内存没有被回收
- GC 频繁导致 CPU 增高:根因是有个页面使用到了 react-helmet 库管理 document head 信息,helmet 又使用了 react-side-effect 来处理 props 变化引发的副作用,问题就出现在我们 Mock 了 window/document 等信息,让库误认为当前运行环境在浏览器端,进而将本应无副作用的 string 处理,变成了 DOM 处理,让 Node.js 内 new space 的空间增多,进而引发频繁 GC。
可以看到目前的 SSR 方案也并不是完美的,虽然做了沙箱,但是本质他们还是运行在同一个线程之内的,共享同一个 CPU 资源和内存资源。当出现 GC 引发的 CPU 问题,或 OOM 引发的问题,就会导致整体的不可用。
所以解决这些问题的方案就只能做多进程,一个页面的 SSR 就启动一个独立进程,由主进程管控数据分发、应用更新,这样能充分利用多核 CPU,不至于一个页面 Bug 引发整体的雪崩。
反思:
- 之前做的平台更偏研发效率,SSR 能解决一定的业务问题
- 不一定一开始就要最完美的方案,保留一定扩展性,能解决当下问题就是最好的
其他
22 年也有一些效果不错的优化:
- 帮助中心的核心 API 性能提升 70%,帮助中心 JS 全站开发,主要优化的是对 MySQL 调用相关的业务层的逻辑优化。同时也解决其稳定性问题,之前总会因为流量的翻倍导致服务短时间不可用;
- Star 部分也在持续优化:新增了一些页面模板,微应用的文档是在这一年做的
- 客户端的可观测行:本地日志优化,以及用户的一键上报
- 重写富文本编辑器,并应用在多条业务线
- etc
23 优化
23 年主要是对现有系统的优化,年中也由主站转岗到了国际站。
SSR & Star & 客户端
- SSR 一些接入文档,最佳实践之类的文档编写
- Star 权限管理;支持培训物料类目
- 客户端监控体系的建设,接入现有监控平台
- 客户端打包平台的持续优化等
国际站
国际站的技术沉淀基本等价于 3 年前的主站,所以还有很多问题需要解决,以及一些提效工具都没用上,感觉和大多数业务线有些脱节。
国际站有很大的特点:多语言、多货币、多集群,依赖的很多三方也不同,例如登录、支付场景。以上都和前端息息相关,其中和开发方式密切度非常高的,就是多语言。
而多语言,目前公司已有基建也比较完善:语言包 CDN 化 + 配置后台 + 项目粒度管理 + VSCode 插件提效。但是也由很多问题:治理困难,例如如何清理一些无用词条;验证困难,例如如何验证其他语种的有效性等。我目前还没有想到比较好的解决手段。
Magi 配置平台
除此之外,页面配置化的能力,对于运营可快速尝试各种增长手段也至关重要。目前运营会采用上文提到的 TMS 来搭建一些营销页、产品介绍页。但也有一些是不满足需求的:SEO、性能、多语言等。
除了 SEO 之外,另外 2 条通过优化 TMS 都还能解决。因为 TMS 的整体架构决定了,想要能支持 SSR 很难,更不必说内置的或二方的组件了。除了页面编排需求之外,还有这些诉求:
- 开发者编写的页面也需要有配置化的能力,而针对特定功能开发特定的后台,成本极高
- 页面配置化需要能根据国家、人群维度进行不同的展示
- 分站点,例如不同国家不同站点
为了满足以上需求,计划造一个低代码配置平台,以及低代码引擎。目前还处于非常早期的阶段,仅完成整体的架构设计和部分 Core & Editor 逻辑的编写。
总结
至此,酷家乐的旅程告一段落。
这段旅程里,做了很多针对研发效率、质量方面的工作,也为其他岗位角色(UED、运营、市场)带来了人效的提升。我相信每一份努力和效率的提升,都会让酷家乐进步一点点,让我们在这个竞争激烈的市场上赢得胜利的机会多一点点。在这里收获满满,未来祝愿群核科技越来越好!
再额外聊一下关于离职,我的看法。我们常看到某些同学因为个别同事的离职,而内心动摇,也决定离职,我曾经也这样。但是在加入酷家乐前,就告诉自己,直面自己内心,不要在乎他人的去留,只要能确定自己能有成长、有收获、与自己规划相符就足够了,共勉。
2023-12-08
于 良渚