对当前前端技术栈的理解

最近被问到对一些对技术的理解,虽说很多时候自己感觉对技术选择有自己的理解,知道背后的原因,但还是不大能说的上来。这儿就总结一下对所经历的一些技术的理解吧。谈到理解,总免不了介绍技术出现的原因,解决的问题,可能的方案以及未来的可能的形态。比如,不少人认为前端玩的一些概念是在后端存在很久的东西,这个现象也确实存在。虽然 web 技术诞生有一定的年头了,不过就前端而言,可能要在 2005 年之后 Ajax 出现之后以及 SPA 成为趋势之后,才算是真正的开始发展。JS 和 CSS 存在的问题也才逐渐的突出起来。

前端工程化

关于工程化这个问题,个人以为张云龙的一系列博客已经把这个问题讲的很清楚了。工程化可以说是一个很大很笼统的话题,说实在的,我没有找到相关的英文文章,好像这个是国内才有的名词一样。个人认为这个概念本身也很模糊,有人按模块化、组件化、规范化、自动化来进行划分。模块化,组件化估计都不再是一个问题了,自动化又可以用 CI/CD 来取代。不过,这儿也分别谈谈对这些内容的理解。

模块化

模块化意味着分而治之,也是计算机解决复杂问题所惯用的方法。现在估计很难找到一门语言没有自己的模块化系统,一门语言不仅要有自己的模块系统,还要有包管理系统,这都是生态的基础。但是 JS 很长时间根本没有这些东西,在 Ajax 出现之前大家也没有使用 JS 构建复杂的交互应用,问题并不突出。分散在各个文件的变量在没有命名空间的情况下很容易产生冲突,管理各个文件之间的依赖也是一个问题。

个人认为模块系统出现是早晚的事情,当然也有人认为是从 Node.js 出来的 CommonJS 推动了模块系统。ECMAScript 标准迟迟未确定,CMDAMD 出现并应用在很多的应用当中。现在 CMDAMD 都已经成了历史,很少会见到有人问它们之间的区别了。

说完 JS,还要谈下 CSS。有人将 CSS 理解为一份配置,这份配置还不支持变量,代码复用,逻辑控制。不过,还好 sassless 的出现弥补了这些缺陷。CSS 也没有作用范围的概念,一份样式会影响到整个应用。不过各种命名规范, css modules 以及 style in JS 也算是一种解决方案。或许 web components 提出的方案是最为完美的,不过它并没有成功。

在我看来 CSS 有点特别:CSS 的目的是将内容与表现样式分离,很奇怪的是为什么只有前端存在这个东西。如果我们用 flutter 或者 swift UI 开发与 H5 应用类似交互的应用,我们可以做到非常的相似,不过这些技术都没有类似 CSS 的东西。从这点说,CSS 不是一个必要的存在,很大程度上是一个历史的产物。至于 CSS 会不会消亡,或者被 style in JS 取代,我是持否定态度的。自身对 style in JS 的感受是:认同这个观点但在实际应用中并没有带来太大的收益,甚至有时把问题搞复杂了;对于绝大多数以展示为目的的网站来说,CSS 依然是一个最佳的方案。

组件化

组件化也是分治思想的体现。组件化的思想不止是在前端,像 《架构整洁之道》 有些历史的书中的花了很大的篇幅在讲设计原则和组件构建原则;即使里面讨论的组件和前端的组件并不一样,不过里面的设计思想是可以借鉴的。与模块化相比,组件是一个可复用的功能单元,包括 JS,CSS 以及其他资源。在 React 流行的今天,估计大家对组件这个概念已经司空见惯了。在 React 之前,大家觉得 web components 会是未来的方向,当时的polymer 就像现在的 React 一样香。

webpack 相比与 gulp,一个特点就是它提供的管理资源方式是以组件为单位的或者说我们是以组件的视角来管理资源的,而使用 gulp 我们是按文件类型来管理资源的。这也是随着 React 的流行 webpack 取代 gulp 主宰市场的一个原因。

React 之前,大家也会封装组件,但是没有将所有的东西当成组件对待。就目前来看,这种思想俨然已经成为主流,使用flutter 以及 swift UI构建应用时也是一样的思想:将大的页面或者组件拆分为小的组件并把它们组装起来。

在我最开始接触 React 的时候,想找一些最佳实践,找到官网上的 Thinking in react 翻译成中文或许是 React 编程思想,与 Java 编程思想(Thinking in Java)相近,不过这个只是很短的一篇文章。整个文章也只是在教我们如何把大的组件做拆分。所以,在我看来,React 开发就是设计可维护和可复用的组件。到底怎样设计组件或者我们应该遵循哪些原则,个人认为 SOLID 以及 组件构建原则(组件构建原则可以说是 SOLID 的延伸)。

