前后端开发概述

概述

​ 把一些前后端概念性比较强的理念放在这里,不放代码,不提供案例,仅供理解核心概念参考。

https://segmentfault.com/a/1190000038774393?utm_source=sf-related

微前端

微前端架构具备以下几个核心价值:

  • 技术栈无关 主框架不限制接入应用的技术栈,微应用具备完全自主权

  • 独立开发、独立部署 微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新

  • 增量升级

    在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略

  • 独立运行时 每个微应用之间状态隔离,运行时状态不共享

微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。

js隔离

如何确保各个子应用之间的全局变量不会互相干扰,从而保证每个子应用之间的软隔离?

这个问题比样式隔离的问题更棘手,社区的普遍玩法是给一些全局副作用加各种前缀从而避免冲突。但其实我们都明白,这种通过团队间的”口头“约定的方式往往低效且易碎,所有依赖人为约束的方案都很难避免由于人的疏忽导致的线上 bug。那么我们是否有可能打造出一个好用的且完全无约束的 JS 隔离方案呢?

即在应用的 bootstrap 及 mount 两个生命周期开始之前分别给全局状态打下快照,然后当应用切出/卸载时,将状态回滚至 bootstrap 开始之前的阶段,确保应用对全局状态的污染全部清零。而当应用二次进入时则再恢复至 mount 前的状态的,从而确保应用在 remount 时拥有跟第一次 mount 时一致的全局上下文。

css隔离

社区通常的实践是通过约定 css 前缀的方式来避免样式冲突,即各个子应用使用特定的前缀来命名 class,或者直接基于 css module 方案写样式。对于一个全新的项目,这样当然是可行,但是通常微前端架构更多的目标是解决存量/遗产 应用的接入问题。很显然遗产应用通常是很难有动力做大幅改造的。

最主要的是,约定的方式有一个无法解决的问题,假如子应用中使用了三方的组件库,三方库在写入了大量的全局样式的同时又不支持定制化前缀?比如 a 应用引入了 antd 2.x,而 b 应用引入了 antd 3.x,两个版本的 antd 都写入了全局的 .menu class,但又彼此不兼容怎么办?

解决方案与子应用入口文件相关。我们只需要在应用切出/卸载后,同时卸载掉其样式表即可,原理是浏览器会对所有的样式表的插入、移除做整个 CSSOM 的重构,从而达到 插入、卸载 样式的目的。这样即能保证,在一个时间点里,只有一个应用的样式表是生效的。

上文提到的 HTML Entry 方案则天生具备样式隔离的特性,因为应用卸载后会直接移除去 HTML 结构,从而自动移除了其样式表。

当子应用被替换或卸载时,subApp 节点的 innerHTML 也会被复写,//alipay.com/subapp.css 也就自然被移除样式也随之卸载了。

<html>
  <body>
    <main id="subApp">
      // 子应用完整的 html 结构
      <link rel="stylesheet" href="//alipay.com/subapp.css">
      <div id="root">....</div>
    </main>
  </body>
</html>

入口

子应用提供什么形式的资源作为渲染入口?

JS Entry 的方式通常是子应用将资源打成一个 entry script,比如 single-spa 的 example 中的方式。但这个方案的限制也颇多,如要求子应用的所有资源打包到一个 js bundle 里,包括 css、图片等资源。除了打出来的包可能体积庞大之外的问题之外,资源的并行加载等特性也无法利用上。

HTML Entry 则更加灵活,直接将子应用打出来 HTML 作为入口,主框架可以通过 fetch html 的方式获取子应用的静态资源,同时将 HTML document 作为子节点塞到主框架的容器中。这样不仅可以极大的减少主应用的接入成本,子应用的开发方式及打包方式基本上也不需要调整,而且可以天然的解决子应用之间样式隔离的问题(后面提到)。

父子应用通信

在微前端架构中,我们应该按业务划分出对应的子应用,而不是通过功能模块划分子应用。这么做的原因有两个:

  1. 在微前端架构中,子应用并不是一个模块,而是一个独立的应用,我们将子应用按业务划分可以拥有更好的可维护性和解耦性。

  2. 子应用应该具备独立运行的能力,应用间频繁的通信会增加应用的复杂度和耦合度。

