1.5 前端架构师的职责
根据前面对前端工程化的定义,前端架构师的职责可以简单概括为两方面:
● 根据业务特征设计合理的前端技术架构。
● 根据架构特征搭建高效的前端工程服务体系。
1.5.1 技术架构
无外界条件干预的孤立系统会自发地朝着热力学平衡——最大熵状态——演化,这是经典的热力学第二定律。熵(Entropy)是热力学[9]中的一个概念[10],后被引申到化学、统计学甚至哲学等学科。目前一种较普遍的引申意义来自统计学中对熵的定义[11]:熵用于衡量系统的混乱和无序程度,熵增(正熵)代表系统趋向无序,熵减(负熵)代表系统趋向有序。将熵的定义引申到软件架构中同样适用。架构难以满足日益增长的功能需求,如果任其野蛮生长,碎片化会越来越严重、越来越无序,最终达到一个难以继续维护和扩展的阈值,便会得到一种极端的结果:彻底重构。架构师的职责便是通过及时、有效的干预[12],令软件架构产生负熵,以保持系统的有序。
“有序”和“无序”的意义均是以人类世界为参考系的,比如我们认为云应该飘在天上、水应该往低处流、生物应该生存。而这些均不是热力学所描述的“孤立系统”,之所以“有序”是受到了外界作用力的影响,漂浮的云是因为有空气浮力,水往低处流是因为受到重力的牵引,生物能够生存是因为新陈代谢。与外界进行交互产生负熵才能实现系统的“有序”,然而这种“有序”并非永久性的,熵仍然缓慢地增加,生物终究会死亡。电影《无姓之人》描述了一种极致的熵减世界:人类无法自然死亡。人体产生的正熵与通过外力(基因改造技术)产生的负熵达到平衡,保持绝对的“有序”。
人体的内脏器官均有不同的分工,比如肺属于呼吸系统、胃属于消化系统、肝属于排毒系统等。各个器官完成各自分属功能的同时组合为一个完整的有机整体。如果将软件系统类比为人体,那么系统的各个模块则对应人体不同的器官。结合前面所述,不论是宏观范畴的Web应用分层架构,还是微观范畴的前后端分离,基本的原则是一致的,即通过合理地解耦各个组件/层级的功能提高系统的高效性和灵活性,所有组件/层级在完成各自功能的同时组合为完整的软件系统。从中可以总结出软件架构的两个基本要素:分治和聚合。
分,即将问题化整为零,各个击破;合,即将各个模块化零为整,融会贯通。这种理念不仅仅是软件架构的根本,也同样适用于其他工作领域。具体到实际开发工作,在进行软件架构设计之前往往需要充分的准备工作,比如对编程语言的选择、对技术规范的制定以及根据业务类型进行合理的技术选型等。
编程语言
不论何种业务类型和架构模式,前端项目始终无法脱离HTML、CSS、JavaScript这三种核心技术。一些特殊的业务类型可能会涉及其他编程语言,比如使用WebGL的复杂图形应用会涉及GLSL[13],计算密集型项目可以大胆地使用最近推出的WebAssembly等。虽然编程语言的选择相对单一,但任何一个都不是“等闲之辈”:
● 从严格意义上来说,HTML和CSS是标记类语言,并不能称为编程语言。两者原始的语法非常简单且缺乏可编程性,所以,通常前端开发者并不会直接编写HTML和CSS,而是借助一些工具和框架,目的是令源代码具有可编程性,以便于维护和迭代。前端架构师的工作之一便是选择适用于业务类型、同时可提高开发和维护效率的工具和框架,并且制定相应的开发规范。
● JavaScript是一种非常灵活的编程语言,这是一种优势,但同时也给大型项目和复杂架构带来了一定的隐患,所以在很多情况下需要通过框架或规范去限制JavaScript的灵活性,比如弱类型机制。JavaScript的一个核心问题是异步编程,如何避免回调地狱(Callback Hell)令源代码更利于迭代和维护并且同时兼顾性能,这些均属于前端架构师的工作范畴。
简言之,前端架构师在编程语言方面的工作并非集中于语言本身,而是在充分了解语言特性的前提下制定适用于业务类型的开发规范和技术栈。
技术规范
成熟的技术团队通常会设立代码审查(Code Review)制度,这样一方面可以发现代码逻辑的缺陷以及算法对性能的不良影响;另一方面是为了纠正开发者不规范的编码方式。编码规范并不是衡量开发者技术能力的主要因素,也并不能直接影响产品的功能和性能,但几乎所有开发者都不喜欢命名不规范[14]、没有注释和文档的代码。
除了编码规范以外,项目源码的组织结构、依赖管理以及第三方技术选型同样是技术规范的一部分。技术规范的优劣并没有绝对的评判标准,其唯一的原则是一致性。统一的技术规范能够显著提高团队协作和项目迭代的效率,这种优势随着团队规模和项目量级的增长被逐步放大。
组件化
组件化是代码复用的一种经典实施模式。前端技术发展至今,对于前端组件的定义不仅停留在UI层面,而且融入了一些面向对象的理念,比如封装性、扩展性、可组合性、可复用性等。按照与产品逻辑耦合程度的强弱,前端组件又可分为基础组件和业务组件。前端组件需要建立在既定的设计规范之上,比如Google的Material Design、阿里的Ant Design等。除此之外,前端组件的具体形态必须适用于所采用的技术架构,比如在同构编程场景下组件需要同时兼顾浏览器环境和Node.js环境。
前后端分离
前后端分离的宗旨是将前端开发与后端开发解耦,进而实现开发、维护、部署甚至发布的相对独立性,提高开发效率和快速响应问题。目前较普遍的前后端分离方案分为两类:SPA架构和Node.js渲染层。SPA架构是一种极端的分离方案,其优势在于成本低、可离线等。但是由于其抛弃了服务器渲染,所以不利于传统的SEO,适用于无SEO需求的应用类型,比如Hybrid应用[15]。Node.js渲染层的优势在于对SEO友好、首屏速度快,并且提供了同构编程的可行性。但是相对于SPA架构来说,后者实施成本略高。
性能
性能是评估应用程序高可用性最重要的指标之一,对于用户群体广泛、设备多样、场景复杂的互联网产品来说,性能也是抢占市场的核心竞争力之一。业内有许多性能影响产品市场的案例,没有人喜欢操作卡顿、响应缓慢的应用程序,几乎所有人都会选择性能更优的同类产品。
对于用户来说,Web网站的性能通常表现为首屏加载时间、操作响应速度等。从技术角度来讲,Web应用整体架构的任何环节(包括软件和硬件)都能影响网站的性能,比如服务器分布式架构、负载均衡、数据缓存层等。作为应用最上层的前端来说,性能优化的具体措施更多的是从软件层面出发的,可以归结为两类:加载性能和执行性能。提升加载性能的主要目标是尽可能快地将网站呈现给用户;提升执行性能的主要目标是快速响应用户的操作。然而需要谨记的是:快并非性能优化的唯一指标,现实开发中往往需要在性能和功能之间进行权衡,切勿一味地追求性能而影响产品功能。前端架构师需要在深刻理解浏览器渲染原理、编程语言特性、HTTP等知识的前提下制定适用于前端并且与Web整体架构相契合的性能优化策略。
1.5.2 工程服务体系
成本控制是工程的核心关注点,对于需要依靠快速迭代来争夺市场的互联网产品来说,时间成本是最昂贵的成本之一。作为软件工程在前端范畴的具体实践,前端工程化最基本的原则是在保证产品功能的前提下尽可能地降低迭代所消耗的成本。具体到实际工作中,成本又可以细分为人力成本和沟通成本。前面所述技术架构层面的设计除了前后端分离涉及与后端工程师的协作以外,其他各方面均局限于前端范畴,只能力求在局部范围内实现熵减架构。而如果将视野扩展到Web应用整体,仅仅保证前端架构的熵减还远远不够。一个完整的迭代周期需要依次经历开发、测试、部署和发布环节,产品上线后需要及时跟踪和响应用户的反馈以及监控生产环境的稳定性。通常每个环节都有专门的团队或者人员负责,各团队的管理和工作保持一定的独立性有利于组织架构的调整和技术架构的演进。然而跨团队协作的沟通成本是非常昂贵的,降低成本不能只依靠人与人之间的书面和语言沟通,还需要使用技术手段建立合理的协作规范、工具和平台。所以前端工程服务体系的目标便很明确了:
● 降低开发本身所消耗的人力成本。
● 降低跨团队协作消耗的沟通成本。
开发
业务的需求、功能的量级,甚至不同的开发阶段都有可能影响协作开发的具体模式。根据协作模式的不同可以将开发工作分为个人独立开发、团队内协作开发和跨团队协作开发。这三种开发类型各自典型的成本消耗分别为:个人独立开发过程中由重复性体力劳动所消耗的人力成本;团队内多人协作开发过程中由历史代码交接、模块集成所消耗的人力和沟通成本;以及跨团队协作开发过程中由各团队技术规范差异和开发进度不同步所消耗的时间、人力和沟通成本。基于以上问题,前端工程服务体系针对开发阶段的目标为:
● 减少重复性体力劳动。
● 建立规范的代码版本管理规范。
● 辅助跨团队并行开发。
构建
在前端领域,构建是一个比较新的词汇,随着前端技术的演进,对于构建功能的需求和相关工具也在不断发展。最早一批前端构建工具(如YUI Compressor[16])的功能仅仅是对JavaScript和CSS文件进行压缩,以便提高网站的加载速度。随后LESS、SASS等CSS预编译语言兴起,在构建中加入了对CSS预编译的支持。时至今日,构建已经成为现代前端开发不可或缺的一部分,也是前端工程服务体系中最重要的环节之一。业内对前端构建的普遍需求除了压缩和CSS预编译之外,还包括对模块体系(ES6 Modules、AMD、CommonJS)的支持、ECMAScript规范转译、自动生成CSS Sprite以及特定开发框架(如React、Vue)编译等。由此可以归纳出前端构建所针对的几个方向,如下所述。
● 编程语言:构建针对编程语言的相关功能可以理解为编译(Compile),即将源代码转换为客户端可执行代码的过程。除了原生的JavaScript和CSS以外,CSS预编译、特定开发框架编译均属于此类。
● 性能优化:比如压缩混淆、自动生成CSS Sprite、动态模块按需加载等。
● 部署策略:比如给静态资源URL加入Hash指纹和CDN路径等。
● 开发效率:比如文档生成、动态构建等。
● 审查评估:比如规范审查、性能评估等。
测试
虽然前端技术发展迅速,但是测试前端应用仍然比较困难,一是由于GUI应用普遍难以测试;二是因为前端技术过于灵活。这两点也是前端测试的突破口。首先,通过制定统一的代码规范甚至编程范式对JavaScript编程的灵活性进行一定程度的限制,令JavaScript代码更容易测试。其次,得益于React、Vue等前端框架的流行,在Node.js环境中将HTML文档内容渲染为字符串,从而可以进行UI快照测试。最后,在端到端测试和集成测试阶段需要尽可能地消除测试环境和生产环境的差异,避免无效的测试样例。
部署
前端部署的资源主要是JavaScript、CSS、图片等静态文件,而在“大前端”架构下会涉及Node.js服务代码。不论何种资源,部署的目的简单讲就是把代码“放”到指定的服务器上,从这个角度理解,所有类型的资源对于部署来说都是等同的。所以部署最核心的地方并不是对不同类型资源的处理,而是对流程的控制。这项原则对于任何职能部门来说都是适用的。此外,作为应用发布前的重要环节,部署需要做到稳定和精准。然而对于不同规模的团队来讲,有时候不得不在两者之间做一些取舍。
持续化
开发、构建、测试和部署组成了一个完整的迭代流程,工程化的第一步是合理地使用工具以提高各个环节独立的工作效率;第二步是搭建自动化流程来提高跨团队协作开发的效率,降低迭代整体所消耗的时间成本;最终的目的是持续化。持续化是一个宏观话题,作为狭义范畴的前端工程服务体系,对于持续化的支持分为两方面:
● 前端范畴内的持续化。
● 作为Web应用整体持续化体系的一个子集。
持续化是一个非常庞大的技术体系,近些年业内与此相关的讨论和研究从未停止。本书并不会深入持续化的各个技术要点,作者也并非持续化领域的技术专家,所以本书仅以作者在自身工作经历中获取的经验和心得为基础,聚焦于前端范畴内的持续化实施。
监控与统计
很多前端团队在制定工程化方案时并没有将监控与统计作为必需的环节考虑,一方面是由于大部分前端涉及的统计数据是与产品相关(比如PV、UV)的,并非是技术团队主要关注的数据;另一方面则是考虑到部署前端监控和统计系统的性价比问题。
通常,Web应用与技术相关的指标包括性能表现和稳定性。对于前端来说,很多性能问题可以借助工具在测试甚至开发阶段暴露出来,但这并不意味着在生产环境下统计性能数据缺乏必要性,真实的用户数据是模拟不出来的。另外,很多人对于前端存在这样的误解:在集成测试阶段基本可以覆盖绝大部分的交互逻辑甚至边界逻辑,线上即使出现问题也是极其边缘的,只会影响很小一部分用户,并且只要不涉及后台数据,Web网站出点小问题“打个补丁”就好了。之所以有这种误解是由于混淆了前端稳定性监控的关注点。
对前端稳定性的监控一方面针对的是前端本身的交互逻辑,但是这仅是非常小的一部分,其更多针对的是数据接口,也就是服务端的稳定性。数据接口提供给前端调用,并不会直接面向用户,所以一旦出现bug,用户的第一反应是这是前端的问题。前端团队接收到用户的反馈之后经过调试发现是服务端的问题,进而再反馈至相关负责团队。这是一个非常漫长的过程,并且会消耗大量的沟通成本。而如果可以在前端调用接口没有返回预期结果时立即反馈至监控平台并且报告错误信息,开发团队便可以在第一时间定位到问题的症结,从而缩短修复问题的时间,从而间接地提升了产品的竞争力。除此之外,监控和统计也是持续化工程体系不可或缺的一部分。