tmallfe / tmallfe.github.io

天猫前端
http://tmallfe.github.io
3.93k stars 508 forks source link

天猫前端基础技术体系MAP简介 #38

Open maisui99 opened 8 years ago

maisui99 commented 8 years ago

前端基础技术体系是一个非常宽泛的概念,涉及到非常多的点,前端这个行业本身易入门难精通的一部分原因也是每一次的技术深入都需要技术广度上有提升,这些广度以前覆盖了HTTP、其他后端语言、操作系统、印刷设计等,现在由于移动设备的兴起,广度要求的点做了更多的扩充,移动设备多样化、国际化、Native技术等等。列举这些不是想说明前端有多复杂,而是技术体系本身就不是一个独立的存在,需要结合更多其他领域才能有更好的发展,其实其他技术的发展也是类似。

历史

在我2011年刚加入口碑前端团队的时候,三七一直在强调,我们现在的前端团队是国内最棒的。得益于当时YUI成熟的体系,我们在业务中落地了在当时非常“潮”的技术。比如本地的模块构建和发布工具att、CDN的combo服务,YUI的模块版本化管理机制(KISSY模块规范的原型),基于Base/Attribute/Plugin/Widget的模块生命周期管理等等。而使用YUI3开发web应用这篇文章里对web模块开发的思考在现在依然不会过时。

YUI和jQuery的最大区别就是体系和库的区别,对于庞大的淘系业务,只有体系化的技术方案才能保证研发效率和工作流的稳定,保证线上质量和用户体验。YUI体系在传统库的基础上,涵盖了YQL(浏览器端类sql实现)、YETI(浏览器端自动测试)、YUI-Target-Environments等等。

当时前端基础体系强调的核心是兼容性和性能,我们需要大量的复杂庞大的库来保证一套代码能够多浏览器运行,而低效的浏览器执行能力和缓慢的网速也让前端展现部分称为用户访问速度的瓶颈。于是有了:

  1. CDN解决图片在线优化、同域名连接数、Combo及多地域资源加载问题
  2. 前端发布工具雏形,自动sprite、base64、文件合并
  3. css/js分开管理到css和js之间开始存在明确的依赖关系
  4. ajax解决刷新页面重新请求所有资源缓慢的问题(虽然有缓存)

    MAP 1.0

2012年,天猫启动了MAP项目(Tmall Front-end Architechture & Publish Mechanism)。

当时团队面临了一些大部分前端团队都困扰的难题,这部分阶段称为MAP 1.0。

  1. 团队有一定规模,但是开发规范、工具、流程不一致,团队之间缺少沟通。
  2. 原始的发布机制和开发工具,覆盖式发布。
  3. 多套模块库TML、MUI,KISSY版本繁杂,代码共用少。
  4. 前端工作流不明确不一致,效率低,前端在业务中非常被动。

    MAP 2.0 - 扁平化、细粒度

基于上述MAP1.0时代的问题,MAP2.0做了针对性的调整:

  1. CDN静态文件路径语义化和非覆盖版本化管理,解决了前后端发布的顺序问题
  2. Base(KISSY 1.3 + MUI 2.0) + App(业务代码) 扁平化双层结构
  3. 前后端一致的静态文件引用方式feloader.use,覆盖java端应用
  4. git+本地开发工具,统一的工作流,代码可管理且高效

    MAP 3.0 - 扁平化、细粒度、跨平台

因为智能终端设备的普及,原来的前端目标环境从多浏览器变成了多终端,整个I/O交互的变化对前端体系提出了多端构建的要求。

  1. 多终端带来的1套数据多套view,对前后端分离的要求更加严格
  2. native和web界限模糊,前端模块需要覆盖Native App
  3. 无线环境对页面性能和体验要求更高

跨终端的MAP 3.0计划启动,为了保证一致的模块体系和模块快速开发和落地,跨终端组件的技术方案基础还是KISSY 1.4(KISSY 1.3升级1.4),大批组件开始支持移动端,各类频道活动也开始全面覆盖移动端。

同时,基于Node渲染服务在经历2014双11的考验后,开始全面走向业务,前端的模块渲染不满足于当前简单的异步渲染,毕竟同步输出渲染结果始终是最快的方式,于是我们对前端模块的规范进行了扩充:前端模块应该是包含js+css+模板+schema