规范化

规范化比较容易理解。如果我们有多个前端项目,我们希望他们的组织结构,编码规范都尽可能一致。
就编码规范来说,一般来说用工具就可以维护 ESLint CSSLint StyleLint
另外,可能有设计规范,组件交互的设计语义可能适用于不同的场景。

自动化

自动化简单来说就是用工具解决人工重复劳动。比如自动构建和部署,测试。

构建

我们需要理解我们为什么需要构建工具以及我们构建的目的。一个根本原因是本地开发和线上需求不一致。我们的构建需求有很多:

  1. 编译 JS 和 SCSS 以支持不同浏览器。
  2. 压缩资源以更好优化资源加载速度
  3. 文件指纹以充分利用浏览器缓存
  4. 生成 HTML 文件
  5. 本地开发 HRM Soucemap

即使没有 webpack 一定会有其它的工具解决上面的问题。记得最开始接触前端时使用的是 java 工具来压缩文件。Node.js 的出现可以说将这部分内容交给前端自己解决了。从 gruntgulp 再到现在的 webpack,工具逐渐成熟,但是解决的根本问题是存在重合的。

当我们只有一个项目时,我们会考虑用 webpack 或者 create-react-app 作为构建工具。非常自然地,如果我们有多个项目,我们不希望维护多份 webpack 的配置,如果 create-react-app 不能满足我们的需求,我们可能会基于 webpack 封装自己的构建工具。这也是 roadhog 之类工具二次封装的目的。就业务需求而言,我们对 webpack 的配置细节不感兴趣,可能我们的配置只是想告诉工具:我们使用 scss + css modules。二次封装可以屏蔽掉这些细节,让我们不再需要感知到 webpack 。我一直希望有一个统一的配置规范,大概类似与 chart 图表数据格式吧。这样子,不管是 roadhog 也好,或者其它的二次封装工具,都可以很好的兼容。

自动部署

前端的部署方式比较简单,将打包的资源发到 CDN 即可。部署方式每个公司流程不同,我们现在使用 bitbucket pipeline 做的持续集成。在每次代码提交或者分支变动都会触发对应的任务,同时也有定时任务,每周指定时间重新部署开发环境。至于发布方式,我们使用 terraform 作为编排工具,将构建的资源同步到 S3,同时设置文件的缓存策略。同样用 terraform 管理 CDN 的配置,将我们域名的请求映射到 S3 上。

测试

可以说之所以我们能够做到自动部署,对每次发布有信心,是因为我们有足够的测试确保我们的系统稳定。
关于测试我已经在一篇文章里面谈过了 谈谈测试与代码质量

前端框架

首先要指出的是:引入一个框架是有代价的,必须慎重考虑,我们最不希望的就是我们的项目与一个具体的框架耦合。想象一下,项目从 Angular 迁移到 React,或者从 Vue 迁移到 React,有多少代码是可以复用的。同时,引入框架也意味着牺牲了灵活性。在一些架构设计指导中,最后的最后才会对框架进行选择,而这在前端却恰恰相反,往往最最开始做的决策就是确定选用哪一个流行框架。
前端之所以不同,一个原因是因为前端中的框架与后端中的语言一样是最基础的东西,现在使用原生 JS 开发直接应用根本不现实。最少也需要 jQuery 这样的库来解决兼容问题,但是 XSS 安全问题又防不胜防。另外一个主要原因是,前端的特征决定的,前端在整个系统设计中是最上层的,也是最不稳定的,我们需要使用框架迅速构建原型。个人认为,这个也是前端难题之一:我们认为的理想项目设计可能并满足客户需求,我们必须妥协和权衡。

React 不是一个完整的框架,与 Angular 这种大而全的框架比,React 更像是一个 UI 库。自然会有一些前端团队封装自己的框架,比如 Dva.js。记得当初团队也想做类似的事情,之后离开团队也不知道了。整体来说,大家已经对 Redux 流程已经比较熟悉,也并不觉得 React-Router 需要再进行封装。 异步操作使用 redux-saga 的感受是,这个确实比 redux-thunk 强大,但是绝大部分情况 redux-thunk 已经够用了,而且代码更为简洁,再说 redux-saga 的测试写起来一点也不愉快。 最后最不愿意看到的就是将 dispatch 注入到组件中,dispatch 可以做任何事情,本质上就是破坏了组件的封装。主观上的原因是我们不希望依赖太多的框架,即使做了选择,替换的成本也不要太高。

