作者
王令宇(天镶)
出品
阿里巴巴新零售淘系技术部
导读:作者从17年双十一前开始接手天猫搜索前端,开发第一个需求——H5凑单页,到今天已经将近两年了。在这两年里,天猫搜索的前端体系发生了比较大的变化。今天分享一篇阶段性的总结文章,记录天猫搜索前端技术的过去、现在,以及自己作为业务目前的唯一前端对未来的思考。
大体划分
首先基于前端技术的演进,大体上可以将天猫搜索前端的发展历程和未来趋势总结成几个时代:
PC时代
H5时代
MV*时代
Weex时代
搭建时代
深度搭建时代
智能时代
之所以这么划分,主要是基于天猫搜索的前端技术方向以及天猫乃至淘系前端技术体系发生的较大变化。而在这其中可以再提炼时代的关键词:
封闭:PC时代、H5时代、MV*时代
开放:Weex时代、搭建时代、深度搭建时代
智能:智能时代
下面就来介绍各个时代天猫前端的技术状态和一些思考。
1
PC时代
PC时代可以说是天猫搜索前端的上古时代,那还是手机流量非常贵的3g/2g时代。因此大多还是简单的WAP页面,大多数人都还是习惯用PC进行购物。
?技术方案
模块化PC时代的前端技术方案采用的是KISSY+MUI3,MUI3就是那套KISSY的模块规范KMD。页面上还有一些非常老旧的YUI依赖。在那个jQuery王霸天下的时代,基本上天猫的PC页面都是采用集团内部根据YUI自研的大而全的KISSY框架。KISSY框架包含了前端所需要的几乎所有基础功能:模块加载器、DOM操作、事件处理、异步请求等等。
页面渲染PC天猫搜索采用了同步渲染的方式,页面的主要内容通过VM模板渲染输出到前端,因此前端需要维护大量的Velocity模板代码以保证HTML中的内容和自己JS代码能够配合得当。需求修改一旦涉及到HTML,就需要改造Velocity模板从而陷入aone发布的同步模板和部署炼狱之中。
模块管理而PC时代的天猫搜索前端页面的模块管理方式十分粗暴,基于业务逻辑划分的大量KISSY模块,他们共同操作一个统一的页面DOM,交叉修改时有发生。模块间的通信也是直接调用模块实例方法来实现,模块耦合非常严重。
?小结由此可以看到那个时代最主要的问题有如下几个:
大而全的框架使得页面笨重、依赖繁杂
前端花费大量成本去维护不熟悉的Velocity代码
基于应用环境,经常性需要同步模板或部署,应用不稳定时甚至无法调试
页面内模块化粗暴,模块耦合严重
重DOM操作,DOM管理混乱
2
H5时代
随着智能手机的普及,4g技术的发展使得流量费用的大幅下降,无线端需求不断增多,流量不断增大,天猫前端也将方向定为mobilefirst。显然PC搜索这一套技术方案无法满足H5搜索需求,因此H5搜索采用了全新的技术方案。
?技术方案模块化
H5搜索采用Zepto+MUI4作为模块化方案,MUI4相对MUI3主要变化是将KMD规范修改成了业内较为通用的AMD规范。同时引入了前端模板来实现DOM的更新,首先Zepto要比大而全的KISSY轻量许多,同时前端模板的引入使得模块自我管理了DOM,有效的降低了模块之间DOM操作交叉引发的混乱。
页面渲染H5搜索的页面渲染基于应用控制,但仅仅将框架、筛选等一些少量DOM同步渲染,大多数DOM如商品列表都是采用异步渲染前端模板的方式实现。因此同步模板中的内容量相对较小,维护成本大幅度降低。同时引入了前后端分离的wormholeapp,前端不再需要维护不熟悉的VM模板,而是维护和异步模板语法相同的xtpl模板,进一步降低了模板维护的成本。
模块管理与PC连翻页都是页面跳转的方式不同,如筛选、翻页等行为的渲染需要前端异步处理,模块通信的需求也相对复杂。H5搜索采用了MDV框架——通过将模块封装成一个个模型,再由MDV建立的模型之间的订阅机制进行模块的实例管理,以此来实现模块通信。
?小结H5搜索采用的技术解决了几个PC搜索时代的问题:
减小依赖,满足了H5的性能要求
基于wormholeapp的前后端分离开发,降低了模板维护成本
引入前端模板,降低了DOM交叉管理的混乱
引入MDV进一步使得模块解耦,规范了模块通信
但随着业务的发展,依然发现有一些问题:
虽然引入了前端模板,但所有更新和状态同步依然是手动的,依旧会频繁地DOM操作
模块划分还是基于逻辑划分,而非基于DOM,因此当一个逻辑模块需要操作多个DOM时,依然会引发交叉
虽然模块对外是个黑盒,但模块内部状态经常因为交互逻辑和业务逻辑繁琐导致混乱,经常出现更新了状态没更新DOM的情况
依赖应用环境,需要经常性同步模板和部署,毕竟wormholeapp也是应用
3
MV*时代
在allin无线的大环境下,由于搜索结构较为固定、无线端开发资源较为充裕等等各种原因,天猫搜索业务在端内完全被Native承接,而H5更加注重端外和站外。这也导致了H5搜索的业务能力和Native脱节,仅保留核心筛选功能以及唤端功能。因此H5搜索很长一段时间几乎没有前端资源投入,而在年8月左右由于组织架构调整,我手上的天猫商品业务交接后,我开始逐步接手天猫搜索前端业务。而我要做的第一个需求,就是一个包含H5搜索80%功能的H5搜索凑单页。
?技术方案模块化我和团队同学采用Preact+MUI5方案来实现,MUI5其实就是MUI4的升级,采用CommonJS的写法来编写模块,再经过构建工具编译封装生成AMD模块。其实当时会场已经大规模采用VueWeex了,那我们为什么要采用Preact呢,主要是基于如下几点考量:
团队内对于React技术栈有相当程度的了解
Rax的发展和普及以及Weex团队的一些调整,让我们认为Rax是未来Weex的主要DSL
Preact相对于其他框架更加轻量,而搜索的页面虽然有一定交互,但比较简单,刚好能满足需求
H5是未来大的趋势,当时兄弟团队在天猫超市的极致H5体验和沉淀给了我们充足的信心
因此在这个阶段,我们使用Preact做了很多的搜索周边的功能页面,如上面提到的H5凑单、还有猫搜的领券弹层、凑单领券页面、搜索分类页面等等。
页面渲染
由于Preact的异步渲染特性,页面渲染我们直接抛弃了应用,而是采用斑马源码页面的方式。这样的好处是,前端不再需要和aone频繁的打交道,服务端只需要封装一个mtop接口给前端,前端在斑马上创建基础HTML引入资源并调用接口获取数据渲染即可,前端自己完全掌控了调试、开发节奏。由于对于应用的依赖仅限于一个mtop接口,因此前端可以提前定好数据规范,在服务端开发接口的同时基于本地调试工具进行mock,大大降低了联调成本。
模块管理React生态有非常多的模块管理方案,我们调研过的Flux、Redux、Reflux等等都可以满足我们的需求。但经过仔细的思考我们选择直接撸,不使用任何框架来管理模块通信。如此考量的原因主要遵循如下理念:
首先我们需要规划页面的模块结构和通信结构
若通信结构与模块结构有较大的出入,且通信结构并非扁平,那么我们就需要引入模块通信框架来实现通信结构的扁平化
若通信结构与模块结构出入不大,且通信结构本身扁平,那么我们就没有必要引入通信框架
保持模块结构清晰、分离,面向未来,若未来业务发展逐渐繁琐,可以再引入框架来处理模块通信
?小结引入MV*框架后,我们的技术方案又得到了一些提升:
由于引入MV*框架,模块的划分不再基于逻辑而是基于DOM,DOM的操作不再需要手动处理,DOM交叉操作被彻底杜绝;
使用斑马源码页面,不再依赖应用环境,更方便的数据模拟和调试。不再需要维护模板;
模块内部状态基于MV*管理,状态不再混乱,保证了DOM和模块状态的一致性;
这里还是有一些问题:
H5总是面对性能的挑战
依然是源码开发,不够灵活,也无法共建
每来一个需求,就需要页面整体修改和发布,有类似行业定制,只能重新开发一个
借鉴天猫超市极致H5经验,搜索在分类页做了非常多性能优化尝试,比如:
1、代码优化
列表拆分cell进行懒加载
接入crossimage优化图片加载的cdn后缀
自建solution,去除通用solution中各种无用依赖
升级最新的loader来提升计算性能
2、DNS优化
dns-prefetch,提前处理dns
收敛图片和资源域名
3、接口优化
使用dlp进行mtop接口缓存
4、埋点优化
令箭埋点延迟到pageload后发布
采用非覆盖式的aplus脚本,利用浏览器缓存
采用post发送,触发sendBeacon
5、加载优化
增加serviceworker层缓存js、css和图片
使用zcache缓存页面模板、js和css
6、体感优化
使用ranger自动添加URL隐藏导航栏参数,防止页面加载后手动隐藏出现的抖动
4
Weex时代
17年双十一之后,搜索一成不变的模式也受到挑战,在这个拥有大量流量和强用户心智的场景下,搜索能够玩出更多场景化、行业化、品牌化的玩法。因此产品上需要更多快速试错的机会,也导致对于动态化的诉求则日益增加。而由于一些组织架构调整,天猫搜索的客户端缩减到2人——1个ios和1个安卓。客户端需求开发因为需要发版而迭代缓慢。因此催生了Native内嵌Weex坑位的技术方案和Weex模板下发的Oreo平台。
?技术方案模块化其实在我进入到搜索之前,Weex坑位方案和Oreo就已经在小范围使用了,有一部分客户端同学写的Weex1.0的Vue模块,另外还有一些Weapp模块。这些模块由于人员调整和离职已经很难找到仓库源码了。另外这些模块也是仅面向Weex的,对于H5来说是完全无法使用的。因此在我进入搜索之后,提出了Prax的解决方案,本质上就是使用Rax0.x(当时还是0.4)+Preact通过编译转换供Weex和Web同时使用。之所以要用Preact,主要是考虑到我们有端外H5场景,但是Rax当时H5性能是真的堪忧,而且当时的Weex团队方向对H5性能优化投入并不多。另外从上面MV*时代可以看出我们在Preact+MUI5的技术方案上也有较多的沉淀。最终我们确定了采用编写Preact代码,通过自动化工具转换成Rax代码的方式来实现一份源码同时复用在Weex和Web的技术方案。前端的开发流程就变成了如下:
编写包含基础可复用纯UI代码的Prax模块
对于Native内嵌Weex坑位,编写一个Rax模块,Rax模块引用Prax模块的Rax部分,并增加诸如埋点、容器联动等功能,并通过专门的模板发布系统发布上线以供坑位使用
对于Prax页面,编写一个斑马模块或页面,引用Prax模块。通过斑马平台发布上线,在端内使用Prax页面的Weex版本,在端外引用Prax页面的H5版本
页面渲染Prax页面采用斑马来处理页面渲染。由于源码页面都是采用打包的方式,因此脚手架中分别有Web和Rax两个打包webpack配置,构建时候分别运行生成WeexBundle和WebBundle。由于Web端本质上就是之前Preact的方式,因此渲染沿用了过去Preact的页面框架。而Rax则是开发了一个全新的页面框架。斑马渲染Weex页面本质上是通过xtpl控制WeexBundle代码生成,所以源码页面编译出来的Weexbundle只需要在外通过xtpl增加一层Weex的头尾就可以了。
对于Native内嵌坑位,则是采用模板下发的方式。前端会编译生成一个自身可运行的WeexBundle,包含头尾等完整信息,然后通过发布推送到搜索应用的机器上。当客户端的搜索请求到达服务端,服务端基于业务逻辑确定需要使用哪些模块,将模块相关的信息告诉客户端,这些信息包括模块名称、模块位置、模块数据。客户端渲染Native页面时根据模块位置创建对应容器,并根据模板名称请求Weex代码,最后将代码渲染到Weex容器中,并传入模块数据,实现模块最终渲染。而在Rax代码中,我们可以通过window.__weex_data__拿到客户端塞给容器的数据。当然这些适配都可以在构建层面处理,一般情况下开发者只需要