关于Node服务的介绍可以参考文章:天猫双11前端分享系列(四):大规模 Node.js 应用

其中,模板可以在发布的时候编译成模板文件和一个模板脚本,分别支持在服务端渲染和浏览器端异步渲染。渲染所依赖的数据格式根据schema文件来约束。

还有一些其他的变更包括:

  1. 本地发开工具gulp化,业务有更强的定制能力,实现弱中心和灵活的开发环境
  2. 从if到schema,端到端数据接口规范形成,模块通过数据实现业务化

还有,ReactNativ在2015双11落地,实现了同个模块多端构建的能力。前端模块的规范变成:js+css+模板+native模板+schema。独立native模板的主要原因还是目前没有好的解决方案来实现普通web模板到native模板的编译,或者换个顺序也不行,所以还是独立维护一份Native的版本。

模块的规范基本成形,现在我们的模块开发目录大概是这样的:

- build
- src
--- index-pc.js
--- index.js
--- index-native.js
--- index-pc.less
--- index.less
--- schema.json
--- seed.json

关于ReactNative的详细可以参考:天猫双11前端分享系列(三):浅谈 React Native与双11

同时Native组件在web中调用也是正在尝试的方向,将web中一些非常复杂容易有性能问题的模块直接替换成Native,可以实现页面切换时部分组件不刷新等功能,提升用户体验。

MAP 4.0(现在)

MAP 3.0对于2015年来说是相对比较完善的方案了,但是暴露了大量的问题。

比如文章2015天猫双11前端分享系列(一):活动页面的性能优化里的描述,统一基于KISSY 1.4的模块在无线端已经成为性能瓶颈,引用文章里的一句话:脚本体积过大,目前基础脚本文件大小在100k上,占了我们规范标准的一半以上体积

当时而言,最简单的实现方案就是无线的模块独立一套基础库,比如使用zepto或者kissy mini,而pc的模块继续沿用KISSY。但是,一方面KISSY本身相对社区解决方案已经不够新鲜,pc/无线不一致的技术体系对开发效率来说也是一个负担,更会造成大量的技术方案重复实现。

拥抱社区

React还是原生

团队的第一反应是React,一方面是社区的成熟,另一方面ReactNative实践下来确实能满足业务需求,如果基于React构建模块,然后编译结合ReactNative,解决模块在Native下运行的方案,看起来非常美好。也算是真正实现了一次开发,多端运行的能力。

但是问题依然也比较明显:

  1. React本身无法解决所有问题,渲染之外依然需要大量其他工具lib支持,这些lib无法做到多端运行,或者说即使可以,也是一个非常私有的实现,后续更新也是问题。
  2. React本身的脚本还是太大,对于简单的无线页面依然是一个负担
  3. React本身有服务端解决方案,可以实现同步加载,但是同步浏览器、客户端的State本身复杂度上较高,无法工程化解决的话,长期来说也是一个坑。

审视React的过程中,最大的收获其实是Babel。通过gulp-babel + babel-polyfill的方案,我们可以在本地开发最新ES标准的代码,并在IE8及以上都能顺利运行(虽然有一些坑)。刚好2015双11之后,天猫已经彻底抛弃IE6/7,于是babel的引入就成为最先敲定的方案。

同时,基于commonjs规范进行本地开发也是一个共识,基于有编译过程,本地开发完全可以和Node一致采用commonjs规范,至于最后编译成amd、cmd或者是其他浏览器模块运行规范可以由构建工具决定。

接下来,引入Babel之后,前端是不是可以完全基于规范开发,不要再引入各种框架/库什么的?

然而,基本不可能,没有公共库就没有复用,就会有大量重复代码,而社区有大量的lib库,显然比内部写的公共库更加完善。参与社区模块的建设显然比闷声造轮子更有意义。而React本身也可以作为模块库中的一个模块存在,复杂业务可以使用React,简单业务也可以基于原生开发。

于是最后,我们确定:

问题并没有结束,引入社区lib是不是意味着要引入类似webpack的打包机制?KISSY 6已经拥抱npm,这也是社区模块的最佳实践。下面是一些对比。