与其他对比

为什么不用iframe

iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。

  1. url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。

  2. UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..

  3. 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。

  4. 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。

微服务

传统的WEB应用核心分为业务逻辑、适配器以及API或通过UI访问的WEB界面。业务逻辑定义业务流程、业务规则以及领域实体。适配器包括数据库访问组件、消息组件以及访问接口等

尽管也是遵循模块化开发,但最终它们会打包并部署为单体式应用。例如Java应用程序会被打包成WAR,部署在Tomcat或者Jetty上。

这种单体应用比较适合于小项目,优点是:

  • 开发简单直接,集中式管理

  • 基本不会重复开发

  • 功能都在本地,没有分布式的管理开销和调用开销

当然它的缺点也十分明显,特别对于互联网公司来说:

  • 开发效率低:所有的开发在一个项目改代码,递交代码相互等待,代码冲突不断
  • 代码维护难:代码功能耦合在一起,新人不知道何从下手
  • 部署不灵活:构建时间长,任何小修改必须重新构建整个项目,这个过程往往很长
  • 稳定性不高:一个微不足道的小问题,可以导致整个应用挂掉
  • 扩展性不够:无法满足高并发情况下的业务需求

现在主流的设计一般会采用微服务架构。其思路不是开发一个巨大的单体式应用,而是将应用分解为小的、互相连接的微服务。一个微服务完成某个特定功能,比如乘客管理和下单管理等。每个微服务都有自己的业务逻辑和适配器。一些微服务还会提供API接口给其他微服务和应用客户端使用。

优点

微服务架构有很多重要的优点。首先,它解决了复杂性问题。它将单体应用分解为一组服务。虽然功能总量不变,但应用程序已被分解为可管理的模块或服务。这些服务定义了明确的RPC或消息驱动的API边界。微服务架构强化了应用模块化的水平,而这通过单体代码库很难实现。因此,微服务开发的速度要快很多,更容易理解和维护。

其次,这种体系结构使得每个服务都可以由专注于此服务的团队独立开发。只要符合服务API契约,开发人员可以自由选择开发技术。这就意味着开发人员可以采用新技术编写或重构服务,由于服务相对较小,所以这并不会对整体应用造成太大影响。

第三,微服务架构可以使每个微服务独立部署。开发人员无需协调对服务升级或更改的部署。这些更改可以在测试通过后立即部署。所以微服务架构也使得CI/CD成为可能。

最后,微服务架构使得每个服务都可独立扩展。我们只需定义满足服务部署要求的配置、容量、实例数量等约束条件即可。比如我们可以在EC2计算优化实例上部署CPU密集型服务,在EC2内存优化实例上部署内存数据库服务。

缺点:

微服务的另一个主要缺点是微服务的分布式特点带来的复杂性。开发人员需要基于RPC或者消息实现微服务之间的调用和通信,而这就使得服务之间的发现、服务调用链的跟踪和质量问题变得的相当棘手。

微服务的另一个挑战是分区的数据库体系和分布式事务。更新多个业务实体的业务交易相当普遍。这些类型的事务在单体应用中实现非常简单,因为单体应用往往只存在一个数据库。但在微服务架构下,不同服务可能拥有不同的数据库。CAP原理的约束,使得我们不得不放弃传统的强一致性,而转而追求最终一致性,这个对开发人员来说是一个挑战。

微服务架构对测试也带来了很大的挑战。传统的单体WEB应用只需测试单一的REST API即可,而对微服务进行测试,需要启动它依赖的所有其他服务。这种复杂性不可低估。

WebAssembly

wasm并不是一种编程语言,而是一种新的字节码格式,目前,主流浏览器都已经支持 wasm。与 JavaScript 需要解释执行不同的是,wasm字节码和底层机器码很相似可快速装载运行,因此性能相对于 JavaScript 解释执行有了很大的提升。

WebAssembly(缩写为 Wasm)是基于堆栈的虚拟机的二进制指令格式。Wasm 被设计为可移植目标,用于编译高级语言(如 C / C ++ / Rust),从而可以在 Web 上为客户端和服务器应用程序进行部署。

