二十七工作者线程之服务工作者

Posted 乘风xs

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二十七工作者线程之服务工作者相关的知识,希望对你有一定的参考价值。

服务工作者线程(service worker)是一种类似浏览器中代理服务器的线程,可以拦截外出请求和缓存响应。
这可以让网页在没有网络连接的情况下正常使用,因为部分或全部页面可以从服务工作者线程缓存中提供服务。
服务工作者线程也可以使用 Notifications API、Push API、Background Sync API 和Channel Messaging API。
与共享工作者线程类似,来自一个域的多个页面共享一个服务工作者线程。
不过,为了使用 Push API等特性,服务工作者线程也可以在相关的标签页或浏览器关闭后继续等待到来的推送事件。

对于大多数开发者而言,服务工作者线程在两个主要任务上最有用:充当网络请求的缓存层启用推送通知

1服务工作者线程基础

1.1ServiceWorkerContainer

服务工作者线程没有全局构造函数,而是通过ServiceWorkerContainer管理,其实例保存在navigator.serviceWorker属性中。

console.log(navigator.serviceWorker);// ServiceWorker...

1.2 创建服务工作者线程

服务工作者线程同样是在还不存在时创建新实例,在存在时连接到已有实
。ServiceWorkerContainer 没有通过全局构造函数创建,而是暴露了 register()方法,该方法以与 Worker()或 SharedWorker()构造函数相同的方式传递脚本 URLregister()返回一个期约,解决为ServiceWorkerGlobalScope对象,在注册失败时拒绝。

navigator.serviceWorker.register('./emptyServiceWorker.js').then(console.log, console.error);

注册服务工作者线程的一种非常常见的模式是基于特性检测,并在页面的 load 事件中操作。

if ('serviceWorker' in navigator) 
	 window.addEventListener('load', () => 
	 	navigator.serviceWorker.register('./serviceWorker.js');
	 );

1.3 使用ServiceWorkerContainer对象

支持的事件处理程序

事件处理程序说明
oncontrollerchange在获得新激活的ServiceWorkerRegistration时触发
onerror服务工作者线程内部触发ErrorEvent错误类型时触发,或者服务工作者内部抛出错误时触发
onmessage件在服务脚本向父上下文发送消息时触发

支持的属性和方法

属性或者方法说明
ready返回期约,解决为激活的ServiceWorkerRegistration对象,该期约不会拒绝
controller返回与当前页面关联的激活的ServiceWorker对象,没有时返回null
register()接受url和配置options,创建或者更新ServiceWorkerRegistration
getRegisteration()返回期约,解决为与提供作用域匹配的ServiceWorkerRegistration对象,没有就返回undefined
getRegistrations()返回期约,解决为与ServiceWorkerContainer关联的ServiceWorkerRegistration对象数组,没有时返回空数组
startMessage()开始通过Client.postMessage()派发消息

1.4使用ServiceWorkerRegistration对象

使用navigator.serviceWorker.register()返回的期约解决为ServiceWorkerRegistration对象。多次使用同一个url进行注册,会返回同一个ServiceWorkerRegistration对象。
支持的事件处理程序

事件说明
onupdatefound服务工作者线程触发updatefound事件时发生,此事件在服务工作者线程开始安装新版本时触发,表现为ServiceWorkerRegistration.installing收到一个新的服务工作者线程

支持的属性和方法

属性或者方法说明
scope返回服务工作者完成的url路径
navigationPreload返回与注册对象关联的NavigationPreloadManager对象
pushManage返回与注册对象关联的PushManager实例
installing若存在,则返回状态为installing的服务工作者线程,否则为null
waiting若存在,则返回状态为waiting的服务工作者线程,否则为null
active若存在,则返回状态为activating或者active的服务工作者线程
getNotifications()返回期约,解决为Notification对象的数组
showNotifications()显示通知,可以配置title和options参数
update()直接从服务器重新请求服务脚本,如果新脚本不同,则重新初始化
unregister()取消服务工作者线程的注册,该方法会在服务工作者线程执行完在取消注册

1.5使用ServiceWorker对象

获取该对象的方式:

  • navigator.serviceWorker.controller,通过ServiceWorkerContainer对象的controller属性
  • 通过ServiceWorkerRegistration对象active属性

支持的事件处理程序

事件说明
onstatechange状态发生变化的时候触发