个人对 Angular 还是颇有好感的,大而全的框架虽说上手有难度,但是开发起来效率真的很高。绝大多数应用还谈不上性能瓶颈问题,很多项目只是本身的设计问题。就我经手的项目而言,有些项目也是用最新技术栈,前端开发也不是新手,但是项目代码只能说不敢恭维。

MVVM 的出现让表现和数据分离,前端只需要关心数据变化,而不用关系操作 DOM。这种分离让业务逻辑更加容易复用。Vue 最初更像是简易版的 Angular,当初调研的时候很难想象现在会这么成功。

依赖收集 Vue(Mobx)与 Redux 相比较的话,更像是比较不同的编程范式,函数式或者面向对象。在土澳函数式编程比国内更为流行,以至于前端团队有些人主张用 Reason 作为开发语言。个人感觉 redux 更加无脑一些(虽然有点啰嗦),Mobx 所需要的抽象设计能力要求更高,不大相信一般的团队能够管理好。
就性能而言的话,Redux 比较简单:有人把它比喻成一棵大树,所有的变动都要经过树干。这是它的缺点,任何的变动都可能导致整个应用的重绘。connect 会保证如果没有数据变化不去触发 render,但是这个粒度是 container 的粒度。
当数据发生变动的时候,依赖收集则可以精确地调用观察该数据的函数。当然函数一定是包装后的函数。依赖收集是有代价的,所以初次渲染的性能肯定不如 Redux。后续数据变化时,虽说会重新收集依赖,但是成本低了很多,仅有观察的函数会触发更新。一般来说更新时,性能要比 Redux 好些。单单进行这样的比较意义不大,如果真的存在瓶颈,每个都能够进行优化。根据具体场景,比如渲染列表中的元素项经常发生变动,那我们可以控制依赖收集的粒度,或者在 redux 中将 container 划分的更细一些以减少不必要的 render。

GraphQL

微服务的出现和流行,对前后端合作有着很大的影响。微服务更加倾向于提供通用的 API,前端一个组件所依赖的数据可能对应到几个不同的微服务上。前端与微服务端就需要一个网关服务,网关整合各个微服务为前端提供更为友好的 API,可以简单理解为 Facade 模式。有时也成为 BFF。前端作为消费者,要比后端更为关心这些接口,也更适合维护 BFF。就个人经历而言,还是有不少团队将这部分工作交给后端来做,个人认为可能的原因是:网关服务与前端开发差异较大,即使使用 Node.js 也可以说是完全不同的东西,而网关服务对后端同学而言只是一个普通的服务而已,对前端同学来说可能公司的基础设施根本就不支持 Node.js 服务。

刚说到 BFF 由前端维护更为合适,但是 GraphQL 的出现可能改变了这一点。如果后端提供的微服务本身是支持 GraphQL 同时又提供了一个网关来整合,就没有理由再交给前端来维护了。

对于 open API 来说,经常会根据 query 参数返回不同的数据字段,或许就是 GraphQL 的雏形,GraphQL 是一种更加优雅的方案。
单单将 GraphQLREST 相比较,各有优劣,GraphQL 并不会取代 REST

GraphQL 的优点

  1. query 非常优雅简洁,返回结果与 query 相近
  2. 按需组合查询,节省网络带宽
  3. 提供 schema 保障前后端的规范。类型校验
  4. 强大的 subscription

缺点也很明显:

  1. 完全不同的 cache 机制,不能有效利用 HTTP 缓存
  2. N+1 query 引起性能瓶颈
  3. query 组合的粗细粒度,不能第一时间呈现给用户信息。

或许 GraphQL 更适合需要适配多端不同 query 的情况吧。

微前端

个人并不看好微前端,主要是觉得其实这个新概念并没有解决什么实际的问题。最经典的例子是几个不同技术栈的整合,现实中很少见到这样的场景。比较常见的是,整个项目太大了,划分为不同的产品,交给不同的团队,每个团队都希望高度自治独立部署发布。
简单来说我觉得微前端的问题是很难找到一个全局最优解。如果是单一的应用,可以减少相同代码的加载,可以做更好的拆分动态加载来达到更好的性能。最重要的是,团队组织的问题反映到产品上,很难提供给用户一个顺滑的体验。每个团队要求的自治和独立发布通过更好的组件化也能够实现,微前端不是唯一的方案。如果功能划分的功能之间没有联系,那设计成独立的子应用,会更简单。

微前端需要解决的问题:主应用加载器,路由管理,消息通信。

目前公司的技术选择

Graphql

