微信笔试题:LazyMan实现

实现一个函数LazyMan,执行情况如下:

LazyMan("Hank")
// 输出:Hi! This is Hank!
 
LazyMan("Hank").sleep(10).eat("dinner")
// 输出 Hi! This is Hank!
// 等待10秒..
// 输出 Wake up after 10
// 输出 Eat dinner
 
LazyMan("Hank").eat("dinner").eat("supper")
// 输出 Hi This is Hank!
// 输出 Eat dinner
// 输出 Eat supper
 
LazyMan("Hank").sleepFirst(5).eat("supper")
// 等待5秒
// 输出 Hi This is Hank!
// 输出 Eat supper

在有Promise的情况下,还是很好做的:

function LazyMan(name) {
  function sleep(time) {
    return new Promise(resolve => setTimeout(resolve, time * 1000));
  }

  function addToQueue(queue, item, addToHead) {
    queue[addToHead ? 'unshift' : 'push'](item);
  }

  function resolvePromiseQueue(queue) {
    queue.reduce((last, cur) => {
      return last.then(cur);
    }, Promise.resolve());
  }

  const QUEUE_NAME = Symbol('QueueName'); // 通过Symbol ,使queue成为“私有”成员

  return new class LazyMan {
    constructor() {
      this[QUEUE_NAME] = [() => console.log(`Hi! This is ${name}!`)];
      Promise.resolve().then(resolvePromiseQueue.bind(null, this[QUEUE_NAME]));
    }
    eat(food) {
      addToQueue(this[QUEUE_NAME], () => console.log(`Eat ${food}`));
      return this;
    }
    sleep(time, sleepFirst) {
      addToQueue(
        this[QUEUE_NAME],
        () => sleep(time).then(() => console.log(`Wake up after ${time}`)),
        sleepFirst
      );
      return this;
    }
    sleepFirst(time) {
      this.sleep(time, true);
      return this;
    }
  }();
}

Cookie那些事儿

面试别人的时候,经常碰到自诩精通HTTP协议的人,我一般会丢个问题“HTTP协议是否有状态?”,来试探是否真的“了解”HTTP协议,很多人听到问题就懵逼了,这TM还是我认识的HTTP协议么。偶尔碰到一个说是无状态的,我继续追问“既然HTTP是无状态的,那么我打开一个网站,然后刷新一下,服务器能否知道这次的访问者和上次的访问者是否是同一个客户端?”,然后继续懵逼,转而把答案改为“那应该是有状态的吧?”

所以啊,我们很多时候学技术性东西,还是要追求甚解,知其然,知其所以然。

HTTP协议当然是无状态的,我们在实际场景中是如何区分客户的呢?传统的做法就是利用Cookie来标记用户,用户第一次访问服务器,服务器便在Response Header中添加Set-Cookie,写入标记该用户的唯一标识符,用户二次访问时,将该标识符带回,服务器即可区分该用户是否是之前的用户了。

说到我们本文的主角Cookie,当然不是那么简简单单的事情(事实上是我自己一直以来将其看得过于简单,后来脸被打肿)。

两种方式设置cookie:

  • Set-Cookie(http方式)
    服务端通过在Response Header中添加Set-Cookie,告知客户端如何存储cookie,一般格式:
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>;Max-Age=<non-zero-digit>;Expires=<date>;Path=<path-value>;SameSite=<Strict|Lax>; Secure; HttpOnly
  • document.cookie(代码方式)
    客户端(通常指浏览器)通过javascript直接给document.cookie赋值设置cookie
document.cookie="<cookie-name>=<cookie-value>; Domain=<domain-value>;Max-Age=<non-zero-digit>;Expires=<date>;Path=<path-value>;SameSite=<Strict|Lax>; Secure;"

很明显,除了客户端无法设置HttpOnly外,其他与服务端基本一致。

另外,需要注意的是,书写顺序问题:<cookie-name>=<cookie-value>;必须写在最前面,后面的其他项顺序则可以随意了。这样也就限制了一句设置代码只能设置一个cookie,服务端需要在一次http响应中添加多个set-cookie header来一次性写入多个cookie,客户端则需要调用多次document.cookie赋值才能设置多个cookie。

cookie-name=cookie-value

根据RFC6265,二者可取值情况如下:
– cookie-name 区分大小写,字母、数字以及!#$%&’*+-.^_|`~。
– cookie-value 是可选的。支持字母、数字及!#$%&'()*+-./:<=>?@[]^_{|}`~。关于编码:许多应用会对 cookie 值按照URL编码(URL encoding)规则进行编码,但是按照 RFC 规范,这不是必须的。不过满足规范中对于 cookie-value 所允许使用的字符的要求是有用的。