WebAssembly 的开放标准是在 W3C 社区组(包括来自所有主要浏览器的代表)和 W3C 工作组中开发的。

WebAssembly 或称 wasm 是一个实验性的低端编程语言,应用于浏览器内的客户.WebAssembly 是便携式的抽象语法树,被设计来提供比 JavaScript 更快速的编译及运行。WebAssembly 将让开发者能运用自己熟悉的编程语言(最初以 C/C++ 作为实现目标)编译,再藉虚拟机引擎在浏览器内运行。

WebAssembly 的开发团队分别来自 Mozilla、Google、Microsoft、Apple,代表着四大网络浏览器 Firefox、Chrome、Microsoft Edge、Safari。

2017 年 11 月,以上四个浏览器都开始实验性的支持 WebAssembly。

WebAssembly 于 2019 年 12 月 5 日成为万维网联盟(W3C)的推荐,与 HTML,CSS 和 JavaScript 一起,成为 Web 的第四种语言。

WebAssembly 是一种新的编码方式,可以在现代的网络浏览器中运行 - 它是一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行,并为诸如 C / C ++ 等语言提供一个编译目标,以便它们可以在 Web 上运行。它也被设计为可以与 JavaScript 共存,允许两者一起工作。

特点

高效快捷

Wasm 堆栈机设计为以节省大小和加载时间的二进制格式进行编码。WebAssembly 旨在通过利用广泛平台上可用的通用硬件功能,以本机速度执行。

对于网络平台而言,WebAssembly 具有巨大的意义——它提供了一条途径,以使得以各种语言编写的代码都可以以接近原生的速度在 Web 中运行。

在这种情况下,以前无法以此方式运行的客户端软件都将可以运行在 Web 中。

WebAssembly 被设计为可以和 JavaScript 一起协同工作——通过使用 WebAssembly 的 JavaScript API,你可以把 WebAssembly 模块加载到一个 JavaScript 应用中并且在两者之间共享功能。

这允许你在同一个应用中利用 WebAssembly 的性能和威力以及 JavaScript 的表达力和灵活性,即使你可能并不知道如何编写 WebAssembly 代码。

而且,更棒的是,这是通过 W3C WebAssembly Community Group 开发的一项网络标准,并得到了来自各大主要浏览器厂商的积极参与。

安全

WebAssembly 描述了一种内存安全的沙盒执行环境,该环境甚至可以在现有 JavaScript 虚拟机内部实现。当嵌入 Web 时,WebAssembly 将强制执行浏览器的同源和权限安全策略。

传统从 JS 代码,在浏览器端运行,是有被拿到源代码的可能(即使你加密了);

WebAssembly 使用二进制 (.wasm文件)的形式,在这方面跨出一步

开放且可调试

WebAssembly 旨在以文本格式漂亮地打印,以便手工调试,测试,实验,优化,学习,教学和编写程序。在 Web 上查看 Wasm 模块的来源时,将使用文本格式。

开放式网络平台的一部分

WebAssembly 旨在维护 Web 的无版本,经过功能测试和向后兼容的性质。WebAssembly 模块将能够调用和退出 JavaScript 上下文,并通过可从 JavaScript 访问的相同 Web API 访问浏览器功能。WebAssembly 还支持非 Web 嵌入。

Serverless

Serverless 是前端圈近两年比较火热的词汇,通过 Serverless 这种服务形态,用户在使用对应的服务时,不需要关心或较少关心服务器的硬件资源、软件资源、稳定性等等,完全托管给云计算厂商,用户只需要专注自己应用代码本身,上传执行函数到相应云计算平台,按照函数运行的时长按量付费即可

演进史

云计算诞生后,用户可以直接购买云主机(VM),把基础物理硬件和网络的管理都交由供应商管理,多用户租用一台物理机,减少了用户硬件管理成本,我们通常称之为 IaaS(Infrastructure-as-a-Service)。