npm的优势对比:

npm cdn combo
模块内聚性高,对外API统一 模块被调用方式不可控
执行期无seed,降低运行期复杂度 seed体积庞大,也是性能上的负担

cdn combo的优势对比:

npm cdn combo
无法按需加载,页面公共部分无法动态更新 支持按需加载、多端加载
组件间存在重复代码 颗粒化拼装,少重复代码,可跨页面缓存

combo还是打包本质的区别是 动态打包 还是 静态打包;combo其实也是一种打包,只是是一种url显性表达合并配置的打包方案。

从模块颗粒化及页面性能考虑,cdn combo还是主要的方案,毕竟对于天猫的活动、频道页面来说,保证首屏体验是性能优化非常重要的一部分,只有基于combo才能动态按需地实现首屏同步加载,非首屏动态异步加载。npm方式也能做但是对于页面级需要有一个构建过程,而cdn combo只需要模块构建,页面只是一个模块的组合而不需要构建。

基于CDN combo,未来可以做更多的优化:

  1. 最大化公用缓存,尤其是在mobile可以控制容器的场景如zcache(可缓存的容量是有限制的如果没限制打包更简单)
  2. 未来基于数据分析动态化精细化跨页面资源加载(例如希望针对部分地区首先加载a,b 对另一部分地区首先加载 abc,打包就必须针对每种case打出不同内容)

同时,基于npm打包的优势也需要考虑引入到体系中。 比如一个组件内不需要暴露外面的util.js 一定是和组件其他文件被外界使用,此时被combo反而带来的合并及读取的开销,此时更适合打包对外统一暴露一个文件出口。所以有复用价值的combo才合适

综合下来其实是combo和打包配合的方案,combo用来完成有复用价值部分的管理,打包用来完成无复用价值的内聚。

而seed体积精简可以通过服务端的动态seed合并及去掉已同步加载模块的seed来实现,保证页面上seed的内容一定是会使用到的。

由于开发期引入了babel,调试时就不能简单绑定host到本地了,需要引入source map,但是sourcemap对combo后的url无法解析,所以需要在机制上实现调试时解combo,客户端和服务端都支持一下就可以了,具体实现可以见后面加载机制里的设计。

多端加载能力

如上面描述,确认了前端工作流的大部分细节:

  1. 模块使用CDN combo实现按需加载
  2. ES + commonjs的开发规范
  3. 基于原生规范的颗粒化组件,简单lib如zepto解决简单业务,react组件解决复杂业务数据解耦问题

接下来就是多端加载的解决了。

第一步就是需要一个Loader,这里需要把Native组件一起考虑进来,于是我们提供了这样的Loader加载策略,feloader是目前加载器的名字,wormhole是服务端模板渲染服务。

无论是开发Native还是Web,都采用一样的方式生成配置文件,基于配置文件,模块之间的调用关系就可以明确,然后服务端可以根据依赖表输出combo uri及直接对Native模块进行组合。

浏览器端目前由于无法直接运行commonjs规范的脚本,解决方案也非常简单,在构建工具打包的时候,包上一层define(modName, deps, function(require, exports, module))即可。

Node渲染服务也需要实现基于模块配置的依赖分析,生成combo uri,如果是native模块,则直接分析完依赖后,将模块内容拼接成一个大的脚本返回。

MAP 4.0体系下的前端开发

疑问

1)从KISSY迁移到目前的开发模式,改动很大,以后还会不会有这样的大范围升级?

答案是,升级肯定会有,保持依赖模块和使用技术的新鲜才能提升业务效率,毕竟用户环境、外部社区也是在不断升级。但是不会有大范围的升级:一方面我们采用基于浏览器标准规范的方式开发,这一部分属于相对比较稳定,变化不会对研发造成负面影响,另一方面模块本身颗粒化,升级一样也是颗粒化,快速迭代的方式,而不是所有模块一起选一个特定时间集中式改造,减少对业务的影响,让升级融入到业务开发中。

2)基础体系变化怎么大,如何解决兼容问题?

兼容问题确实比较大,首先Node容器和本地开发工具都需要向前兼容,这个是必须的。

