Harttle

百度资深研发工程师,北京大学物理学学士和计算机科学硕士。2016年加入百度,曾负责和参与百度搜索Web极速浏览框架、MIP开源项目的研发,目前负责搜索结果页和搜索推荐业务。LiquidJS 的作者,贡献于San、Realworld Apps、hightlight.js、ALE、HTML5 Standard等项目。

导读:前端发展飞速,从最开始的静态页面到 JavaScript,再从 PC 端到移动端,随着大前端的复杂度不断提升,很多公司开始前后端分离,剥离出前、后端架构设计。那我们来看看,前端架构设计是什么?曾经非常简单的前端架构发展到现在有哪些问题,遇到前端代码体量巨大、跨团队协作效率、代码耦合、技术栈落后等问题又该怎么解决?

一、什么是前端架构?

前端架构这一词,相信很多人的定义都不太一样;按照拆词的解释来看,我理解为“前端”+“架构”。前端是指,Web 端的前台页面,包括网页的内容、样式、脚本等,这三者通常封装在组件中,可能是模板引擎的文件模块,也可能是 MVVM 框架里的组件。“架构”就更好理解了,架构一词来自建筑行业,可以理解是房屋的整体结构、框架。结合前端和架构的概念,“前端架构”可以理解为,Web 页面组件的抽象和组织方式。

又因为各个公司的业务不同,每个公司的前端架构发展都不一样,这里,我会拿百度移动端经典的搜索场景来给大家举例,希望从百度的移动端架构演进过程中,发现一些共性的问题。


二、百度移动端背景及问题

为什么是以百度来举例?是因为百度是国内搜索引擎的领头人,并且,目前一直处于行业领先状态。据 statcounter 前瞻产业研究院在 2019 年中国搜索引擎行中可以知道,百度搜索占全世界搜索引擎市场份额12.3%,居第二位,仅次于谷歌。所以用百度来举例,更具有代表性。

言归正传,打开百度 App 你会发现,百度前端直接分为首页和搜索结果页,搜索结果页是搜索的主要入口,每天承载着十亿级流量。

不仅如此,搜索结果页承载着许多产品线的需求和下游模块的运行时,每年内部的研发人员会提供五百多个产品需求,为十几个下游模块提供基础库和运行时。甚至还有后端协同,从图 1 我们可以看出结果页的整体架构。

△图 1:百度搜索结果页的整体架构

针对整体的架构设计,有这些问题:

  • 细分业务线众多,单个库代码庞大;

  • 平均每月有 200+ 提交,3w+ 行代码;

  • 80+ 开发者在同一个代码库中开发;

  • 没有人能完全掌握模块整体技术。

于是,梳理出三个方面的问题:

1. 人员职责不清晰,单个模块同时承担了多个团队的职责

  • 框和 Tab:“全部”和垂类搜索共用;

  • 运营产品:渗透在结果页代码库里;

  • 其他:结果列表、用户反馈、搜索推荐、体验日志、速度日志、计费逻辑……

2. 代码耦合严重

  • 容易出错,代码逻辑脆弱;

  • 结构僵化,不易新增功能;

  • 依赖牢固,代码很难复用。

3. 技术栈落后

  • 页面没有组件化。没有 Vue、没有 React,还在用 Smarty 模板;

  • 无法支持 Node.js。Smarty 模板强依赖 PHP 环境;

  • 工具链落后。没有 TypeScript、没有 Jest。

这三个问题最终会影响到研发效率以及产品质量。那么百度又是怎么去具体做的呢?架构优化的目标只有两个,一是满足业务需求,二是技术上能对框架和工具灵活升级(也是为了持续的满足业务需求)。根据“满足业务需求”这一目标,百度内部是制定了三个层面的方向。(如图 2)

  • 底层基础层是贴近社区,因为据内部调研来看,造轮子的成本不高,但是维护这些轮子成本极高,如果想更快的迭代,还是建议贴近社区,去用些开源的事情或者去贡献开源。主要是解决技术栈落后以及职责不清晰等问题。

  • 中间层是独立模块,主要是应对之前提到的职责不清晰的问题以及交付效率低等问题。主要是解决职责不清晰以及交付效率低等问题。

  • 顶层就是组件化,在独立模块的基础上去做组件化,加速业务的迭代。

△图 2:业务需求的三个方向

三、怎么解决

根据这里提到的方向和目标,怎么结合百度自己的架构落地呢?首先,回顾下百度的架构,如下图 3 可以看到。

图 3:百度搜索结果页的整体架构

1. 这里有两块日志,意味着同一套代码要在两个部分维护;除了重复之外,它们的差异会对后续的维护引入更高的成本;

2. 底层这个 HHVM+PHP 和社区更加拥抱 Node.js 会有冲突。