随着软件的发展和容器技术的兴起,计算环境由 VM 发展到更小粒度的容器,在容器中可以运行不同的软件服务,PaaS(Platform-as-a-Service) 和 CaaS(Container-as-a-Service) 也开始映入眼帘。用户使用平台基础软件如 Database、消息等开发自己的应用,使用容器镜像构建和部署应用,最后托管给平台。

继续向前发展,应用的运行演变为更细粒度函数的运行,用户开发特定业务的处理函数,托管给函数平台,按需使用相关的后端服务,通过特定条件的触发完成开发者业务逻辑函数的计算。用户无需为应用持续付费,只需支付函数运行时产生的资源消耗费用,而这,就是 Serverless 服务的模型。

基本架构

Serverless 架构由两部分组成,即 Faas 和 BaaS。

Faas

FaaS(Function-as-a-Service)即为函数运行平台,用户无需搭建庞大的服务系统,只需要上传自己的逻辑函数如一些定时任务、数据处理任务等到云函数平台,配置执行条件触发器、路由等等,完成基础函数的注册。

Faas 运行函数的容器是无状态的,上一次的运行效果和下一次的运行效果是无关的。如果需要存储状态,则需要使用云储存或者云数据库。

Faas 函数如果长时间未使用,容器就会对其进行回收。所以函数在首次调用或长时间未使用时,容器就需要重新创建该函数的实例,这个过程称为冷启动,一般耗时为数百毫秒。

Faas是通过事件驱动的,当一个任务被触发时,比如 HTTP 请求,API Gateway 接受请求、解析和认证,传递对应参数给云函数平台,平台中执行对应回调函数,配合 DB、MQ 等 BaaS 服务在特定容器中完成计算,最终将结果返回给用户。函数执行完成后,一般会被 FaaS 平台销毁,释放对应容器,等待下一个函数运行。

既然有冷启动,就有热启动。例如容器刚刚调用完函数,过一会又有新的事件触发。这时由于函数仍未被回收,所以可以直接复用原有的函数实例,这被称为热启动。

Faas 如果单独使用的话,那它只适合部署一些工具类函数。因为它是无状态的,每次运行都可能是在不同的容器上,它不知道上一个函数的运行结果。所以如果要使用 Serverless 来部署整个应用,还得额外购买 OSS 云存储或者云数据库来提供数据存储服务(也就是需要配合 Baas 来使用)。

Baas

BaaS(Backend-as-a-Service)包含了后端服务组件,它是基于 API 的第三方服务,用于实现应用程序中的核心功能,包含常用的数据库、对象存储、消息队列、日志服务等等

Faas与Baas

Faas 其实是一个云计算平台,用户可以将自己写的函数托管到平台上运行。而 Baas 则是提供一系列的服务给用户运用,用户通过 API 调用。

其他不同点:

  • Faas 无状态,Baas 有状态。
  • Faas 运行的是函数,由开发者自己编写;Baas 提供的是服务,不需要开发者自己开发。

可以说 Faas 和 Baas 是两个不同的东西,但它们有一个共同点,就是无需自己管理服务器和资源的分配、整理,所以都属于 Serverless。

应用场景

Serverless 其实是通过事件驱动的,当一个任务被触发时,比如 HTTP 请求,API Gateway 接受请求、解析和认证,传递对应参数给云函数平台,平台中执行对应回调函数,配合 DB、MQ 等 BaaS 服务在特定容器中完成计算,最终将结果返回给用户。函数执行完成后,一般会被 FaaS 平台销毁,释放对应容器,等待下一个函数运行。

优缺点

优点

Serverless 最大的优点就是自动扩展伸缩、无需自己管理。

在以往部署一个应用时,需要经历购买服务器、安装操作系统、购买域名等等一系列步骤,应用才能真正的上线。后来有了云服务器,我们就省去了购买服务器、安装操作系统这些操作步骤。只需要在云服务器上搭建环境、安装数据库就可以部署应用了。

但是这仍然有个问题,当网站访问量过大时,你需要增加服务器;访问量过小时,需要减少服务器。如果使用 Serverless,你就不需要考虑这些,云服务商会帮你管理这一切。云服务商会根据你的访问量自动调整所需的资源。

缺点

当应用部署在云上,并且使用云存储或云数据库,那可能会让我们的应用访问速度变得比较慢。因为网络的访问速度比内存和硬盘差了一到两个数量级。

灰度发布/蓝绿部署/滚动发布

在一般情况下,升级服务器端应用,需要将应用源码或程序包上传到服务器,然后停止掉老版本服务,再启动新版本。但是这种简单的发布方式存在两个问题,一方面,在新版本升级过程中,服务是暂时中断的,另一方面,如果新版本有 BUG,升级失败,回滚起来也非常麻烦,容易造成更长时间的服务不可用。

蓝绿部署

所谓蓝绿部署,是指同时运行两个版本的应用,蓝绿部署的时候,并不停止掉老版本,而是直接部署一套新版本,等新版本运行起来后,再将流量切换到新版本上。但是蓝绿部署要求在升级过程中,同时运行两套程序,对硬件的要求就是日常所需的二倍,比如日常运行时,需要 10 台服务器支撑业务,那么使用蓝绿部署,你就需要购置二十台服务器。

滚动发布

所谓滚动升级,就是在升级过程中,并不一下子启动所有新版本,是先启动一台新版本,再停止一台老版本,然后再启动一台新版本,再停止一台老版本,直到升级完成,这样的话,如果日常需要 10 台服务器,那么升级过程中也就只需要 11 台就行了。

但是滚动升级有一个问题,在开始滚动升级后,流量会直接流向已经启动起来的新版本,但是这个时候,新版本是不一定可用的,比如需要进一步的测试才能确认。那么在滚动升级期间,整个系统就处于非常不稳定的状态,如果发现了问题,也比较难以确定是新版本还是老版本造成的问题。

为了解决这个问题,我们需要为滚动升级实现流量控制能力。

灰度发布

灰度发布(又名金丝雀发布,起源是,矿井工人发现,金丝雀对瓦斯气体很敏感,矿工会在下井之前,先放一只金丝雀到井中,如果金丝雀不叫了,就代表瓦斯浓度高)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。

灰度期:灰度发布开始到结束期间的这一段时间,称为灰度期。

在灰度发布开始后,先启动一个新版本应用,但是并不直接将流量切过来,而是测试人员对新版本进行线上测试,启动的这个新版本应用,就是我们的金丝雀。如果没有问题,那么可以将少量的用户流量导入到新版本上,然后再对新版本做运行状态观察,收集各种运行时数据,如果此时对新旧版本做各种数据对比,就是所谓的 A/B 测试。

当确认新版本运行良好后,再逐步将更多的流量导入到新版本上,在此期间,还可以不断地调整新旧两个版本的运行的服务器副本数量,以使得新版本能够承受越来越大的流量压力。直到将 100% 的流量都切换到新版本上,最后关闭剩下的老版本服务,完成灰度发布。

如果在灰度发布过程中(灰度期)发现了新版本有问题,就应该立即将流量切回老版本上,这样,就会将负面影响控制在最小范围内

总结

在新版本应用发布时,为了服务器不停机升级,使用灰度发布策略,在灰度发布开始时,使用 HTTP Header 匹配指定测试人员的流量到新版本上,然后当新版本内部测试通过后,可以再按百分比,将用户流量一点一点导入到新版本中,比如先导入 10% 观察一下运行情况,然后再导入 20%,如此累加,直到将流量全部导入到新版本上,最后完成升级,如果期间发现问题,就立即取消升级,将流量切回到老版本。

运用灰度发布,就再也不需要加班到深夜进行停机升级了,在白天就可以放心大胆地、安全地发布新版本

单元测试

测试是保证代码质量的重要环节,web项目的单元测试虽然不能完全完成功能测试,但是却能保证底层单一模块的工作质量,并且在代码重构的时候保证对外接口不会发生变化。

经常会提到的敏捷开发,单元测试就是其中必不可少的一步。因此单元测试的需要,尤其是自动化单元测试不可忽略,而且应当作为整个团队的关键责任-而不仅仅是软件开发人员的责任。

  • 单元:相对独立功能模块,类、模块、方法。
  • 又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。
  • 用来检验程式的内部逻辑,也称为个体测试、结构测试或逻辑驱动测试。