支持的属性和方法

属性或者方法说明
scriptUrl解析后注册服务工作者线程的URL
state服务工作者线程的状态, installing/installed/activating/activated/redundant

1.6服务工作者的安全限制

受到加载脚本对应源的常规限制;
只能在安全上下文使用(HTTPS);
可以通过window.isSecureContext判断是否是安全上下文

1.7 ServiceWorkerGlobalScope

继承自WorkerGlobalScope,扩展了以下属性和方法

属性或者方法说明
caches返回服务工作者线程的CacheStorage对象
clients返回服务工作者线程的Clients接口,用于访问底层Client对象
registration返回服务工作者线程的ServiceWorkerRegistration对象
skipWaiting()强制服务工作者线程进入活动状态,需要跟Clients.claim()一起使用
fetch()在服务工作者线程内发送常规网络请求,用于在服务工作者线程确实有必要发送实际网络请求(而不是返回缓存值)

服务工作者线程的全局作用域下可以监听的事件

  • 服务工作者线程状态
    • install, 在服务工作者线程进入安装状态时触发, 每个服务工作者线程接受到的第一个事件,每个服务工作者只会调用一次
    • activate, 在服务工作者进入激活或者已激活状态时触发
  • Fetch API
    • fetch, 在服务工作者线程截获来自主页面的fetch()请求时触发
  • Message API
    • message, 在服务工作者通过postMessage()获取数据时触发
  • Notification API
    • notificationclick, 在系统告诉浏览器用户点击了ServiceWorkerRegistration.showNotification()生成的通知时触发
    • notificationclose, 在系统告诉浏览器用户关闭或者取消了显示ServiceWorkerRegistration.showNotification()生成的通知时触发
  • Push API
    • push, 在服务工作者线程接收到推送消息时触发
    • pushsubscriptionchange, 在应用控制以外的因素导致推送状态发生变化时触发

1.8 服务工作者线程作用域的限制

服务工作者线程只能拦截作用域内的客户端发送的请求
作用域是相对于获取服务脚本的路径定义的,若是没有明确指定,则作用域为服务脚本的路径,遵循目录权限模型

扩展服务工作者作用域的方式有两种

  • 通过包含想要的作用域的路径提供服务脚本
  • 给服务脚本的响应添加Service-Worker-Allowed头部,将其值设置为想要的作用域。

2. 服务工作者线程缓存

在服务工作者线程之前,网页缺少缓存网络请求的稳健机制。浏览器一直使用 HTTP 缓存,但 HTTP缓存并没有javascript 暴露编程接口,且其行为是受 JavaScript 运行时外部控制的

  • 服务工作者线程缓存不自动缓存任何请求
  • 服务工作者线程缓存没有到期失效的概念
  • 服务工作者线程缓存必须手动更新和删除
  • 缓存版本必须手动管理
  • 唯一的浏览器强制逐出策略基于服务工作者线程缓存占用的空间
    本质上,服务工作者线程缓存机制是一个双层字典,其中顶级字典的条目映射到二级嵌套字典。顶级字典是 CacheStorage 对象,可以通过服务工作者线程全局作用域的 caches 属性访问。顶级字典中的每个值都是一个 Cache 对象,该对象也是个字典,是 Request 对象到 Response 对象的映射

2.1 CacheStorage对象

类似于异步Map, 字符串到Cache对象的映射,通过全局对象的caches属性暴露
has()/delete()/keys()/等方法与Map类似,但是基于期约实现,获取缓存的方法是open(strKey)

caches.open('v1').then(console.log);// Cache...
caches.has('v1').then(console.log);// true
caches.delete('v1').then(() => caches.has('v1')).then(console.log); // false
caches.keys().then(console.log) // ['v1',...]

match()方法根据Request对象搜索CacheStorage中所有的Cache对象,返回匹配的第一个响应,搜索顺序是CacheStorage.keys()的顺序。

// 创建一个请求键和两个响应值
const request = new Request('');
const response1 = new Response('v1');
const response2 = new Response('v2');
// 用同一个键创建两个缓存对象,最终会先找到 v1
// 因为它排在 caches.keys()输出的前面
caches.open('v1')
.then((v1cache) => v1cache.put(request, response1))
.then(() => caches.open('v2'))
.then((v2cache) => v2cache.put(request, response2))
.then(() => caches.match(request))
.then((response) => response.text())
.then(console.log); // v1