尽管目前实验了Chrome及Firefox支持更多的字符串,甚至中文,但由于各浏览器、各服务端解析可能存在不一致的情况,还是推荐按标准形式,可以减少古怪问题出现的概率。

  • __Secure-前缀,以 __Secure- 为前缀的 cookie(其中连接符是前缀的一部分),必须与 secure 属性一同设置,同时必须应用于安全页面(即使用 HTTPS 访问的页面)。
  • __Host-前缀,以 __Host- 为前缀的 cookie,必须与 secure 属性一同设置,必须应用于安全页面(即使用 HTTPS 访问的页面),必须不能设置 domain 属性 (也就不会发送给子域),同时 path 属性的值必须为“/”。

__Secure-前缀为例讲解其如何弥补后面即将提到的Secure的不足之处:
由于发往服务端的Cookie只有key-value对,并不包含domain、secure之类的其它信息。当我们用token作为用户标识时,设置domain为www.a.comcookie,并带上了Secure设置,尽管www.a.com为https,但攻击者(中间人)完全可以构造一个非https的网站xxx.a.com诱导用户访问(被劫持的网站,不需要真实存在),然后设置domain为.a.com的cookie,当服务端拿到这两个token时并没法分别哪个是安全的token。
但是我们若不以token作为cookie name,而是使用__Secure-token,攻击者就无计可施了,因为设置__Secure-前缀的cookie必须同时设置Secure,而Secure的设置只能在https链接中,攻击者又无法通过中间人的方式攻击https。

Domain=domain-value

用于设置cookie生效的范围。若不设置该项,浏览器默认将其标记为HostOnly,也就是只有在完全匹配的hostname(注意是hostname,而不是host,也就是说cookie的domain不区分端口)下面方可读取cookie。比如,我在a.com下面设置cookie时没有指定domain,那么我在sub.a.com下面就无法读取到该cookie。

若设置domain,domain的取值只能是当前hostname或父级域,比如,在sub.a.com下面,可选的domain只能是sub.a.com或者a.com,其它取值将会被浏览器忽略。

另外,非常值得注意的是,一些顶级域本来就有两段或多段,比如:.com.cn.usa.gov.edu.cn等等,我们就无法在形如a.com.cn的网站中设置domain为.com.cn的cookie。

除了上面提到的多段顶级域名,一些提供网站服务的第三方平台,比如github.io、sina sae等,提供子域名给各个用户,也就是说w3c.github.ioalibaba.github.io不是同一个网站,那么在w3c.github.io下就不能写domain为.github.io的cookie。

由于上面提到的两种情况存在,Mozilla很早之前就为此建立了一个列表Public Suffix List,专门用于维护顶级域名及类似于github.io这样的第三方网站提供商的域名。据Public Suffix List官网介绍,目前使用该列表的软件包括Firefox、Chrome、IE等主流浏览器及其他一些对域名查询有需求的软件。
如果希望自己的网站添加进该列表,可以来这里提PR(被通过应该需要很多手续)。

Path=path-value

指定一个 URL 路径,这个路径必须出现在要请求的资源的路径中才可以发送 Cookie 首部。字符 %x2F (“/”) 可以解释为文件目录分隔符,此目录的下级目录也满足匹配的条件(例如,如果 path=/docs,那么 “/docs”, “/docs/Web/” 或者 “/docs/Web/HTTP” 都满足匹配的条件)。
另外,由于domain与path是分开解析的,所以a=1;domain=.a.com;path=/x/y,也能在sub.a.com/x/y下被读取。

Expires=date

date应该是符合<day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT格式的字符串,用于标识cookie的过期时间。若不设置该项(下面的max-age也不设置)则cookie是会话级的,浏览器关闭则清除该cookie。

Max-Age=non-zero-digit

non-zero-digit是只在 cookie 失效之前需要经过的秒数。一位或多位非零(1-9)数字(ps:其实0也可以,只是没意义,浏览器可以实现为直接忽略)。一些老的浏览器(ie6、ie7 和 ie8)不支持这个属性。对于其他浏览器来说,假如二者 (指 Expires 和Max-Age) 均存在,那么 Max-Age 优先级更高。

SameSite=Strict|Lax|Unset(默认)

允许服务器设定一则 cookie 不随着跨域请求一起发送,这样可以在一定程度上防范跨站请求伪造攻击(CSRF)。
– Strict 任何时候都不跨域发送cookie,所以当你发现从a网站进入b网站,b网站的登陆状态总是失效的,然后刷新一下又正常了,不要悲伤、不要诧异,检查下你用于标记登陆状态的cookie是不是设置了samesite为strict
– Lax 点击a标签、form的get请求、通过location改变、通过window.open方式打开时会携带cookie,而ajax、script、link、img、iframe、form的post发起的跨域请求则不会携带cookie
– Unset 默认值,任何时候都会跨域携带cookie
samesite目前属于实验性属性,还未进入标准,目前(2018.10.08)兼容性不是太乐观can i use

Secure