单元测试的重要性

由于存在浏览器解析环境、用户操作习惯等差异,前端程序的许多问题是无法捕捉或重现的,现在前端程序的测试多是黑盒测试,即靠点击点击点击来寻找程序bug。这种方式既费时费力,又无法保证测试的覆盖面。同时,前端逻辑和交互越来越复杂,和其他编程语言一样,一个函数,一个模块,在修改bug或添加新功能的过程中,很容易就产生新的bug,或使老的bug复活。这种情况下,反复进行黑盒测试,其工作量和测试质量是可想而知的。

  • 反正都要手动测试,所以不如代码自动化。
  • 为了实现依赖接口编程,大型软件项目多人合作时必须要有的
  • 首先,得让你的代码能够测试
  • 增强代码自信

黑盒测试

  • bug无法捕捉、重现
  • 费力,工作量大
  • 覆盖面低
  • 反复出现bug

单元测试

  • 并不是所有的 js 都需要单元测试。中大型项目
  • 并不是所有的 js 都能够单元测试。良好的模块化和解耦

TDD与BDD、相关概念

TDD:测试驱动开发(Test-Driven Development)测试驱动开发是敏捷开发中的一项核心实践和技术,也是一种设计方法论。TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。TDD的基本思路就是通过测试来推动整个开发的进行,但测试驱动开发并不只是单纯的测试工作,而是把需求分析,设计,质量控制量化的过程。TDD首先考虑使用需求(对象、功能、过程、接口等),主要是编写测试用例框架对功能的过程和接口进行设计,而测试框架可以持续进行验证。

BDD:行为驱动开发(Behavior Driven Development)行为驱动开发是一种敏捷软件开发的技术,它鼓励软件项目中的开发者、QA和非技术人员或商业参与者之间的协作。主要是从用户的需求出发,强调系统行为。BDD最初是由Dan North在2003年命名,它包括验收测试和客户测试驱动等的极限编程的实践,作为对测试驱动开发的回应。

测试套件”(test suite):describe (moduleName, testDetails)。可以嵌套使用,明白、易懂即可。describe块称为”测试套件”(test suite),表示一组相关的测试。它是一个函数,第一个参数是测试套件的名称(”加法函数的测试”),第二个参数是一个实际执行的函数。

测试用例”(test case):it (info, function)。具体的测试语句,可多个。it块称为”测试用例”(test case),表示一个单独的测试,是测试的最小单位。它也是一个函数,第一个参数是测试用例的名称(”1 加 1 应该等于 2”),第二个参数是一个实际执行的函数。

  • info,写期望的正确输出的简要一句话文字说明。info:当该it 的block内的test failed的时候控制台就会把详细信息打印出来。

  • 测试用例之中,只要有一个断言为false,这个测试用例就会失败,只有所有断言都为true,测试用例才会通过。

  • function,具体测试函数,一个测试用例内部,包含一个或多个断言(assert)。

断言指的是对代码行为的预期,会返回一个布尔值,表示代码行为是否符合预期。

  • 所有的测试用例(it块)都应该含有一句或多句的断言。
  • 断言是编写测试用例的关键
  • 断言功能由断言库来实现,Mocha本身不带断言库,所以必须先引入断言库。
  • 断言库有很多种,Mocha并不限制使用哪一种。

单元测试生命周期

每个测试块(describe)有4个周期函数:before、beforeEach、afterEach、after

周期函数 存在周期 主要功能
before() 在本区块的所有测试用例之前执行 用于同一的桩数据导入等功能
beforeEach() 在本区块的每个测试用例之前执行 用于清理测试环境,删除或回滚相关数据
afterEach() 在本区块的每个测试用例之后执行 可以用于准备测试用例所需的前置条件
after() 在本区块的所有测试用例之后执行 可以用于准备测试用例所需的后置条件

测试用例结构:

a. Setup: 准备好环境和数据,跑这个测试用例之前的准备

b. Execution:执行测试(测试用例的实现的主要代码)