2.2 Cache对象

Cache 键可以是 URL 字符串,也可以是 Request 对象。这些键会映射到Response 对象;
服务工作者线程缓存只考虑缓存 HTTP 的 GET 请求

  • 填充Cache对象的方法

    • put(request, response),在键(Request对象或者URL字符串)和值同时存在的情况下用于添加缓存项
    • add(request), 只有Request对象或者URL时,使用此方法发送fetch()请求,并缓存响应。
    • addAll(requests),对每一项分别调用add()

  • 检索Cache的方法

    • matchAll(request, options),返回期约,解决为匹配缓存中的Response对象数组,可以对结构类似的缓存进行批量操作,如删除缓存在/images目录的值
    • match(request, options), 相当于matchAll(request, options)[0]

  • 缓存是否命中取决于 URL 字符串和/或 Request 对象 URL 是否匹配

  • Cache 对象使用 Request 和 Response 对象的 clone()方法创建副本
    Cache.match()/Cache.matchAll()/CacheStorage.match()支持可选的配置项options参数

配置项说明
cacheName只有CacheStorage.matchAll()支持,设置字符串时,只会匹配Cache键为指定字符串的缓存值
ignoreSearchboolean,匹配url时忽略查询字符串
ignoreMethodboolean, 设置为true时,匹配url时忽略查询请求的HTTP方法
ignoreVary匹配时是否忽略HTTP头部的Vary头部
const request1 = new Request('https://www.foo.com');
const response1 = new Response('fooResponse',  headers: 'Vary': 'Accept' );
const acceptRequest1 = new Request('https://www.foo.com', headers:  'Accept': 'text/json'  );
caches.open('v1')
	.then((cache) => 
 		cache.put(request1, response1)
 			.then(() => cache.match(acceptRequest1))
 			.then(console.log) // undefined
 			.then(() => cache.match(acceptRequest1,  ignoreVary: true ))
 			.then(console.log); // Response 
); 

2.3 最大存储空间

使用 StorageEstimate API 可以近似地获悉有多少空间可用(以字节为单位),以及当前使用了多少空间。此方法只在安全上下文中可用

navigator.storage.estimate().then(console.log); 

3. 服务工作者线程客户端

服务工作者线程会使用 Client 对象跟踪关联的窗口、工作线程或服务工作者线程。服务工作者线程可以通过 Clients 接口访问这些 Client 对象。该接口暴露在全局上下文的 self.clients 属性上。
Client对象支持的属性和方法

属性或者方法说明
id返回客户端的全局唯一标识符
type返回表示客户端类型的字符串window/worker/sharedworker/
url返回客户端的url
postMessage()用于向单个客户端发送消息

Clients接口支持的方法

方法说明
get()通过期约返回Client对象,接受id为参数
matchAll()通过期约返回Client对象, 接受options参数配置项
openWindow(url)在新的窗口打开指定url,给当前服务工作者添加一个新的Client
claim()强制设置当前服务工作者线程以控制作用域中的所有客户端

4 服务工作者线程与一致性

理解服务工作者线程最终用途十分重要:让网页能够模拟原生应用程序。要像原生应用程序一样,服务工作者线程必须支持版本控制(versioning)。

  • 代码一致性
  • 数据一致性

避免有损一致性的现象

  • 服务者工作线程提早失败
  • 服务工作者线程激进更新
  • 未激活的服务工作者线程消极活动
  • 活动的服务工作者线程粘连

5. 服务工作者线程的生命周期

Service Worker 规范定义了 6 种服务工作者线程可能存在的状态:已解析(parsed)、安装中(installing)、已安装(installed)、激活中(activating)、已激活(activated)和已失效(redundant)。
上述状态的每次变化都会在 ServiceWorker 对象上触发 statechange 事件.

6.控制反转和服务工作者线程持久化

  • 虽然专用工作者线程和共享工作者线程是有状态的,但服务工作者线程是无状态的。更具体地说,服务工作者线程遵循控制反转(IoC,Inversion of Control)模式并且是事件驱动的。
  • 工作者线程的生命周期与它所控制的客户端的生命周期无关

7. 通过updateViaCache管理服务文件缓存