一个带有安全属性的 cookie 只有在请求使用SSL和HTTPS协议的时候才会被发送到服务器,同时无法在非https的页面通过document.cookie读取,这可以有效防范SSl strip后cookie失窃。然而,保密或敏感信息永远不要在 HTTP cookie 中存储或传输,因为整个机制从本质上来说都是不安全的,比如前述协议并不意味着所有的信息都是经过加密的。
新版Chrome与Firefox已经不支持在非https的链接中设置Secure了。

HttpOnly

带此标识的cookie只能以http的方式设置于读取,也就是说,通过document.cookie的方式既不能写入也不能读取带HttpOnly标识的cookie,可有效防止xss的方式窃取cookie。事实上,一般后端web框架的sessionid一般都是设置了HttpOnly的。

第三方Cookie

跨域设置cookie的问题也顺带提一下,我已经在此处跌倒几次没长记性了:

首先现代浏览器,在a.com引入b.com的资源(link、img、script等),是可以正常写入与携带cookie的(ps:老版本的IE需要配置P3P),现在的广告追踪就是利用这个功能。

a.com下面通过ajax请求b.com,由于某些不可描述的原因,b.com会通过http的方式向浏览器写入cookie,未做任何处理的情况下,这种方式并不会正常写入cookie,而需要通过浏览器与服务端双方友好协商,你情我愿的情况下方可成功,流程就是:浏览器端发请求的时候告诉服务端,请同意我跨域带cookie给你,我也同意你跨域向我写cookie,如果这时候服务端应答:我愿意。

那么他们就愉快的牵手永远幸福的生活下去了。。

偏了偏了,差点写成言情小说了。

浏览器通过fetch请求时配置{credentials:'include'},通过XMLHttpRequest请求时配置xhr.withCredentialstrue,然后服务端Response Header里面添加 Access-Control-Allow-Credentials: true就OK了。 当然这也还要注意,在配置CORS相关的Response Header时,若Access-Control-Allow-Credentials的值为trueAccess-Control-Allow-Origin则不能设置为*,一来不安全,二来浏览器会报错。

参考链接:

现阶段我所理解的事件循环

流程如图:
event loop

浏览器与Nodejs比较,首先相同点:
微任务都是需要清空后再执行后续任务,也就是说,微任务中产生的其它微任务也会被加入到本轮循环的微任务队列末尾执行。

不同点:
1. Nodejs流程会复杂很多,微任务队列就分了两个:Next Tick Queue与Other Micro Queue,宏任务队列主要分了四个:Timers Queue,IO Callbacks Queue,Check Queue,Close Callbacks Queue

  1. 执行过程中,浏览器每轮事件循环只会从唯一的宏队列中取一个宏任务执行,而在node中存在多个宏队列,且每次执行完一个宏任务队列的所有任务,就会检查一遍两个微任务队列并执行,然后再执行下一个宏任务队列。
    这点就造成了下面的这个代码在浏览器与nodejs中的结果不一样:
setTimeout(()=>{
    console.log(1);
    Promise.resolve().then(()=>console.log(2))
});
setTimeout(()=>console.log(3));
// 浏览器分别输出1,2,3
// Nodejs输出1,3,2   (ps:在shell中执行的话,先把代码缩到一行)
  1. 浏览器端宏任务不仅有定时器、MessageChannel,根据HTML 规范中,还有:
  • 事件回调(非阻塞方式插入dom回调[???]、UI事件回调)
  • XHR 回调
  • IndexDB 数据库操作等 I/O
  • history.back

另外,由于setTimeout(fn,time),这个time就算被设置为0,也会被引擎优化到其它值(一般是4),所以setTimeout与setImmediate如果同时写在同步代码中,他们的执行顺序是无法确定的。

试想一下setTimeout(fn1,0)与setImmediate(fn2)同时出现在同步代码中:
1.若在同步代码执行完成前,fn1被加入到了Timers Queue,那么由于Timers Queue先于Check Queue,这种情况下fn1会先于fn2被执行

2.若在同步代码执行完成前,fn1不能被加入到了Timers Queue(同步代码执行过快,小于4ms),那么由于Timers Queue为空,Check Queue中有fn2,这种情况下fn2会先于fn1被执行

那么如何确保二者的顺序不这么玄乎呢?

我们注意到Timers Queue与Check Queue之间还有个I/O Queue,如果我们将setTimeout与setImmediate同时写于I/O操作的回调中,则可以肯定上面的fn2会早于fn1执行。

最后,有时间还是要多读标准文档

那些年被我们忽视的Vary:Origin

之前刷知乎有看到一篇文章,讲Vary头未配置引起资源缓存错乱的问题,当时只是大概瞄了下,对Vary这个头有了点印象,便没有继续深究。

今天同事碰到一个奇怪的bug,由于chrome65开始禁止通过a标签设置download属性下载跨域资源,官方说明在这里:Block cross-origin ,同事打算先用xhr读取图片,然后转成base64塞到a标签实现下载。