c. Validation:验证结果

d. Cleanup:现场恢复,一般与a相反。不影响跑后面的测试用例。

TDD相关接口:

suite:定义一组测试用例。

suiteSetup:此方法会在这个suite所有测试用例执行前执行一次,只一次,这是跟setup的区别。

setup:此方法会在每个测试用例执行前都执行一遍。

test:具体执行的测试用例实现代码。

teardown:此方法会在每个测试用例执行后都执行一遍,与setup相反。

suiteTeardown:此方法会在这个suite所有测试用例执行后执行一次,与suiteSetup相反。

前端单元测试框架

前端测试框架有

  • Mocha
  • Jasmine
  • Jest
  • Tape
  • Karma

Mocha

npm install mocha --save-dev
const add = require("./add");
const assert = require("assert");

// describe:定义一组测试
describe("加法函数测试", function() {
    before(function() {
        // runs before all tests in this block
    });
    
    // it: 定义一个测试用例
    it("1 加 1 应该等于 2", function() {
        // assert: nodejs内置断言模块
        assert.equal(add(1, 1), 2);
    });
    
    after(function() {
        // runs after all test in this block
    });
});

断言库

Mocha 支持should.js, chai, expect.js, better-assert, unexpected等断言库

//assert
assert.ok(add(1, 1));
assert.equal(add(1, 1), 2);
//shouldjs
(add(1, 1)).should.be.a.Number();
(add(1, 1)).should.equal(2);
//expectjs
expect(add(1, 1)).to.be.a("number");
expect(add(1, 1)).to.equal(2);
//chai支持should, expect, assert三种语法

should.jsexpect.js相较于assert语义性更强,且支持类型检测,而should.js在语法上更加简明,同时支持链式语法.and

  • chai.js断言库:接口丰富,文档齐全,可以对各种接口进行断言。
  • expect 库应用是非常广泛,拥有很好的链式结构和仿自然语言的方法。
  • 通常写同一个断言会有几个方法,比如:expect(response).to.be(true) 和 expect(response).equal(true)。

expect和should是BDD风格的,二者使用相同的链式语言来组织断言,但不同在于他们初始化断言的方式:expect使用构造函数来创建断言对象实例,而should通过为Object.prototype新增方法来实现断言(所以should不支持IE);expect直接指向chai.expect,而should则是chai.should()。

expect断言风格

  • ok :检查是否为真
  • true:检查对象是否为真
  • to.be、to:作为连接两个方法的链式方法
  • not:链接一个否定的断言,如 expect(false).not.to.be(true)
  • a/an:检查类型(也适用于数组类型)
  • include/contain:检查数组或字符串是否包含某个元素
  • below/above:检查是否大于或者小于某个限定值

assert风格是三种断言风格中唯一不支持链式调用的,Chai提供的assert风格的断言和node.js包含的assert模块非常相似。

Mocha 支持4种 hook,包括before / after / beforeEach / afterEach

Mocha 默认每个测试用例执行2000ms,超出时长则报错,所以在测试代码中如果有异步操作,则需要通过done函数来明确测试用例结束。done接受Error参数。

Mocha 在node环境下运行时,不支持 BOM 和 DOM 接口,需要引入jsdomjsdom-global库。

Mocha命令行基本用法:

  • mocha:默认运行test子目录里面的测试脚本,不包括子文件
  • mocha add.test.js:当前目录下面的该测试脚本。
  • mocha file1 file2 file3 : mocha命令后面紧跟测试脚本的路径和文件名,可以指定多个测试用例。