为了尽可能传播更新后的服务脚本,常见的解决方案是在响应服务脚本时设置 Cache-Control:max-age=0 头部。这样浏览器就能始终取得最新的脚本文件。但是这个方案只能由服务端控制客户端
为了让客户端能控制自己的更新行为,可以通过 updateViaCache 属性设置客户端对待服务脚本的方式。该属性可以在注册服务工作者线程时定义,可以是如下三个字符串值。

  • imports, 默认值,永远不会被缓存, importScripts()的脚本按照各自的cache-control决定
  • all,无特殊待遇,按照cache-control头部决定是否缓存
  • none, 顶级服务脚本和importScripts()的脚本都不会被缓存
navigator.serviceWorker.register('/serviceWorker.js', 
 updateViaCache: 'none'
); 

8. 强制服务工作者线程操作

update()

9.服务工作者线程消息

postMessage()
客户端先发送消息,服务端接收到的参数中会有一个source指向客户端

/************serviceWorker.js*****************/
self.onmessage = (data, source) => 
	console.log(`service worker heard: $data`
	source.postMessage('bar');

/***************main.js*********************/
navigator.serviceWorker.onmessage = (data) => 
	console.log(`client heard: $data`);

navigator.serviceWorker.reigster('./serviceWorker.js')
.then((registration) => 
	if (registration.active)
		registration.active.postMessage('foo');
	

// main.js中也可以使用controller属性
navigator.serviceWorker.reigster('./serviceWorker.js')
.then(() => 
	if (navigator.serviceWorker.controller)
		navigator.serviceWorker.controller.postMessage('foo');
	

若是要服务端先发送消息,可以使用clients.matchAll()获取客户端

/**********ServiceWorker.js*****/
self.onmessage = (data) => 
 console.log('service worker heard:', data);
;
self.onactivate = () => 
 self.clients.matchAll(includeUncontrolled: true)
 .then((clientMatches) => clientMatches[0].postMessage('foo'));
;
/***********main.js**************/
navigator.serviceWorker.onmessage = (data, source) => 
 console.log('client heard:', data);
 source.postMessage('bar');
;
navigator.serviceWorker.register('./serviceWorker.js') 

10.拦截fetch()事件

这种拦截能力不限于 fetch()方法发送的请求,也能拦截对 JavaScript、CSS、图片和html(包括对主 HTML 文档本身)等资源发送的请求。
FetchEvent 继承自 ExtendableEvent。让服务工作者线程能够决定如何处理 fetch 事件的方法是 event.respondWith()

10.1 从网络返回

self.onfetch = (fetchEvent) => 
	fetchEvent.responseWith(fetch(fetchEvent.request));

10.2 从缓存返回

self.onfetch = (fetchEvent) => 
 fetchEvent.respondWith(caches.match(fetchEvent.request));
; 

10.3 从网络返回,缓存作为后备

self.onfetch = (fetchEvent) => 
	 fetchEvent.respondWith(
		 fetch(fetchEvent.request)
		 .catch(() => caches.match(fetchEvent.request))
	 );
; 

10.4 从缓存返回,网络作为后备

self.onfetch = (fetchEvent) => 
	 fetchEvent.respondWith(
		 caches.match(fetchEvent.request)
		 .then((response) => response || fetch(fetchEvent.request))
	 );
; 

10.5通用后备

self.onfetch = (fetchEvent) => 
	 fetchEvent.respondWith(
	 // 开始执行“从缓存返回,以网络为后备”策略
		 caches.match(fetchEvent.request)
		 .then((response) => response || fetch(fetchEvent.request))
		 .catch(() => caches.match('/fallback.html'))
	 );
;

11.推送通知

为了在 PWA 应用程序中支持推送通知,必须支持以下 4 种行为。

  • 服务工作者线程必须能够显示通知。
  • 服务工作者线程必须能够处理与这些通知的交互。
  • 服务工作者线程必须能够订阅服务器发送的推送通知。
  • 服务工作者线程必须能够处理推送消息,即使应用程序没在前台运行或者根本没打开。

11.1 显示通知

navigator.serviceWorker.register('./serviceWorker.js')
.then((registration) => 
	 Notification.requestPermission()
	 	.then((status) => 
	 		if (status === 'granted') 
	 		registration.showNotification('foo');
	 	
	 );
); 
// 服务工作者内部也可
self.onactivate = () => self.registration.showNotification('bar');

11.2 处理通知事件

self.onnotificationclick = (notification) => 
 	console.log('notification click', notification);
;
self.onnotificationclose = (notification) => 
 	console.log('notification close', notification);
;

11.3 订阅推送事件

navigator.serviceWorker.register('./serviceWorker.js')
.then((registration) => 
	 registration.pushManager.subscribe(
		 applicationServerKey: key, // 来自服务器的公钥
		 userVisibleOnly: true
	 );
); 

// 服务工作者内部
self.onactivate = () => 
	 self.registration.pushManager.subscribe(
		 applicationServerKey: key, // 来自服务器的公钥
		 userVisibleOnly: true
	 );
; 

11.4 处理推送事件

/*************main.js**************/
navigator.serviceWorker.register('./serviceWorker.js')
.then((registration) => 
	 // 请求显示通知的授权
	 Notification.requestPermission()
	 	.then((status) => 
			 if (status === 'granted') 
			 // 如果获得授权,只订阅推送消息
			 registration.pushManager.subscribe(
			 	applicationServerKey: key, // 来自服务器的公钥
			 	userVisibleOnly: true
			 );
		 
	 );
);
/**************ServiceWorker.js**************/
// 收到推送事件后,在通知中以文本形式显示数据
self.onpush = (pushEvent) => 
 // 保持服务工作者线程活动到通知期约解决
 pushEvent.waitUntil(
 	self.registration.showNotification(pushEvent.data.text())
 );
;
// 如果用户单击通知,则打开相应的应用程序页面
self.onnotificationclick = (notification) => 
 clients.openWindow('https://example.com/clicked-notification');
; 

JMeter学习(二十七)Jmeter常见问题

收集工作中JMeter遇到的各种问题
 
1.  JMeter的工作原理是什么?
  向服务器提交请求;从服务器取回请求返回的结果。
 
2.  JMeter的作用?
  JMeter可以用于测试静态或者动态资源的性能(文件、Servlets、Perl脚本、java对象、数据库和查询、ftp服务器或者其他的资源)。JMeter用于模拟在服务器、网络或者其他对象上附加高负载以测试他们提供服务的受压能力,或者分析他们提供的服务在不同负载条件下的总性能情况。你可以用JMeter提供的图形化界面分析性能指标或者在高负载情况下测试服务器/脚本/对象的行为。
 
3.   怎样能看到jmeter提供的脚本范例?
  在\JMeter\jakarta-jmeter-2.0.3\xdocs\demos目录下。
 
4.   怎样设置并发用户数?
  选中可视化界面中左边树的Test Plan节点,单击右键,选择Add-> Thread Group,其中Number of Threads参数用来设置发送请求的用户数目。
 
5.  JMeter的运行指示?
  Jmeter在运行时,右上角有个单选框大小的小框框,运行是该框框为绿色,运行完毕后,该框框为白色。
 
6.  User Parameters的作用是什么?
  提高脚本可用性
 
7.   在result里会出现彩色字体的http response code,说明什么呢?
  Http response code是http返回值,彩色字体较引人注目,可以使用户迅速关注。象绿色的302就说明在这一步骤中,返回值取自本机的catch,而不是server
 
8.   怎样计算Ramp-up period时间?
  Ramp-up period是指每个请求发生的总时间间隔,单位是秒。如果Number of Threads设置为5,而Ramp-up period是10,那么每个请求之间的间隔就是10/5,也就是2秒。Ramp-up period设置为0,就是同时并发请求。
 
9.    Get和Post的区别?
  他们是http协议的2种不同实现方式。Get是指server从Request URL取得所需参数。从result中的request中可以看到,get可以看到参数,但是post是主动向server发送参数,所以一般看不到这些参数的。
 
10. 哪些原因可能导致error的产生?
  a.   Http错误,包括不响应,结果找不到,数据错误等等;
  b.   JMeter本身原因产生的错误。
 
11. 为什么Aggregate Report结果中的Total值不是真正的总和?
  JMeter给结果中total的定义是并不完全指总和,为了方便使用,它的值表现了所在列的代表值,比如min值,它的total就是所在列的最小值。下图就是total在各列所表示的意思。
 
12.  JMeter的Thread Number是提供多个不同用户并发的功能么?
  不是,Thread Number仅仅是指并发数,如果需要实现多个不同用户并发,我们应该采用其它方法,比如通过在jmeter外建立csv文件的方法来实现。
 
13. 同时并发请求时,若需要模拟不同的用户同时向不同的server并发请求,怎样实现呢?
  方法很灵活,我们可以将不同的server在thread里面预先写好。或者预先将固定的变量值写入csv文件,这样还可以方便修改。然后将文件添加到User Parameters。
 
14.   User Parameter中的DUMMY是什么意思?
  当其具体内容是${__CSVRead(${__property(user.dir)}${FILENAME},next())}时用来模拟读文件的下一行。
 
15.   当测试对象在多server间跳转时,应该怎样处理?
  程序运行时,有些http和隐函数会携带另外的server IP,我们可以从他们的返回值中获取。
 
16.   为何测试对象是http和https混杂出现?
  Https是加密协议,为了安全,一般不推荐使用http,但是有些地方,使用https过于复杂或者较难实现,会采用http协议。
 
17.   Http和https的默认端口是什么?
  Apache server (Http)的默认端口是80;
  SSL (Https)的默认端口是443。
 
18.   为何在run时,有些页面失败,但是最后不影响结果?
  原因较多,值得提及的一种是因为主流页面与它不存在依赖关系,所以即使这样的页面出错,也不会影响运行得到正常结果,但是这样会影响到测试的结果以及分析结果。
 
19. 为什么脚本刚开始运行就有错误,其后来的脚本还可运行?
  在Thread Group中有相关设置,如果选择了continue,即使前面的脚本出现错误,整个thread仍会运行直到结束。选择Stop Thread会结束当前thread;选择Stop Test则会结束全部的thread。推荐选项是Stop Thread。
 
20.     在Regular expression_r Extractor会看到Template的值是$1$,这个值是什么意思呢?
  $1$是指取第一个()里面的值。如果Regular expression_r的数值有多个,用这种方法可以避免不必要的麻烦。

21.    Regular expression_r中的(.*)是什么意思?
  那是一个正则表达式(regular expression_r)。’.’等同于sql语言中的’?’,表示可有可无。’*’表示0个或多个。’()’表示需要取值。(.*)表达任意长度的字符串。
 
22.   在读取Regular expression_r时要注意什么?
  一定要保证所取数值的绝对唯一性。
 
23.   怎样才能判断什么样的情况需要添加Regular expression_r Extractor?
  检查Http Request中的Send Parameters,如果有某个参数是其前一个page中所没有给出的,就要到原文件中查找,并添加Regular expression_r Extractor到其前一page的http request中。
 
24.   在自动获取的脚本中有时会出现空的http request,是什么意思呢?
  是因为在获取脚本时有些错误,是脚本工具原因。在run时这种错误不参与运行的。
 
25.   在运行结果中为何有rate为N/A的情况出现?
  可能因为JMeter自身问题造成,再次运行可以得到正确结果。
 
26.   常用http错误代码有哪些?
  400无法解析此请求。
  403禁止访问:访问被拒绝。
  404找不到文件或目录。
  405用于访问该页的HTTP动作未被许可。
  410文件已删除。
  500服务器内部错误。
  501标题值指定的配置没有执行。
  502 Web服务器作为网关或代理服务器时收到无效的响应。
 
27.    Http request中的Send Parameters是指什么?
  是指code中写定的值和自定义变量中得到的值,就是在运行页面时需要的参数。
 
28.    Parameters在页面中是不断传递的么?
  是的。参数再产生后会在页面中一直传递到所需页面。所以我们可以在动态参数产生时捕获它,也可以在所需页面的上一页面捕获。(但是这样可能有错误,最好在产生页面获取)
 
29.   在使用JMeter测试时,是完全模拟用户操作么?造成的结果也和用户操作完全相同么?
  是的。JMeter完全模拟用户操作,所以操作记录会全部写入DB.在运行失败时,可能会产生错误数据,这就取决于脚本检查是否严谨,否则错误数据也会进入DB,给程序运行带来很多麻烦。

以上是关于二十七工作者线程之服务工作者的主要内容,如果未能解决你的问题,请参考以下文章

JMeter学习(二十七)Jmeter常见问题

工作那些事(二十七)项目经理在项目中是什么角色?

React工作记录二十七moment处理日期格式

第二十七天

SpringBoot(二十七)整合Redis之分布式锁

Python学习笔记(二十七)多进程 (进程和线程开始)