然鹅,理想很丰满,现实挺骨干,实现的时候,在服务端配置Access-Control-Allow-Origin:*后,浏览器端发送xhr并没有得到该响应头,然后浏览器报跨域错误了。

实验发现,我们把该图片单独拧出来在这个浏览器上通过fetch请求依然报错,当换个电脑或者浏览器通过fetch请求时并不会报错,看来是浏览器缓存了什么。

于是乎,尝试了在原来的浏览器上通过在图片地址后面加随机参数,然后fetch,绕过了缓存果然不会有跨域错误。

那么,第一次的不能跨域的缓存是哪来的呢?

通过了解业务流程发现,上传完图片后会有一次预览的过程,而这个预览的过程是通过img标签加载的,服务器并不会返回Access-Control-Allow-Origin:*,而浏览器就缓存了该url及其响应header,下次,就算想通过跨域的方式访问,而浏览器则不会再去问服务器,直接从缓存里面取header告诉其不能跨域,这实际违背了服务器的意愿。

这是一种情况,浏览器缓存了没有跨域信息的头。

前面的文章里面还提到另一种情况:

某资源允许a.com与b.com跨域访问,用户在a.com域名下访问该资源后,浏览器会缓存Access-Control-Allow-Origin:a.com,下次,用户再在b.com域名下访问,浏览器直接从缓存拿到的也是允许a.com跨域,从而报跨域错误。

要解决这个缓存错乱的问题就需要配置我们标题中提到的Vary了。

未配置Vary之前,浏览器的缓存以url为唯一区分,配置Vary后,浏览器会以二者结合作为区分,刚好是我们所需的。

Vary其它取值情况看文档,不具体说明,聊聊可以解决问题的配置,Vary:Origin。

Origin是个有意思的header,(PS:主要是我一直不了解它)。仅在CORS请求与POST请求下才会携带该请求头,也就是说,我们直接通过img标签加载的请求并不会有Origin,而后通过xhr请求,同样的url,由于属于CORS请求,所以会带有Origin,浏览器会将其当成两种不同的资源。前面提到的另一种情况,从不同域名请求同一个资源也是同样的道理。

问题迎刃而解。

不过针对我们项目碰到的问题,要是使用阿里云OSS,高兴为时过早了,因为阿里云后台仅支持配置9种可选的响应header,并不包含Vary。 (┬_┬)

不过,既然发现了问题根源,解决问题就从根源着手。

既然是第一次通过img加载图片造成的缓存“污染”,那么,我们让img加载图片的方式走CORS不就好了么?简直太机智了。。img crossorigin

<img src="xxxx" crossorigin />

nginx学习笔记

变量

自定义变量

set $a 123;
set $b “$a 456”;
set $c “{$b}789”

自定义变量在配置被加载时创建,执行时才被赋值。定义全局有效,甚至可以跨vhost,但值仅在内部跳转间有效。详见

常用内置变量

$request
$request_uri
$uri
$args
$arg_xxx
$header_xxx
$cookie_xxx
更多。。。

!!!任何时候都不应该去覆盖内置变量,有些内置变量被覆盖会导致报错甚至进程崩溃。

map

对于配置CORS的域名白名单,使用map是比if更好的选择,毕竟If Is Evil

map只能写于http模块,但不用担心访问任何location都会计算map,nginx的ngx_map模块实现了只有在该用户变量被实际读取时才会执行,也就是惰性求值。更多参考

# $http_origin是自变量,而$cors_header是因变量
map $http_origin $cors_header {
    default "";
    "~^https?://(localhost|www\.abc\.com" "$http_origin";
}

server {
    ...
    location / {
        add_header Access-Control-Allow-Origin $cors_header;
        try_files $uri @other_location; # This try_files is working
    }
    ...
 }

rewrite

server中的rewrite break和last没什么区别,都会去匹配location,所以没必要用last再发起新的请求,可以留空,以下针对location中的rewrite:

last

用重写后的uri重新去匹配location,默认10次匹配不到报500错误

break

终止rewrite阶段,执行后续的其它阶段,对比last,不会重新去匹配location

redirect

302临时重定向

permanent

301永久重定向

if

由于nginx中的配置是分阶段执行的,而非一般编程语言中的线性执行,所以执行顺序看上去会有些怪异,再加上nginx内部对于if处理的一些机制,让初学者很是不解。看文章:How nginx “location if” works

location匹配

语法:

 location [=|~|~*|^~] /uri/ { … }
 location @name { ... } 
= 表示精确匹配。只有请求的url路径与后面的字符串完全相等时,才会命中。
~ 表示该规则是使用正则定义的,区分大小写。
~* 表示该规则是使用正则定义的,不区分大小写。
^~ 表示如果该符号后面的字符是最佳匹配,采用该规则,不再进行后续的查找。