所以,百度同学把目标架构调整为图 4 所示。

△图 4:结果页的目标架构

图 4 中可以看到:

  1. 把日志、搜索框、相关搜索、性能打点等独立成单独的模块,有专门的同学来独立维护和迭代;

  2. 在前后端之间加了一层渲染层;让业务代码和后端的逻辑分开;

  3. 在底层加了 Node.js 机制。

目标、方向都解决好之后,就得看如何实施。对于一个小体量的库来说,从零构建架构就行;但是对于百度来说,实施也是难点。不仅要考虑平滑迁移、性能不退化,还要考虑长期可维护性、安全性、跨平台等。

前文也提到了,基本思路是按照基础设施、模块拆分、组件化的步骤执行;基础设施是业务模块划分的关键,完善的自动化和工具链是模块化的前提;模块化拆分可以为业务和团队提供更好的横向扩展能力;模块化的基础上,可以进一步在模块内部建设组件化方案来加速业务迭代。

在基础设施需要关注的事情包括:

  • TypeScript:大型项目必备,提前发现问题;也是跨平台的基础;

  • 持续集成:确保每次变更新增功能和修复问题的同时,不引入新的问题;

  • 单元测试:在重构之初引入,帮助防退化和辅助设计。

模块化拆分需要关注的事情包括:

  • 识别和定义业务边界,把大一统的仓库分割成若干独立的小仓库;

  • 在子模块内建设自动化机制,独立地选型、开发、上线。

注意:

模块化拆分不是技术问题,而是业务问题。只有根据业务和产品进行垂直划分,才有可能达到解耦和独立迭代的目的。否则只是形式上拆分耦合的代码,会造成更大的维护和沟通成本。

由于组件是业务模块内部的选型,组件化的方案相对比较自由。只需要不严重影响性能,且能够平滑过渡即可。

四、落地方案

1. 模块化

具体的落地方案,我们也用一张图(图5)来表示。可以看到它分为服务端和浏览器端两部分。

  • 服务端关心的问题是业务模块的划分以及运行时的组合;

  • 浏览器端关心的问题是依赖的解决以及如何支持组件化方案。

△图 5:具体的落地方案

2. 服务端

百度是把整个大模块拆分成多个独立业务模块,最终页面由模块组合而成。这要求业务模块具有统一的接口,即上图所示的 Molecule 接口,它定义了模块如何渲染、有哪些依赖等信息。因为渲染过程封装在了模块内部,所以整个架构可以支持多语言、多框架。

相信你也发现,Molecule 和微服务非常相似。它们的关键区别在于,微服务的服务之间通过 IPC 互相操作,且每个服务可以独立伸缩、独立部署;而 Molecule 的各模块存在于同一个进程里。虽然有这样的区别,Molecule 仍然可以实现和微服务近乎相同的特性,如图 6 所示。

△图 6:Molecule 和微服务的比较

图 7 展示的是一个具体的业务模块的服务端入口文件,其中 ToptipController 是实现了由 Molecule 提供的控制器接口;这个接口要求提供一个渲染函数,接受一个字典类型的数据,返回渲染之后的页面内容。由调用方决定如何组装页面。

△图 7:具体的业务模块的服务端入口文件

如上是业务模块提供方的接口。此外 Molecule 机制还为调用方(组装最终页面的那一侧)提供了方便的接口,可以在需要引入子模块的地方,传入子模块名称和参数即可在运行时渲染出来。整个机制的原理很简单,但实际使用中可能还需要引入命名空间、考虑模块版本等问题。

3. 客户端

那么客户端如何运行起来呢?我们也需要把每个模块的浏览器端组件运行起来,困难在于组件之间的依赖和代码共享。这些组件可能位于不同的代码库并属于不同的业务,所以我们需要一个非常松散的依赖方式。

这里我们引入的是一个依赖注入的容器(图 8),总的来说,框架逻辑和通用工具都封装成具体的Service提供给业务模块使用,每个业务模块则需要定义它依赖于哪些Service。

△图 8:客户端设计

图 9 形象地描述了组件、Service 和容器间的关系。

△图 9:组件、Service 和容器之间的关系

其中蓝色代表具体的Service,其他颜色表示独立的业务模块。运行时容器会负责解决每个业务模块的依赖,并把这些业务模块组装起来,最终得到可交互的 Web 页面。

注意:

业务模块之间是独立的,一个业务模块无法依赖于其他业务模块,只能依赖于通用 Service。因此如果存在业务模块之间的产品逻辑耦合,可能需要一个通用 Service 作为媒介,比如容器里提供一个起事件总线作用的 EventService。

图10是业务模块的客户端代码示例。它的依赖通过构造函数来声明,运行时容器负责依赖的创建,而业务模块只需要关心依赖的使用。正是使用和创建操作的分离,使得业务模块之间、业务模块和页面框架之间可以解耦,可以独立地开发、独立地测试。

△图 10:业务模块的客户端代码示例

以上是模块拆分的整体方案,我们回顾一下:在服务端通过一个叫做 Molecule 的接口来组合业务模块;在浏览器端通过一个 DI 容器来解决依赖关系,并启动所有业务模块。

4. 组件化

组件化方案直接影响业务开发的的效率,换句话说,组件化方案某种程度上决定了业务同学写怎样的代码。组件化也可以帮助解决职责不清晰等问题。我们选的组件化方案是 San,你也可以基于你的业务或偏好选择 Vue 或者 React。业务代码的迁移比较直观,就是从 Smarty 模板迁移到 San 组件,从 HTML 字符串拼接变成有业务语义的组件结构。

接下来重点关注组件化方案的两个关键技术问题,跨平台和页面性能。

1)跨平台

我们有非常多的业务代码,有上千个模板、几十万行代码,这些代码需要迁移到组件化方案上来,而且要确保后端从 PHP 迁移到 Node.js 的整个过程中,业务代码不需要重新开发。所以业务组件如何跨平台呢?关键在于抽象。

  • 高层语言:我们业务代码需要使用一个足够高层的语言,这里我们用的是 TypeScript,可以翻译到多个平台;

  • 依赖反转:我们的高层的业务的模块不应该依赖于具体的底层模块,而是它只依赖于接口,这样才有可能在不同的平台给它替换掉不同的底层的实现;

  • 抽象接口:最后是 Molecule 这个接口的设计应该足够的简单;Molecule 接口不依赖底层实现,比如 PHP 的具体 API。

做到以上几点就可以完成平滑的过渡。这个过程中又分为三个阶段(图 11)。

△图 11:平台过渡的三个阶段

2)页面性能

引入前端框架通常意味着体积增加,性能下降,而性能直接影响搜索收入,因此页面性能是项目成功的关键。如果性能会比模板引擎的性能差,那么这个项目很可能会夭折。如何去保证页面性能?着重介绍两个优化点。

  • 引入 SSR:引入服务端渲染,首屏性能可以得到明显提升;

  • SSR 优化:传统的 SSR 上还需要进一步优化性能。

引入SSR。为了解释SSR的重要性,请看图12。浏览器加载页面分为四步:请求页面、请求外链资源、执行脚本、渲染组件。从图中的对比可以看出,CSR在前面三步的时候,用户都是看不到页面的;而引入SSR之后,在第二步用户就能看到请求回来的页面。SSR它最大的一个用途就是提升首屏时间。

△图 12:CSR和SSR的比较

SSR 优化。只是引入 SSR 还不能让性能达到预期,因为相比于模板引擎直接拼接字符串,SSR 需要递归渲染组件,尤其是递归 VNode 比较耗时。对此 San SSR 相比于 Vue/React SSR 做了很多改进。

  • 去 VNode:编译期递归 VNode,运行时只做 HTML 拼接;

  • 编译期计算:尽可能把工作移到编译期,减小运行时开销;

图 13 展示了最终的 San SSR 和改造前的 Smarty 模板引擎的性能对比。

△图 13:最终的 San SSR 和改造前的 Smarty 模板引擎的性能对比

可以看到 Smarty 和 San SSR 在不同的场景会有不同的表现,因为它们的渲染方式非常不同。最终搜索结果页的组件化的 SSR 上线之后,线上实验效果显示比 Smarty 要快 10ms左右。这个已经是一个很不错的效果了,我们用组件化从性能上打败了模版引擎。

五、结语

针对百度搜索引擎在架构演化中遇到的问题,相信在其他领域也会有一些共性的东西。通过百度的解决思路,希望能对正在做前端架构的你有一些启发。

往期推荐

欧创新:深度解析DDD中台和微服务设计

领域驱动专家张逸文字脱口秀:简单工厂不简单

DDD专家张逸:《解构领域驱动设计》前言

Hacker News热文:请停止学习框架,学习领域驱动设计(DDD)(获500个点赞)

京东平台研发朱志国:领域驱动设计(DDD)理论启示

DDD专家张逸:构建领域驱动设计知识体系

领域驱动设计(DDD)在美团点评业务系统的实践

当DDD遇上微服务

DDD战略篇:架构设计的响应力

可视化与领域驱动设计

领域驱动设计(DDD)前夜:面向对象思想

领域驱动设计(DDD):领域和子域

DDD专家张逸:复杂与架构演进的关系

滕云:DDD实现之路

   END
#技术人必备#

点个在看,让更多人看见

百度十亿级流量的搜索前端,是怎么做架构升级的?相关推荐

  1. 十亿级流量的搜索前端,是怎么做架构升级的?

  2. 被面试官问懵B了,十亿级数据ES搜索怎么优化?

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 面试题 es 在数据量很大的情况下(数十亿级别)如何提高查询效率啊 ...

  3. 十亿级流量下,我与Redis时延小突刺的战斗史

    作者:vivo互联网服务器团队-Wang Shaodong 一.背景 某一日收到上游调用方的反馈,提供的某一个Dubbo接口,每天在固定的时间点被短时间熔断,抛出的异常信息为提供方dubbo线程池被耗 ...

  4. 亿级流量电商详情页系统实战-25.亿级流量商品详情页的多级缓存架构介绍

    1.前言 很多人以为,有了redis缓存,就可以支持对高并发的业务场景了.其实做复杂的缓存,如支撑电商复杂的场景下的高并发的缓存,遇到的问题是非常非常之多,绝对不是说简单的访问一下redis就可以了. ...

  5. 亿级流量搜索前端,是怎么做架构升级的?

    一.什么是前端架构? 前端架构这一词,相信很多人的定义都不太一样:按照拆词的解释来看,我理解为"前端"+"架构".前端是指,Web 端的前台页面,包括网页的内容 ...

  6. 当亿级流量App开始“玩”,到底在玩什么?

    文|螳螂财经(TanglangFin) 作者| 陈小江 爱玩.会玩,是企业的一种高级境界. 著名畅销书作家斯科特·麦克凯恩在其<一切行业都是娱乐业>一书中,根据体验经济市场的发展特征,总结 ...

  7. 最新亿级流量电商详情页系统的大型高并发与高可用缓存架构实战第一版附全套资料

    课程介绍(非升级版) 对于高并发的场景来说,比如电商类,o2o,门户,等等互联网类的项目,缓存技术是Java项目中最常见的一种应用技术.然而,行业里很多朋友对缓存技术的了解与掌握,仅仅停留在掌握red ...

  8. 亿级流量电商详情页系统的大型高并发与高可用缓存架构实战 目录

    对于高并发的场景来说,比如电商类,o2o,门户,等等互联网类的项目,缓存技术是Java项目中最常见的一种应用技术.然而,行业里很多朋友对缓存技术的了解与掌握,仅仅停留在掌握redis/memcache ...

  9. 单枪匹马想要搞定亿级流量?2021阿里都换成这个牛逼架构了

    你见过这样的淘宝吗? ▲淘宝第一个版本 马云神秘的项目--淘宝 2003年4月7日,马云在杭州成立了一个神秘的组织.他叫来十位员工,要他们签了一份协议,这份协议要求他们立刻离开阿里巴巴集团,去做一个神 ...

最新文章

  1. 深入Redis内部-Redis 源码讲解
  2. 由Effiproz DataBase来看.NET开源数据库发展
  3. PHP获取文件的绝对路径
  4. sql server紧急状态下登录脚本
  5. linux的基础知识——多进程并发服务器
  6. Java 集合体系详解——List体系有序集合
  7. 如何解决Win11系统蓝牙鼠标经常掉线
  8. 组策略里更改更新和设置客户端首页
  9. 1,2,2,3,3,4,4,4,......
  10. Unity游戏接入TypeSDK集成笔记
  11. 网站优化JS css压缩
  12. Shell函数和正则表达式
  13. Excel 快速填充序号
  14. bzoj 4816 数字表格 —— 反演
  15. java实现 蓝桥杯 算法提高 盾神与条状项链
  16. 某大型软件公司售前软件工程师面试题附答案
  17. 人人网冷落主业,押注智能硬件
  18. Markdown 插入图片的方法
  19. excel更改页眉页脚_带格式日期的Excel页脚
  20. ESP8266-12F最小系统板程序烧录

热门文章

  1. 好玩gan_效果超赞服务器挤爆!用GAN生成人像油画火了,带你一秒回到文艺复兴...
  2. gateway请求拦截_spring cloud gateway 拦截request Body
  3. Android横向滚动卡片,Android实现横向滑动卡片效果
  4. mysql导入的excel更新_excel导入数据库,存在则更新不存在添加
  5. 扫地机器人哪个品牌好?五款口碑最好的扫地机器人
  6. 判断玫瑰花数c语言程序设计教程课后答案,编写程序判断某个四位整数是否为玫瑰花数(用自定义函数)...
  7. 什么是堆栈的构造方法?向上生长堆栈,向下生长堆栈
  8. 数据结构之查找算法:B树
  9. 【C语言笔记初级篇】第五章:操作符
  10. (计算机组成原理)第五章中央处理器-第三节1:CPU内部单总线数据通路中数据的流动