# 概述
把一些前后端概念性比较强的理念放在这里,不放代码,不提供案例,仅供理解核心概念参考。
https://segmentfault.com/a/1190000038774393?utm_source=sf-related
# 微前端
微前端架构具备以下几个核心价值:
技术栈无关 主框架不限制接入应用的技术栈,微应用具备完全自主权
独立开发、独立部署 微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
增量升级
在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
独立运行时 每个微应用之间状态隔离,运行时状态不共享
微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith (opens new window))后,随之而来的应用不可维护的问题。这类问题在企业级 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>
2
3
4
5
6
7
8
9
# 入口
子应用提供什么形式的资源作为渲染入口?
JS Entry 的方式通常是子应用将资源打成一个 entry script,比如 single-spa 的 example (opens new window) 中的方式。但这个方案的限制也颇多,如要求子应用的所有资源打包到一个 js bundle 里,包括 css、图片等资源。除了打出来的包可能体积庞大之外的问题之外,资源的并行加载等特性也无法利用上。
HTML Entry 则更加灵活,直接将子应用打出来 HTML 作为入口,主框架可以通过 fetch html 的方式获取子应用的静态资源,同时将 HTML document 作为子节点塞到主框架的容器中。这样不仅可以极大的减少主应用的接入成本,子应用的开发方式及打包方式基本上也不需要调整,而且可以天然的解决子应用之间样式隔离的问题(后面提到)。
# 父子应用通信
在微前端架构中,我们应该按业务划分出对应的子应用,而不是通过功能模块划分子应用。这么做的原因有两个:
- 在微前端架构中,子应用并不是一个模块,而是一个独立的应用,我们将子应用按业务划分可以拥有更好的可维护性和解耦性。
- 子应用应该具备独立运行的能力,应用间频繁的通信会增加应用的复杂度和耦合度。
# 与其他对比
为什么不用iframe
iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。
url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
# 微服务
传统的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 社区组 (opens new window)(包括来自所有主要浏览器的代表)和 W3C 工作组 (opens new window)中开发的。
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
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
断言库
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三种语法
2
3
4
5
6
7
8
9
10
should.js
和expect.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 接口,需要引入jsdom
和jsdom-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
})
2
3
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是业内比较出色的方案。整体是在浏览器端实现代码的编译、打包、构建和运行。
# 重构
原则
- 事不过三,三则重构。即不能重复写同样的代码,在这种情况下要去重构。
- 如果一段代码让人很难看懂,那就该考虑重构了。
- 如果已经理解了代码,但是非常繁琐或者不够好,也可以重构。
- 过长的函数,需要重构。
- 一个函数最好对应一个功能,如果一个函数被塞入多个功能,那就要对它进行重构了。(4 和 5 不冲突)
- 重构的关键在于运用大量微小且保持软件行为的步骤,一步步达成大规模的修改。每个单独的重构要么很小,要么由若干小步骤组合而成。
手段:
- 提取重复代码,封装成函数
- 拆分功能太多的函数
- 变量/函数改名
- 替换算法
- 以函数调用取代内联代码
- 移动语句
- 折分嵌套条件表达式
- 将查询函数和修改函数分离