优先级:
1. 首先先检查使用前缀字符定义的location,选择最长匹配的项并记录下来
2. 如果找到了精确匹配的location,也就是使用了=修饰符的location,结束查找,使用它的配置
3. 然后按顺序查找使用正则定义的location,如果匹配则停止查找,使用它定义的配置 (所以正则匹配是有先后顺序的)
4. 如果没有匹配的正则location,则使用前面记录的最长匹配前缀字符location

demo

location  = / {
  # 精确匹配 / ,主机名后面不能带任何字符串
  [ configuration A ]
}

location  / {
  # 因为所有的地址都以 / 开头,所以这条规则将匹配到所有请求
  # 但是正则和最长字符串会优先匹配
  [ configuration B ]
}

location /documents/ {
  # 匹配任何以 /documents/ 开头的地址,匹配符合以后,记住还要继续往下搜索
  # 只有后面的正则表达式没有匹配到时,这一条才会采用这一条
  [ configuration C ]
}

location ~ /documents/Abc {
  # 匹配任何以 /documents/Abc 开头的地址,匹配符合以后,还要继续往下搜索
  # 只有后面的正则表达式没有匹配到时,这一条才会采用这一条
  [ configuration CC ]
}

location ^~ /images/ {
  # 匹配任何以 /images/ 开头的地址,匹配符合以后,停止往下搜索正则,采用这一条。
  [ configuration D ]
}

location ~* \.(gif|jpg|jpeg)$ {
  # 匹配所有以 gif,jpg或jpeg 结尾的请求
  # 然而,所有请求 /images/ 下的图片会被 config D 处理,因为 ^~ 到达不了这一条正则
  [ configuration E ]
}

location /images/ {
  # 字符匹配到 /images/,继续往下,会发现 ^~ 存在
  [ configuration F ]
}

location /images/abc {
  # 最长字符匹配到 /images/abc,继续往下,会发现 ^~ 存在
  # F与G的放置顺序是没有关系的
  [ configuration G ]
}

location ~ /images/abc/ {
  # 只有去掉 config D 才有效:先最长匹配 config G 开头的地址,继续往下搜索,匹配到这一条正则,采用
    [ configuration H ]
}

root 与 alias

当访问xx.com/a/b时,
配置:

localtion /a/b {
    root html/test;
}

则实际访问地址为:html/test/a/b/index.html。是root + location的结果

配置:

localtion /a/b {
    alias html/test;
}

则实际访问地址为:html/test/index.html。是将location匹配的uri部分替换为alias之后的结果

近期移动端网站碰到的问题及解决方案总结

  1. 在QQ内置浏览器上,一个大致如下布局的页面:
<ul>
  <li><a href="/a">商品a</a></li>
  <li><a href="/b">商品b</a></li>
  <li><a href="/c">商品c</a></li>
  <li><a href="/d">商品d</a></li>
  <li><a href="/e">商品e</a></li>
  <!-- more ...-->
</ul>

上下滑动页面过程中,极易误点入商品的链接中。
猜测原因为:QQ内置浏览器对于click事件的触发判断时间存在问题,导致用户在滑动过程中误触发了click事件。

解决方案:使用模拟的tap事件。Hammer.JS fastclick

2.依然是在QQ内置浏览器上,一个如下布局的页面:

<body>
    <h1>商品展示</h1>
    <ul>
      <li><a href="/a">商品a</a></li>
      <li><a href="/b">商品b</a></li>
      <li><a href="/c">商品c</a></li>
      <li><a href="/d">商品d</a></li>
      <li><a href="/e">商品e</a></li>
      <!-- more ... --> 
    </ul>
</body>
*{
    margin:0;
    padding:0;
}
html,body{
    height:100%
}
h1{
    height:10%;
}
ul{
    height:90%;
    list-style:none;
    overflow:hidden auto;
}

说人话就是:页面的高度刚好100%,所以,页面的纵向滚动条不会出现,上部的h1与下部的ul高度都固定,加起来刚好是100%,然后,ul在子元素多的情况下会出现纵向滚动条。
那么问题来了:ul区域内,上滑页面是没问题的,但是,下滑大概率会触发QQ内置浏览器原生的下拉事件,导致ul滚动失效,浏览器整体被拉下来了。
猜测原因为:用户不断上滑后的首次下滑,浏览器内部会检测整个网页区域是否可滑动,没有的话便会触发原生的事件,导致ul的滚动事件失效

解决方案:在ul之后再加一个元素,高度为1px,使得浏览器刚好出现原生滚动条,但又不影响页面的整体效果

3.用户名片展示简介,前后都需要双引号图片包裹,最多显示三行,超出的话末尾显示省略号,依然还是需要反引号图片,效果如图:


多行文本超出显示省略号,是比较容易的,纯CSS就可以实现:

  word-break: break-all;
  overflow:hidden;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;