通配符:

  • mocha spec/{my,awesome}.js
  • mocha test/unit/*.js

生成格式

  • mocha –reporter spec:默认为spec格式,可设置其他格式。
  • mocha –recursive -R markdown > spec.md 。

网页查看

npm install –save-dev mochawesome

在gulp中运行mocha

安装gulp-mocha插件

npm install gulp-mocha --save-dev

gulpfile

gulp.task('mocha',function() {
	return 
})

Jasmine

Jasmine 是一个功能全面的测试框架,内置断言expect;但是有全局声明,且需要配置,相对来说使用更复杂、不够灵活。

npm install jasmine --save-dev

Jasmine 的语法与 Mocha 非常相似,不过断言采用内置的expect()

Jest

Jest 是一个功能全面的“零配置”测试框架,既集成了各种工具,且无需配置即可使用。

npm install --save-dev jest

Jest 中以test定义一个测试用例,且自带断言expect,断言库功能强大,但语法相较于should.js来说更复杂。

普通匹配:toBe, not.toBe

空匹配:toBeNull, toBeUndefined, toBeDefine, toBeTruthy, toBeFalsy

数字大小:toBeGreaterThan, toBeGreaterThanOrEqual, toBeLessThan, toEqual, toBeCloseTo(用于浮点数)

正则匹配:toMatch

数组查询:toContain

构造匹配:toEqual(expect.any(constructor))

Jest 同样有四个hook,beforeAll/beforeEach/afterAll/afterEach

Jest 内置对 DOM 和 BOM 接口的支持。

Jest 内置覆盖统计,为了更方便地进行相关配置,我们可以创建一个配置文件jest.config.js

然后将package.json中的命名修改一下:"test-jest": "jest"

其他语言测试框架

单元测试与集成测试、功能测试

三种测试的主要作用

  • 单元测试,单个组件正常工作,开发阶段;用来确保每个组件正常工作 —— 测试组件的 API 。
  • 集成测试,不同组件互相合作,中间阶段;用来确保不同组件互相合作 —— 测试组件的 API, UI, 或者边缘情况(比如数据库I/O,登录等等)。
  • 功能测试,主要测试界面,开发完成。 用来确保整个应用会按照用户期望的那样运行 —— 主要测试界面

三种测试在不同阶段的重要性

  • 开发阶段,主要是程序员反馈。这时单元测试很有用。

  • 在中间阶段,主要是能够在发现问题时立刻停下来。这时各种测试都很有用。

  • 在生产环境,主要是运行功能测试套件,确保部署的时候没有弄坏什么东西。

DDD驱动测试

e2e测试

前端代码打包

前端bundleless

随着业务的发展,前端代码的复杂度越来越高,构建方面展露新的问题:

过去需要打包的原因:

1.http1.1各浏览器有并行连接限制

2.浏览器不支持模块系统(如commonjs不能在浏览器直接运行)

3.代码依赖关系与顺序管理

可以开始不打包的原因:

1.http2.0多路并用

2.各大浏览器逐一支持ESM

3.越来越多的npm包拥抱ESM(尽管还有很多包不是)

目前构建主要分为两种:

1是基于服务的构建方式。通常服务于实际生产。可以再细分成本地服务构建与远端服务构建。本地服务构建就是我们常规的操作,基本被webpack统治,是bundle方案的代表,snowpack、Vite、Web Dev Server是目前比较火的Bundleless方案,发展迅猛。远端服务构建则是依托云能力的玩法,把构建过程放在服务端完成。

2是基于浏览器的构建方式。通常面向Demo的快速搭建或预览方案。Codesandbox、StackBlitz、CodePen和Riddel是业内比较出色的方案。整体是在浏览器端实现代码的编译、打包、构建和运行。

重构

原则

  1. 事不过三,三则重构。即不能重复写同样的代码,在这种情况下要去重构。
  2. 如果一段代码让人很难看懂,那就该考虑重构了。
  3. 如果已经理解了代码,但是非常繁琐或者不够好,也可以重构。
  4. 过长的函数,需要重构。
  5. 一个函数最好对应一个功能,如果一个函数被塞入多个功能,那就要对它进行重构了。(4 和 5 不冲突)
  6. 重构的关键在于运用大量微小且保持软件行为的步骤,一步步达成大规模的修改。每个单独的重构要么很小,要么由若干小步骤组合而成。

手段:

  1. 提取重复代码,封装成函数
  2. 拆分功能太多的函数
  3. 变量/函数改名
  4. 替换算法
  5. 以函数调用取代内联代码
  6. 移动语句
  7. 折分嵌套条件表达式
  8. 将查询函数和修改函数分离