公司成立的有些年头了,不少页面还是 asp 弹窗的古老形式,交互并不友好。近几年,后端迁移切换成微服务,网关服务也是后端在做的。前端新项目也是最新的 React + Redux 技术栈。在整合产品的时候,我们的 BFF 技术栈选择基于 GraphQLNode.js。就我们的项目来说,在我看来没有特别的原因选择 GraphQL,我们并没有多端不同 query 适配的情况,还未用到 subscription。使用 GraphQL 更像是对新技术的一种投资。我们选择的是 Apollo + express,算是比较大众的一个方案,因为Apollo 比较容易入门,生态也是最强大的。

使用感受:前端代码更简洁一些。带来的问题是,附加的一些页面数据可能会影响到首次 query 性能,导致不能及时的呈现给用户关键信息。N + 1 query 问题并不是很常见,可以根据情况单独的处理。另外网关这层对微服务这层做了 cache。

因为是第一个应用,没有太多的约束。这里面也有一些问题:

  1. 第一个问题比较普遍,没有一个一个规范。
  2. 鉴权,日志,异常处理,配置加载都是自己处理的,不能为其它项目复用。
  3. 模块化的问题,Apollo 官方文档是按功能组织 GraphQL 的文件,复用是一个问题。Federation 太过激进。

所以,我考虑在 Apollo 之上引入企业框架,将这些可以复用的部分移出去。egg.js 不合适:egg.js 定义了一套规范,在此之上有不少可用的插件,在我们这儿没有什么需要的插件,定时任务这些也用不到。egg.js 内置对多核支持,对我们来说,性能也不是问题,长期 CPU 和内存利用率在个位数,如有需要也是选择 PM2。

模块化的需求是这样的场景:我们考虑到一些业务模块的复用,比如我的这个网关会有用户模块,新的网关服务也可能需要。我们希望能够直接复用这个模块,但是 Apollo 官网的方案 Federation 太过激进。 graph-module 也可以满足我们的需求。
个人更倾向于 nest.js,有几个原因:

  1. 提供了 IoC 容器,可以更好的组织代码
  2. GraphQL 整合的不错
  3. 内建的一些日志和异常处理功能可以直接使用
  4. 生态已经不错了,另外发展的势头也很好
  5. 支持 type-script

注意:依赖注入改变了控制流程,单说依赖注入是不与框架耦合的,只是在与具体容器整合的时候才有部分耦合的代码。更直白点说,如果我们换一个 IoC 容器,特定的注解和模块的注册是需要修改的,其它部分逻辑是不变的。

大公司与小公司的机会

当前端团队有了一定的规模之后,自然而然的就会有技术团队和业务团队之分,这样的好处是统一技术规范避免重复建设。可以说,就是这样的分工造就大公司非常丰富的基础设施。一方面我们很羡慕技术团队有机会接触新技术,但是也知道技术团队并不好做。难做的是在大公司里面其实很多东西都已经很成熟了,再进行创新已经是非常难的事情。公司需要的是产出,而不仅仅是调研,业务团队就像是客户,必须切实的解决他们的痛点,他们才愿意接受这些技术。

大公司的一些需求小公司可能从未遇到过,所以也不太容易感受到大公司推出的技术解决的问题。通常大公司有很多的部门,每个部门的业务可能截然不同,每个团队可能有多个产品。所以,很自然地,基础架构团队可能会考虑支持不能部门的业务,所提供的工具有很强的定制能力。比如, 阿里的 egg.js,介绍的时候就是框架的框架,很难说它对应的是哪个产品,你可以基于 egg.js 定制一套适合你们部门的统一的基础框架(假如 begg.js)。然后你们部门的团队再选择基于 begg.js 开发。如果你有类似的需求,估计不需要解释也能明白。而大多数人还是将 egg.js 与其它框架直接比较,毕竟他们的需求就是如此。而 egg.js 官网又没有强调这种区别,直接为更广大的开发者开放像 begg 这样已经封装过的框架。
另一个产品,Fusion UI 也是为了支持不同部门的不同产品,避免组件库的重复建设,估计更少公司会有这种需求。
即使是构建工具,也可能每个部门也有不同的定制需求,记得有一个团队曾经就在考虑提供封装构建脚本的能力。

可以看到,大公司的技术团队有机会比较深入的探索新技术,也有能力追求的更加极致。相比较而言,小公司缺乏基础设施,机会也比较多,不过比较更多的是整合一些方案,毕竟也不适合投入太多。

未来的一些可能

现在后端都在拥抱 serviceless 了,前端是时候考虑 spaless 了。像 codesandbox 这样,我们可以不关心构建,部署,监控,性能优化。甚至部署的服务可以根据收集的数据反馈,动态的进行优化。

从整体上想,类似的重复工作或许将来都可以在这样的平台上来完成了。

评论