那个反引号的显示就需要花点心思了,毕竟我们无法知道文本到底显示了几行,也无法知道文本是否已经到了结尾。
解决思路:三行文本区域高度自适应,设置为相对定位,然后在文本末尾放置一个span标签,背景设置为反引号图片,定位设置为绝对定位,不要设置定位值,然后通过js判断spanbottom值,正常情况,若bottom值为0,说明文本未超出显示区域,若bottom值小于0,说明文本被挤下去了,这时,手动给span设置right为0,bottom为0,就达成目的了。

在线效果

看到一个大牛的实现,脑洞很大啊。

4.safari回退的时候白屏(Vue)
原因未知,反馈该类问题的很多vue#5533mint-ui#937 ,都没有给出问题的根源。
临时解决方案:
①fetch到数据后调用:

Vue.nextTick().then(() => {
    window.scrollTo(0, 1);
    window.scrollTo(0, 0);
});

②利用vue-router

const router = new VueRouter({
  routes: [],
  scrollBehavior () {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve({ x: 0, y: 1 });
      }, 0);
    })
  }
})

③改变布局方式
参考vue#7229

HTTPS握手时序图

https handshake
大图
感觉还有些不明白的地方,等明白了再补上。
源码:

title HTTPS Handshake

Client->Server: 1.协议版本\n2.加密方式列表\n3.支持的压缩算法\n4.random_C
Server->Client: 1.确认协议\n2.确认加密方式\n3.确认压缩方式\n4.random_S\n5.数字证书
note left of Client: 验证证书
note left of Client: 产生Pre-master
note left of Client: enc_key=Fuc(random_C, random_S, Pre-Master)
note left of Client: enc_key加密之前的握手信息得到encrypted_handshake_message
Client->Server: 1.用证书携带的公钥加密后的Pre-Master \n2.参数协商完成通知\n3.encrypted_handshake_message
note right of Server: 用私钥解密得到Pre-Master
note right of Server: enc_key=Fuc(random_C, random_S, Pre-Master)
note right of Server: 用enc_key解密Client传来的encrypted_handshake_message并验证
note right of Server: 用enc_key加密之前的握手信息得到新的encrypted_handshake_message
Server->Client: 1.参数协商完成通知\n2.encrypted_handshake_message
note left of Client:  用enc_key解密Server传来的encrypted_handshake_message并验证

在线绘制地址:https://www.websequencediagrams.com/

jest + vue-test-utils为vue项目写单元测试

1.安装依赖库

npm i -D @vue/test-utils babel-jest  jest  jest-serializer-vue  vue-jest

2.配置jest (package.json)

{   "scripts":{
        "test": "jest"
    },
    "jest": {
    "testURL": "http://localhost",
    "moduleFileExtensions": [
      "js",
      "vue"
    ],
    "moduleNameMapper": {
      "^@/(.*)$": "<rootDir>/src/$1"
    },
    "transform": {
      "^.+\\.js$": "<rootDir>/node_modules/babel-jest",
      ".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"
    },
    "snapshotSerializers": [
      "<rootDir>/node_modules/jest-serializer-vue"
    ]
  }
}

3.配置Babel (.babelrc)

{
    "env": {
    "test": {
      "presets": [["env", { "targets": { "node": "current" } }]]
    }
  }
}

4.项目根目录新建test文件夹
添加测试文件 test.spec.js

import { shallowMount } from '@vue/test-utils';
import ContentLength from '@/components/ContentLength';

describe('Component.ContentLength', () => {
  const wrapper = shallowMount(ContentLength, {
    propsData: {
      content: 'Hello',
      maxLength: 10,
    },
  });

  it('传值正确', () => {
    expect(wrapper.vm.content).toBe('Hello');
    expect(wrapper.vm.maxLength).toBe(10);
  });

  it('未超过长度渲染正确', () => {
    expect(wrapper.find('.exceed').exists()).toBe(false);
  });

  it('超过长度渲染正确', () => {
    wrapper.setProps({
      content: 'Hello World!',
    });
    expect(wrapper.find('.exceed').exists()).toBe(true);
  });

  it('长度计算正确', () => {
    wrapper.setProps({
      content: 'Hello World!',
    });
    expect(wrapper.vm.currentLength).toBe(12);
    wrapper.setProps({
      content: '你好世界!',
      mode: 'content',
    });
    expect(wrapper.vm.currentLength).toBe(10);
  });
});

5.运行测试

npm run test

ubuntu下运行Puppeteer

通过npm安装完puppeteer后若运行报错,可能需要通过apt-get安装一些缺少的依赖

sudo apt install gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget

vps后台运行puppeteer,除了用nohup外,还可以使用screen,常用命令:

screen -ls   # 例出所有已经创建的screen
screen -r  1234  # 连上id为1234的screen
screen -d 1234   # detach id为1234的screen
screen -S abc  #创建一个名叫abc的screen

在screen中通过ctrl+a 然后按d键退出screen

Promise杂谈

最近又花时间理了一下Promise的设计逻辑,有一些小收获整理下。

控制反转(IOC)

如果我们使用Spring(Java)或Laravel(PHP)这样的后端语言框架,我们不可避免的会与控制反转(IOC)、依赖注入(DI)这俩概念打交道,事实上,他们是同一个概念站在不同角度的描述。
我们有如下类,Man继承自People

class People{
    work(){}
}

class Man extends People{}

现有另一个Work类:

class Work{
    constructor(){
        this.people = new Man()
    }
    doWork(){
        this.people.work()
    }
}

这样,我们就把Man写死在了Work里面。
现在我们增加一个Woman,也继承自People:

class Woman extends People{}

我们想让Woman工作的时候,要么就重新写一个类Work1,要么就只能把Man改为Woman,这显然不是一种好的实现。通过控制反转的思想,我们重构Work:

class Work{
    constructor(people){
        if(people instanceof People){
            this.people = people
        }else{
            throw new TypeError('类Work构造函数应该传入类People的实例')
        }
    }
    doWork(){
        this.people.work()
    }
}

然后,我们可以分别在Work中使用ManWoman

    const man = new Man()
    const woman = new Woman()
    new Work(man).work()
    new Work(woman).work()

显然,重构以后,使用ManWoman的控制权交到了使用Work类的人手中,而不是创建Work类的人手中,这就是控制反转。

控制反转的情况,在Javascript代码经常打交道的回调函数处体现得淋漓尽致:

function work(workFn){
    workFn && workFn()
}

work(function(){
    console.log('A')
})

work(function(){
    console.log('B')
})

然而,控制反转被应用于回调这样的场景带来了诸多问题:
1. work会不会真的调用workFn?
2. work会调用几次workFn?
3. work会同步还是异步调用workFn?
4. 传入workFn的参数是否恒定?
5. work内部出现错误或调用workFn出现错误如何处理?
6. 无法组合多个回调逻辑(并联、串联)

出现这么多疑问,终究归到两个字:信任。由于work可能由其他小伙伴提供,甚至极端的可能由第三方提供,这种情况下,我们把回调函数交给work,内心是充满诸多不安的,我们无法信任work是否会如我们期望的方式执行。

而Promise的出现,反转了控制反转,很好的解决了代码的信任问题。

承诺

Promise即承诺,Promise的实现遵循Promise的相关规范(目前为Promise/A+翻译版原文)

Promise会在语法层面上确保promise状态发生改变时,会去调用用户传递给then方法的回调函数,并自动返回一个经Promise.resolve包装的promise,这也就是之所以Promise取名叫Promise的原因吧。换句话说,我们把控制反转反转了,但这个回调也不是由我们自己调用,而是交由一个可靠的系统(Promise)来调用,确保用户的回调函数执行情况是可以被信任的。

Promise只有三种状态:PendingFulfilledRejected,而且状态变化的方向只能是:
Pending->Fulfilled

Pending->Rejected
一旦状态改变,就不会再次发生变化,确保了回调问题2、4,只会调用一次业务逻辑,且值是确定的。
状态改变的同时,Promise内部就会异步调用Promise.prototype.then的回调函数,要么成功,要么失败,此解决了回调问题3,promise的回调永远都是异步的
配合 Promise.race即可处理某些Promise一直处于Pending状况的问题,解决回调问题1
配合Promise.prototype.catch可以很方便的处理promise中的异常情况,解决回调问题5
配合Promise.all可以实现多个Promise的并联,再加上Promise的链式传递特性,解决回调问题6

两个关于Promise的坑

1.Promise.race

Promise.race用于处理一般竞态问题,参数为一个迭代器可迭代对象,该对象中的任意成员状态发生改变,Promise.race返回的promise状态随即发生改变。
然而,当该对象里面没有值的情况下,Promise.race返回的promise将永远处于pending状态。
Promise.all对于空的可迭代对象则永远返回一个resolved状态的promise
此情此景类似于Array.prototype.someArray.prototype.every:
[].some(()=>{}) === false
[].every(()=>{}) === true

2.thenable

Promise.resolve 接受的参数可能为一个promise对象或其他对象,当这个对象不是Promise对象的实例时,若这个对象有then方法或其原型链上面有then方法,我们称这个对象为thenable对象,Promise.resolve内部会执行这个then方法,并传入onFulfilledonRejected两个原生函数,用于调用决定返回的Promise实例的状态。
此策略用于将jQuery.Deferred()返回的对象及其他类Promise对象转成原生Promise,然而,此举导致我们在resolve一个值时需要格外小心,这个值的原型链上面是否有自定义的then方法,比如:

Array.prototype.then = function(){}
const pro = Promise.resolve([1,23])

pro将永远处于pending状态。

要弄瘫一个现代化的网站(大量依托于Promise来实现异步逻辑),只需要一行代码:

Object.prototype.then = function(){}

而且造成的错误原因还很不好查找根源。

PS:
1.若某对象有next方法,则其可能是迭代器
2.若某对象有[Symbol.iterator]方法,则其是可迭代对象

生成器对象既是迭代器,也是可迭代对象,其[Symbol.iterator]方法执行后返回其自身

一个添加千分位分隔符的正则

在只使用正则的情况下,把一个整数用千分位分隔符(半角逗号)分隔,如
原始字符:

198123123123123

变为

198,123,123,123,123

分析:
– 从后往前每三个数字加一个逗号
– 如果恰好是3的倍数个数字,最前面不需要逗号
– 由于强制逗号后面必须跟3位数字,可能需要用到零宽断言找到符合条件的位置

思路:
– 1.可以找空白位置插入逗号
– 2.可以找空白位置的前一位,替换成该位数字+逗号

思路1、

1.找到所有后面接三位数字的位置,不消耗位置

/(?=\d{3})/g

2.这个位置后面的数字个数应该是3的倍数,也就是最终恰好匹配到结尾($)

/(?=(\d{3})+$)/g

3.这个位置不能是开始位置

/(?!^)(?=(\d{3})+$)/g

4.测试

'123123123123123123123123'.replace(/(?!^)(?=(\d{3})+$)/g,',')
'9123123123123123123123123'.replace(/(?!^)(?=(\d{3})+$)/g,',')
'99123123123123123123123123'.replace(/(?!^)(?=(\d{3})+$)/g,',')
思路2、

1.找到所有挨着的三个数字

/\d{3}/

2.重复匹配1中的结果,直到匹配最末尾的结束符($)

/X+$/           // X指代(1)中匹配的3个数字位

3.找到所有这样的位置,它的后面是(2)中找到的位置

/(?=X+$)/g          // X指代(1)中匹配的3个数字位

4.这个位置的前面必须有一个数字(确保在数字位数是3的倍数的时候,不在最开始添加逗号),这个数字我们需要捕获

/(\d)(?=X+$)/g          // X指代(1)中匹配的3个数字位

5.将X换回

/(\d)(?=(\d{3})+$)/g

6.测试

'123123123123123123123123'.replace(/(\d)(?=(?:\d{3})+$)/g,'$1,')
'9123123123123123123123123'.replace(/(\d)(?=(?:\d{3})+$)/g,'$1,')
'99123123123123123123123123'.replace(/(\d)(?=(?:\d{3})+$)/g,'$1,')

考一个匹配手机号的正则

要求:
* 以’86-‘开头时,后面必须是1开头的十一位数字
* 以’其它数字-‘开头时,后面必须是6位以上数字
* 纯数字的手机,必须是1开头的十一位的纯数字

比如:
* 86-12345678901 【合法】
* 86-23456789012 【不合法】
* 86-123456 【不合法】
* 186-123456 【合法】
* 186-12345 【不合法】
* 123456 【不合法】
* 12345678901 【合法】
* 23456789012 【不合法】

其实,主要的难点在于,86与186的区分,’86-‘后面是有’1开头的十一位数字’约束的,而’186-‘则要求是6位以上数字就好了。这种情况用正向否定断言很好解决:


/(^(?!86-)\d+-\d{6,}$)|(^((86-)?1\d{10})$)/

127.0.0.1 vs localhost vs 0.0.0.0

127.0.0.1

被看做是永不会宕掉的地址,因为虚拟地址是不需要和网卡绑定,所以电脑在没有安装网卡时就可以ping通,通常用来检查TCP/IP协议栈是否正常。属于环回地址,那什么是环回地址?

环回地址:主机用于向自身发送通信的一个特殊地址。环回地址为同一台设备上运行的 TCP/IP 应用程序和服务之间相互通信提供了一条捷径。

同一台主机上的两项服务若使用环回地址而非分配的主机地址,就可以绕开 TCP/IP 协议栈的下层。尽管只使用 127.0.0.1 这一个地址,但地址 127.0.0.0 到127.255.255.255 均予以保留。此地址块中的任何地址都将环回到本地主机中。此地址块中的任何地址都绝不会出现在任何网络中。

与 IPv4 一样,IPv6 也提供了特殊环回地址以供测试使用,发送到此地址的数据报会环回到发送设备。不过,IPv6 中用于此功能的地址只有一个,而不是一个地址块。环回地址为”0:0:0:0:0:0:0:1″,一般用零的压缩形式表示为“::1”。

localhost

可以理解为域名,只不过在系统内部默认指向127.0.0.1

0.0.0.0

在不同环境有不同含义:

路由器:通常代表默认路由

服务器:代表本机的所有IPv4地址,若一个主机有两个或多个:192.168.1.2 和 10.1.2.1 等,当本机的服务器监听在 0.0.0.0 上时,通上面两个IP都可以访问到。

IPv6中使用”::”表示。