而对于前端开发的模块,我们提供了一个kissy-polyfill方案让AMD模块在KISSY页面上运行,这样可以让大家快速开始基于新的方式开发模块,然后这个模块可以在老页面上运行。

具体实现也很简单

其他就是一些加载器细节的patch,比如已经加载模块的配置可以重写什么的。

3)目前MUI体系引入了zepto作为基础DOM/event的解决方案,如何支持IE8/9?是不是依然需要无线PC两套基础库?

目前我们是这么解决的:

  1. 开发时统一使用zepto
  2. feloader中有这么一段处理:ie8/9下将zepto alias到jquery,这里会有一些细微的差别,但是目前看这个模式是可行且确实对开发者来说基本无感知

    结尾

很多人其实觉得前端基础技术体系已经很难有突破,但是虽然MAP升级了那么多版本,ES规范也在不停升级,但是很多背后的思想还是一致,只是在原来的基础上不断优化细节,然后提高扩展性。这也是为什么前端一直在强调基础的重要性,CSS2,ES3依然是基础的重要组成部分,工具框架只是外壳,理解为什么这么设计远重要于知道如何使用。

daemonchen commented 8 years ago

mark

wssgcg1213 commented 8 years ago

好潮,源于社区,反哺社区

对于 cdn combo,模块的更新机制是如何实现的,比如有一个uri: ??a-1.0,b-2.0,c-1.1

然后过了几天,a模块升级到1.1版本了,这个时候这个combo uri是不是就变成另一个了,这样是不是就无法利用到浏览器缓存,那不是跟npm打包没什么区别了吗,这里的细节是如何做的?

maisui99 commented 8 years ago

@wssgcg1213 本质上combo和打包都是一种合并策略。缓存是相对于模块级别,比如

X页面??a-1.0和??b-2.0,c-1.1 Y页面??a-1.0和??c-2.0,d-1.1

这样X和Y之前可以共用缓存a。

而且由于url上显式声明了模块,可以在app里解开combo,单独缓存文件,这样更新其中部分文件也不会影响其他同个url模块的缓存。

combo 对于打包适用场景更广泛一些。

cuitianze commented 8 years ago

意思是贵猫把ReactNative全迁移成Weex么? 鬼道之前还想全民ReactNative的,我由此入坑,没想到你们不用了,友谊的小船说翻就翻。

maisui99 commented 8 years ago

@cuitianze 怎么说呢,原因很多哈,Weex目前集团支持力度更大,所以全部迁移过去了。。ReactNative也是非常好的方案哈。。鬼道也专职做weex项目了。

acrazing commented 7 years ago

@maisui99 解开之后分开发两个请求??a-1.1, ??b-1.0吗? 这样之前一次combo过的请求??a-1.0,b-1.1就不生效了吧, 相当于还是走了两次请求?

@wssgcg1213 我觉得combo最大的作用不在缓存, 而是在跨模块开发的同步更新上, 比如一个项目有两个入口模块e, f都依赖于另一个模块a, 如果a更新了, 提前打包方案得把e, f, a都重新打一遍, 这样在小应用里面没啥事, 但是项目太大的话就麻烦了.

maisui99 commented 7 years ago

@acrazing 实际上,在天猫/淘宝手机客户端里,我们的文件缓存策略是支持拆开combo进行缓存的。

而在HTTP2以后,直接把combo拆开即可,打包本身也没什么优势了。

ibufu commented 7 years ago

@maisui99 关于模块加载,有没有考虑过SystemJS呢?

maisui99 commented 7 years ago

@ibufu systemJs只是规范的一种实现,目前我们已经维护了一个loader的情况下,后续还是会更倾向于基于自己的loader实现规范。

Froguard commented 7 years ago

很多人其实觉得前端基础技术体系已经很难有突破,但是虽然MAP升级了那么多版本,ES规范也在不停升级,但是很多背后的思想还是一致,只是在原来的基础上不断优化细节,然后提高扩展性。这也是为什么前端一直在强调基础的重要性,CSS2,ES3依然是基础的重要组成部分,工具框架只是外壳,理解为什么这么设计远重要于知道如何使用。

大赞!:tada::tada::tada: