Quantcast
Channel: IT瘾javascript推荐
Viewing all 148 articles
Browse latest View live

深入理解JS引擎的执行机制

$
0
0

深入理解JS引擎的执行机制

首先,请牢记2点:

(1) JS是单线程语言

(2) JS用过Event Loop是JS的执行机制。想深入了解JS的执行,就等于深入了解JS里的event loop

1.灵魂三问 : JS为什么是单线程的? 为什么需要异步? 单线程又是如何实现异步的呢?

技术的出现,都跟现实世界里的应用场景密切相关的。

同样的,我们就结合现实场景,来回答这三个问题

(1) JS为什么是单线程的?

JS最初被设计用在浏览器中,那么想象一下,如果浏览器中的JS是多线程的。

场景描述:

那么现在有2个进程,process1 process2,由于是多进程的JS,所以他们对同一个dom,同时进行操作

process1 删除了该dom,而process2 编辑了该dom,同时下达2个矛盾的命令,浏览器究竟该如何执行呢?

这样想,JS为什么被设计成单线程应该就容易理解了吧。

(2) JS为什么需要异步?
场景描述:

如果JS中不存在异步,只能自上而下执行,如果上一行解析时间很长,那么下面的代码就会被阻塞。
对于用户而言,阻塞就意味着"卡死",这样就导致了很差的用户体验

所以,JS中存在异步执行。

(3) JS单线程又是如何实现异步的呢?

既然JS是单线程的,只能在一条线程上执行,又是如何实现的异步呢?

是通过的事件循环(event loop),理解了event loop机制,就理解了JS的执行机制

2.JS中的event loop(1)

例1,观察它的执行顺序

    console.log(1)
    setTimeout(function(){
        console.log(2)
    },0)

    console.log(3)
    

运行结果是: 1 3 2

也就是说,setTimeout里的函数并没有立即执行,而是延迟了一段时间,满足一定条件后,才去执行的,这类代码,我们叫异步代码。

所以,这里我们首先知道了JS里的一种分类方式,就是将任务分为: 同步任务和异步任务

图片描述

按照这种分类方式:JS的执行机制是

  • 首先判断JS是同步还是异步,同步就进入主进程,异步就进入event table
  • 异步任务在event table中注册函数,当满足触发条件后,被推入event queue
  • 同步任务进入主线程后一直执行,直到主线程空闲时,才会去event queue中查看是否有可执行的异步任务,如果有就推入主进程中

以上三步循环执行,这就是event loop

所以上面的例子,你是否可以描述它的执行顺序了呢?

console.log(1) 是同步任务,放入主线程里
setTimeout() 是异步任务,被放入event table, 0秒之后被推入event queue里
console.log(3 是同步任务,放到主线程里

当 1、 3在控制条被打印后,主线程去event queue(事件队列)里查看是否有可执行的函数,执行setTimeout里的函数

3.JS中的event loop(2)

所以,上面关于event loop就是我对JS执行机制的理解,直到我遇到了下面这段代码

例2:

 setTimeout(function(){
     console.log('定时器开始啦')
 });
 new Promise(function(resolve){
     console.log('马上执行for循环啦');
     for(var i = 0; i < 10000; i++){
         i == 99 && resolve();
     }
 }).then(function(){
     console.log('执行then函数啦')
 });
 console.log('代码执行结束');

尝试按照,上文我们刚学到的JS执行机制去分析

setTimeout 是异步任务,被放到event table

new Promise 是同步任务,被放到主进程里,直接执行打印 console.log('马上执行for循环啦')

.then里的函数是 异步任务,被放到event table

 console.log('代码执行结束')是同步代码,被放到主进程里,直接执行
 

所以,结果是 【马上执行for循环啦 --- 代码执行结束 --- 定时器开始啦 --- 执行then函数啦】吗?

亲自执行后,结果居然不是这样,而是【马上执行for循环啦 --- 代码执行结束 --- 执行then函数啦 --- 定时器开始啦】

那么,难道是异步任务的执行顺序,不是前后顺序,而是另有规定? 事实上,按照异步和同步的划分方式,并不准确。

而准确的划分方式是:

  • macro-task(宏任务):包括整体代码script,setTimeout,setInterval
  • micro-task(微任务):Promise,process.nextTick

clipboard.png

按照这种分类方式:JS的执行机制是

  • 执行一个宏任务,过程中如果遇到微任务,就将其放到微任务的【事件队列】里
  • 当前宏任务执行完成后,会查看微任务的【事件队列】,并将里面全部的微任务依次执行完

重复以上2步骤,结合event loop(1) event loop(2) ,就是更为准确的JS执行机制了。

尝试按照刚学的执行机制,去分析例2:

首先执行script下的宏任务,遇到setTimeout,将其放到宏任务的【队列】里

遇到 new Promise直接执行,打印"马上执行for循环啦"

遇到then方法,是微任务,将其放到微任务的【队列里】

打印 "代码执行结束"

本轮宏任务执行完毕,查看本轮的微任务,发现有一个then方法里的函数, 打印"执行then函数啦"

到此,本轮的event loop 全部完成。


下一轮的循环里,先执行一个宏任务,发现宏任务的【队列】里有一个 setTimeout里的函数,执行打印"定时器开始啦"

所以最后的执行顺序是【马上执行for循环啦 --- 代码执行结束 --- 执行then函数啦 --- 定时器开始啦】

4. 谈谈setTimeout

这段setTimeout代码什么意思? 我们一般说: 3秒后,会执行setTimeout里的那个函数

 setTimeout(function(){
    console.log('执行了')
 },3000)    

但是这种说并不严谨,准确的解释是: 3秒后,setTimeout里的函数被会推入event queue,而event queue(事件队列)里的任务,只有在主线程空闲时才会执行。

所以只有满足 (1)3秒后 (2)主线程空闲,同时满足时,才会3秒后执行该函数

如果主线程执行内容很多,执行时间超过3秒,比如执行了10秒,那么这个函数只能10秒后执行了


python使用深度神经网络实现识别暹罗与英短

$
0
0

先来上两张图看看那种猫是暹罗?那种猫是英短?
第一张暹罗

第二张英短

你以后是不是可以识别了暹罗和英短了?大概能,好像又不能。这是因为素材太少了,我们看这两张图能分别提取出来短特征太少了。那如果我们暹罗短放100张图,英短放100张图给大家参考,再给一张暹罗或者英短短照片是不是就能识别出来是那种猫了,即使不能完全认出来,是不是也有90%可能是可以猜猜对。那么如果提供500张暹罗500张英短短图片呢,是不是猜对的概率可以更高?
我们是怎么识别暹罗和英短的呢?当然是先归纳两种猫的特征如面部颜色分布、眼睛的颜色等等,当再有一张要识别短图片时,我们就看看面部颜色分布、眼睛颜色是不是可暹罗的特征一致。
同样把识别暹罗和英短的方法教给计算机后,是不是计算机也可以识别这两种猫?
那么计算机是怎么识别图像的呢?先来看一下计算机是怎么存储图像的。

图像在计算机里是一堆按顺序排列的数字,1到255,这是一个只有黑白色的图,但是颜色千变万化离不开三原色——红绿蓝。

这样,一张图片在计算机里就是一个长方体!depth为3的长方体。每一层都是1到255的数字。
让计算机识别图片,就要先让计算机了解它要识别短图片有那些特征。提取图片中的特征就是识别图片要做的主要工作。
下面就该主角出场了,卷及神经网络(Convolutional Neural Network, CNN).
最简单的卷积神经网络就长下面的样子。

分为输入、卷积层、池化层(采样层)、全连接和输出。每一层都将最重要的识别信息进行压缩,并传导至下一层。
卷积层:帮助提取特征,越深(层数多)的卷积神经网络会提取越具体的特征,越浅的网络提取越浅显的特征。
池化层:减少图片的分辨率,减少特征映射。
全连接:扁平化图片特征,将图片当成数组,并将像素值当作预测图像中数值的特征。
•卷积层
卷积层从图片中提取特征,图片在计算机中就上按我们上面说的格式存储的(长方体),先取一层提取特征,怎么提取?使用卷积核(权值)。做如下短操作:

观察左右两个矩阵,矩阵大小从6×6 变成了 4×4,但数字的大小分布好像还是一致的。看下真实图片:

图片好像变模糊了,但这两个图片大小没变是怎么回事呢?其实是用了如下的方式:same padding

在6×6的矩阵周围加了一圈0,再做卷积的时候得到的还是一个6×6的矩阵,为什么加一圈0这个和卷积核大小、步长和边界有关。自己算吧。
上面是在一个6×6的矩阵上使用3X3的矩阵做的演示。在真实的图片上做卷积是什么样的呢?如下图:

对一个32x32x3的图使用10个5x5x3的filter做卷积得到一个28x28x10的激活图(激活图是卷积层的输出).
•池化层
减少图片的分辨率,减少特征映射。怎么减少的呢?
池化在每一个纵深维度上独自完成,因此图像的纵深保持不变。池化层的最常见形式是最大池化。
可以看到图像明显的变小了。如图:

在激活图的每一层的二维矩阵上按2×2提取最大值得到新的图。真实效果如下:

随着卷积层和池化层的增加,对应滤波器检测的特征就更加复杂。随着累积,就可以检测越来越复杂的特征。这里还有一个卷积核优化的问题,多次训练优化卷积核。
下面使用apple的卷积神经网络框架TuriCreate实现区分暹罗和英短。(先说一下我是在win10下装的熬夜把电脑重装了不下3次,系统要有wls,不要用企业版,mac系统和ubuntu系统下安装turicreae比较方便)
首先准备训练用图片暹罗50张,英短50长。测试用图片10张。
上代码:(开发工具anaconda,python 2.7)

数据放到了h盘image目录下,我是在win10下装的ubuntu,所以h盘挂在mnt/下。

test的文件:(x指暹罗,y指英短,这样命名是为了代码里给测试图片区分猫咪类型)

test_data[‘label’] = test_data[‘path’].apply(lambda path: ‘xianluo’ if ‘x’ in path else ‘yingduan’)
第一次结果如下:

训练精度0.955 验证精度才0.75 正确率才0.5。好吧,看来是学习得太少,得上三年高考五年模拟版,将暹罗和英短的图片都增加到100张。在看结果。

这次训练精度就达到0.987了,验证精度1.0,正确率1.0 牛逼了。
看下turicreate识别的结果:

我们实际图片上猫是:(红色为真实的猫的类型-在代码里根据图片名称标记的,绿色为识别出来的猫的类型)

可以看到两者是一致的。牛逼了训练数据才两百张图片,就可以达到这种效果。

python使用深度神经网络实现识别暹罗与英短,首发于 Cobub

程序员练级攻略(2018) 与我的专栏

$
0
0

写极客时间8个月了,我的专栏现在有一定的积累了,今天想自己推荐一下。因为最新的系列《程序员练级攻略(2018)版》正在连载中,而且文章积累量到了我也有比较足的自信向大家推荐我的这个专栏了。推荐就从最新的这一系统的文章开始。

2011年,我在 CoolShell上发表了 《 程序员技术练级攻略》一文,得到了很多人的好评(转载的不算,在我的网站上都有近1000W的访问量了)。并且陆续收到了一些人的反馈,说跟着这篇文章找到了不错的工作。几年过去,也收到了好些邮件和私信,希望我把这篇文章更新一下,因为他们觉得有点落伍了。是的, 老实说,抛开这几年技术的更新迭代不说,那篇文章写得也不算特别系统,同时标准也有点低,当时是给一个想要入门的朋友写的,所以,非常有必要从头更新一下《程序员练级攻略》这一主题

目前,我在我极客时间的专栏上更新《程序员练级攻略(2018版)》。升级版的《程序员练级攻略》会比Coolshell上的内容更多,也更专业。这篇文章有【入门篇】、【修养篇】、【专业基础篇】、【软件设计篇】、【高手成长篇】五大篇章,它们会帮助你从零开始,一步步地,系统地,从陌生到熟悉,到理解掌握,从编码到设计再到架构,从码农到程序员再到工程师再到架构师的一步一步进阶,完成从普通到精通到卓越的完美转身……

这篇文章是我写得最累也是最痛苦的文章,原因如下:

  •   学习路径的梳理。这是一份计算编程相关知识地图,也是一份成长和学习路径。所以有太多的推敲了,知识的路径,体,地图……这让我费了很多工夫,感觉像在编写一本教材一样,即不能太高大上,也不能误人子弟。
  • 新旧知识的取舍。另外,因为我的成长经历中很多技术都成了过去时,所以对于新时代的程序员应该学习新的技术,然后,很多基础技术在今天依然管用,所以,在这点上,哪些要那些不要,也花了我很多的工夫。
  • 文章书籍的推荐。为了推荐最好的学习资料和资源,老实说,我几乎翻遍了整个互联网,进行了大量的阅读和比较。这个过程让我也受益非浅。一开始,这篇文章的大小居然在500K左右,太多的信息就是没有信息,所以在信息的筛选上我花费了很多的工夫,删掉了60%的内容。但是,依然很宠大。

总之,你一定会被这篇文章的内容所吓到的,是的,我就是故意这样做的,因为,这本来就没有什么捷径,也不可能速成,很多知识都是硬骨头,你只能一口一口的啃,我故意这样做就是为了让你不要有“速成”的幻想,也可以轻而一举的吓退那些不想用功不想努力的人

但是,我们也要知道《易经》有云:“ 取法其上,得乎其中,取法其中,得乎其下,取法其下,法不得也”。所以,我这里会给你立个比较高标准,你要努力达到,相信我,就算是达不到,也会比你一开始期望的要高很多……

下面是这份练级攻略的目录,目前只在极客时间上发布,你需要付费阅读(在本文最后有相关的二维码)。

 

那么,除程序员练级攻略外,我还写了哪些内容?下面是迄今为止我所有的文章的目录。你可以在下面看一下相关的目录。这也算是我开收费专栏来8个月给大家的一份答卷吧。我也没有想到,我居然写了这么多的文章,而且对很多人都很有用。

首先是个人成长和经验之谈的东西,在这里的文章还没有完全更新完,未来要更新什么我也不清楚,但是可以呈现出来的内容和方向如下所示,供你参考。对于个人成长中的内容,都是我多年来的心得和体会,从读者的反馈来看是非常不错的,你一定要要阅读的。

分布式系统架构,我一共出了两个系列,一个是分布式系统架构的本质,另一个是设计模式。前者偏概念,后者偏技术。这里旨在让你看到整个分布式系统设计的一个非常系统的蓝图,但是因为在手机端上,不可能写得非常细,所以,会缺失一些细节,这些细节我是故意缺失的,主要是有几方面的原因,

  • 一方面,这是为了阅读的效果,手机上的文章不过长,所以,不能有太多的细节。
  • 另一方面,也是是想留给大家自行学习,而不是一定要我把饭喂到你的嘴里,你才能吃得着。 学习不只是为要答案,而是学方法
  • 最后是我的私心,因为我也在创业,所以,技术细节上东西正是我在做的产品,所以,如果你想了解得更细,你需要和我有更商业合作。

 

区块链的技术专栏本来不在我的写作计划中的,但是因为来问我这方面的技术人太多了,所以,就被问了一系列的文章,这里的文章除了一些技术上的科普,同样有有很多我的观点,你不但可以学到技术,还可以了解一些金融知识和相关的逻辑,我个人觉得这篇文章是让你有独立思考的文章。

我的专栏还在继续,接下来还有一个系列的文章——《从技术到管理》,欢迎关注,也欢迎扫码订阅。

最后友情提示一下:在手机上学习并不是最好的学习方式,也不要在我的专栏上进行学习,把我的专栏当成一个你的助手,当成一个向导,当成一个跳板,真正的学习还是要在线下,专心的,系统地、有讨论地、不断实践地学习,这点希望大家切记!

 

(全文完)


关注CoolShell微信公众账号可以在手机端搜索文章

(转载本站文章请注明作者和出处 酷 壳 – CoolShell,请勿用于任何商业用途)

——=== 访问 酷壳404页面寻找遗失儿童。 ===——

浏览器输入url到发起http请求所经历的过程

$
0
0

用户输入url

当用户输入url,操作系统会将输入事件传递到浏览器中,在这过程中,浏览器可能会做一些预处理,比如 Chrome 会根据历史统计来预估所输入字符对应的网站,例如输入goog,根据之前的历史发现 90% 的概率会访问「www.google.com 」,因此就会在输入回车前就马上开始建立 TCP 链接甚至渲染了。

接着是输入url之后,点击回车,这时浏览器会对 URL 进行检查,首先判断协议,如果是 http 就按照 Web 来处理,另外还会对这个 URL 进行安全检查

安全检查完成之后,在浏览器内核中会先查看缓存,然后设置 UA 等 HTTP 信息,接着调用不同平台下网络请求的方法。

注意:
浏览器和浏览器内核是不同的概念,浏览器指的是 Chrome、Firefox,而浏览器内核则是 Blink、Gecko,浏览器内核只负责渲染,GUI 及网络连接等跨平台工作则是浏览器实现的

http网络请求

通过 DNS 查询 IP;
通过 Socket 发送数据

dns查询ip

DNS,英文是Domain Name System,中文叫域名系统,是Internet的一项服务,他将域名和IP地址相互映射的一个分布式数据库

假设用户在浏览器中输入的是www.google.com,大概过程:

如果输入的是域名,则需要进行dns查询,将域名解析成ip;

进行DNS查询的主机或软件叫做DNS解析器,用户使用的工作站或电脑都属于解析器。域名解析就是利用DNS解析器得到对应IP过程,解析器会向域名服务器进行查询处理。

主要过程如下:

  1. 从浏览器缓存中查找域名www.google.com的IP地址
  2. 在浏览器缓存中没找到,就在操作系统缓存中查找,这一步中也会查找本机的hosts看看有没有对应的域名映射(当然已经缓存在系统DNS缓存中了)
  3. 在系统中也没有的话,就到你的路由器来查找,因为路由器一般也会有自己的DNS缓存

如果以上都没有找到,则继续往下向dns域名服务器查询

  • 用户电脑的解析器向LDNS(也就是Local DNS,互联网服务提供商ISP),发起域名解析请求,查询www.google.com的IP地址,这是一个递归查找过程
  • 在缓存没有命中的情况下,LDNS向根域名服务器.查询www.google.com的IP地址,LDNS的查询过程是一个迭代查询的过程
  • 根告诉LDNS,我不知道www.google.com对应的IP,但是我知道你可以问com域的授权服务器,这个域归他管
  • LDNS向com的授权服务器问www.google.com对应的IP地址
  • com告诉LDNS,我不知道www.google.com对应的IP,但是我知道你可以问google.com域的授权服务器,这个域归他管
  • LDNS向google.com的授权服务器问www.google.com对应的IP地址
  • google.com查询自己的ZONE文件(也称区域文件记录),找到了www.google.com对应的IP地址,返回给LDNS
  • LDNS本地缓存一份记录,把结果返回给用户电脑的解析器
  • 在这之后,用户电脑的解析器拿到结果后,缓存在自己操作系统DNS缓存中,同时返回给浏览器,浏览器依旧会缓存一段时间。

注意
域名查询时有可能是经过了CDN调度器的(如果有cdn存储功能的话)

而且,需要知道dns解析是很耗时的,因此如果解析域名过多,会让首屏加载变得过慢,可以考虑dns-prefetch优化

tcp/ip请求

有了 IP 地址,就可以通过 Socket API 来发送数据了,这时可以选择 TCP 或 UDP 协议。

http本质是tcp协议。

TCP是一种面向有连接的传输层协议。他可以保证两端(发送端和接收端)通信主机之间的通信可达。他能够处理在传输过程中丢包、传输顺序乱掉等异常情况;此外他还能有效利用宽带,缓解网络拥堵。

建立TCP连接一开始都要经过三次握手:

第一次握手,请求建立连接,发送端发送连接请求报文
第二次握手,接收端收到发送端发过来的报文,可知发送端现在要建立联机。然后接收端会向发送端发送一个报文

第三次握手,发送端收到了发送过来的报文,需要检查一下返回的内容是否是正确的;若正确的话,发送端再次发送确认包

在TCP连接建立完成之后就可以发送HTTP请求了。

注意
浏览器对同一个域名有连接数限制,大部分是 6,http1.0中往往一个资源下载就需要对应一个tcp/ip请求,而像 HTTP 2.0 协议尽管只使用一个 TCP 连接来传输数据,但性能反而更好,而且还能实现请求优先级。

参考文章:
http://fex.baidu.com/blog/201...
https://blog.csdn.net/dojiang...
https://segmentfault.com/a/11...

前端监控实践——FMP的智能获取算法

$
0
0

今天来给大家介绍下前端监控中一个特定指标的获取算法,有人会问,为啥就单单讲一个指标?这是因为,目前大部分的指标,比如白屏时间,dom加载时间等等,都能通过现代浏览器提供的各种api去进行较为精确的获取,而今天讲的这个指标,以往获取他的方式只能是通过逻辑埋点去获取它的值,因此在做一些前端监控时,需要根据业务需要去改变页面对这个值的埋点方式,会比较繁琐,恰巧最近刚刚好在做一些前端监控相关的项目,遇到这个问题时就在想,能不能通过一种无须埋点的方式,将这个值给获取到?倒腾了一段时间,终于把算法弄出来了,今天就来给大家介绍下————FMP(first meaning paint) 指标的智能获取算法

什么是FMP

解答这个问题之前,我们先来了解下现代前端监控性能的主要指标统计方法,在2013年之后,标准组织推出了 performance timing api,如下图

这个api统计了浏览器从网址开始导航到 window.onload事件触发的时间点,比如请求开始的时间点—— requestStart,响应结束的时间点—— responseEnd,通过这些时间点我们可以计算出一些对页面加载质量有指导意见的时长,比如以下几个:

  • TTFB : ResponseStart - RequestStart (首包时间,关注网络链路耗时)
  • FPT : ResponseEnd - FetchStart (首次渲染时间 / 白屏时间)
  • TTI : DomInteractive - FetchStart (首次可交付时间)
  • Ready : DomContentLoadEventEnd - FetchStart (加载完成时间)
  • Load : LoadEventStart - FetchStart (页面完全加载时间)

通过这些指标我们可以得到很多有用的web端网页加载信息,建立对网页性能概况

以上的指标可以对网页进行数值化的衡量,但是其实这种衡量只能体现一个视角的性能观点,比如TTFB很快,就能代表用户能够很快的看到页面的内容嘛?这个不一定是成立的,因此人们有开始从用户的视角去分析网页加载的性能情况,将用户看待加载过程,分成了以下几个阶段:

  • 页面是否正在正常加载 (happening)
  • 页面加载的内容是否已经足够(useful)
  • 页面是否已经可以操作了 (usable)
  • 页面是否可以交互,动画是否顺畅(delightful)

而我们今天讨论的 FMP(first meaningful paint),其实就是回答 is it useful,加载的内容是否已经足够,其实这是一个很难被定义的概念。每个网页都有自己的特点,只有开发者和产品能够比较确定哪个元素加载的时间点属于 FMP,今天我们就来讨论一下,如何比较智能的去找出页面那个主要的元素,确定页面的 FMP

成为FMP元素的条件

首先我们可以看看下面的图:

我们可以发现在页面中比较 useful的内容,都是含有信息量比较丰富的,比如图片,视频,动画,另外就是占可视面积较大的,页面中还存在两种形态的内容可以被视为是 useful的,一种是单一的块状元素,另外一种是由多个元素组合而成的大元素,比如视频元素,banner图,这种属于单一的块状元素,而像图片列表,多图像的组合,这种属于元素组合
总结一下成为FMP元素的条件:

  • 体积占比比较大
  • 屏幕内可见占比大
  • 资源加载元素占比更高(img, svg , video , object , embed, canvas)
  • 主要元素可能是多个组成的

算法如何设计

前面介绍了 FMP的概念还有成为 FMP的条件,接下来我们来看看如何设计 FMP获取的算法,按照上面的介绍,我们知道算法分为以下两个部分:

  1. 获取 FMP元素
  2. 计算 FMP元素的加载时间

如果有了解过浏览器加载原理的同学都知道,浏览器在在获取到html页面之后会逐步的对html文档进行解析,遇到javascript会停止html文档的解析工作,执行javascript,执行完继续解析html,直到整个页面解析完成为止。页面除了html文档中的元素加载,可能在执行javascript的时候,会产生动态的元素片段加载,一般来说,首屏元素会在这期间加载。因此我们只需要监控元素的加载和加载的时间点,然后再进行计算。

具体的算法流程如下图

相关的代码链接我已经放在最后面了,下面我会逐步的讲解整个算法流程

我把整个流程分为两个下面两个部分:

  1. 监听元素加载,主要是为了确定普通元素加载的时间点
  2. 确定 FMP元素,计算出最终的 FMP

下面我们按照步骤来分析

初始化监听

  • 可以看到首先我们先执行了 firstSnapshot方法,用于记录在代码执行之前加载的元素的时间点
  • 接下来初始化 MutationObserver,开始监听 document的加载情况,在发生回调的时候,记录下当前到 performance.timing.fetchStart的时间间隔,然后对body的元素进行深度遍历,进行打点,记录是在哪一次回调的时候记录的,如下图

  • 监听的最后我们会将在 window.onload的时候去触发检查是否停止监听的条件,如下图


如果监听的时间超过 LIMIT,或者发生回调的时间间隔已经超过1s中,我们认为页面已经稳定,停止dom元素加载的监听,开始进入计算过程

完成监听,进行元素得分计算

  • 首先前面我们说了,我们的元素对于页面的贡献是不同的,资源加载的元素会对用户视觉感官的影响比较大,比如图片,带背景的元素,视频等等,因此我设计了一套权重系统,如下:


可以看到 svg, img的权重为2, canvas, object, embed, video的权重为4,其他的元素为1,
也就是说,如果一个图片面积为1/2首屏面积,其实他的影响力会和普通元素占满首屏的影响力一样

  • 接着我们回到代码,我们首先会对整个页面进行深度优先遍历搜索,然后对每一个元素进行进行分数计算,如下图


可以看到我们通过 element.getBoundingClientRect获取了元素的位置和大小,然后通过计算 "width * height * weight * 元素在viewport的面积占比"的乘积,确定元素的最终得分,然后将改元素的子元素得分之和与其得分进行比较,去较大值,记录得分元素集

通过计算确定 FMP元素,计算最终 FMP时间

通过上面的步骤我们获取到了一个集合,这个集合是"可视区域内得分最高的元素的集合",我们会对这个集合的得分取均值,然后过滤出在平均分之上的元素集合,然后进行时间计算

可以看到分为两种情况去处理:

  1. weight为1的普通元素,那么我们会通过元素上面的标记,去查询之前保存的时间集合,得到这个元素的加载时间点
  2. weight不为1的元素,那么其实就存在资源加载情况,元素的加载时间其实是资源加载的时间,我们通过 performance.getEntries去获取对应资源的加载时间,获取元素的加载速度

最后去所有元素最大的加载时间值,作为页面加载的 FMP时间

最后

以上就是整个算法的比较具体的流程,可能有人会说,这个东西算出来的就是准确的么?这个算法其实是按照特征分析,特定的规则总结出来的算法, 总体来说还是会比较准确,当然web页面的布局如果比较奇特,可能是会存在一些偏差的情况。也希望大家能够一起来丰富这个东西,为 FMP这个计算方法提出自己的建议
附上代码 链接

从 0 到 1 再到 100, 搭建、编写、构建一个前端项目

$
0
0

从 0 到 1 再到 100, 搭建、编写、构建一个前端项目

1. 选择现成的项目模板还是自己搭建项目骨架

搭建一个前端项目的方式有两种:选择现成的项目模板、自己搭建项目骨架。

选择一个现成项目模板是搭建一个项目最快的方式,模板已经把基本的骨架都搭建好了,你只需要向里面填充具体的业务代码,就可以通过内置的工具与命令构建代码、部署到服务器等。

一般来说,一个现成的项目模板会预定义一定的目录结构、书写方式,在编写项目代码时需要遵循相应的规范;也会内置必要的工具,比如 .editorconfigeslintstylelintprettierhuskylint-staged等;也会内置必要的命令( package.json | scripts),比如 本地开发:npm run dev本地预览:npm run start构建:npm run build部署:npm run deploy等。

社区比较好的项目模板:

这些模板的使用又分为两种:使用 git直接克隆到本地、使用命令行创建。

(使用现有模板构建的项目,可以跳过第 2 ~ 7 步)

1.1 使用 git直接克隆到本地

这是一种真正意义上的模板,可以直接到模板项目的 github主页,就能看到整个骨架,比如 react-boilerplateant-design-provue-element-adminreact-starter-kit

react-boilerplate为例:

克隆到本地:

git clone --depth=1 https://github.com/react-boilerplate/react-boilerplate.git <你的项目名字>

切换到目录下:

cd <你的项目名字>

一般来说,接下来运行 npm run install安装项目的依赖后,就可以运行;有些模板可能有内置的初始化命令,比如 react-boilerplate

npm run setup

启动应用:

npm start

这时,就可以在浏览器中预览应用了。

1.2 使用命令行创建

这种方式需要安装相应的命令,然后由命令来创建项目。

create-react-app为例:

安装命令:

npm install -g create-react-app

创建项目:

create-react-app my-app

运行应用:

cd my-app
npm start

1.3 自己搭建项目骨架

如果你需要定制化,可以选择自己搭建项目的骨架,但这需要开发者对构建工具如 webpacknpmnode及其生态等有相当的了解与应用,才能完美的把控整个项目。

下面将会一步一步的说明如何搭建一个定制化的项目骨架。

2. 选择合适的规范来写代码

js模块化的发展大致有这样一个过程 iife => commonjs/amd => es6,而在这几个规范中:

  • iife: js原生支持,但一般不会直接使用这种规范写代码
  • amd: requirejs定义的加载规范,但随着构建工具的出现,便一般不会用这种规范写代码
  • commonjs: node的模块加载规范,一般会用这种规范写 node程序
  • es6: ECMAScript2015定义的模块加载规范,需要转码后浏览器才能运行

这里推荐使用 es6的模块化规范来写代码,然后用工具转换成 es5的代码,并且 es6的代码可以使用 Tree shaking功能。

参考:

3. 选择合适的构建工具

对于前端项目来说,构建工具一般都选用 webpackwebpack提供了强大的功能和配置化运行。如果你不喜欢复杂的配置,可以尝试 parcel

参考:

4. 确定是单页面应用(SPA)还是多页面应用

因为单页面应用与多页面应用在构建的方式上有很大的不同,所以需要从项目一开始就确定,使用哪种模式来构建项目。

4.1 多页面应用

传统多页面是由后端控制一个 url对应一个 html文件,页面之间的跳转需要根据后端给出的 url跳转到新的 html上。比如:

http://www.example.com/page1 -> path/to/page1.html
http://www.example.com/page2 -> path/to/page2.html
http://www.example.com/page3 -> path/to/page3.html

这种方式的应用,项目里会有多个入口文件,搭建项目的时候就需要对这种多入口模式进行封装。另外,也可以选择一些封装的多入口构建工具,如 lila

4.2 单页面应用

单页面应用(single page application),就是只有一个页面的应用,页面的刷新和内部子页面的跳转完全由 js来控制。

一般单页面应用都有以下几个特点:

  • 本地路由,由 js定义路由、根据路由渲染页面、控制页面的跳转
  • 所有文件只会加载一次,最大限度重用文件,并且极大提升加载速度
  • 按需加载,只有真正使用到页面的时候,才加载相应的文件

这种方式的应用,项目里只有一个入口文件,便无需封装。

参考:

5. 选择合适的前端框架与 UI 库

一般在搭建项目的时候就需要定下前端框架与 UI 库,因为如果后期想更换前端框架和 UI 库,代价是很大的。

比较现代化的前端框架:

一些不错的组合:

参考:

6. 定好目录结构

一个好的目录结构对一个好的项目而言是非常必要的。

一个好的目录结构应当具有以下的一些特点:

  1. 解耦:代码尽量去耦合,这样代码逻辑清晰,也容易扩展
  2. 分块:按照功能对代码进行分块、分组,并能快捷的添加分块、分组
  3. 编辑器友好:需要更新功能时,可以很快的定位到相关文件,并且这些文件应该是很靠近的,而不至于到处找文件

比较推荐的目录结构:

多页面应用

|-- src/ 源代码目录

    |-- page1/ page1 页面的工作空间(与这个页面相关的文件都放在这个目录下)
        |-- index.html html 入口文件
        |-- index.js js 入口文件
        |-- index.(css|less|scss) 样式入口文件
        |-- html/ html 片段目录
        |-- (css|less|scss)/ 样式文件目录
        |-- mock/ 本地 json 数据模拟
        |-- images/ 图片文件目录
        |-- components/ 组件目录(如果基于 react, vue 等组件化框架)
        |-- ...
        
    |-- sub-dir/ 子目录
        |-- page2/ page2 页面的工作空间(内部结构参考 page1)
            |-- ...
        
    |-- ...
    
|-- html/ 公共 html 片段
|-- less/ 公共 less 目录
|-- components/ 公共组件目录
|-- images/ 公共图片目录
|-- mock/ 公共 api-mock 文件目录
|-- ...

单页面应用

|-- src/ 源代码目录
    |-- page1/ page1 页面的工作空间
        |-- index.js 入口文件
        |-- services/ service 目录
        |-- models/ model 目录
        |-- mock/ 本地 json 数据模拟
        |-- images/ 图片文件目录
        |-- components/ 组件目录(如果基于 react, vue 等组件化框架)
        |-- ...
        
    |-- module1/ 子目录
        |-- page2/ page2 页面的工作空间(内部结构参考 page1)
        
    |-- ...
    
|-- images/ 公共图片目录
|-- mock/ 公共 api-mock 文件目录
|-- components/ 公共组件目录   
|-- ...

参考:

7. 搭建一个好的脚手架

搭建一个好的脚手架,能够更好的编写代码、构建项目等。

可以查看 搭建自己的前端脚手架了解一些基本的脚手架文件与工具。

比如:

|-- /                              项目根目录
    |-- src/                       源代码目录
    |-- package.json               npm 项目文件
    |-- README.md                  项目说明文件
    |-- CHANGELOG.md               版本更新记录
    |-- .gitignore                 git 忽略配置文件
    |-- .editorconfig              编辑器配置文件
    |-- .npmrc                     npm 配置文件
    |-- .npmignore                 npm 忽略配置文件
    |-- .eslintrc                  eslint 配置文件
    |-- .eslintignore              eslint 忽略配置文件
    |-- .stylelintrc               stylelint 配置文件
    |-- .stylelintignore           stylelint 忽略配置文件
    |-- .prettierrc                prettier 配置文件
    |-- .prettierignore            prettier 忽略配置文件
    
    |-- .babelrc                   babel 配置文件
    |-- webpack.config.js          webpack 配置文件
    |-- rollup.config.js           rollup 配置文件
    |-- gulpfile.js                gulp 配置文件
    
    |-- test/                      测试目录
    |-- docs/                      文档目录
    |-- jest.config.js             jest 配置文件
    |-- .gitattributes             git 属性配置
  • .editorconfig: 用这个文件来统一不同编辑器的一些配置,比如 tab转 2 个空格、自动插入空尾行、去掉行尾的空格等, http://editorconfig.org
  • eslintstylelintprettier: 规范化代码风格、优化代码格式等
  • huskylint-staged: 在 git提交之前对代码进行审查,否则不予提交
  • .gitlab-ci.yml: gitlab ci持续集成服务

参考:

=================================================

到这里为止,一个基本的项目骨架就算搭建好了。

8. 使用版本控制系统管理源代码(git)

项目搭建好后,需要一个版本控制系统来管理源代码。

比较常用的版本管理工具有 gitsvn,现在一般都用 git

一般开源的项目可以托管到 http://github.com,私人的项目可以托管到 https://gitee.comhttps://coding.net/,而企业的项目则需要自建版本控制系统了。

自建版本控制系统主要有 gitlabgogsgiteagitlab是由商业驱动的,比较稳定,社区版是免费的,一般建议选用这个; gogs, gitea是开源的项目,还不太稳定,期待进一步的更新。

所以, git + gitlab是不错的配合。

9. 编写代码

编写代码时, js选用 es6的模块化规范来写(如果喜欢用 TypeScript,需要加上 ts-loader),样式可以用 lessscsscss来写。

js模块文件时,注释可以使用 jsdoc的规范来写,如果配置相应的工具,可以将这些注释导出接口文档。

因为脚手架里有 huskylint-staged的配合,所以每次提交的代码都会进行代码审查与格式优化,如果不符合规范,则需要把不规范的代码进行修改,然后才能提交到代码仓库中。

比如 console.log(haha.hehe);这段代码就会遇到错误,不予提交:

图片描述

这个功能定义在 package.json中:

{"devDependencies": {             工具依赖
    "babel-eslint": "^8.2.6","eslint": "^4.19.1","husky": "^0.14.3","lint-staged": "^7.2.0","prettier": "^1.14.0","stylelint": "^9.3.0","eslint-config-airbnb": "^17.0.0","eslint-config-prettier": "^2.9.0","eslint-plugin-babel": "^5.1.0","eslint-plugin-import": "^2.13.0","eslint-plugin-jsx-a11y": "^6.1.0","eslint-plugin-prettier": "^2.6.2","eslint-plugin-react": "^7.10.0","stylelint-config-prettier": "^3.3.0","stylelint-config-standard": "^18.2.0"
  },"scripts": {                     可以添加更多命令
    "precommit": "npm run lint-staged","prettier": "prettier --write \"./**/*.{js,jsx,css,less,sass,scss,md,json}\"","eslint": "eslint .","eslint:fix": "eslint . --fix","stylelint": "stylelint \"./**/*.{css,less,sass,scss}\"","stylelint:fix": "stylelint \"./**/*.{css,less,sass,scss}\" --fix","lint-staged": "lint-staged"
  },"lint-staged": {                 对提交的代码进行检查与矫正
    "**/*.{js,jsx}": ["eslint --fix","prettier --write","git add"
    ],"**/*.{css,less,sass,scss}": ["stylelint --fix","prettier --write","git add"
    ],"**/*.{md,json}": ["prettier --write","git add"
    ]
  }
}
  • 如果你想禁用这个功能,可以把 scripts"precommit"改成 "//precommit"
  • 如果你想自定 eslint检查代码的规范,可以修改 .eslintrc, .eslintrc.js等配置文件
  • 如果你想自定 stylelint检查代码的规范,可以修改 .stylelintrc, .stylelintrc.js等配置文件
  • 如果你想忽略某些文件不进行代码检查,可以修改 .eslintignore, .stylelintignore配置文件

参考:

10. 组件化

当项目拥有了一定量的代码之后,就会发现,有些代码是很多页面共用的,于是把这些代码提取出来,封装成一个组件,供各个地方使用。

当拥有多个项目的时候,有些组件需要跨项目使用,一种方式是复制代码到其他项目中,但这种方式会导致组件代码很难维护,所以,一般是用另一种方式:组件化。

组件化就是将组件独立成一个项目,然后在其他项目中安装这个组件,才能使用。

一般组件化会配合私有 npm 仓库一起用。

|-- project1/ 项目1
    |-- package.json
    
|-- project2/ 项目2
    |-- package.json    

|-- component1/ 组件1
    |-- package.json

|-- component2/ 组件2
    |-- package.json

project1中安装 component1, component2组件:

# package.json
{"dependencies": {"component1": "^0.0.1","component2": "^0.0.1"
  }
}
import compoennt1 from 'compoennt1';
import compoennt2 from 'compoennt2';

如果想要了解怎样写好一个组件( npm package),可以参考 从 1 到完美,写一个 js 库、node 库、前端组件库

参考:

11. 测试

测试的目的在于能以最少的人力和时间发现潜在的各种错误和缺陷,这在项目更新、重构等的过程中尤其重要,因为每当更改一些代码后,你并不知道这些代码有没有问题、会不会影响其他的模块。如果有了测试,运行一遍测试用例,就知道更改的代码有没有问题、会不会产生影响。

一般前端测试分以下几种:

  1. 单元测试:模块单元、函数单元、组件单元等的单元块的测试
  2. 集成测试:接口依赖(ajax)、I/O 依赖、环境依赖(localStorage、IndexedDB)等的上下文的集成测试
  3. 样式测试:对样式的测试
  4. E2E 测试:端到端测试,也就是在实际生产环境测试整个应用

一般会用到下面的一些工具:

另外,可以参考 聊聊前端开发的测试

12. 构建

一般单页面应用的构建会有 npm run build的命令来构建项目,然后会输出一个 html文件,一些 js/css/images ...文件,然后把这些文件部署到服务器就可以了。

多页面应用的构建要复杂一些,因为是多入口的,所以一般会封装构建工具,然后通过参数传入多个入口:

npm run build -- page1 page2 dir1/* dir2/all --env test/prod
  • page1, page2确定构建哪些页面; dir1/*, dir2/all某个目录下所有的页面; all, *整个项目所有的页面
  • 有时候可能还会针对不同的服务器环境(比如测试机、正式机)做出不同的构建,可以在后面加参数
  • --用来分割 npm本身的参数与脚本参数,参考 npm - run-script了解详情

多页面应用会导出多个 html文件,需要注意这些导出的 html不要相冲突了。

当然,也可以用一些已经封装好的工具,如 lila

13. 部署

在构建好项目之后,就可以部署到服务器了。

传统的方式,可以用 ftp, sftp等工具,手动传到服务器,但这种方式比较笨拙,不够自动化。

自动化的,可以用一些工具部署到服务器,如 gulpgulp-ssh,当然也可以用一些封装的工具,如 md-synclila

md-sync为例:

npm install md-sync --save-dev

md-sync.config.js配置文件:

module.exports = [
  {
    src: './build/**/*',
    remotePath: 'remotePath',
    server: {
      ignoreErrors: true,
      sshConfig: {
        host: 'host',
        username: 'username',
        password: 'password'
      }
    },
  },
  {
    src: './build/**/*.html',
    remotePath: 'remotePath2',
    server: {
      ignoreErrors: true,
      sshConfig: {
        host: 'host',
        username: 'username',
        password: 'password'
      }
    },
  },
];

package.jsonscripts配置好命令:

"scripts": {"deploy": "md-sync"
}
npm run deploy

另外,一般大型项目会使用持续集成 + shell命令(如 rsync)部署。

14. 持续集成测试、构建、部署

一般大型工程的的构建与测试都会花很长的时间,在本地做这些事情的话就不太实际,这就需要做持续集成测试、构建、部署了。

持续集成工具用的比较多的:

jenkins是通用型的工具,可以与 githubbitbucketgitlab等代码托管服务配合使用,优点是功能强大、插件多、社区活跃,但缺点是配置复杂、使用难度较高。

gitlab cigitlab内部自带的持续集成功能,优点是使用简单、配置简单,但缺点是不及 jenkins功能强大、绑定 gitlab才能使用。

gitlab为例(任务定义在 .gitlab-ci.yml中):

stages:
  - install
  - test
  - build
  - deploy

# 安装依赖
install:
  stage: install
  only:
    - dev
    - master
  script:
    - npm install

# 运行测试用例
test:
  stage: test
  only:
    - dev
    - master
  script:
    - npm run test

# 编译
build:
  stage: build
  only:
    - dev
    - master
  script:
    - npm run clean
    - npm run build

# 部署服务器
deploy:
  stage: deploy
  only:
    - dev
    - master
  script:
    - npm run deploy

以上配置表示只要在 devmaster分支有代码推送,就会进行持续集成,依次运行:

  • npm install
  • npm run test
  • npm run clean
  • npm run build
  • npm run deploy

最终完成部署。如果中间某个命令失败了,将停止接下的命令的运行,并将错误报告给你。

这些操作都在远程机器上完成。

=================================================

到这里为止,基本上完成了一个项目的搭建、编写、构建。

15. 清理服务器上过期文件

现在前端的项目基本上都会用 webpack打包代码,并且文件名( html文件除外)都是 hash化的,如果需要清除过期的文件而又不想把服务器上文件全部删掉然后重新构建、部署,可以使用 sclean来清除过期文件。

16. 收集前端错误反馈

当用户在用线上的程序时,怎么知道有没有出 bug;如果出 bug 了,报的是什么错;如果是 js 报错,怎么知道是那一行运行出了错?

所以,在程序运行时捕捉 js脚本错误,并上报到服务器,是非常有必要的。

这里就要用到 window.onerror了:

window.onerror = (errorMessage, scriptURI, lineNumber, columnNumber, errorObj) => {
  const data = {
    title: document.getElementsByTagName('title')[0].innerText,
    errorMessage,
    scriptURI,
    lineNumber,
    columnNumber,
    detailMessage: (errorObj && errorObj.message) || '',
    stack: (errorObj && errorObj.stack) || '',
    userAgent: window.navigator.userAgent,
    locationHref: window.location.href,
    cookie: window.document.cookie,
  };

  post('url', data); // 上报到服务器
};

线上的 js脚本都是压缩过的,需要用 sourcemap文件与 source-map查看原始的报错堆栈信息,可以参考 细说 js 压缩、sourcemap、通过 sourcemap 查找原始报错信息了解详细信息。

参考:

后续

更多博客,查看 https://github.com/senntyou/blogs

作者: 深予之 (@senntyou)

版权声明:自由转载-非商用-非衍生-保持署名( 创意共享3.0许可证

你不知道的Node.js性能优化,读了之后水平直线上升

$
0
0
本文由云+社区发表

“当我第一次知道要这篇文章的时候,其实我是拒绝的,因为我觉得,你不能叫我写马上就写,我要有干货才行,写一些老生常谈的然后加上好多特技,那个 Node.js 性能啊好像 Duang~ 的一下就上去了,那读者一定会骂我,Node.js 根本没有这样搞性能优化的,都是假的。” ------ 斯塔克·成龙·王


1、使用最新版本的 Node.js

仅仅是简单的升级 Node.js 版本就可以轻松地获得性能提升,因为几乎任何新版本的 Node.js 都会比老版本性能更好,为什么?

Node.js 每个版本的性能提升主要来自于两个方面:

  • V8 的版本更新;
  • Node.js 内部代码的更新优化。

例如最新的 V8 7.1 中,就优化了某些情形下闭包的逃逸分析,让 Array 的一些方法得到了性能提升:

img

Node.js 的内部代码,随着版本的升级,也会有明显的优化,比如下面这个图就是 require的性能随着 Node.js 版本升级的变化:

img

每个提交到 Node.js 的 PR 都会在 review 的时候考虑会不会对当前性能造成衰退。同时也有专门的 benchmarking 团队来监控性能变化,你可以在这里看到 Node.js 的每个版本的性能变化:

https://benchmarking.nodejs.org/

所以,你可以完全对新版本 Node.js 的性能放心,如果发现了任何在新版本下的性能衰退,欢迎提交一个 issue。

如何选择 Node.js 的版本?

这里就要科普一下 Node.js 的版本策略:

  • Node.js 的版本主要分为 Current 和 LTS;
  • Current 就是当前最新的、依然处于开发中的 Node.js 版本;
  • LTS 就是稳定的、会长期维护的版本;
  • Node.js 每六个月(每年的四月和十月)会发布一次大版本升级,大版本会带来一些不兼容的升级;
  • 每年四月发布的版本(版本号为偶数,如 v10)是 LTS 版本,即长期支持的版本,社区会从发布当年的十月开始,继续维护 18 + 12 个月(Active LTS + Maintaince LTS);
  • 每年十月发布的版本(版本号为奇数,例如现在的 v11)只有 8 个月的维护期。

举个例子,现在(2018年11月),Node.js Current 的版本是 v11,LTS 版本是 v10 和 v8。更老的 v6 处于 Maintenace LTS,从明年四月起就不再维护了。去年十月发布的 v9 版本在今年六月结束了维护。

img

对于生产环境而言,Node.js 官方推荐使用最新的 LTS 版本,现在是 v10.13.0。


2、使用 fast-json-stringify加速 JSON 序列化

在 JavaScript 中,生成 JSON 字符串是非常方便的:

const json = JSON.stringify(obj)

但很少人会想到这里竟然也存在性能优化的空间,那就是使用 JSON Schema来加速序列化。

在 JSON 序列化时,我们需要识别大量的字段类型,比如对于 string 类型,我们就需要在两边加上 ",对于数组类型,我们需要遍历数组,把每个对象序列化后,用 ,隔开,然后在两边加上 [],诸如此类等等。

如果已经提前通过 Schema 知道每个字段的类型,那么就不需要遍历、识别字段类型,而可以直接用序列化对应的字段,这就大大减少了计算开销,这就是 fast-json-stringfy的原理。

根据项目中的跑分,在某些情况下甚至可以比 JSON.stringify快接近 10 倍!

img

一个简单的示例:

const fastJson = require('fast-json-stringify')
const stringify = fastJson({
    title: 'Example Schema',
    type: 'object',
    properties: {
        name: { type: 'string' },
        age: { type: 'integer' },
        books: {
            type: 'array',
            items: {
                type: 'string',
                uniqueItems: true
            }
        }
    }
})

console.log(stringify({
    name: 'Starkwang',
    age: 23,
    books: ['C++ Primier', '響け!ユーフォニアム~']
}))
//=> {"name":"Starkwang","age":23,"books":["C++ Primier","響け!ユーフォニアム~"]}

在 Node.js 的中间件业务中,通常会有很多数据使用 JSON 进行,并且这些 JSON 的结构是非常相似的(如果你使用了 TypeScript,更是这样),这种场景就非常适合使用 JSON Schema 来优化。


3、提升 Promise 的性能

Promise 是解决回调嵌套地狱的灵丹妙药,特别是当自从 async/await 全面普及之后,它们的组合无疑成为了 JavaScript 异步编程的终极解决方案,现在大量的项目都已经开始使用这种模式。

但是优雅的语法后面也隐藏着性能损耗,我们可以使用 github 上一个已有的跑分项目进行测试,以下是测试结果:

file                               time(ms)  memory(MB)
callbacks-baseline.js                   380       70.83
promises-bluebird.js                    554       97.23
promises-bluebird-generator.js          585       97.05
async-bluebird.js                       593      105.43
promises-es2015-util.promisify.js      1203      219.04
promises-es2015-native.js              1257      227.03
async-es2017-native.js                 1312      231.08
async-es2017-util.promisify.js         1550      228.74

Platform info:
Darwin 18.0.0 x64
Node.JS 11.1.0
V8 7.0.276.32-node.7
Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz × 4

我们可以从结果中看到,原生 async/await + Promise 的性能比 callback 要差很多,并且内存占用也高得多。对于大量异步逻辑的中间件项目而言,这里的性能开销还是不能忽视的。

通过对比可以发现,性能损耗主要来自于 Promise 对象自身的实现,V8 原生实现的 Promise 比 bluebird 这样第三方实现的 Promise 库要慢很多。而 async/await 语法并不会带来太多的性能损失。

所以对于大量异步逻辑、轻量计算的中间件项目而言,可以在代码中把全局的 Promise 换为 bluebird 的实现:

global.Promise = require('bluebird');

4、正确地编写异步代码

使用 async/await 之后,项目的异步代码会非常好看:

const foo = await doSomethingAsync();
const bar = await doSomethingElseAsync();

但因此,有时我们也会忘记使用 Promise 给我们带来的其它能力,比如 Promise.all()的并行能力:

// bad
async function getUserInfo(id) {
    const profile = await getUserProfile(id);
    const repo = await getUserRepo(id)
    return { profile, repo }
}

// good
async function getUserInfo(id) {
    const [profile, repo] = await Promise.all([
        getUserProfile(id),
        getUserRepo(id)
    ])
    return { profile, repo }
}

还有比如 Promise.any()(此方法不在ES6 Promise标准中,也可以使用标准的 Promise.race()代替),我们可以用它轻松实现更加可靠快速的调用:

async function getServiceIP(name) {
    // 从 DNS 和 ZooKeeper 获取服务 IP,哪个先成功返回用哪个
    // 与 Promise.race 不同的是,这里只有当两个调用都 reject 时,才会抛出错误
    return await Promise.any([
        getIPFromDNS(name),
        getIPFromZooKeeper(name)
    ])
} 

5、优化 V8 GC

关于 V8 的垃圾回收机制,已经有很多类似的文章了,这里就不再重复介绍。推荐两篇文章:

我们在日常开发代码的时候,比较容易踩到下面几个坑:

坑一:使用大对象作为缓存,导致老生代(Old Space)的垃圾回收变慢

示例:

const cache = {}
async function getUserInfo(id) {
    if (!cache[id]) {
        cache[id] = await getUserInfoFromDatabase(id)
    }
    return cache[id]
}

这里我们使用了一个变量 cache作为缓存,加速用户信息的查询,进行了很多次查询后, cache对象会进入老生代,并且会变得无比庞大,而老生代是使用三色标记 + DFS 的方式进行 GC 的,一个大对象会直接导致 GC 花费的时间增长(而且也有内存泄漏的风险)。

解决方法就是:

  • 使用 Redis 这样的外部缓存,实际上像 Redis 这样的内存型数据库非常适合这种场景;
  • 限制本地缓存对象的大小,比如使用 FIFO、TTL 之类的机制来清理对象中的缓存。

坑二:新生代空间不足,导致频繁 GC

这个坑会比较隐蔽。

Node.js 默认给新生代分配的内存是 64MB(64位的机器,后同),但因为新生代 GC 使用的是 Scavenge 算法,所以实际能使用的内存只有一半,即 32MB。

当业务代码频繁地产生大量的小对象时,这个空间很容易就会被占满,从而触发 GC。虽然新生代的 GC 比老生代要快得多,但频繁的 GC 依然会很大地影响性能。极端的情况下,GC 甚至可以占用全部计算时间的 30% 左右。

解决方法就是,在启动 Node.js 时,修改新生代的内存上限,减少 GC 的次数:

node --max-semi-space-size=128 app.js

当然有人肯定会问,新生代的内存是不是越大越好呢?

随着内存的增大,GC 的次数减少,但每次 GC 所需要的时间也会增加,所以并不是越大越好,具体数值需要对业务进行压测 profile 才能确定分配多少新生代内存最好。

但一般根据经验而言, 分配 64MB 或者 128MB 是比较合理的


6、正确地使用 Stream

Stream 是 Node.js 最基本的概念之一,Node.js 内部的大部分与 IO 相关的模块,比如 http、net、fs、repl,都是建立在各种 Stream 之上的。

下面这个经典的例子应该大部分人都知道,对于大文件,我们不需要把它完全读入内存,而是使用 Stream 流式地把它发送出去:

const http = require('http');
const fs = require('fs');

// bad
http.createServer(function (req, res) {
    fs.readFile(__dirname + '/data.txt', function (err, data) {
        res.end(data);
    });
});

// good
http.createServer(function (req, res) {
    const stream = fs.createReadStream(__dirname + '/data.txt');
    stream.pipe(res);
});

在业务代码中合理地使用 Stream 能很大程度地提升性能,当然是但实际的业务中我们很可能会忽略这一点,比如采用 React 服务器端渲染的项目,我们就可以用 renderToNodeStream

const ReactDOMServer require('react-dom/server')
const http = require('http')
const fs = require('fs')
const app = require('./app')

// bad
const server = http.createServer((req, res) => {
    const body = ReactDOMServer.renderToString(app)
    res.end(body)
});

// good
const server = http.createServer(function (req, res) {
    const stream = ReactDOMServer.renderToNodeStream(app)
    stream.pipe(res)
})

server.listen(8000)

使用 pipeline 管理 stream

在过去的 Node.js 中,处理 stream 是非常麻烦的,举个例子:

source.pipe(a).pipe(b).pipe(c).pipe(dest)

一旦其中 source、a、b、c、dest 中,有任何一个 stream 出错或者关闭,会导致整个管道停止,此时我们需要手工销毁所有的 stream,在代码层面这是非常麻烦的。

所以社区出现了 pump这样的库来自动控制 stream 的销毁。而 Node.js v10.0 加入了一个新的特性: stream.pipeline,可以替代 pump 帮助我们更好的管理 stream。

一个官方的例子:

const { pipeline } = require('stream');
const fs = require('fs');
const zlib = require('zlib');

pipeline(
    fs.createReadStream('archive.tar'),
    zlib.createGzip(),
    fs.createWriteStream('archive.tar.gz'),
    (err) => {
        if (err) {
            console.error('Pipeline failed', err);
        } else {
            console.log('Pipeline succeeded');
        }
    }
);

实现自己的高性能 Stream

在业务中你可能也会自己实现一个 Stream,可读、可写、或者双向流,可以参考文档:

Stream 虽然很神奇,但自己实现 Stream 也可能会存在隐藏的性能问题,比如:

class MyReadable extends Readable {
    _read(size) {
        while (null !== (chunk = getNextChunk())) {
            this.push(chunk);
        }
    }
}

当我们调用 new MyReadable().pipe(xxx)时,会把 getNextChunk()所得到的 chunk 都 push 出去,直到读取结束。但如果此时管道的下一步处理速度较慢,就会导致数据堆积在内存中,导致内存占用变大,GC 速度降低。

而正确的做法应该是,根据 this.push()返回值选择正确的行为,当返回值为 false时,说明此时堆积的 chunk 已经满了,应该停止读入。

class MyReadable extends Readable {
    _read(size) {
        while (null !== (chunk = getNextChunk())) {
            if (!this.push(chunk)) {
                return false  
            }
        }
    }
}

这个问题在 Node.js 官方的一篇文章中有详细的介绍: Backpressuring in Streams


7、C++ 扩展一定比 JavaScript 快吗?

Node.js 非常适合 IO 密集型的应用,而对于计算密集的业务,很多人都会想到用编写 C++ Addon 的方式来优化性能。但实际上 C++ 扩展并不是灵丹妙药,V8 的性能也没有想象的那么差。

比如,我在今年九月份的时候把 Node.js 的 net.isIPv6()从 C++ 迁移到了 JS 的实现,让大多数的测试用例都获得了 10%- 250% 不等的性能提升( 具体PR可以看这里)。

JavaScript 在 V8 上跑得比 C++ 扩展还快,这种情况多半发生在与字符串、正则表达式相关的场景,因为 V8 内部使用的正则表达式引擎是 irregexp,这个正则表达式引擎比 boost中自带的引擎( boost::regex)要快得多。

还有一处值得注意的就是,Node.js 的 C++ 扩展在进行类型转换的时候,可能会消耗非常多的性能,如果不注意 C++ 代码的细节,性能会很大地下降。

这里有一篇文章对比了相同算法下 C++ 和 JS 的性能(需翻墙): How to get a performance boost using Node.js native addons。其中值得注意的结论就是,C++ 代码在对参数中的字符串进行转换后( String::Utf8Value转为 std::string),性能甚至不如 JS 实现的一半。只有在使用 NAN 提供的类型封装后,才获得了比 JS 更高的性能。

img

换句话说,C++ 是否比 JavaScript 更加高效需要具体问题具体分析,某些情况下,C++ 扩展不一定就会比原生 JavaScript 更高效。如果你对自己的 C++ 水平不是那么有信心,其实还是建议用 JavaScript 来实现,因为 V8 的性能比你想象的要好得多。


8、使用 node-clinic 快速定位性能问题

说了这么多,有没有什么可以开箱即用,五分钟见效的呢?当然有。

node-clinicNearForm开源的一款 Node.js 性能诊断工具,可以非常快速地定位性能问题。

npm i -g clinic
npm i -g autocannon

使用的时候,先开启服务进程:

clinic doctor -- node server.js

然后我们可以用任何压测工具跑一次压测,比如使用同一个作者的 autocannon(当然你也可以使用 ab、curl 这样的工具来进行压测。):

autocannon http://localhost:3000

压测完毕后,我们 ctrl + c 关闭 clinic 开启的进程,就会自动生成报告。比如下面就是我们一个中间件服务的性能报告:

img

我们可以从 CPU 的使用曲线看出,这个中间件服务的性能瓶颈不在自身内部的计算,而在于 I/O 速度太慢。clinic 也在上面告诉我们检测到了潜在的 I/O 问题。

下面我们使用 clinic bubbleprof来检测 I/O 问题:

clinic bubbleprof -- node server.js

再次进行压测后,我们得到了新的报告:

img

这个报告中,我们可以看到, http.Server在整个程序运行期间,96% 的时间都处于 pending 状态,点开后,我们会发现调用栈中存在大量的 empty frame,也就是说,由于网络 I/O 的限制,CPU 存在大量的空转,这在中间件业务中非常常见,也为我们指明了优化方向不在服务内部,而在服务器的网关和依赖的服务相应速度上。

想知道如何读懂 clinic bubbleprof生成的报告,可以看这里: https://clinicjs.org/bubblepr...

同样,clinic 也可以检测到服务内部的计算性能问题,下面我们做一些“破坏”,让这个服务的性能瓶颈出现在 CPU 计算上。

我们在某个中间件中加入了空转一亿次这样非常消耗 CPU 的“破坏性”代码:

function sleep() {
    let n = 0
    while (n++ < 10e7) {
        empty()
    }
}
function empty() { }

module.exports = (ctx, next) => {
    sleep()
    // ......
    return next()
}

然后使用 clinic doctor,重复上面的步骤,生成性能报告:

img

这就是一个非常典型的 同步计算阻塞了异步队列的“病例”,即主线程上进行了大量的计算,导致 JavaScript 的异步回调没法及时触发,Event Loop 的延迟极高。

对于这样的应用,我们可以继续使用 clinic flame来确定到底是哪里出现了密集计算:

clinic flame -- node app.js

压测后,我们得到了火焰图(这里把空转次数减少到了100万次,让火焰图看起来不至于那么极端):

img

从这张图里,我们可以明显看到顶部的那个大白条,它代表了 sleep函数空转所消耗的 CPU 时间。根据这样的火焰图,我们可以非常轻松地看出 CPU 资源的消耗情况,从而定位代码中哪里有密集的计算,找到性能瓶颈。

此文已由作者授权腾讯云+社区发布


Node.js 指南(HTTP事务的剖析)

$
0
0

HTTP事务的剖析

本指南的目的是让你充分了解Node.js HTTP处理的过程,我们假设你在一般意义上知道HTTP请求的工作方式,无论语言或编程环境如何,我们还假设你对Node.js EventEmittersStreams有点熟悉,如果你对它们不太熟悉,那么值得快速阅读每个API文档。

创建服务器

任何节点Web服务器应用程序在某些时候都必须创建Web服务器对象,这是通过使用 createServer完成的。

const http = require('http');

const server = http.createServer((request, response) => {
  // magic happens here!
});

传递给 createServer的函数对于针对该服务器发出的每个HTTP请求都会调用一次,因此它被称为请求处理程序,实际上, createServer返回的 Server对象是一个 EventEmitter,我们这里只是创建 server对象的简写,然后稍后添加监听器。

const server = http.createServer();
server.on('request', (request, response) => {
  // the same kind of magic happens here!
});

当HTTP请求命中服务器时,node使用一些方便的对象调用请求处理函数来处理事务、 requestresponse,我们很快就会讲到。

为了实际处理请求,需要在 server对象上调用 listen方法,在大多数情况下,你需要传递给 listen的是你希望服务器监听的端口号,还有一些其他选项,请参阅API参考。

方法、URL和Headers

处理请求时,你可能要做的第一件事就是查看方法和URL,以便采取适当的措施,Node通过将方便的属性放在 request对象上来使这相对轻松。

const { method, url } = request;
注意: request对象是 IncomingMessage的一个实例。

这里的 method将始终是普通的HTTP方法/动作, url是没有服务器、协议或端口的完整URL,对于典型的URL,这意味着包括第三个正斜杠后的所有内容。

Headers也不远,它们在自己的 request对象中,被称为 headers

const { headers } = request;
const userAgent = headers['user-agent'];

这里需要注意的是,无论客户端实际发送它们的方式如何,所有headers都仅以小写字母表示,这简化了为任何目的解析headers的任务。

如果重复某些headers,则它们的值将被覆盖或以逗号分隔的字符串连接在一起,具体取决于header,在某些情况下,这可能会有问题,因此 rawHeaders也可用。

请求体

收到 POSTPUT请求时,请求体可能对你的应用程序很重要,获取body数据比访问请求headers更复杂一点,传递给处理程序的 request对象实现了 ReadableStream接口,就像任何其他流一样,可以在其他地方监听或传输此流,我们可以通过监听流的 'data''end'事件来直接从流中获取数据。

每个 'data'事件中发出的块是一个 Buffer,如果你知道它将是字符串数据,那么最好的方法是在数组中收集数据,然后在 'end',连接并对其进行字符串化。

let body = [];
request.on('data', (chunk) => {
  body.push(chunk);
}).on('end', () => {
  body = Buffer.concat(body).toString();
  // at this point, `body` has the entire request body stored in it as a string
});
注意:这看起来有点单调乏味,而且在很多情况下确实如此,幸运的是,在 npm上有像 concat-streambody这样的模块可以帮助隐藏一些逻辑,在走这条路之前,要很好地了解正在发生的事情,这就是为什么你在这里!

关于错误的简单介绍

由于 request对象是一个 ReadableStream,它也是一个 EventEmitter,发生错误时的行为与此类似。

request流中的错误通过在流上发出 'error'事件来呈现,如果你没有该事件的侦听器,则会抛出错误,这可能会导致Node.js程序崩溃。因此,你应该在请求流上添加 'error'侦听器,即使你只是记录它并继续前进(虽然最好发送某种HTTP错误响应,稍后会详细介绍)。

request.on('error', (err) => {
  // This prints the error message and stack trace to `stderr`.
  console.error(err.stack);
});

还有其他方法可以处理这些错误,例如其他抽象和工具,但始终要注意错误可能并且确实会发生,并且你将不得不处理它们。

到目前为止我们已经得到了什么

此时,我们已经介绍了如何创建服务器,并从请求中获取方法、URL、headers和body,当我们将它们放在一起时,它可能看起来像这样:

const http = require('http');

http.createServer((request, response) => {
  const { headers, method, url } = request;
  let body = [];
  request.on('error', (err) => {
    console.error(err);
  }).on('data', (chunk) => {
    body.push(chunk);
  }).on('end', () => {
    body = Buffer.concat(body).toString();
    // At this point, we have the headers, method, url and body, and can now
    // do whatever we need to in order to respond to this request.
  });
}).listen(8080); // Activates this server, listening on port 8080.

如果我们运行此示例,我们将能够接收请求,但不会响应它们,实际上,如果你在Web浏览器中请求此示例,则你的请求将超时,因为没有任何内容被发送回客户端。

到目前为止,我们还没有涉及响应对象,它是 ServerResponse的一个实例,它是一个 WritableStream,它包含许多用于将数据发送回客户端的有用方法,接下来我们将介绍。

HTTP状态码

如果不设置它,响应中的HTTP状态码始终为 200,当然,并非每个HTTP响应都保证这一点,并且在某些时候你肯定希望发送不同的状态码,为此,你可以设置 statusCode属性。

response.statusCode = 404; // Tell the client that the resource wasn't found.

还有其他一些快捷方式,我们很快就会看到。

设置响应Headers

Headers是通过一个名为 setHeader的方便方法设置的。

response.setHeader('Content-Type', 'application/json');
response.setHeader('X-Powered-By', 'bacon');

在响应上设置headers时,大小写对其名称不敏感,如果重复设置标题,则设置的最后一个值是发送的值。

显式发送Header数据

我们已经讨论过的设置headers和状态码的方法假设你正在使用“隐式headers”,这意味着在开始发送body数据之前,你需要依赖node在正确的时间为你发送headers。

如果需要,可以将headers显式写入响应流,为此,有一个名为 writeHead的方法,它将状态码和headers写入流。

response.writeHead(200, {'Content-Type': 'application/json','X-Powered-By': 'bacon'
});

一旦设置了headers(隐式或显式),你就可以开始发送响应数据了。

发送响应体

由于 response对象是 WritableStream,因此将响应体写入客户端只需使用常用的流方法即可。

response.write('<html>');
response.write('<body>');
response.write('<h1>Hello, World!</h1>');
response.write('</body>');
response.write('</html>');
response.end();

流上的 end函数也可以接收一些可选数据作为流上的最后一位数据发送,因此我们可以如下简化上面的示例。

response.end('<html><body><h1>Hello, World!</h1></body></html>');
注意:在开始向body写入数据块之前设置状态和headers很重要,这是有道理的,因为headers在HTTP响应中位于body之前。

关于错误的另一件事

response流也可以发出 'error'事件,在某些时候你也必须处理它,所有关于 request流错误的建议仍然适用于此处。

把它放在一起

现在我们已经了解了如何进行HTTP响应,让我们把它们放在一起,在前面的示例的基础上,我们将创建一个服务器,用于发回用户发送给我们的所有数据,我们将使用 JSON.stringify将该数据格式化为JSON。

const http = require('http');

http.createServer((request, response) => {
  const { headers, method, url } = request;
  let body = [];
  request.on('error', (err) => {
    console.error(err);
  }).on('data', (chunk) => {
    body.push(chunk);
  }).on('end', () => {
    body = Buffer.concat(body).toString();
    // BEGINNING OF NEW STUFF

    response.on('error', (err) => {
      console.error(err);
    });

    response.statusCode = 200;
    response.setHeader('Content-Type', 'application/json');
    // Note: the 2 lines above could be replaced with this next one:
    // response.writeHead(200, {'Content-Type': 'application/json'})

    const responseBody = { headers, method, url, body };

    response.write(JSON.stringify(responseBody));
    response.end();
    // Note: the 2 lines above could be replaced with this next one:
    // response.end(JSON.stringify(responseBody))

    // END OF NEW STUFF
  });
}).listen(8080);

Echo服务器示例

让我们简化前面的示例来进行一个简单的echo服务器,它只是在响应中发送请求中收到的任何数据,我们需要做的就是从请求流中获取数据并将该数据写入响应流,类似于我们之前所做的。

const http = require('http');

http.createServer((request, response) => {
  let body = [];
  request.on('data', (chunk) => {
    body.push(chunk);
  }).on('end', () => {
    body = Buffer.concat(body).toString();
    response.end(body);
  });
}).listen(8080);

现在让我们调整一下,我们只想在以下条件下发送echo:

  • 请求方法是 POST
  • URL是/echo。

在任何其他情况下,我们只想响应 404

const http = require('http');

http.createServer((request, response) => {
  if (request.method === 'POST' && request.url === '/echo') {
    let body = [];
    request.on('data', (chunk) => {
      body.push(chunk);
    }).on('end', () => {
      body = Buffer.concat(body).toString();
      response.end(body);
    });
  } else {
    response.statusCode = 404;
    response.end();
  }
}).listen(8080);
注意:通过这种方式检查URL,我们正在做一种“路由”的形式,其他形式的路由可以像 switch语句一样简单,也可以像 express这样的整个框架一样复杂,如果你正在寻找可以进行路由的东西,请尝试使用 router

现在让我们来简化一下吧,请记住, request对象是 ReadableStreamresponse对象是 WritableStream,这意味着我们可以使用 pipe将数据从一个引导到另一个,这正是我们想要的echo服务器!

const http = require('http');

http.createServer((request, response) => {
  if (request.method === 'POST' && request.url === '/echo') {
    request.pipe(response);
  } else {
    response.statusCode = 404;
    response.end();
  }
}).listen(8080);

我们还没有完成,正如本指南中多次提到的,错误可以而且确实会发生,我们需要处理它们。

为了处理请求流上的错误,我们将错误记录到 stderr并发送 400状态码以指示 Bad Request,但是,在实际应用程序中,我们需要检查错误以确定正确的状态码和消息是什么,与通常的错误一样,你应该查阅错误文档。

在响应中,我们只是将错误记录到 stderr

const http = require('http');

http.createServer((request, response) => {
  request.on('error', (err) => {
    console.error(err);
    response.statusCode = 400;
    response.end();
  });
  response.on('error', (err) => {
    console.error(err);
  });
  if (request.method === 'POST' && request.url === '/echo') {
    request.pipe(response);
  } else {
    response.statusCode = 404;
    response.end();
  }
}).listen(8080);

我们现在已经介绍了处理HTTP请求的大部分基础知识,此时,你应该能够:

  • 使用请求处理程序函数实例化HTTP服务器,并让它侦听端口。
  • request对象中获取headers、URL、方法和body数据。
  • 根据 request对象中的URL和/或其他数据做出路由决策。
  • 通过 response对象发送headers、HTTP状态码和body数据。
  • request对象和 response对象管道数据。
  • 处理 requestresponse流中的流错误。

从这些基础知识中,可以构建用于许多典型用例的Node.js HTTP服务器,这些API提供了许多其他功能,因此请务必阅读有关 EventEmittersStreamsHTTP的API文档。


上一篇:Node.js中的定时器


常见六大Web 安全攻防解析

$
0
0

前言

在互联网时代,数据安全与个人隐私受到了前所未有的挑战,各种新奇的攻击技术层出不穷。如何才能更好地保护我们的数据?本文主要侧重于分析几种常见的攻击的类型以及防御的方法。

想阅读更多优质原创文章请猛戳 GitHub博客

一、XSS

XSS (Cross-Site Scripting),跨站脚本攻击,因为缩写和 CSS重叠,所以只能叫 XSS。跨站脚本攻击是指通过存在安全漏洞的Web网站注册用户的浏览器内运行非法的HTML标签或JavaScript进行的一种攻击。

跨站脚本攻击有可能造成以下影响:

  • 利用虚假输入表单骗取用户个人信息。
  • 利用脚本窃取用户的Cookie值,被害者在不知情的情况下,帮助攻击者发送恶意请求。
  • 显示伪造的文章或图片。

XSS 的原理是恶意攻击者往 Web 页面里插入恶意可执行网页脚本代码,当用户浏览该页之时,嵌入其中 Web 里面的脚本代码会被执行,从而可以达到攻击者盗取用户信息或其他侵犯用户安全隐私的目的

XSS 的攻击方式千变万化,但还是可以大致细分为几种类型。

1.非持久型 XSS(反射型 XSS )

非持久型 XSS 漏洞,一般是通过给别人发送 带有恶意脚本代码参数的 URL,当 URL 地址被打开时,特有的恶意代码参数被 HTML 解析、执行。


举一个例子,比如页面中包含有以下代码:

<select><script>
        document.write(''
            + '<option value=1>'
            +     location.href.substring(location.href.indexOf('default=') + 8)
            + '</option>'
        );
        document.write('<option value=2>English</option>');</script></select>

攻击者可以直接通过 URL (类似: https://xxx.com/xxx?default=<script>alert(document.cookie)</script>) 注入可执行的脚本代码。不过一些浏览器如Chrome其内置了一些XSS过滤器,可以防止大部分反射型XSS攻击。

非持久型 XSS 漏洞攻击有以下几点特征:

  • 即时性,不经过服务器存储,直接通过 HTTP 的 GET 和 POST 请求就能完成一次攻击,拿到用户隐私数据。
  • 攻击者需要诱骗点击,必须要通过用户点击链接才能发起
  • 反馈率低,所以较难发现和响应修复
  • 盗取用户敏感保密信息

为了防止出现非持久型 XSS 漏洞,需要确保这么几件事情:

  • Web 页面渲染的所有内容或者渲染的数据都必须来自于服务端。
  • 尽量不要从 URLdocument.referrerdocument.forms等这种 DOM API 中获取数据直接渲染。
  • 尽量不要使用 eval, new Function()document.write()document.writeln()window.setInterval()window.setTimeout()innerHTMLdocument.createElement()等可执行字符串的方法。
  • 如果做不到以上几点,也必须对涉及 DOM 渲染的方法传入的字符串参数做 escape 转义。
  • 前端渲染的时候对任何的字段都需要做 escape 转义编码。

2.持久型 XSS(存储型 XSS)

持久型 XSS 漏洞,一般存在于 Form 表单提交等交互功能,如文章留言,提交文本信息等,黑客利用的 XSS 漏洞,将内容经正常功能提交进入数据库持久保存,当前端页面获得后端从数据库中读出的注入代码时,恰好将其渲染执行。

举个例子,对于评论功能来说,就得防范持久型 XSS 攻击,因为我可以在评论中输入以下内容

主要注入页面方式和非持久型 XSS 漏洞类似,只不过持久型的不是来源于 URL,referer,forms 等,而是来源于 后端从数据库中读出来的数据。持久型 XSS 攻击不需要诱骗点击,黑客只需要在提交表单的地方完成注入即可,但是这种 XSS 攻击的成本相对还是很高。

攻击成功需要同时满足以下几个条件:

  • POST 请求提交表单后端没做转义直接入库。
  • 后端从数据库中取出数据没做转义直接输出给前端。
  • 前端拿到后端数据没做转义直接渲染成 DOM。

持久型 XSS 有以下几个特点:

  • 持久性,植入在数据库中
  • 盗取用户敏感私密信息
  • 危害面广

3.如何防御

对于 XSS 攻击来说,通常有两种方式可以用来防御。

1) CSP

CSP 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少 XSS 攻击。

通常可以通过两种方式来开启 CSP:

  • 设置 HTTP Header 中的 Content-Security-Policy
  • 设置 meta 标签的方式 <meta http-equiv="Content-Security-Policy">

这里以设置 HTTP Header 来举例:

  • 只允许加载本站资源
Content-Security-Policy: default-src 'self'
  • 只允许加载 HTTPS 协议图片
Content-Security-Policy: img-src https://*
  • 允许加载任何来源框架
Content-Security-Policy: child-src 'none'

如需了解更多属性,请查看 Content-Security-Policy文档

对于这种方式来说,只要开发者配置了正确的规则,那么即使网站存在漏洞,攻击者也不能执行它的攻击代码,并且 CSP 的兼容性也不错。

2) 转义字符

用户的输入永远不可信任的,最普遍的做法就是转义输入输出的内容,对于引号、尖括号、斜杠进行转义

function escape(str) {
  str = str.replace(/&/g, '&amp;')
  str = str.replace(/</g, '&lt;')
  str = str.replace(/>/g, '&gt;')
  str = str.replace(/"/g, '&quto;')
  str = str.replace(/'/g, '&#39;')
  str = str.replace(/`/g, '&#96;')
  str = str.replace(/\//g, '&#x2F;')
  return str
}

但是对于显示富文本来说,显然不能通过上面的办法来转义所有字符,因为这样会把需要的格式也过滤掉。对于这种情况,通常采用白名单过滤的办法,当然也可以通过黑名单过滤,但是考虑到需要过滤的标签和标签属性实在太多,更加推荐使用白名单的方式。

const xss = require('xss')
let html = xss('<h1 id="title">XSS Demo</h1><script>alert("xss");</script>')
// -> <h1>XSS Demo</h1>&lt;script&gt;alert("xss");&lt;/script&gt;
console.log(html)

以上示例使用了 js-xss 来实现,可以看到在输出中保留了 h1 标签且过滤了 script 标签。

3) HttpOnly Cookie。

这是预防XSS攻击窃取用户cookie最有效的防御手段。Web应用程序在设置cookie时,将其属性设为HttpOnly,就可以避免该网页的cookie被客户端恶意JavaScript窃取,保护用户cookie信息。

二、CSRF

CSRF(Cross Site Request Forgery),即跨站请求伪造,是一种常见的Web攻击,它利用用户已登录的身份,在用户毫不知情的情况下,以用户的名义完成非法操作。

1.CSRF攻击的原理

下面先介绍一下CSRF攻击的原理:

完成 CSRF 攻击必须要有三个条件:

  • 用户已经登录了站点 A,并在本地记录了 cookie
  • 在用户没有登出站点 A 的情况下(也就是 cookie 生效的情况下),访问了恶意攻击者提供的引诱危险站点 B (B 站点要求访问站点A)。
  • 站点 A 没有做任何 CSRF 防御

我们来看一个例子: 当我们登入转账页面后,突然眼前一亮 惊现"XXX隐私照片,不看后悔一辈子"的链接,耐不住内心躁动,立马点击了该危险的网站(页面代码如下图所示),但当这页面一加载,便会执行 submitForm这个方法来提交转账请求,从而将10块转给黑客。

2.如何防御

防范 CSRF 攻击可以遵循以下几种规则:

  • Get 请求不对数据进行修改
  • 不让第三方网站访问到用户 Cookie
  • 阻止第三方网站请求接口
  • 请求时附带验证信息,比如验证码或者 Token

1) SameSite

可以对 Cookie 设置 SameSite 属性。该属性表示 Cookie 不随着跨域请求发送,可以很大程度减少 CSRF 的攻击,但是该属性目前并不是所有浏览器都兼容。

2) Referer Check

HTTP Referer是header的一部分,当浏览器向web服务器发送请求时,一般会带上Referer信息告诉服务器是从哪个页面链接过来的,服务器籍此可以获得一些信息用于处理。可以通过检查请求的来源来防御CSRF攻击。正常请求的referer具有一定规律,如在提交表单的referer必定是在该页面发起的请求。所以 通过检查http包头referer的值是不是这个页面,来判断是不是CSRF攻击

但在某些情况下如从https跳转到http,浏览器处于安全考虑,不会发送referer,服务器就无法进行check了。若与该网站同域的其他网站有XSS漏洞,那么攻击者可以在其他网站注入恶意脚本,受害者进入了此类同域的网址,也会遭受攻击。出于以上原因,无法完全依赖Referer Check作为防御CSRF的主要手段。但是可以通过Referer Check来监控CSRF攻击的发生。

3) Anti CSRF Token

目前比较完善的解决方案是加入Anti-CSRF-Token。即发送请求时在HTTP 请求中以参数的形式加入一个随机产生的token,并在服务器建立一个拦截器来验证这个token。服务器读取浏览器当前域cookie中这个token值,会进行校验该请求当中的token和cookie当中的token值是否都存在且相等,才认为这是合法的请求。否则认为这次请求是违法的,拒绝该次服务。

这种方法相比Referer检查要安全很多,token可以在用户登陆后产生并放于session或cookie中,然后在每次请求时服务器把token从session或cookie中拿出,与本次请求中的token 进行比对。由于token的存在,攻击者无法再构造出一个完整的URL实施CSRF攻击。但在处理多个页面共存问题时,当某个页面消耗掉token后,其他页面的表单保存的还是被消耗掉的那个token,其他页面的表单提交时会出现token错误。

4) 验证码

应用程序和用户进行交互过程中,特别是账户交易这种核心步骤,强制用户输入验证码,才能完成最终请求。在通常情况下,验证码够很好地遏制CSRF攻击。 但增加验证码降低了用户的体验,网站不能给所有的操作都加上验证码。所以只能将验证码作为一种辅助手段,在关键业务点设置验证码。

三、点击劫持

点击劫持是一种视觉欺骗的攻击手段。攻击者将需要攻击的网站通过 iframe 嵌套的方式嵌入自己的网页中,并将 iframe 设置为透明,在页面中透出一个按钮诱导用户点击。

1. 特点

  • 隐蔽性较高,骗取用户操作
  • "UI-覆盖攻击"
  • 利用iframe或者其它标签的属性

2. 点击劫持的原理

用户在登陆 A 网站的系统后,被攻击者诱惑打开第三方网站,而第三方网站通过 iframe 引入了 A 网站的页面内容,用户在第三方网站中点击某个按钮(被装饰的按钮),实际上是点击了 A 网站的按钮。
接下来我们举个例子:我在优酷发布了很多视频,想让更多的人关注它,就可以通过点击劫持来实现

iframe {
width: 1440px;
height: 900px;
position: absolute;
top: -0px;
left: -0px;
z-index: 2;
-moz-opacity: 0;
opacity: 0;
filter: alpha(opacity=0);
}
button {
position: absolute;
top: 270px;
left: 1150px;
z-index: 1;
width: 90px;
height:40px;
}</style>
......<button>点击脱衣</button><img src="http://pic1.win4000.com/wallpaper/2018-03-19/5aaf2bf0122d2.jpg"><iframe src="http://i.youku.com/u/UMjA0NTg4Njcy" scrolling="no"></iframe>


从上图可知,攻击者通过图片作为页面背景,隐藏了用户操作的真实界面,当你按耐不住好奇点击按钮以后,真正的点击的其实是隐藏的那个页面的订阅按钮,然后就会在你不知情的情况下订阅了。

3. 如何防御

1)X-FRAME-OPTIONS

X-FRAME-OPTIONS是一个 HTTP 响应头,在现代浏览器有一个很好的支持。这个 HTTP 响应头 就是为了防御用 iframe 嵌套的点击劫持攻击。

该响应头有三个值可选,分别是

  • DENY,表示页面不允许通过 iframe 的方式展示
  • SAMEORIGIN,表示页面可以在相同域名下通过 iframe 的方式展示
  • ALLOW-FROM,表示页面可以在指定来源的 iframe 中展示

2)JavaScript 防御

对于某些远古浏览器来说,并不能支持上面的这种方式,那我们只有通过 JS 的方式来防御点击劫持了。

<head><style id="click-jack">
    html {
      display: none !important;
    }</style></head><body><script>
    if (self == top) {
      var style = document.getElementById('click-jack')
      document.body.removeChild(style)
    } else {
      top.location = self.location
    }</script></body>

以上代码的作用就是当通过 iframe 的方式加载页面时,攻击者的网页直接不显示所有内容了。

四、URL跳转漏洞

定义:借助未验证的URL跳转,将应用程序引导到不安全的第三方区域,从而导致的安全问题。

1.URL跳转漏洞原理

黑客利用URL跳转漏洞来诱导安全意识低的用户点击,导致用户信息泄露或者资金的流失。其原理是黑客构建恶意链接(链接需要进行伪装,尽可能迷惑),发在QQ群或者是浏览量多的贴吧/论坛中。
安全意识低的用户点击后,经过服务器或者浏览器解析后,跳到恶意的网站中。

恶意链接需要进行伪装,经常的做法是熟悉的链接后面加上一个恶意的网址,这样才迷惑用户。

诸如伪装成像如下的网址,你是否能够识别出来是恶意网址呢?

http://gate.baidu.com/index?act=go&url=http://t.cn/RVTatrd
http://qt.qq.com/safecheck.html?flag=1&url=http://t.cn/RVTatrd
http://tieba.baidu.com/f/user/passport?jumpUrl=http://t.cn/RVTatrd

2.实现方式:

  • Header头跳转
  • Javascript跳转
  • META标签跳转

这里我们举个Header头跳转实现方式:

<?php
$url=$_GET['jumpto'];
header("Location: $url");
?>
http://www.wooyun.org/login.php?jumpto=http://www.evil.com

这里用户会认为 www.wooyun.org都是可信的,但是点击上述链接将导致用户最终访问 www.evil.com这个恶意网址。

3.如何防御

1)referer的限制

如果确定传递URL参数进入的来源,我们可以通过该方式实现安全限制,保证该URL的有效性,避免恶意用户自己生成跳转链接

2)加入有效性验证Token

我们保证所有生成的链接都是来自于我们可信域的,通过在生成的链接里加入用户不可控的Token对生成的链接进行校验,可以避免用户生成自己的恶意链接从而被利用,但是如果功能本身要求比较开放,可能导致有一定的限制。

五、SQL注入

SQL注入是一种常见的Web安全漏洞,攻击者利用这个漏洞,可以访问或修改数据,或者利用潜在的数据库漏洞进行攻击。

1.SQL注入的原理

我们先举一个万能钥匙的例子来说明其原理:

<form action="/login" method="POST"><p>Username: <input type="text" name="username" /></p><p>Password: <input type="password" name="password" /></p><p><input type="submit" value="登陆" /></p></form>

后端的 SQL 语句可能是如下这样的:

let querySQL = `
    SELECT *
    FROM user
    WHERE username='${username}'
    AND psw='${password}'
`;
// 接下来就是执行 sql 语句...

这是我们经常见到的登录页面,但如果有一个恶意攻击者输入的用户名是 admin' --,密码随意输入,就可以直接登入系统了。why! ----这就是SQL注入

我们之前预想的SQL 语句是:

SELECT * FROM user WHERE username='admin' AND psw='password'

但是恶意攻击者用奇怪用户名将你的 SQL 语句变成了如下形式:

SELECT * FROM user WHERE username='admin' --' AND psw='xxxx'

在 SQL 中, ' --是闭合和注释的意思,-- 是注释后面的内容的意思,所以查询语句就变成了:

SELECT * FROM user WHERE username='admin'

所谓的万能密码,本质上就是SQL注入的一种利用方式。

一次SQL注入的过程包括以下几个过程:

  • 获取用户请求参数
  • 拼接到代码当中
  • SQL语句按照我们构造参数的语义执行成功

**SQL注入的必备条件:
1.可以控制输入的数据
2.服务器要执行的代码拼接了控制的数据**。

我们会发现SQL注入流程中与正常请求服务器类似,只是黑客控制了数据,构造了SQL查询,而正常的请求不会SQL查询这一步, SQL注入的本质:数据和代码未分离,即数据当做了代码来执行。

2.危害

  • 获取数据库信息

    • 管理员后台用户名和密码
    • 获取其他数据库敏感信息:用户名、密码、手机号码、身份证、银行卡信息……
    • 整个数据库:脱裤
  • 获取服务器权限
  • 植入Webshell,获取服务器后门
  • 读取服务器敏感文件

3.如何防御

  • 严格限制Web应用的数据库的操作权限,给此用户提供仅仅能够满足其工作的最低权限,从而最大限度的减少注入攻击对数据库的危害
  • 后端代码检查输入的数据是否符合预期,严格限制变量的类型,例如使用正则表达式进行一些匹配处理。
  • 对进入数据库的特殊字符(',",,<,>,&,*,; 等)进行转义处理,或编码转换。基本上所有的后端语言都有对字符串进行转义处理的方法,比如 lodash 的 lodash._escapehtmlchar 库。
  • 所有的查询语句建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中,即不要直接拼接 SQL 语句。例如 Node.js 中的 mysqljs 库的 query 方法中的 ? 占位参数。

六、OS命令注入攻击

OS命令注入和SQL注入差不多,只不过SQL注入是针对数据库的,而OS命令注入是针对操作系统的。OS命令注入攻击指通过Web应用,执行非法的操作系统命令达到攻击的目的。只要在能调用Shell函数的地方就有存在被攻击的风险。倘若调用Shell时存在疏漏,就可以执行插入的非法命令。

命令注入攻击可以向Shell发送命令,让Windows或Linux操作系统的命令行启动程序。也就是说,通过命令注入攻击可执行操作系统上安装着的各种程序。

1.原理


黑客构造命令提交给web应用程序,web应用程序提取黑客构造的命令,拼接到被执行的命令中,因黑客注入的命令打破了原有命令结构,导致web应用执行了额外的命令,最后web应用程序将执行的结果输出到响应页面中。

我们通过一个例子来说明其原理,假如需要实现一个需求:用户提交一些内容到服务器,然后在服务器执行一些系统命令去返回一个结果给用户

// 以 Node.js 为例,假如在接口中需要从 github 下载用户指定的 repo
const exec = require('mz/child_process').exec;
let params = {/* 用户输入的参数 */};
exec(`git clone ${params.repo} /some/path`);

如果 params.repo传入的是 https://github.com/admin/admin.github.io.git确实能从指定的 git repo 上下载到想要的代码。
但是如果 params.repo传入的是 https://github.com/xx/xx.git && rm -rf /* &&恰好你的服务是用 root 权限起的就糟糕了。

2.如何防御

  • 后端对前端提交内容进行规则限制(比如正则表达式)。
  • 在调用系统命令前对所有传入参数进行命令行参数转义过滤。
  • 不要直接拼接命令语句,借助一些工具做拼接、转义预处理,例如 Node.js 的 shell-escape npm

给大家推荐一个好用的BUG监控工具 Fundebug,欢迎免费试用!

参考资料

Puppeteer前端自动化测试实践

$
0
0
本篇内容将记录并介绍使用Puppeteer进行自动化网页测试,并依靠约定来避免反复修改测试用例的方案。主要解决页面众多时,修改代码导致的牵连错误无法被发现的运行时问题。文章首发于 个人博客

起因

目前我们在持续开发着一个几十个页面,十万+行代码的项目,随着产品的更迭,总会出现这样的问题。在对某些业务逻辑或者功能进行添加或者修改的时候(尤其是通用逻辑),这些通用的逻辑或者组件往往会牵扯到一些其他地方的问题。由于测试人员受限,我们很难在完成一个模块单元后,对所有功能重新测试一遍。
同时,由于环境及数据的区别,(以及在开发过程中对代码完备性的疏忽),代码会在某些特殊数据的解析和和展示上出现问题,在开发和测试中很难去发现。总的来说,我们希望有一个这样的工具,帮我们解决上述几个问题:

  1. 在进行代码和功能改动后,能够自动访问各个功能的页面,检测问题
  2. 针对大量的数据内容,进行批量访问,检测对于不同数据的展示是否存在问题
  3. 测试与代码功能尽量不耦合,避免每次上新功能都需要对测试用例进行修改,维护成本太大
  4. 定期的测试任务,及时发现数据平台针对新数据的展示完备性

其中,最重要的问题,就是将测试代码与功能解耦,避免每次迭代和修改都需要追加新的测试用例。我们如何做到这一点呢?首先我们来梳理下测试平台的功能。

功能设定

由于我们的平台主要是进行数据展示,所以我们在测试过程中,主要以日常的展示数据为重心即可,针对一些复杂的表单操作先不予处理。针对上述的几个问题,我们针对自动化测试工具的功能如下:

  1. 依次访问各个页面
  2. 访问各个页面的具体内容,如时间切换、选项卡切换、分页切换、表格展开行等等
  3. 针对数据表格中的详情链接,选择前100条进行访问,并进行下钻页的继续测试
  4. 捕获在页面中的错误请求
  5. 对错误信息进行捕获,统计和上报

根据以上的梳理,我们可以把整个应用分为几个测试单元

  • 页面单元,检测各功能页面访问的稳定性
  • 详情页单元,根据页面的数据列表,进行批量的详情页跳转,检测不同参数下详情页的稳定性
  • 功能单元,用于检测页面和详情页各种展示类型点击切换后是否产生错误

图片描述

通过这样的划分,我们 针对各个单元进行具体的测试逻辑书写用例,这样就可以避免再添加新功能和页面时,频繁对测试用例进行修改了。

Puppeteer

带着上面我们的需求,我们来看下Puppeteer的功能和特性,是否能够满足我们的要求。

文档地址

Puppeteer是一个Node库,它提供了一个高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome。Puppeteer 默认以 headless 模式运行,但是可以通过修改配置文件运行“有头”模式。

我们可以使用Puppeteer完成以下工作:

  • 访问页面,进行截图
  • 自动进行键盘输入,提交表单
  • 模拟点击等用户操作
  • 等等等等。。

我们来通过一些小案例,来介绍他们的基本功能:

访问一个带有ba认证的网站

puppeteer可以创建page实例,并使用goto方法进行页面访问,page包含一系列方法,可以对页面进行各种操作。

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  // ba认证
  await page.authenticate({
    username,
    password
  });
  // 访问页面
  await page.goto('https://example.com');
  // 进行截图
  await page.screenshot({path: 'example.png'});

  await browser.close();
})();

访问登陆页面,并进行登录

首先,对于SPA(单页面应用),我们都知道,当页面进入后,客户端代码才开始进行渲染工作。我们需要等到页面内容渲染完成后,再进行对应的操作。我们有以下几种方法来使用

waitUntil

puppeteer针对页面的访问,切换等,提供了waitUntil参数,来确定满足什么条件才认为页面跳转完成。包括以下事件:

  • load - 页面的load事件触发时
  • domcontentloaded - 页面的DOMContentLoaded事件触发时
  • networkidle0 - 不再有网络连接时触发(至少500毫秒后)
  • networkidle2 - 只有2个网络连接时触发(至少500毫秒后)

通过waitUnitl,我们可以当页面请求都完成之后,确定页面已经访问完成。

waitFor

waitFor方法可以在指定动作完成后才进行resolve

// wait for selector
await page.waitFor('.foo');
// wait for 1 second
await page.waitFor(1000);
// wait for predicate
await page.waitFor(() => !!document.querySelector('.foo'));

我们可以利用waitForSelector方法,当登录框渲染成功后,才进行登录操作

// 等待密码输入框渲染
await page.waitFor('#password');
// 输入用户名
await page.type('input#username', "username");
// 输入密码
await page.type('input#password', "testpass");

// 点击登录按钮
await Promise.all([
  page.waitForNavigation(), // 等跳转完成后resolve
  page.click('button.login-button'), // 点击该链接将间接导致导航(跳转)
]);

await page.waitFor(2000)

// 获取cookies
const cookies = await page.cookies()

针对列表内容里的链接进行批量访问

主要利用到page实例的选择器功能

const table = await page.$('.table')
const links = await table.$$eval('a.link-detail', links =>
  links.map(link => link.href)
);

// 循环访问links
...

进行错误和访问监听

puppeteer可以监听在页面访问过程中的报错,请求等等,这样我们就可以捕获到页面的访问错误并进行上报啦,这也是我们进行测试需要的基本功能~

// 当发生页面js代码没有捕获的异常时触发。
page.on('pagerror', () => {})
// 当页面崩溃时触发。
page.on('error', () => {})
// 当页面发送一个请求时触发
page.on('request')
// 当页面的某个请求接收到对应的 response 时触发。
page.on('response')

通过以上的几个小案例,我们发现Puppeteer的功能非常强大,完全能够满足我们以上的对页面进行自动访问的需求。接下来,我们针对我们的测试单元进行个单元用例的书写

最终功能

通过我们上面对测试单元的规划,我们可以规划一下我们的测试路径

访问网站 -> 登陆 -> 访问页面1 -> 进行基本单元测试 -> 获取详情页跳转链接 -> 依次访问详情页 -> 进行基本单元测试

-> 访问页面2 ...

所以,我们可以拆分出几个大类,和几个测试单元,来进行各项测试

// 包含基本的测试方法,log输出等
class Base {}

// 详情页单元,进行一些基本的单元测试
class PageDetal extends Base {}

// 页面单元,进行基本的单元测试,并获取并依次访问详情页
class Page extends PageDetal {}

// 进行登录等操作,并依次访问页面单元进行测试
class Root extends Base {}

同时,我们如何在功能页面变化时,跟踪到测试的变化呢,我们可以针对我们测试的功能,为其添加自定义标签test-role,测试时,根据自定义标签进行测试逻辑的编写。

例如针对时间切换单元,我们做一下简单的介绍:

// 1. 获取测试单元的元素
const timeSwitch = await page.$('[test-role="time-switch"]');

// 若页面没有timeSwitch, 则不用进行测试
if (!timeSwitch) return

// 2. time switch的切换按钮
const buttons = timeSwitch.$$('.time-switch-button')

// 3. 对按钮进行循环点击
for (let i = 0; i < buttons.length; i++) {
  const button = buttons[i]

  // 点击按钮
  await button.click()

  // 重点! 等待对应的内容出现时,才认定页面访问成功
  try {
    await page.waitFor('[test-role="time-switch-content"]')
  } catch (error) {
    reportError (error)
  }

  // 截图
  await page.screenshot()
}

上面只是进行了一个简单的访问内容测试,我们可以根据我们的用例单元书写各自的测试逻辑,在我们日常开发时,只需要对需要测试的内容,加上对应的test-role即可。

总结

根据以上的功能划分,我们很好的将一整个应用拆分成各个测试单元进行单元测试。需要注意的是,我们目前仅仅是对页面的可访问性进行测试,仅仅验证当用户进行各种操作,访问各个页面单元时页面是否会出错。并没有对页面的具体展示效果进行测试,这样会和页面的功能内容耦合起来,就需要单独的测试用例的编写了。

Javascript 面试中经常被问到的三个问题!

$
0
0

图片描述

本文不是讨论最新的 JavaScript 库、常见的开发实践或任何新的 ES6 函数。相反,在讨论 JavaScript 时,面试中通常会提到三件事。我自己也被问到这些问题,我的朋友们告诉我他们也被问到这些问题。

然,这些并不是你在面试之前应该学习的唯一三件事 - 你可以通过 多种方式更好地为即将到来的面试做准备 - 但面试官可能会问到下面是三个问题,来判断你对 JavaScript语言的理解和 DOM的掌握程度。

让我们开始吧!注意,我们将在下面的示例中使用原生的 JavaScript,因为面试官通常希望了解你在没有 jQuery 等库的帮助下对JavaScript 和 DOM 的理解程度。

问题 1: 事件委托代理

在构建应用程序时,有时需要将事件绑定到页面上的按钮、文本或图像,以便在用户与元素交互时执行某些操作。

如果我们以一个简单的待办事项列表为例,面试官可能会告诉你,当用户点击列表中的一个列表项时执行某些操作。他们希望你用 JavaScript 实现这个功能,假设有如下 HTML 代码:

<ul id="todo-app"><li class="item">Walk the dog</li><li class="item">Pay bills</li><li class="item">Make dinner</li><li class="item">Code for one hour</li></ul>

你可能想要做如下操作来将事件绑定到元素:

document.addEventListener('DOMContentLoaded', function() {
  let app = document.getElementById('todo-app');
  let times = app.getElementsByClassName('item');

  for (let item of items) {
    item.addEventListener('click', function(){
      alert('you clicked on item: ' + item.innerHTML);
    })
  }
})

虽然这在技术上是可行的,但问题是要将事件分别绑定到每个项。这对于目前 4个元素来说,没什么大问题,但是如果在待办事项列表中添加了 10,000项(他们可能有很多事情要做)怎么办?然后,函数将创建 10,000 个独立的事件侦听器,并将每个事件监听器绑定到 DOM ,这样代码执行的效率非常低下。

在面试中,最好先问面试官用户可以输入的最大元素数量是多少。例如,如果它不超过 10,那么上面的代码就可以很好地工作。但是如果用户可以输入的条目数量没有限制,那么你应该使用一个更高效的解决方案。

如果你的应用程序最终可能有数百个事件侦听器,那么更有效的解决方案是将一个事件侦听器实际绑定到整个容器,然后在单击它时能够访问每个列表项, 这称为 事件委托,它比附加单独的事件处理程序更有效。

下面是事件委托的代码:

document.addEventListener('DOMContentLoaded', function() {
  let app = document.getElementById('todo-app');

  app.addEventListener('click', function(e) {
    if (e.target && e.target.nodeName === 'LI') {
      let item = e.target;
      alert('you clicked on item: ' + item.innerHTML)
    }
  })
})

问题 2: 在循环中使用闭包

闭包常常出现在面试中,以便面试官衡量你对 JS 的熟悉程度,以及你是否知道何时使用闭包。

闭包基本上是内部函数可以访问其范围之外的变量。 闭包可用于实现隐私和创建函数工厂, 闭包常见的面试题如下:

编写一个函数,该函数将遍历整数列表,并在延迟3秒后打印每个元素的索引。

经常不正确的写法是这样的:

const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
  setTimeout(function() {
    console.log('The index of this number is: ' + i);
  }, 3000);
}

如果运行上面代码, 3秒延迟后你会看到,实际上每次打印输出是 4,而不是期望的 0,1,2,3

为了正确理解为什么会发生这种情况,了解为什么会在 JavaScript 中发生这种情况将非常有用,这正是面试官试图测试的内容。

原因是因为 setTimeout函数创建了一个可以访问其外部作用域的函数(闭包),该作用域是包含索引 i的循环。 经过 3秒后,执行该函数并打印出 i的值,该值在循环结束时为 4,因为它循环经过 0,1,2,3,4并且循环最终停止在 4

实际上有 多处方法来正确的解这道题:

const arr = [10, 12, 15, 21];

for (var i = 0; i < arr.length; i++) {
  setTimeout(function(i_local){
    return function () {
      console.log('The index of this number is: ' + i_local);
    }
  }(i), 3000)
}


const arr = [10, 12, 15, 21];
for (let i = 0; i < arr.length; i++) {
  setTimeout(function() {
    console.log('The index of this number is: ' + i);
  }, 3000);
}

问题 3:事件的节流(throttle)与防抖(debounce)

有些浏览器事件可以在短时间内快速触发多次,比如调整窗口大小或向下滚动页面。例如,监听页面窗口滚动事件,并且用户持续快速地向下滚动页面,那么滚动事件可能在 3 秒内触发数千次,这可能会导致一些严重的性能问题。

如果在面试中讨论构建应用程序,出现滚动、窗口大小调整或按下键等事件请务必提及 防抖(Debouncing)函数节流(Throttling)来提升页面速度和性能。这两兄弟的本质都是以 闭包的形式存在。通过对事件对应的回调函数进行包裹、以自由变量的形式缓存时间信息,最后用 setTimeout 来控制事件的触发频率。

Throttle: 第一个人说了算

throttle 的主要思想在于:在某段时间内,不管你触发了多少次回调,都只认第一次,并在计时结束时给予响应。

这个故事里,‘裁判’ 就是我们的节流阀, 他控制参赛者吃东西的时机, “参赛者吃东西”就是我们频繁操作事件而不断涌入的回调任务,它受 “裁判” 的控制,而计时器,就是上文提到的以自由变量形式存在的时间信息,它是 “裁判” 决定是否停止比赛的依据,最后,等待比赛结果就对应到回调函数的执行。

总结下来,所谓的“节流”,是通过在一段时间内无视后来产生的回调请求来实现的。只要 裁判宣布比赛开始,裁判就会开启计时器,在这段时间内,参赛者就尽管不断的吃,谁也无法知道最终结果。

对应到实际的交互上是一样一样的:每当用户触发了一次 scroll 事件,我们就为这个触发操作开启计时器。一段时间内,后续所有的 scroll 事件都会被当作“参赛者吃东西——它们无法触发新的 scroll 回调。直到“一段时间”到了,第一次触发的 scroll 事件对应的回调才会执行,而“一段时间内”触发的后续的 scroll 回调都会被节流阀无视掉。

现在一起实现一个 throttle:

// fn 是我们需要包装的事件回调, interval 是时间间隔的阈值
function throttle(fn, interval) {
  // last 为上一次触发回调时间
  let last = 0

  // 将 throttle 处理结果当然函数返回
  return function () {
    // 保留调用时的 this 上下文
    let context = this
    // 保留调用时传入的参数
    let args = arguments
    // 记录本次触发回调的时间
    let now = +new Date()

    // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
    if (now - last >= interval) {
      // 如果时间间隔大于我们设定的时间间隔阈值,则执行回调
      last = fn.apply(context, args)
    }
  }
}

// 用 throttle来包装 scroll 的回调
const better_scroll = throttle(() => { console.log('触发了滚动事件')}, 1000)
document.addEventListener('scroll', better_scroll)

Debounce: 最后一个参赛者说了算

防抖的主要思想在于:我会等你到底。在某段时间内,不管你触发了多少次回调,我都只认最后一次。

继续大胃王比赛故事,这次换了一种比赛方式,时间不限,参赛者吃到不能吃为止,当每个参赛都吃不下的时候,后面10分钟如果没有人在吃,比赛结束,如果有人在10分钟内还能吃,则比赛继续,直到下一次10分钟内无人在吃时为止。

对比 throttle 来理解 debounce: 在 throttle 的逻辑里, ‘裁判’ 说了算,当比赛时间到时,就执行回调函数。而 debounce 认为最后一个参赛者说了算,只要还能吃的,就重新设定新的定时器。

现在一起实现一个 debounce:

// fn 是我们需要包装事件回调,delay 是每次推迟执行的等待时间
function debounce(fn, delay) {
  // 定时器
  let timer = null

  // 将 debounce 处理结果当作函数返回
  return function () {
    // 保留调用时的 this 上下文
    let context = this
    // 保留调用时传入的参数
    let args = arguments

    // 每次事件被触发时,都去清除之前的旧定时器
    if (timer) {
      clearTimeout(timer)
    }
    // 设立新定时器
    timer = setTimeout(function() {
      fn.apply(context, args)
    }, delay)
  }
}

// 用 debounce 来包装 scroll 的回调
const better_scroll = debounce(() => { console.log('发了滚动事件')}, 1000)
document.addEventListener('scroll', better_scroll)

用 Throttle 来优化 Debounce

debounce 的问题在于它“太有耐心了”。试想,如果用户的操作十分频繁——他每次都不等 debounce 设置的 delay 时间结束就进行下一次操作,于是每次 debounce 都为该用户重新生成定时器,回调函数被延迟了不计其数次。频繁的延迟会导致用户迟迟得不到响应,用户同样会产生“这个页面卡死了”的观感。

为了避免弄巧成拙,我们需要借力 throttle 的思想,打造一个“有底线”的 debounce——等你可以,但我有我的原则:delay 时间内,我可以为你重新生成定时器;但只要delay的时间到了,我必须要给用户一个响应。这个 throttle 与 debounce “合体”思路,已经被很多成熟的前端库应用到了它们的加强版 throttle 函数的实现中:

// fn是我们需要包装的事件回调, delay是时间间隔的阈值
function throttle(fn, delay) {
  // last为上一次触发回调的时间, timer是定时器
  let last = 0, timer = null
  // 将throttle处理结果当作函数返回
  
  return function () { 
    // 保留调用时的this上下文
    let context = this
    // 保留调用时传入的参数
    let args = arguments
    // 记录本次触发回调的时间
    let now = +new Date()
    
    // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
    if (now - last < delay) {
    // 如果时间间隔小于我们设定的时间间隔阈值,则为本次触发操作设立一个新的定时器
       clearTimeout(timer)
       timer = setTimeout(function () {
          last = now
          fn.apply(context, args)
        }, delay)
    } else {
        // 如果时间间隔超出了我们设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应
        last = now
        fn.apply(context, args)
    }
  }
}

// 用新的throttle包装scroll的回调
const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)

document.addEventListener('scroll', better_scroll)

参考:

Throttling and Debouncing in JavaScript
The Difference Between Throttling and Debouncing
Examples of Throttling and Debouncing
Remy Sharp’s blog post on Throttling function calls
前端性能优化原理与实践

原文:

https://medium.freecodecamp.o...

你的点赞是我持续分享好东西的动力,欢迎点赞!

一个笨笨的码农,我的世界只能终身学习!

更多内容请关注公众号 《大迁世界》

前端性能优化不完全手册

$
0
0

性能优化是一门大学问,本文仅对个人一些积累知识的阐述,欢迎下面补充。

抛出一个问题,从输入 url地址栏到所有内容显示到界面上做了哪些事?
  • 1.浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP地址;
  • 2.建立 TCP连接(三次握手);
  • 3.浏览器发出读取文件( URL中域名后面部分对应的文件)的 HTTP请求,该请求报文作为 TCP三次握手的第三个报文的数据发送给服务器;
  • 4.服务器对浏览器请求作出响应,并把对应的 html 文本发送给浏览器;
  • 5.浏览器将该 html文本并显示内容;
  • 6.释放 TCP连接(四次挥手);
上面这个问题是一个面试官非常喜欢问的问题,我们下面把这6个步骤分解,逐步细谈优化。

一、 DNS解析

  • DNS`解析:将域名解析为ip地址 ,由上往下匹配,只要命中便停止

    • 走缓存
    • 浏览器DNS缓存
    • 本机DNS缓存
    • 路由器DNS缓存
    • 网络运营商服务器DNS缓存 (80%的DNS解析在这完成的)
    • 递归查询
优化策略:尽量允许使用浏览器的缓存,能给我们节省大量时间。

二、 TCP的三次握手

  • SYN (同步序列编号)ACK(确认字符)

    • 第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等 待Server确认。
    • 第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。
    • 第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。

三、浏览器发送请求

优化策略:
    • 1. HTTP协议通信最耗费时间的是建立 TCP连接的过程,那我们就可以使用 HTTP Keep-Alive,在 HTTP 早期,每个 HTTP 请求都要求打开一个 TCP socket连接,并且使用一次之后就断开这个 TCP连接。 使用 keep-alive可以改善这种状态,即在一次TCP连接中可以持续发送多份数据而不会断开连接。通过使用 keep-alive机制,可以减少 TCP连接建立次数,也意味着可以减少 TIME_WAIT状态连接,以此提高性能和提高 http服务器的吞吐率(更少的 tcp连接意味着更少的系统内核调用
    • 2.但是, keep-alive并不是免费的午餐,长时间的 TCP连接容易导致系统资源无效占用。配置不当的 keep-alive,有时比重复利用连接带来的损失还更大。所以,正确地设置 keep-alive timeout时间非常重要。(这个 keep-alive_timout时间值意味着:一个 http产生的 tcp连接在传送完最后一个响应后,还需要 holdkeepalive_timeout秒后,才开始关闭这个连接),如果想更详细了解可以看这篇文章 keep-alve性能优化的测试结果
    • 3.使用 webScoket通信协议,仅一次 TCP握手就一直保持连接,而且他对二进制数据的传输有更好的支持,可以应用于即时通信,海量高并发场景。 webSocket的原理以及详解
    • 4.减少 HTTP请求次数,每次 HTTP请求都会有请求头,返回响应都会有响应头,多次请求不仅浪费时间而且会让网络传输很多无效的资源,使用前端模块化技术 AMD CMD commonJS ES6等模块化方案将多个文件压缩打包成一个,当然也不能都放在一个文件中,因为这样传输起来可能会很慢,权衡取一个中间值
    • 5.配置使用懒加载,对于一些用户不立刻使用到的文件到特定的事件触发再请求,也许用户只是想看到你首页上半屏的内容,但是你却请求了整个页面的所有图片,如果用户量很大,那么这是一种极大的浪费
    • 6.服务器资源的部署尽量使用同源策略

    四、服务器返回响应,浏览器接受到响应数据

    五、浏览器解析数据,绘制渲染页面的过程

    • 先预解析(将需要发送请求的标签的请求发出去)
    • 从上到下解析 html文件
    • 遇到HTML标签,调用html解析器将其解析 DOM
    • 遇到 css标记,调用css解析器将其解析 CSSOM
    • link阻塞 - 为了解决闪屏,所有解决闪屏的样式
    • style非阻塞,与闪屏的样式不相关的
    • DOM树和 CSSOM树结合在一起,形成 render
    • layout布局 render渲染
    • 遇到 script标签,阻塞,调用 js解析器解析 js代码,可能会修改 DOM树,也可能会修改 CSSOM
    • DOM树和 CSSOM树结合在一起,形成 render
    • layout布局 render渲染(重排重绘)
    • script标签的属性

      • async 异步 谁先回来谁就先解析,不阻塞
      • defer 异步 按照先后顺序(defer)解析,不阻塞
      • script标签放在body下,放置多次重排重绘,能够操作dom
    性能优化策略:
    • 需要阻塞的样式使用 link引入,不需要的使用 style标签(具体是否需要阻塞看业务场景)
    • 图片比较多的时候,一定要使用懒加载,图片是最需要优化的, webpack4中也要配置图片压缩,能极大压缩图片大小,对于新版本浏览器可以使用 webp格式图片webP详解,图片优化对性能提升最大。
    • webpack4配置 代码分割,提取公共代码成单独模块。方便缓存
        /*
        runtimeChunk 设置为 true, webpack 就会把 chunk 文件名全部存到一个单独的 chunk 中,
        这样更新一个文件只会影响到它所在的 chunk 和 runtimeChunk,避免了引用这个 chunk 的文件也发生改变。
        */
        runtimeChunk: true, 
        splitChunks: {
          chunks: 'all'  // 默认 entry 的 chunk 不会被拆分, 配置成 all, 就可以了
        }
      }
        //因为是单入口文件配置,所以没有考虑多入口的情况,多入口是应该分别进行处理。
    • 对于需要事件驱动的 webpack4配置懒加载的,可以看这篇 webpack4优化教程,写得非常全面
    • 一些原生 javaScriptDOM操作等优化会在下面总结

    六、 TCP的四次挥手,断开连接


    终结篇:性能只是 load 时间或者 DOMContentLoaded 时间的问题吗?

    • RAIL

      • Responce响应,研究表明,100ms内对用户的输入操作进行响应,通常会被人类认为是立即响应。时间再长,操作与反应之间的连接就会中断,人们就会觉得它的操作有延迟。例如:当用户点击一个按钮,如果100ms内给出响应,那么用户就会觉得响应很及时,不会察觉到丝毫延迟感。
      • Animaton现如今大多数设备的屏幕刷新频率是60Hz,也就是每秒钟屏幕刷新60次;因此网页动画的运行速度只要达到60FPS,我们就会觉得动画很流畅。
      • Idle RAIL规定,空闲周期内运行的任务不得超过50ms,当然不止RAIL规定,W3C性能工作组的Longtasks标准也规定了超过50毫秒的任务属于长任务,那么50ms这个数字是怎么得来的呢?浏览器是单线程的,这意味着同一时间主线程只能处理一个任务,如果一个任务执行时间过长,浏览器则无法执行其他任务,用户会感觉到浏览器被卡死了,因为他的输入得不到任何响应。为了达到100ms内给出响应,将空闲周期执行的任务限制为50ms意味着,即使用户的输入行为发生在空闲任务刚开始执行,浏览器仍有剩余的50ms时间用来响应用户输入,而不会产生用户可察觉的延迟。
      • Load如果不能在1秒钟内加载网页并让用户看到内容,用户的注意力就会分散。用户会觉得他要做的事情被打断,如果10秒钟还打不开网页,用户会感到失望,会放弃他们想做的事,以后他们或许都不会再回来。
      如何使网页更丝滑?
      • 使用requestAnimationFrame

        • 即便你能保证每一帧的总耗时都小于16ms,也无法保证一定不会出现丢帧的情况,这取决于触发JS执行的方式。假设使用 setTimeout 或 setInterval 来触发JS执行并修改样式从而导致视觉变化;那么会有这样一种情况,因为setTimeout 或 setInterval没有办法保证回调函数什么时候执行,它可能在每一帧的中间执行,也可能在每一帧的最后执行。所以会导致即便我们能保障每一帧的总耗时小于16ms,但是执行的时机如果在每一帧的中间或最后,最后的结果依然是没有办法每隔16ms让屏幕产生一次变化,也就是说,即便我们能保证每一帧总体时间小于16ms,但如果使用定时器触发动画,那么由于定时器的触发时机不确定,所以还是会导致动画丢帧。现在整个Web只有一个API可以解决这个问题,那就是requestAnimationFrame,它可以保证回调函数稳定的在每一帧最开始触发。
      • 避免 FSL

        • 先执行 JS,然后在 JS中修改了样式从而导致样式计算,然后样式的改动触发了布局、绘制、合成。但 JavaScript可以强制浏览器将布局提前执行,这就叫 强制同步布局 FSL

            //读取offsetWidth的值会导致重绘
           const newWidth = container.offsetWidth;
             
            //设置width的值会导致重排,但是for循环内部
            代码执行速度极快,当上面的查询操作导致的重绘
            还没有完成,下面的代码又会导致重排,而且这个重
            排会强制结束上面的重绘,直接重排,这样对性能影响
            非常大。所以我们一般会在循环外部定义一个变量,这里
            面使用变量代替container.offsetWidth;
           boxes[i].style.width = newWidth + 'px';
          }
      • 使用 transform属性去操作动画,这个属性是由合成器单独处理的,所以使用这个属性可以避免布局与绘制。
      • 使用 translateZ(0)开启图层,减少重绘重排。特别在移动端,尽量使用 transform代替 absolute。创建图层的最佳方式是使用will-change,但某些不支持这个属性的浏览器可以使用3D 变形(transform: translateZ(0))来强制创建一个新层。
      • 有兴趣的可以看看这篇文字 前端页面优化
      • 样式的切换最好提前定义好 class,通过 class的切换批量修改样式,避免多次重绘重排
      • 可以先切换 display:none再修改样式
      • 多次的 append 操作可以先插入到一个新生成的元素中,再一次性插入到页面中。
      • 代码复用,函数柯里化,封装高阶函数,将多次复用代码封装成普通函数(俗称方法), React中封装成高阶组件, ES6中可以使用继承, TypeScript中接口继承,类继承,接口合并,类合并。
      • 强力推荐阅读: 阮一峰ES6教程
      • 以及 什么是TypeScript以及入门
    以上都是根据本人的知识点总结得出,后期还会有 React的性能优化方案等出来,路过点个赞收藏收藏~,欢迎提出问题补充~

    基于socket.io快速实现一个实时通讯应用

    $
    0
    0

    随着web技术的发展,使用场景和需求也越来越复杂,客户端不再满足于简单的请求得到状态的需求。实时通讯越来越多应用于各个领域。

    HTTP是最常用的客户端与服务端的通信技术,但是HTTP通信只能由客户端发起,无法及时获取服务端的数据改变。只能依靠定期轮询来获取最新的状态。时效性无法保证,同时更多的请求也会增加服务器的负担。

    WebSocket技术应运而生。

    WebSocket概念

    不同于HTTP半双工协议,WebSocket是基于TCP 连接的全双工协议,支持客户端服务端双向通信。

    WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

    WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

    HTTP与websocket对比

    实现

    原生实现

    WebSocket对象一共支持四个消息 onopen, onmessage, onclose和onerror。

    建立连接

    通过javascript可以快速的建立一个WebSocket连接:

        var Socket = new WebSocket(url, [protocol] );

    以上代码中的第一个参数 url, 指定连接的URL。第二个参数 protocol是可选的,指定了可接受的子协议。

    同http协议使用 http://开头一样,WebSocket协议的URL使用 ws://开头,另外安全的WebSocket协议使用 wss://开头。

    1. 当Browser和WebSocketServer连接成功后,会触发onopen消息。
        Socket.onopen = function(evt) {};
    1. 如果连接失败,发送、接收数据失败或者处理数据出现错误,browser会触发onerror消息。
        Socket.onerror = function(evt) { };
    1. 当Browser接收到WebSocketServer端发送的关闭连接请求时,就会触发onclose消息。
        Socket.onclose = function(evt) { };

    收发消息

    1. 当Browser接收到WebSocketServer发送过来的数据时,就会触发onmessage消息,参数evt中包含server传输过来的数据。
        Socket.onmessage = function(evt) { };
    1. send用于向服务端发送消息。
        Socket.send();

    socket

    WebSocket是跟随HTML5一同提出的,所以在兼容性上存在问题,这时一个非常好用的库就登场了—— Socket.io

    socket.io封装了websocket,同时包含了其它的连接方式,你在任何浏览器里都可以使用socket.io来建立异步的连接。socket.io包含了服务端和客户端的库,如果在浏览器中使用了socket.io的js,服务端也必须同样适用。

    socket.io是基于 Websocket 的Client-Server 实时通信库。

    socket.io底层是基于engine.io这个库。engine.io为 socket.io 提供跨浏览器/跨设备的双向通信的底层库。engine.io使用了 Websocket 和 XHR 方式封装了一套 socket 协议。在低版本的浏览器中,不支持Websocket,为了兼容使用长轮询(polling)替代。

    engine.io

    API文档

    Socket.io允许你触发或响应自定义的事件,除了connect,message,disconnect这些事件的名字不能使用之外,你可以触发任何自定义的事件名称。

    建立连接

        const socket = io("ws://0.0.0.0:port"); // port为自己定义的端口号
        let io = require("socket.io")(http);
        io.on("connection", function(socket) {})

    消息收发

    一、发送数据

        socket.emit(自定义发送的字段, data);

    二、接收数据

        socket.on(自定义发送的字段, function(data) {
            console.log(data);
        })

    断开连接

    一、全部断开连接

        let io = require("socket.io")(http);
        io.close();

    二、某个客户端断开与服务端的链接

        // 客户端
        socket.emit("close", {});
        // 服务端
        socket.on("close", data => {
            socket.disconnect(true);
        });

    room和namespace

    有时候websocket有如下的使用场景:1.服务端发送的消息有分类,不同的客户端需要接收的分类不同;2.服务端并不需要对所有的客户端都发送消息,只需要针对某个特定群体发送消息;

    针对这种使用场景,socket中非常实用的namespace和room就上场了。

    先来一张图看看namespace与room之间的关系:

    namespace与room的关系

    namespace

    服务端

        io.of("/post").on("connection", function(socket) {
            socket.emit("new message", { mess: `这是post的命名空间` });
        });
        
        io.of("/get").on("connection", function(socket) {
            socket.emit("new message", { mess: `这是get的命名空间` });
        });

    客户端

        // index.js
        const socket = io("ws://0.0.0.0:****/post");
        socket.on("new message", function(data) {
            console.log('index',data);
        }
        //message.js
        const socket = io("ws://0.0.0.0:****/get");
        socket.on("new message", function(data) {
            console.log('message',data);
        }

    room

    客户端

        //可用于客户端进入房间;
        socket.join('room one');
        //用于离开房间;
        socket.leave('room one');

    服务端

        io.sockets.on('connection',function(socket){
            //提交者会被排除在外(即不会收到消息)
            socket.broadcast.to('room one').emit('new messages', data);
            // 向所有用户发送消息
            io.sockets.to(data).emit("recive message", "hello,房间中的用户");      
        }

    用socket.io实现一个实时接收信息的例子

    终于来到应用的阶段啦,服务端用 node.js模拟了服务端接口。以下的例子都在本地服务器中实现。

    服务端

    先来看看服务端,先来开启一个服务,安装 expresssocket.io

    安装依赖

        npm install --Dev express
        npm install --Dev socket.io

    构建node服务器

        let app = require("express")();
        let http = require("http").createServer(handler);
        let io = require("socket.io")(http);
        let fs = require("fs");
        http.listen(port); //port:输入需要的端口号
        
        function handler(req, res) {
          fs.readFile(__dirname + "/index.html", function(err, data) {
            if (err) {
              res.writeHead(500);
              return res.end("Error loading index.html");
            }
            res.writeHead(200);
            res.end(data);
          });
        }
        io.on("connection", function(socket) {
            console.log('连接成功');
            //连接成功之后发送消息
            socket.emit("new message", { mess: `初始消息` });
            
        });

    客户端

    核心代码——index.html(向服务端发送数据)

    <div>发送信息</div><input placeholder="请输入要发送的信息" /><button onclick="postMessage()">发送</button>
        // 接收到服务端传来的name匹配的消息
        socket.on("new message", function(data) {
          console.log(data);
        });
        function postMessage() {
          socket.emit("recive message", {
            message: content,
            time: new Date()
          });
          messList.push({
            message: content,
            time: new Date()
          });
        }

    核心代码——message.html(从服务端接收数据)

        socket.on("new message", function(data) {
          console.log(data);
        });

    效果

    实时通讯效果
    实时通讯效果

    客户端全部断开连接
    全部断开

    某客户端断开连接
    某客户端断开连接

    namespace应用
    namespace

    加入房间
    加入房间

    离开房间
    离开房间

    框架中的应用

    npm install socket.io-client

        const socket = require('socket.io-client')('http://localhost:port');
    
        componentDidMount() {
            socket.on('login', (data) => {
                console.log(data)
            });
            socket.on('add user', (data) => {
                console.log(data)
            });
            socket.on('new message', (data) => {
                console.log(data)
            });
        }

    分析webSocket协议

    Headers

    Headers

    请求包

        Accept-Encoding: gzip, deflate
        Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
        Cache-Control: no-cache
        Connection: Upgrade
        Cookie: MEIQIA_VISIT_ID=1IcBRlE1mZhdVi1dEFNtGNAfjyG; token=0b81ffd758ea4a33e7724d9c67efbb26; io=ouI5Vqe7_WnIHlKnAAAG
        Host: 0.0.0.0:2699
        Origin: http://127.0.0.1:5500
        Pragma: no-cache
        Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
        Sec-WebSocket-Key: PJS0iPLxrL0ueNPoAFUSiA==
        Sec-WebSocket-Version: 13
        Upgrade: websocket
        User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1

    请求包说明:

    • 必须是有效的http request 格式;
    • HTTP request method 必须是GET,协议应不小于1.1 如: Get / HTTP/1.1;
    • 必须包括Upgrade头域,并且其值为“websocket”,用于告诉服务器此连接需要升级到websocket;
    • 必须包括”Connection” 头域,并且其值为“Upgrade”;
    • 必须包括”Sec-WebSocket-Key”头域,其值采用base64编码的随机16字节长的字符序列;
    • 如果请求来自浏览器客户端,还必须包括Origin头域 。 该头域用于防止未授权的跨域脚本攻击,服务器可以从Origin决定是否接受该WebSocket连接;
    • 必须包括“Sec-webSocket-Version”头域,是当前使用协议的版本号,当前值必须是13;
    • 可能包括“Sec-WebSocket-Protocol”,表示client(应用程序)支持的协议列表,server选择一个或者没有可接受的协议响应之;
    • 可能包括“Sec-WebSocket-Extensions”, 协议扩展, 某类协议可能支持多个扩展,通过它可以实现协议增强;
    • 可能包括任意其他域,如cookie.

    应答包

    应答包说明:

        Connection: Upgrade
        Sec-WebSocket-Accept: I4jyFwm0r1J8lrnD3yN+EvxTABQ=
        Sec-WebSocket-Extensions: permessage-deflate
        Upgrade: websocket
    • 必须包括Upgrade头域,并且其值为“websocket”;
    • 必须包括Connection头域,并且其值为“Upgrade”;
    • 必须包括Sec-WebSocket-Accept头域,其值是将请求包“Sec-WebSocket-Key”的值,与”258EAFA5-E914-47DA-95CA-C5AB0DC85B11″这个字符串进行拼接,然后对拼接后的字符串进行sha-1运算,再进行base64编码,就是“Sec-WebSocket-Accept”的值;
    • 应答包中冒号后面有一个空格;
    • 最后需要两个空行作为应答包结束。

    请求数据

        EIO: 3
        transport: websocket
        sid: 8Uehk2UumXoHVJRzAAAA
    • EIO:3 表示使用的是engine.io协议版本3
    • transport 表示传输采用的类型
    • sid: session id (String)

    Frames

    WebSocket协议使用帧(Frame)收发数据,在控制台->Frames中可以查看发送的帧数据。

    其中帧数据前的数字代表什么意思呢?

    这是 Engine.io协议,其中的数字是数据包编码:

    <Packet type id> [<data>]

    • 0 open——在打开新传输时从服务器发送(重新检查)
    • 1 close——请求关闭此传输,但不关闭连接本身。
    • 2 ping——由客户端发送。服务器应该用包含相同数据的乓包应答

      客户端发送:2probe探测帧
    • 3 pong——由服务器发送以响应ping数据包。

      服务器发送:3probe,响应客户端
    • 4 message——实际消息,客户端和服务器应该使用数据调用它们的回调。
    • 5 upgrade——在engine.io切换传输之前,它测试,如果服务器和客户端可以通过这个传输进行通信。如果此测试成功,客户端发送升级数据包,请求服务器刷新其在旧传输上的缓存并切换到新传输。
    • 6 noop——noop数据包。主要用于在接收到传入WebSocket连接时强制轮询周期。

    实例

    发送数据

    接收数据

    以上的截图是上述例子中数据传输的实例,分析一下大概过程就是:

    1. connect握手成功
    2. 客户端会发送2 probe探测帧
    3. 服务端发送响应帧3probe
    4. 客户端会发送内容为5的Upgrade帧
    5. 服务端回应内容为6的noop帧
    6. 探测帧检查通过后,客户端停止轮询请求,将传输通道转到websocket连接,转到websocket后,接下来就开始定期(默认是25秒)的 ping/pong
    7. 客户端、服务端收发数据,4表示的是engine.io的message消息,后面跟随收发的消息内容

    为了知道Client和Server链接是否正常,项目中使用的ClientSocket和ServerSocket都有一个心跳的线程,这个线程主要是为了检测Client和Server是否正常链接,Client和Server是否正常链接主要是用ping pong流程来保证的。

    该心跳定期发送的间隔是socket.io默认设定的25m,在上图中也可观察发现。该间隔可通过 配置修改。

    socket通信流程

    参考 engine.io-protocol

    参考文章

    Web 实时推送技术的总结
    engine.io 原理详解

    广而告之

    本文发布于 薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

    欢迎讨论,点个赞再走吧 。◕‿◕。 ~

    让前端开发者失业的技术,Flutter Web初体验

    $
    0
    0
    Flutter是一种新型的“客户端”技术。它的最终目标是替代包含几乎所有平台的开发:iOS,Android,Web,桌面;做到了一次编写,多处运行。掌握Flutter web可能是Web前端开发者翻盘的唯一机会。

    图片描述

    在前些日子举办的Google IO 2019 年度开发者大会上,Flutter web作为一个很亮眼的技术受到了开发者的追捧。这是继Flutter支持Android、IOS等设备之后,又一个里程碑式的版本,后续还会支持windows、linux、Macos、chroms等其他嵌入式设备。Flutter本身是一个类似于RN、WEEX、hHybrid等多端统一跨平台解决方案,真正做到了一次编写,多处运行,它的发展超出了很多人的想象,值得前端开发者去关注,今天我们来体验一下Flutter Web。

    概览

    先了解一下Flutter, 它是一个由谷歌开发的开源移动应用软件开发工具包,用于为Android和iOS开发应用,同时也将是Google Fuchsia下开发应用的主要工具。自从FLutter 1.5.4版本之后,支持了Web端的开发。它采用Dart语言来进行开发,与JavaScript相比,Dart在 JIT(即时编译)模式下,速度与 JavaScript基本持平。但是当Dart以 AOT模式运行时,Dart性能要高于JavaScript。

    Flutter内置了UI界面,与Hybrid App、React Native这些跨平台技术不同,Flutter既没有使用WebView,也没有使用各个平台的原生控件,而是本身实现一个统一接口的渲染引擎来绘制UI,Dart直接编译成了二进制文件,这样做可以保证不同平台UI的一致性。它也可以复用Java、Kotlin、Swift或OC代码,访问Android和iOS上的原生系统功能,比如蓝牙、相机、WiFi等等。我们公司的Now直播、企鹅辅导等项目、阿里的闲鱼等商业化项目已经大量在使用。

    架构

    Flutter 的 Mobile 架构

    Flutter的顶层是用drat编写的框架,包含Material(Android风格UI)和Cupertino(iOS风格)的UI界面,下面是通用的Widgets(组件),之后是一些动画、绘制、渲染、手势库等。
    框架下面是引擎,主要用C / C ++编写,引擎包含三个核心库,Skia是Flutter的2D渲染引擎,它是Google的一个2D图形处理函数库,包含字型、坐标转换,以及点阵图,都有高效能且简洁的表现。Skia是跨平台的,并提供了非常友好的API。第二是Dart 运行时环境以及第三文本渲染布局引擎。
    最底层的嵌入层,它所关心的是如何将图片组合到屏幕上,渲染变成像素。这一层的功能是用来解决跨平台的。

    了解了FLutter 之后,我来说一下今天的重头戏,Flutter for Web。要想知道Flutter为什么能在web上运行,得先来看看它的架构。

    Flutter 的 web架构

    通过对比,可以发现,web框架层和mobile的几乎一模一样。因此只需要重新实现一下引擎和嵌入层,不用变动Flutter API就可以完全可以将UI代码从Android / IOS Flutter App移植到Web。Dart能够使用Dart2Js编译器把Dart代码编译成Js代码。大多数原生App元素能够通过DOM实现,DOM实现不了的元素可以通过Canvas来实现。

    安装

    Flutter Web开发环境搭建,以我的windows环境为例进行讲解,其他环境类似,安装环境比较繁琐,需要耐心,有Android开发经验最好。

    1、在 Windows平台开发的话,官方的环境要求是Windows 7 SP1或更高版本(64位)。

    2、 Java环境,安装Java 1.8 + 版本之上,并配置环境变量,因为android开发依赖Java环境。

    对于Java程序开发而言,主要会使用JDK的两个命令:javac.exe、java.exe。路径:C:Javajdk1.8.0_181bin。但是这些命令由于不属于windows自己的命令,所以要想使用,就需要进行路径配置。单击“计算机-属性-高级系统设置”,单击“环境变量”。在“系统变量”栏下单击“新建”,创建新的系统环境变量(或用户变量,等效)。

    图片描述

    (1)新建->变量名"JAVA_HOME",变量值"C:Javajdk1.8.0_181"(即JDK的安装路径)
    (2)编辑->变量名"Path",在原变量值的最后面加上“;%JAVA_HOME%bin;%JAVA_HOME%jrebin”
    (3)新建->变量名“CLASSPATH”,变量值“.;%JAVA_HOME%lib;%JAVA_HOME%libdt.jar;%JAVA_HOME%libtools.jar”

    3、 Android Studio编辑器,安装Android Studio, 3.0或更高版本。我们需要用它来导入Android license和管理Android SDK以及Android虚拟机。(默认安装即可)

    安装完成之后设置代理,左上角的File-》setting-》搜索proxy,设置公司代理,用来加速下载Android SDK。

    图片描述

    之后点击右上角方盒按钮(SDK Manager),用来选择安装SDK版本,最好选Android 9版本,API28,会有一个很长时间的下载过程。SDK是开发必须的代码库。默认情况下,Flutter使用的Android SDK版本是基于你的 adb (Android Debug Bridge,管理连接手机,已打包在SDK)工具版本。 如果您想让Flutter使用不同版本的Android SDK,则必须将该 ANDROID_HOME 环境变量设置为SDK安装目录。

    图片描述

    右上角有个小手机类型的按钮(AVD Manager),用来设置Android模拟器,创建一个虚拟机。如果你有一台安卓手机,也可以连接USB接口,替代虚拟机。这个过程是调试必须的。安装完成之后,在 AVD (Android Virtual Device Manager) 中,点击工具栏的 Run。模拟器启动并显示所选操作系统版本或设备的启动画面。代表了正确安装。

    图片描述

    4、 安装Flutter SDK

    下载Flutter SDK有多种方法,看看哪种更适合自己:
    Flutter官网下载最新Beta版本的进行安装: https://flutter.dev/docs/deve...
    也可Flutter github项目中去下载,地址为: https://github.com/flutter/fl...
    版本越新越好,不要低于1.5.4。

    将安装包zip解压到你想安装Flutter SDK的路径(如:C:srcflutter;注意,不要将flutter安装到需要一些高权限的路径如C:Program Files)。记住,之后往环境变量的path中添加;C:srcflutterbin,以便于你能在命令行中使用flutter。

    使用镜像
    由于在国内安装Flutter相关的依赖可能会受到限制,Flutter官方为中国开发者搭建了临时镜像,大家可以将如下环境变量加入到用户环境变量中:
    PUB_HOSTED_URL:https://pub.flutter-io.cn
    FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn

    图片描述

    5、安装Dart与Pub。安装webdev、stagehand

    Pub是Dart的包管理工具,类似npm,捆绑安装。
    Dart安装版地址: http://www.gekorm.com/dart-wi...
    默认安装即可,安装之后记住Dart的路径,并且配置到环境变量path中,以便于可以在命令行中使用dart与pub,默认的路径是:C:Program FilesDartdart-sdkbin
    先安装stagehand,stagehand是创建项目必须的工具。查看一下 C:\Users\chunpengliu\AppData\Roaming\Pub\Cache\bin目录下是否包含stagehand和webdev,如果有,添加到环境变量的path里面,如果没有,按下面方法安装:

    pub global activate stagehand

    webdev是一个类似于Koa的web服务器,执行以下命令安装

    pub global activate webdev
    # or
    flutter packages pub global activate webdev

    6、配置编辑器安装Flutter和Dart插件

    Flutter插件是用来支持Flutter开发工作流 (运行、调试、热重载等)。
    Dart插件 提供代码分析 (输入代码时进行验证、代码补全等)。Android Studio的设置在File-》setting-》plugins-》搜索Flutter和Dart,安装之后重启。

    图片描述

    VS code的设置在extension-》搜索Flutter和Dart,安装之后重启。

    图片描述

    7、运行 flutter doctor

    打开一个新的命令提示符或PowerShell窗口并运行以下命令以查看是否需要安装任何依赖项来完成安装:

    flutter doctor

     这是一个漫长的过程,flutter会检测你的环境,并安装所有的依赖,直至:No issues found!,如果有缺失,会就会再那一项前面打x。你需要一一解决。

    图片描述

    一切就绪!

    创建应用

    1、启动 VS Code

    调用 View>Command Palette…(快捷键ctrl+shift+p)
    输入 ‘flutter’, 然后选择 ‘Flutter: New web Project’ 

    图片描述

    输入 Project 名称 (如flutterweb), 然后按回车键
    指定放置项目的位置,然后按蓝色的确定按钮
    等待项目创建继续,并显示main.dart文件。到此,一个Demo创建完成。

    图片描述

    我们看到了熟悉的HTML文件以及项目入口文件main.dart。
    web目录下的index.html是项目的入口文件。main.dart初始化文件,图片相关资源放在此目录。
    lib目录下的main.dart,是主程序代码所在的地方。
    每个pub包或者Flutter项目都包含一个pubspec.yaml。它包含与此项目相关的依赖项和元数据。
    analysis_options.yaml是配置项目的lint规则。
    /dart_tool 是项目打包运行编译生成的文件,页面主程序main.dart.js就在其中。

    2、调试Demo,打开命令行,进入到项目根目录,执行:

    webdev flutterweb

     编译、打包完成之后,自动启动(或者按F5)默认浏览器,看一下转换后的HTML页面结构:

    图片描述

    lib/main.dart是主程序,源码非常简单,整个页面用widgets堆叠而成,区别于传统的html和css。

    import 'package:flutter_web/material.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: MyHomePage(title: 'Flutter Demo Home Page'),
        );
      }
    }
    
    class MyHomePage extends StatelessWidget {
      MyHomePage({Key key, this.title}) : super(key: key);
    
      final String title;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text(title),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text('Hello, World!',
                ),
              ],
            ),
          ), 
        );
      }
    }

    区别与flutter App应用,我们导入的是flutter_web/material.dart库而非flutter/material.dart,这是因为目前App的接口并非和Web的完全通用,不过随着谷歌开发的继续,它们最终会被合并到一块。
    打开pubspec.yaml(类似于package.json),可以看到只有两个依赖包flutter_web和flutter_web_ui,这两个都已在github上开源。dev的依赖页非常少,两个编译相关的包,和一个静态文件分析包。

    name: flutterweb
    description: An app built using Flutter for web
    environment:
      # You must be using Flutter >=1.5.0 or Dart >=2.3.0
      sdk: '>=2.3.0-dev.0.1 <3.0.0'
    dependencies:
      flutter_web: any
      flutter_web_ui: any
    dev_dependencies:
      build_runner: ^1.4.0
      build_web_compilers: ^2.0.0
      pedantic: ^1.0.0
    dependency_overrides:
      flutter_web:
        git:
          url: https://github.com/flutter/flutter_web
          path: packages/flutter_web
      flutter_web_ui:
        git:
          url: https://github.com/flutter/flutter_web
          path: packages/flutter_web_ui

    实战

    接下来,我们创建一个具有图文功能的下载,根据实例来学习flutter,我们将实现下图的页面。它是一个上下两栏的布局,下栏又分为左右两栏。

    图片描述

    第一步:更改主应用内容,打开lib/main.dart文件,替换class MyApp,首先是根组件MyApp,它是一个类组件继承自无状态组件,是项目的主题配置,在home属性中调用了Home组件:

    class MyApp extends StatelessWidget {
      // 应用的根组件
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: '腾讯新闻客户端下载页', //meta 里的titile
          debugShowCheckedModeBanner: false, // 关闭调试bar
          theme: ThemeData(
            primarySwatch: Colors.blue, // 页面主题 Material风格
          ),
          home: Home(), // 启动首页
        );
      }
    }

    第二步,在Home类中,是我们要渲染的页面顶导,运用了AppBar组件,它包括了一个居中的页面标题和居右的搜索按钮。文本可以像css一样设置外观样式。

    class Home extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          backgroundColor: Colors.white,
          appBar: AppBar(
            backgroundColor: Colors.white,
            elevation: 0.0,
            centerTitle: true,
            title: Text( // 中心文本
              "下载页",
              style:
                  TextStyle(color: Colors.black, fontSize: 16.0, fontWeight: FontWeight.w500),
            ),
    // 搜索图标及特性
            actions: <Widget>[ 
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 20.0),
                child: Icon(
                  Icons.search,
                  color: Colors.black,
                ),
              )
            ],
          ),
    //调用body渲染类,此处可以添加多个方法调用
          body: Stack(
            children: [
                Body() 
            ],
          ),
        );
      }
    }

     第三步,创建页面主体内容,一张图加多个文本,使用了文本组件和图片组件,页面结构采用了flex布局,由于两个Expanded的Flex值均为1,因此将在两个组件之间平均分配空间。SizedBox组件相当于一个空盒子,用来设置margin的距离

    class Body extends StatelessWidget {
      const Body({Key key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Row(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: <Widget>[
            Expanded( // 左侧
              flex: 1,
              child: Image.asset(// 图片组件
                "background-image.jpg", // 这是一张在web/asserts/下的背景图
                fit: BoxFit.contain,
              ),
            ),
            const SizedBox(width: 90.0),
            Expanded( // 右侧
              flex:1,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Text( // 文本组件
                    "腾讯新闻",
                    style: TextStyle(
                        color: Colors.black, fontWeight: FontWeight.w600, fontSize: 50.0, fontFamily: 'Merriweather'),
                  ),
                  const SizedBox(height: 14.0),// SizedBox用来增加间距
                  Text(
                    "腾讯新闻是腾讯公司为用户打造的一款全天候、全方位、及时报道的新闻产品,为用户提供高效优质的资讯、视频和直播服务。资讯超新超全,内容独家优质,话题评论互动。",
                    style: TextStyle(
                        color: Colors.black, fontWeight: FontWeight.w400, fontSize: 24.0, fontFamily: "Microsoft Yahei"),
                    textAlign: TextAlign.justify,
                  ),
                  const SizedBox(height: 20.0), 
                  FlatButton(
                    onPressed: () {}, // 下载按钮的响应事件
                    color: Color(0xFFCFE8E4),
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(16.0),
                    ),
                    child: Padding(
                      padding: const EdgeInsets.all(12.0),
                      child: Text("点击下载", style: TextStyle(fontFamily: "Open Sans")),
                    ),
                  ),
                ],
              ),
            ),
            const SizedBox(width: 100.0),
          ],
        );
      }
    }

     到此,页面创建结束,保存,运行webdev serve,就可以看到效果了。

    总结

    FLutter web是Flutter 的一个分支,在开发完App之后,UI层面的FLutter代码在不修改的情况下可以直接编译为Web版,基本可以做到代码100%复用,体验还不错。目前Flutter web作为预览版无论从性能上、易用上还是布局上都超出了预期,触摸体验挺好,虽然体验比APP差一些,但是比传统的web要好很多。试想一下 Flutter 开发iOS 和Android的App 还免费赠送一份Web版,并且比传统的web开发出来的体验还好。Write once ,Run anywhere。何乐而不为?

    我觉得随着谷歌的持续优化,等到正式版发布之后,开发体验越来越好,Flutter开发者会吃掉H5很大一部分份额。Flutter 可能会给目前客户端的开发模式带来一些变革以及分工的变化, Flutter目前的开发体验不是很好, 但是潜力很大,值得前端人员去学习。

    但是目前还是有一部分问题,Flutter web是为客户端开发(尤其是安卓)人员开发准备的,对于前端理解来说学习成本有点高。目前FLutter web和 flutter 还是两个项目,编译环境也是分开的,需要在代码里面修改Flutter相关库的引用为Flutter_web,组件还不能达到完全通用,这个谷歌承诺正在解决中,谷歌的最终目标是Web、移动App、桌面端win mac linux、以及嵌入式版的Flutter代码库之间保持100%的代码可移植性。

    个人感觉,开发体验还不太好,还有很多坑要去踩,版本变更很快。还有社区资源稀少的问题,需要一定长期的积累。兼容性问题,代码转换后大量使用了web components,除了chrome之外,兼容性还是有些问题。

    安利时间

    我们在web开发过程中,都见过或者使用过一些奇技淫巧,这种技术我们统称为黑魔法,这些黑魔法散落在各个角落,为了方便大家查阅和学习,我们做了收集、整理和归类,并在github上做了一个项目—— awesome-blackmargic,希望各位爱钻研的开发者能够喜欢,也希望大家可以把自己的独门绝技分享出来,如果有兴趣可以给我们发pr。

    如果你对Flutter感兴趣,想进一步了解Flutter,加入我们的QQ群(784383520)吧!

    使用Node.js爬取任意网页资源并输出高质量PDF文件到本地~

    $
    0
    0

    detail?ct=503316480&z=0&ipn=d&word=%E6%B5%B7%E8%BE%B9%E5%A3%81%E7%BA%B8&hs=2&pn=0&spn=0&di=10120&pi=0&rn=1&tn=baiduimagedetail&is=0%2C0&ie=utf-8&oe=utf-8&cl=2&lm=-1&cs=3590237416%2C2845421745&os=3026828862%2C3835093178&simid=0%2C0&adpicid=0&lpn=0&ln=30&fr=ala&fm=&sme=&cg=&bdtype=0&oriquery=%E6%B5%B7%E8%BE%B9%E5%A3%81%E7%BA%B8&objurl=http%3A%2F%2Fabc.2008php.com%2F2017_Website_appreciate%2F2017-10-09%2F20171009204205.jpg&fromurl=ippr_z2C%24qAzdH3FAzdH3Fwkv_z%26e3Bdaabrir_z%26e3Bv54AzdH3Fp7h7AzdH3Fda80AzdH3F8aalAzdH3Flcblld_z%26e3Bip4s&gsm=0&islist=&querylist=

    本文适合无论是否有爬虫以及 Node.js基础的朋友观看~
    需求:
    • 使用 Node.js爬取网页资源,开箱即用的配置
    • 将爬取到的网页内容以 PDF格式输出
    如果你是一名技术人员,那么可以看我接下来的文章,否则,请直接移步到我的 github仓库,直接看文档使用即可

    仓库地址: 附带文档和源码,别忘了给个 star

    本需求使用到的技术: Node.jspuppeteer

    • puppeteer官网地址: puppeteer地址
    • Node.js官网地址: 链接描述
    • Puppeteer是谷歌官方出品的一个通过 DevTools协议控制 headless ChromeNode库。可以通过Puppeteer的提供的api直接控制Chrome模拟大部分用户操作来进行UI Test或者作为爬虫访问页面来收集数据。
    • 环境和安装
    • Puppeteer本身依赖6.4以上的Node,但是为了异步超级好用的 async/await,推荐使用7.6版本以上的Node。另外headless Chrome本身对服务器依赖的库的版本要求比较高,centos服务器依赖偏稳定,v6很难使用headless Chrome,提升依赖版本可能出现各种服务器问题(包括且不限于无法使用ssh),最好使用高版本服务器。(建议使用最新版本的 Node.js

    小试牛刀,爬取京东资源

    const puppeteer = require('puppeteer'); //  引入依赖  
    (async () => {   //使用async函数完美异步 
        const browser = await puppeteer.launch();  //打开新的浏览器
        const page = await browser.newPage();   // 打开新的网页 
        await page.goto('https://www.jd.com/');  //前往里面 'url' 的网页
        const result = await page.evaluate(() => {   //这个result数组包含所有的图片src地址
            let arr = []; //这个箭头函数内部写处理的逻辑  
            const imgs = document.querySelectorAll('img');
            imgs.forEach(function (item) {
                arr.push(item.src)
            })
            return arr 
        });
        // '此时的result就是得到的爬虫数据,可以通过'fs'模块保存'
    })()
    
      复制过去 使用命令行命令 ` node 文件名 ` 就可以运行获取爬虫数据了 
    这个 puppeteer 的包 ,其实是替我们开启了另一个浏览器,重新去开启网页,获取它们的数据。
    
    • 上面只爬取了京东首页的图片内容,假设我的需求进一步扩大,需要爬取京东首页

    中的所有 <a> 标签对应的跳转网页中的所有 title的文字内容,最后放到一个数组中

    • 我们的 async函数上面一共分了五步, 只有 puppeteer.launch() ,

    browser.newPage(), browser.close()是固定的写法。

    • page.goto指定我们去哪个网页爬取数据,可以更换内部url地址,也可以多次

    调用这个方法。

    • page.evaluate这个函数,内部是处理我们进入想要爬取网页的数据逻辑
    • page.goto page.evaluate两个方法,可以在 async内部调用多次,

    那意味着我们可以先进入京东网页,处理逻辑后,再次调用 page.goto这个函数,

    注意,上面这一切逻辑,都是 puppeteer这个包帮我们在看不见的地方开启了另外一个
    浏览器,然后处理逻辑,所以最终要调用 browser.close()方法关闭那个浏览器。

    这时候我们对上一篇的代码进行优化,爬取对应的资源。

     const puppeteer = require('puppeteer');
    (async () => {
        const browser = await puppeteer.launch();
        const page = await browser.newPage();
        await page.goto('https://www.jd.com/');
        const hrefArr = await page.evaluate(() => {
            let arr = [];
            const aNodes = document.querySelectorAll('.cate_menu_lk');
            aNodes.forEach(function (item) {
                arr.push(item.href)
            })
            return arr
        });
        let arr = [];
        for (let i = 0; i < hrefArr.length; i++) {
            const url = hrefArr[i];
            console.log(url) //这里可以打印 
            await page.goto(url);
            const result = await page.evaluate(() => { //这个方法内部console.log无效 
                
                  return  $('title').text();  //返回每个界面的title文字内容
            });
            arr.push(result)  //每次循环给数组中添加对应的值
        }
        console.log(arr)  //得到对应的数据  可以通过Node.js的 fs 模块保存到本地
        await browser.close()
    })()
    
    上面有天坑 page.evaluate函数内部的console.log不能打印,而且内部不能获取外部的变量,只能return返回,
    使用的选择器必须先去对应界面的控制台实验过能不能选择DOM再使用,比如京东无法使用querySelector。这里由于
    京东的分界面都使用了jQuery,所以我们可以用jQuery,总之他们开发能用的选择器,我们都可以用,否则就不可以。

    接下来我们直接来爬取 Node.js的官网首页然后直接生成 PDF

    无论您是否了解Node.js和puppeteer的爬虫的人员都可以操作,请您一定万分仔细阅读本文档并按顺序执行每一步
    本项目实现需求:给我们一个网页地址,爬取他的网页内容,然后输出成我们想要的PDF格式文档,请注意,是高质量的PDF文档
    • 第一步,安装 Node.js ,推荐 http://nodejs.cn/download/Node.js的中文官网下载对应的操作系统包
    • 第二步,在下载安装完了 Node.js后, 启动 windows命令行工具(windows下启动系统搜索功能,输入cmd,回车,就出来了)
    • 第三步 需要查看环境变量是否已经自动配置,在命令行工具中输入 node -v,如果出现 v10. ***字段,则说明成功安装 Node.js
    • 第四步 如果您在第三步发现输入 node -v还是没有出现 对应的字段,那么请您重启电脑即可
    • 第五步 打开本项目文件夹,打开命令行工具(windows系统中直接在文件的 url地址栏输入 cmd就可以打开了),输入 npm i cnpm nodemon -g
    • 第六步 下载 puppeteer爬虫包,在完成第五步后,使用 cnpm i puppeteer --save 命令 即可下载
    • 第七步 完成第六步下载后,打开本项目的 url.js,将您需要爬虫爬取的网页地址替换上去(默认是 http://nodejs.cn/)
    • 第八步 在命令行中输入 nodemon index.js即可爬取对应的内容,并且自动输出到当前文件夹下面的 index.pdf文件中
    TIPS: 本项目设计思想就是一个网页一个 PDF文件,所以每次爬取一个单独页面后,请把 index.pdf拷贝出去,然后继续更换 url地址,继续爬取,生成新的 PDF文件,当然,您也可以通过循环编译等方式去一次性爬取多个网页生成多个 PDF文件。

    对应像京东首页这样的开启了图片懒加载的网页,爬取到的部分内容是 loading状态的内容,对于有一些反爬虫机制的网页,爬虫也会出现问题,但是绝大多数网站都是可以的

    const puppeteer = require('puppeteer');
    const url = require('./url');
    (async () => {
        const browser = await puppeteer.launch({ headless: true })
        const page = await browser.newPage()
        //选择要打开的网页  
        await page.goto(url, { waitUntil: 'networkidle0' })
        //选择你要输出的那个PDF文件路径,把爬取到的内容输出到PDF中,必须是存在的PDF,可以是空内容,如果不是空的内容PDF,那么会覆盖内容
        let pdfFilePath = './index.pdf';
        //根据你的配置选项,我们这里选择A4纸的规格输出PDF,方便打印
        await page.pdf({
            path: pdfFilePath,
            format: 'A4',
            scale: 1,
            printBackground: true,
            landscape: false,
            displayHeaderFooter: false
        });
        await browser.close()
    })()

    文件解构设计

    clipboard.png

    数据在这个时代非常珍贵,按照网页的设计逻辑,选定特定的 href的地址,可以先直接获取对应的资源,也可以通过再次使用 page.goto方法进入,再调用 page.evaluate()处理逻辑,或者输出对应的 PDF文件,当然也可以一口气输出多个 PDF文件~
    这里就不做过多介绍了,毕竟 Node.js 是可以上天的,或许未来它真的什么都能做。这么优质简短的教程,请收藏
    或者转发给您的朋友,谢谢。

    [译]保持Node.js的速度-创建高性能Node.js Servers的工具、技术和提示

    $
    0
    0

    pre-tips

    本文翻译自: Keeping Node.js Fast: Tools, Techniques, And Tips For Making High-Performance Node.js Servers

    原文地址: https://www.smashingmagazine....

    中文标题:保持Node.js的速度-创建高性能Node.js Servers的工具、技术和提示

    快速摘要

    Node 是一个非常多彩的平台,而创建network服务就是其非常重要的能力之一。在本文我们将关注最主流的: HTTP Web servers.

    引子

    如果你已经使用Node.js足够长的时间,那么毫无疑问你会碰到比较痛苦的速度问题。JavaScript是一种事件驱动的、异步的语言。这很明显使得对性能的推理变得棘手。Node.js的迅速普及使得我们必须寻找适合这种server-side javacscript的工具、技术。

    当我们碰到性能问题,在浏览器端的经验将无法适用于服务器端。所以我们如何确保一个Node.js代码是快速的且能达到我们的要求呢?让我们来动手看一些实例

    工具

    我们需要一个工具来压测我们的server从而测量性能。比如,我们使用 autocannon

    npm install -g autocannon // 或使用淘宝源cnpm, 腾讯源tnpm

    其他的Http benchmarking tools 包括 Apache Bench(ab)wrk2, 但AutoCannon是用Node写的,对前端来说会更加方便并易于安装,它可以非常方便的安装在 Windows、Linux 和Mac OS X.

    当我们安装了基准性能测试工具,我们需要用一些方法去诊断我们的程序。一个很不错的诊断性能问题的工具便是 Node Clinic。它也可以用npm安装:

    npm install -g clinic

    这实际上会安装一系列套件,我们将使用 Clinic Doctor
    和 Clinic Flame (一个 ox的封装)

    译者注: ox是一个自动剖析cpu并生成node进程火焰图的工具; 而clinic Flame就是基于ox的封装。
    另一方面, clinic工具本身其实是一系列套件的组合,它不同的子命令分别会调用到不同的子模块,例如:
    • 医生诊断功能。The doctor functionality is provided by Clinic.js Doctor.
    • 气泡诊断功能。The bubbleprof functionality is provided by Clinic.js Bubbleprof.
    • 火焰图功能。 The flame functionality is provided by Clinic.js Flame.)

    tips: 对于本文实例,需要 Node 8.11.2 或更高版本

    代码示例

    我们的例子是一个只有一个资源的简单的 REST server:暴露一个 GET 访问的路由 /seed/v1,返回一个大 JSON 载荷。 server端的代码就是一个app目录,里面包括一个 packkage.json (依赖 restify 7.1.0)、一个 index.js和 一个 util.js (译者注: 放一些工具函数)

    // index.js
    const restify = require('restify')
    const server = restify.createServer()
    const { etagger, timestamp, fetchContent } from './util'
    
    server.use(etagger.bind(server)) // 绑定etagger中间件,可以给资源请求加上etag响应头
    
    server.get('/seed/v1', function () {
      fetchContent(req.url, (err, content) => {
        if (err) {
          return next(err)
        }
        res.send({data: content, ts: timestamp(), url: req.url})
        next()
      })
    })
    
    server.listen(8080, function () {
      cosnole.log(' %s listening at %s',  server.name, server.url)
    })
    // util.js
    const restify = require('restify')
    const crypto = require('crypto')
    
    module.exports = function () {
        const content = crypto.rng('5000').toString('hex') // 普通有规则的随机
    
        const fetchContent = function (url, cb) {
            setImmediate(function () {
            if (url !== '/seed/v1') return restify.errors.NotFoundError('no api!')
                cb(content)
            })
        }
        let last = Date.now()
        const TIME_ONE_MINUTE = 60000
        const timestamp = function () {
          const now = Date.now()
          if (now - last >= TIME_ONE_MINITE) {
              last = now
          }
          return last
        }
        const etagger = function () {
            const cache = {}
            let afterEventAttached  = false
            function attachAfterEvent(server) {
                if (attachAfterEvent ) return
                afterEventAttached  = true
                server.on('after', function (req, res) {
                    if (res.statusCode == 200 && res._body != null) {
                        const urlKey = crpto.createHash('sha512')
                            .update(req.url)
                            .digets()
                            .toString('hex')
                        const contentHash = crypto.createHash('sha512')
                        .update(JSON.stringify(res._body))
                        .digest()
                        .toString('hex')
                        if (cache[urlKey] != contentHash) cache[urlKey] = contentHash
                    }
                })
            }
             return function(req, res, next) {
                    // 译者注: 这里attachEvent的位置好像不太优雅,我换另一种方式改了下这里。可以参考: https://github.com/cuiyongjian/study-restify/tree/master/app
                    attachAfterEvent(this) // 给server注册一个after钩子,每次即将响应数据时去计算body的etag值
                const urlKey = crypto.createHash('sha512')
                .update(req.url)
                .digest()
                .toString('hex')
                // 译者注: 这里etag的返回逻辑应该有点小问题,每次请求都是返回的上次写入cache的etag
                if (urlKey in cache) res.set('Etag', cache[urlKey])
                res.set('Cache-Control', 'public; max-age=120')
            }
        }
        return { fetchContent, timestamp, etagger }
    }

    务必不要用这段代码作为最佳实践,因为这里面有很多代码的坏味道,但是我们接下来将测量并找出这些问题。

    要获得这个例子的源码可以去 这里

    Profiling 剖析

    为了剖析我们的代码,我们需要两个终端窗口。一个用来启动app,另外一个用来压测他。

    第一个terminal,我们执行:

    node ./index.js

    另外一个terminal,我们这样剖析他(译者注: 实际是在压测):

    autocannon -c100 localhost:3000/seed/v1

    这将打开100个并发请求轰炸服务,持续10秒。

    结果大概是下面这个样子:

    statavgstdevMax
    耗时(毫秒)3086.811725.25554
    吞吐量(请求/秒)23.119.1865
    每秒传输量(字节/秒)237.98 kB197.7 kB688.13 kB
    231 requests in 10s, 2.4 MB read

    结果会根据你机器情况变化。然而我们知道: 一般的“Hello World”Node.js服务器很容易在同样的机器上每秒完成三万个请求,现在这段代码只能承受每秒23个请求且平均延迟超过3秒,这是令人沮丧的。

    译者注: 我用公司macpro18款 15寸 16G 256G,测试结果如下:

    图片描述

    诊断

    定位问题

    我们可以通过一句命令来诊断应用,感谢 clinic doctor 的 -on-port 命令。在app目录下,我们执行:

    clinic doctor --on-port='autocannon -c100 localhost:3000/seed/v1' -- node index.js
    译者注:
    现在autocannon的话可以使用新的subarg形式的命令语法:
    clinic doctor --autocannon [ /seed/v1 -c 100 ] -- node index.js

    clinic doctor会在剖析完毕后,创建html文件并自动打开浏览器。

    结果长这个样子:

    图片描述

    译者的测试长这样子:
    --on-port语法

    --autocannon语法

    译者注:横坐标其实是你系统时间,冒号后面的表示当前的系统时间的 - 秒数。
    备注:接下来的文章内容分析,我们还是以原文的统计结果图片为依据。

    跟随UI顶部的消息,我们看到 EventLoop 图表,它的确是红色的,并且这个EventLoop延迟在持续增长。在我们深入研究他意味着什么之前,我们先了解下其他指标下的诊断。

    我们可以看到CPU一直在100%或超过100%这里徘徊,因为进程正在努力处理排队的请求。Node的 JavaScript 引擎(也就是V8) 着这里实际上用 2 个 CPU核心在工作,因为机器是多核的 而V8会用2个线程。 一个线程用来执行 EventLoop,另外一个线程用来垃圾收集。 当CPU高达120%的时候就是进程在回收处理完的请求的遗留对象了(译者注: 操作系统的进程CPU使用率的确经常会超过100%,这是因为进程内用了多线程,OS把工作分配到了多个核心,因此统计cpu占用时间时会超过100%)

    我们看与之相关的内存图表。实线表示内存的堆内存占用(译者注:RSS表示node进程实际占用的内存,heapUsage堆内存占用就是指的堆区域占用了多少,THA就表示总共申请到了多少堆内存。一般看heapUsage就好,因为他表示了node代码中大多数JavaScript对象所占用的内存)。我们看到,只要CPU图表上升一下则堆内存占用就下降一些,这表示内存正在被回收。

    activeHandler跟EventLoop的延迟没有什么相关性。一个active hanlder 就是一个表达 I/O的对象(比如socket或文件句柄) 或者一个timer (比如setInterval)。我们用autocannon创建了100连接的请求(-c100), activehandlers 保持在103. 额外的3个handler其实是 STDOUT,STDERROR 以及 server 对象自身(译者: server自身也是个socket监听句柄)。

    如果我们点击一下UI界面上底部的建议pannel面板,我们会看到:
    图片描述

    短期缓解

    深入分析性能问题需要花费大量的时间。在一个现网项目中,可以给服务器或服务添加过载保护。过载保护的思路就是检测 EventLoop 延迟(以及其他指标),然后在超过阈值时响应一个 "503 Service Unavailable"。这就可以让 负载均衡器转向其他server实例,或者实在不行就让用户过一会重试。 overload-protection-module这个过载保护模块能直接低成本地接入到 Express、Koa 和 Restify使用。Hapi 框架也有一个 配置项提供同样的过载保护。(译者注:实际上看overload-protection模块的底层就是通过 loopbench实现的EventLoop延迟采样,而loopbench就是从Hapi框架里抽离出来的一个模块;至于内存占用,则是overload-protection内部自己实现的采样,毕竟直接用memoryUsage的api就好了)

    理解问题所在

    就像 Clinic Doctor 说的,如果 EventLoop 延迟到我们观察的这个样子,很可能有一个或多个函数阻塞了事件循环。认识到Node.js的这个主要特性非常重要:在当前的同步代码执行完成之前,异步事件是无法被执行的。这就是为什么下面 setTimeout 不能按照预料的时间触发的原因。

    举例,在浏览器开发者工具或Node.js的REPL里面执行:

    console.time('timeout')
    setTimeout(console.timeEnd, 100, 'timeout')
    let n = 1e7
    while (n--) Math.random()

    这个打印出的时间永远不会是100ms。它将是150ms到250ms之间的一个数字。 setTimeoiut调度了一个异步操作(console.timeEnd),但是当前执行的代码没有完成;下面有额外两行代码来做了一个循环。当前所执行的代码通常被叫做“Tick”。要完成这个 Tick,Math.random 需要被调用 1000 万次。如果这会花销 100ms,那么timeout触发时的总时间就是 200ms (再加上setTimeout函数实际推入队列时的延时,约几毫秒)

    译者注: 实际上这里作者的解释有点小问题。首先这个例子假如按他所说循环会耗费100毫秒,那么setTimeout触发时也是100ms而已,不会是两个时间相加。因为100毫秒的循环结束,setTimeout也要被触发了。
    另外:你实际电脑测试时,很可能像我一样得到的结果是 100ms多一点,而不是作者的150-250之间。作者之所以得到 150ms,是因为它使用的电脑性能原因使得 while(n--) 这个循环所花费的时间是 150ms到250ms。而一旦性能好一点的电脑计算1e7次循环只需几十毫秒,完全不会阻塞100毫秒之后的setTimeout,这时得到的结果往往是103ms左右,其中的3ms是底层函数入队和调用花掉的时间(跟这里所说的问题无关)。因此,你自己在测试时可以把1e7改成1e8试试。总之让他的执行时间超过100毫秒。

    在服务器端上下文如果一个操作在当前 Tick 中执行时间很长,那么就会导致请求无法被处理,并且数据也无法获取(译者注:比如处理新的网络请求或处理读取文件的IO事件),因为异步代码在当前 Tick 完成之前无法执行。这意味着计算昂贵的代码将会让server所有交互都变得缓慢。所以建议你拆分资源敏感的任务到单独的进程里去,然后从main主server中去调用它,这能避免那些很少使用但资源敏感(译者注: 这里特指CPU敏感)的路由拖慢了那些经常访问但资源不敏感的路由的性能(译者注:就是不要让某个cpu密集的路径拖慢整个node应用)。

    本文的例子server中有很多代码阻塞了事件循环,所以下一步我们来定位这个代码的具体位置所在。

    分析

    定位性能问题的代码的一个方法就是创建和分析“火焰图”。一个火焰图将函数表达为彼此叠加的块---不是随着时间的推移而是聚合。之所以叫火焰图是因为它用橘黄到红色的色阶来表示,越红的块则表示是个“热点”函数,意味着很可能会阻塞事件循环。获取火焰图的数据需要通过对CPU进行采样---即node中当前执行的函数及其堆栈的快照。而热量(heat)是由一个函数在分析期间处于栈顶执行所占用的时间百分比决定的。如果它不是当前栈中最后被调用的那个函数,那么他就很可能会阻塞事件循环。

    让我们用 clinic flame来生成示例代码的火焰图:

    clinic flame --on-port=’autocannon -c100 localhost:$PORT/seed/v1’ -- node index.js
    译者注: 也可以使用新版命令风格:
    clinic flame --autocannon [ /seed/v1 -c200 -d 10 ] -- node index.js

    结果会自动展示在你的浏览器中:

    Clinic可视化火焰图

    译者注: 新版变成下面这副样子了,功能更强大,但可能得学习下怎么看。。

    图片描述

    (译者注:下面分析时还是看原文的图)
    块的宽度表示它花费了多少CPU时间。可以看到3个主要堆栈花费了大部分的时间,而其中 server.on这个是最红的。 实际上,这3个堆栈是相同的。他们之所以分开是因为在分析期间优化过的和未优化的函数会被视为不同的调用帧。带有 *前缀的是被JavaScript引擎优化过的函数,而带有 ~前缀的是未优化的。如果是否优化对我们的分析不重要,我们可以点击 Merge按钮把它们合并。这时图像会变成这样:

    图片描述

    从开始看,我们可以发现出问题的代码在 util.js里。这个过慢的函数也是一个 event handler:触发这个函数的来源是Node核心里的 events模块,而 server.on是event handler匿名函数的一个后备名称。我们可以看到这个代码跟实际处理本次request请求的代码并不在同一个 Tick 当中(译者注: 如果在同一个Tick就会用一个堆栈图竖向堆叠起来)。如果跟request处理在同一个 Tick中,那堆栈中应该是Node的 http模块、net和stream模块

    如果你展开其他的更小的块你会看到这些Http的Node核心函数。比如尝试下右上角的search,搜索关键词 send(restify和http内部方法都有send方法)。然后你可以发现他们在火焰图的右边(函数按字母排序)(译者注:右侧蓝色高亮的区域)

    搜索http处理函数

    可以看到实际的 HTTP 处理块占用时间相对较少。

    我们可以点击一个高亮的青色块来展开,看到里面 http_outgoing.js文件的 writeHead、write函数(Node核心http库中的一部分)

    图片描述

    我们可以点击 all stack返回到主要视图。

    这里的关键点是,尽管 server.on函数跟实际 request处理代码不在一个 Tick中,它依然能通过延迟其他正在执行的代码来影响了server的性能。

    Debuging 调试

    我们现在从火焰图知道了问题函数在 util.js 的 server.on这个eventHandler里。我们来瞅一眼:

    server.on('after', (req, res) => {
      if (res.statusCode !== 200) return
      if (!res._body) return
      const key = crypto.createHash('sha512')
        .update(req.url)
        .digest()
        .toString('hex')
      const etag = crypto.createHash('sha512')
        .update(JSON.stringify(res._body))
        .digest()
        .toString('hex')
      if (cache[key] !== etag) cache[key] = etag
    })

    众所周知,加密过程都是很昂贵的cpu密集任务,还有序列化(JSON.stringify),但是为什么火焰图中看不到呢?实际上在采样过程中都已经被记录了,只是他们隐藏在 cpp过滤器内 (译者注:cpp就是c++类型的代码)。我们点击 cpp 按钮就能看到如下的样子:

    解开序列化和加密的图

    与序列化和加密相关的内部V8指令被展示为最热的区域堆栈,并且花费了最多的时间。 JSON.stringify方法直接调用了 C++代码,这就是为什么我们看不到JavaScript 函数。在加密这里, createHashupdate这样的函数都在数据中,而他们要么内联(合并并消失在merge视图)要么占用时间太小无法展示。

    一旦我们开始推理etagger函数中的代码,很快就会发现它的设计很糟糕。为什么我们要从函数上下文中获取服务器实例?所有这些hash计算都是必要的吗?在实际场景中也没有If-None-Match头支持,如果用if-none-match这将减轻某些真实场景中的一些负载,因为客户端会发出头请求来确定资源的新鲜度。

    让我们先忽略所有这些问题,先验证一下 server.on中的代码是否是导致问题的原因。我们可以把 server.on里面的代码做成空函数然后生成一个新的火焰图。

    现在 etagger 函数变成这样:

    function etagger () {
      var cache = {}
      var afterEventAttached = false
      function attachAfterEvent (server) {
        if (attachAfterEvent === true) return
        afterEventAttached = true
        server.on('after', (req, res) => {})
      }
      return function (req, res, next) {
        attachAfterEvent(this)
        const key = crypto.createHash('sha512')
          .update(req.url)
          .digest()
          .toString('hex')
        if (key in cache) res.set('Etag', cache[key])
        res.set('Cache-Control', 'public, max-age=120')
        next()
      }
    }

    现在 server.on的事件监听函数是个以空函数 no-op. 让我们再次执行 clinic flame:

    clinic flame --on-port='autocannon -c100 localhost:$PORT/seed/v1' -- node index.js
    Copy

    会生成如下的火焰图:
    server.on为空函数后的火焰图

    这看起来好一些,我们会看到每秒吞吐量有所增长。但是为什么 event emit 的代码这么红? 我们期望的是此时 HTTP 处理要占用最多的CPU时间,毕竟 server.on 里面已经什么都没做了。

    这种类型的瓶颈通常因为一个函数调用超出了一定期望的程度。

    util.js顶部的这一句可疑的代码可能是一个线索:

    require('events').defaultMaxListeners = Infinity

    让我们移除掉这句代码,然后启动我们的应用,带上 --trace-warnings flag标记。

    node --trace-warnings index.js

    如果我们在下一个teminal中执行压测:

    autocannon -c100 localhost:3000/seed/v1

    会看到我们的进程输出一些:

    (node:96371) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 after listeners added. Use emitter.setMaxListeners() to increase limit
      at _addListener (events.js:280:19)
      at Server.addListener (events.js:297:10)
      at attachAfterEvent 
        (/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:22:14)
      at Server.
        (/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:25:7)
      at call
        (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:164:9)
      at next
        (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:120:9)
      at Chain.run
        (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:123:5)
      at Server._runUse
        (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:976:19)
      at Server._runRoute
        (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:918:10)
      at Server._afterPre
        (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:888:10)

    Node 告诉我们有太多的事件添加到了 server 对象上。这很奇怪,因为我们有一句判断,如果 after事件已经绑定到了 server,则直接return。所以首次绑定之后,只有一个 no-op 函数绑到了 server上。

    让我们看下 attachAfterEvent函数:

    var afterEventAttached = false
    function attachAfterEvent (server) {
      if (attachAfterEvent === true) return
      afterEventAttached = true
      server.on('after', (req, res) => {})
    }

    我们发现条件检查语句写错了! 不应该是 attachAfterEvent ,而是 afterEventAttached. 这意味着每个请求都会往 server 对象上添加一个事件监听,然后每个请求的最后所有的之前绑定上的事件都要触发。唉呀妈呀!

    优化

    既然知道了问题所在,让我们看看如何让我们的server更快

    低端优化 (容易摘到的果子)

    让我们还原 server.on 的代码(不让他是空函数了)然后条件语句中改成正确的 boolean 判断。现在我们的 etagger 函数这样:

    function etagger () {
      var cache = {}
      var afterEventAttached = false
      function attachAfterEvent (server) {
        if (afterEventAttached === true) return
        afterEventAttached = true
        server.on('after', (req, res) => {
          if (res.statusCode !== 200) return
          if (!res._body) return
          const key = crypto.createHash('sha512')
            .update(req.url)
            .digest()
            .toString('hex')
          const etag = crypto.createHash('sha512')
            .update(JSON.stringify(res._body))
            .digest()
            .toString('hex')
          if (cache[key] !== etag) cache[key] = etag
        })
      }
      return function (req, res, next) {
        attachAfterEvent(this)
        const key = crypto.createHash('sha512')
          .update(req.url)
          .digest()
          .toString('hex')
        if (key in cache) res.set('Etag', cache[key])
        res.set('Cache-Control', 'public, max-age=120')
        next()
      }
    }

    现在,我们再来执行一次 Profile(进程剖析,进程描述)。

    node index.js

    然后用 autocanno 来profile 它:

    autocannon -c100 localhost:3000/seed/v1

    我们看到结果显示有200倍的提升(持续10秒 100个并发)

    图片描述

    平衡开发成本和潜在的服务器成本也非常重要。我们需要定义我们在优化时要走多远。否则我们很容易将80%的时间投入到20%的性能提高上。项目是否能承受?

    在一些场景下,用 低端优化来花费一天提高200倍速度才被认为是合理的。而在某些情况下,我们可能希望不惜一切让我们的项目尽最大最大最大可能的快。这种抉择要取决于项目优先级。

    控制资源支出的一种方法是设定目标。例如,提高10倍,或达到每秒4000次请求。基于业务需求的这一种方式最有意义。例如,如果服务器成本超出预算100%,我们可以设定2倍改进的目标

    更进一步

    如果我们再做一张火焰图,我们会看到:

    图片描述

    事件监听器依然是一个瓶颈,它依然占用了 1/3 的CPU时间 (它的宽度大约是整行的三分之一)

    (译者注: 在做优化之前可能每次都要做这样的思考:) 通过优化我们能获得哪些额外收益,以及这些改变(包括相关联的代码重构)是否值得?

    ==============

    我们看最终终极优化(译者注:终极优化指的是作者在后文提到的另外一些方法)后能达到的性能特征(持续执行十秒 http://localhost:3000/seed/v1 --- 100个并发连接)

    92k requests in 11s, 937.22 MB read[15]

    尽管终极优化后 1.6倍 的性能提高已经很显著了,但与之付出的努力、改变、代码重构 是否有必要也是值得商榷的。尤其是与之前简单修复一个bug就能提升200倍的性能相比。

    为了实现深度改进,需要使用同样的技术如:profile分析、生成火焰图、分析、debug、优化。最后完成优化后的服务器代码,可以在 这里查看。

    最后提高到 800/s 的吞吐量,使用了如下方法:

    这些更改稍微复杂一些,对代码库的破坏性稍大一些,并使etagger中间件的灵活性稍微降低,因为它会给路由带来负担以提供Etag值。但它在执行Profile的机器上每秒可多增加3000个请求。

    让我们看看最终优化后的火焰图:

    所有优化之后的健康火焰图

    图中最热点的地方是 Node core(node核心)的 net 模块。这是最期望的情况。

    防止性能问题

    完美一点,这里提供一些在部署之前防止性能问题的建议。

    在开发期间使用性能工具作为非正式检查点可以避免把性能问题带入生产环境。建议将AutoCannon和Clinic(或其他类似的工具)作为日常开发工具的一部分。

    购买或使用一个框架时,看看他的性能政策是什么(译者注:对开源框架就看看benchmark和文档中的性能建议)。如果框架没有指出性能相关的,那么就看看他是否与你的基础架构和业务目标一致。例如,Restify已明确(自版本7发布以来)将致力于提升性。但是,如果低成本和高速度是你绝对优先考虑的问题,请考虑使用 Fastify,Restify贡献者测得的速度提高17%。

    在选择一些广泛流行的类库时要多加留意---尤其是留意日志。 在开发者修复issue的时候,他们可能会在代码中添加一些日志输出来帮助他们在未来debug问题。如果她用了一个性能差劲的 logger 组件,这可能会像 温水煮青蛙一样随着时间的推移扼杀性能。 pino日志组件是一个 Node.js 中可以用的速度最快的JSON换行日志组件。

    最后,始终记住Event Loop是一个共享资源。 Node.js服务器的性能会受到最热路径中最慢的那个逻辑的约束。

    为什么我们要熟悉这些通信协议? 【精读】

    $
    0
    0

    clipboard.png

    前端的最重要的基础知识点是什么?

    • 原生 javaScriptHTML, CSS.
    • Dom操作
    • EventLoop和渲染机制
    • 各类工程化的工具原理以及使用,根据需求定制编写插件和包。(webpack的plugin和babel的预设包)
    • 数据结构和算法(特别是 IM以及超大型高并发网站应用等,例如 B站
    • 最后便是通信协议
    在使用某个技术的时候,一定要去追寻原理和底层的实现,长此以往坚持,只要自身底层的基础扎实,无论技术怎么变化,学习起来都不会太累,总的来说就是拒绝5分钟技术

    从输入一个 url地址,到显示页面发生了什么出发:

    • 1.浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;
    • 2.建立TCP连接(三次握手);
    • 3.浏览器发出读取文件(URL 中域名后面部分对应的文件)的HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;
    • 4.服务器对浏览器请求作出响应,并把对应的 html 文本发送给浏览器;
    • 5.浏览器将该 html 文本并显示内容;
    • 6.释放 TCP连接(四次挥手);

    目前常见的通信协议都是建立在 TCP链接之上

    那么什么是 TCP

    TCP是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出SYN连接请求后,等待对方回答

    TCP三次握手的过程如下:
    • 客户端发送 SYN报文给服务器端,进入 SYN_SEND状态。
    • 服务器端收到 SYN报文,回应一个 SYN(SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。
    • 客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态。
    • 三次握手完成,TCP客户端和服务器端成功地建立连接,可以开始传输数据了。
    如图所示:

    clipboard.png

    TCP的四次挥手:
    • 建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的。具体过程如下图所示。
    • 某个应用进程首先调用close,称该端执行“主动关闭”(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
    • 接收到这个FIN的对端执行 “被动关闭”(passive close),这个FIN由TCP确认。

    注意:FIN的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。

    • 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
    • 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。 [3]

    既然每个方向都需要一个FIN和一个ACK,因此通常需要4个分节。

    特别提示: SYN报文用来通知, FIN报文是用来同步的

    clipboard.png

    以上就是面试官常问的三次握手,四次挥手,但是这不仅仅面试题,上面仅仅答到了一点皮毛,学习这些是为了让我们后续方便了解他的优缺点。

    TCP连接建立后,我们可以有多种协议的方式通信交换数据:

    最古老的方式一: http 1.0

    • 早先1.0的HTTP版本,是一种无状态、无连接的应用层协议。
    • HTTP1.0规定浏览器和服务器保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器处理完成后立即断开TCP连接(无连接),服务器不跟踪每个客户端也不记录过去的请求(无状态)。
    • 这种无状态性可以借助cookie/session机制来做身份认证和状态记录。而下面两个问题就比较麻烦了。
    • 首先,无连接的特性导致最大的性能缺陷就是无法复用连接。每次发送请求的时候,都需要进行一次TCP的连接,而TCP的连接释放过程又是比较费事的。这种无连接的特性会使得网络的利用率非常低。
    • 其次就是队头阻塞(headoflineblocking)。由于HTTP1.0规定下一个请求必须在前一个请求响应到达之前才能发送。假设前一个请求响应一直不到达,那么下一个请求就不发送,同样的后面的请求也给阻塞了。
    Http 1.0的致命缺点,就是无法复用 TCP连接和并行发送请求,这样每次一个请求都需要三次握手,而且其实建立连接和释放连接的这个过程是最耗时的,传输数据相反却不那么耗时。还有本地时间被修改导致响应头 expires的缓存机制失效的问题~(后面会详细讲)
    • 常见的请求报文~

    clipboard.png

    于是出现了 Http 1.1,这也是技术的发展必然结果~

    • Http 1.1出现,继承了 Http1.0的优点,也克服了它的缺点,出现了 keep-alive这个头部字段,它表示会在建立 TCP连接后,完成首次的请求,并不会立刻断开 TCP连接,而是保持这个连接状态~进而可以复用这个通道
    • Http 1.1并且支持请求管道化,“并行”发送请求,但是这个并行,也不是真正意义上的并行,而是可以让我们把先进先出队列从客户端(请求队列)迁移到服务端(响应队列)
    例如:客户端同时发了两个请求分别来获取html和css,假如说服务器的css资源先准备就绪,服务器也会先发送html再发送css。
    • B站首页,就有 keep-alive,因为他们也有 IM的成分在里面。需要大量复用 TCP连接~

    clipboard.png

    • HTTP1.1好像还是无法解决队头阻塞的问题
    实际上,现阶段的浏览器厂商采取了另外一种做法,它允许我们打开多个TCP的会话。也就是说,上图我们看到的并行,其实是不同的TCP连接上的HTTP请求和响应。这也就是我们所熟悉的浏览器对同域下并行加载6~8个资源的限制。而这,才是真正的并行!

    Http 1.1的致命缺点:

    • 1.明文传输
    • 2.其实还是没有解决无状态连接的
    • 3.当有多个请求同时被挂起的时候 就会拥塞请求通道,导致后面请求无法发送
    • 4.臃肿的消息首部:HTTP/1.1能压缩请求内容,但是消息首部不能压缩;在现今请求中,消息首部占请求绝大部分(甚至是全部)也较为常见.
    我们也可以用 dns-prefetch和 preconnect tcp来优化~
    <link rel="preconnect" href="//example.com" crossorigin><link rel="dns=prefetch" href="//example.com">
    • Tip: webpack可以做任何事情,这些都可以用插件实现

    基于这些缺点,出现了 Http 2.0

    相较于HTTP1.1,HTTP2.0的主要优点有采用二进制帧封装,传输变成多路复用,流量控制算法优化,服务器端推送,首部压缩,优先级等特点。

    HTTP1.x的解析是基于文本的,基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多。而HTTP/2会将所有传输的信息分割为更小的消息和帧,然后采用二进制的格式进行编码,HTTP1.x的头部信息会被封装到HEADER frame,而相应的RequestBody则封装到DATAframe里面。不改动HTTP的语义,使用二进制编码,实现方便且健壮。

    多路复用

    • 所有的请求都是通过一个 TCP 连接并发完成。HTTP/1.x 虽然通过 pipeline 也能并发请求,但是多个请求之间的响应会被阻塞的,所以 pipeline 至今也没有被普及应用,而 HTTP/2 做到了真正的并发请求。同时,流还支持优先级和流量控制。当流并发时,就会涉及到流的优先级和依赖。即:HTTP2.0对于同一域名下所有请求都是基于流的,不管对于同一域名访问多少文件,也只建立一路连接。优先级高的流会被优先发送。图片请求的优先级要低于 CSS 和 SCRIPT,这个设计可以确保重要的东西可以被优先加载完

    流量控制

    • TCP协议通过sliding window的算法来做流量控制。发送方有个sending window,接收方有receive window。http2.0的flow control是类似receive window的做法,数据的接收方通过告知对方自己的flow window大小表明自己还能接收多少数据。只有Data类型的frame才有flow control的功能。对于flow control,如果接收方在flow window为零的情况下依然更多的frame,则会返回block类型的frame,这张场景一般表明http2.0的部署出了问题。

    服务器端推送

    • 服务器端的推送,就是服务器可以对一个客户端请求发送多个响应。除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。当浏览器请求一个html,服务器其实大概知道你是接下来要请求资源了,而不需要等待浏览器得到html后解析页面再发送资源请求。

    首部压缩

    • HTTP 2.0 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;通信期间几乎不会改变的通用键-值对(用户代理、可接受的媒体类型,等等)只 需发送一次。事实上,如果请求中不包含首部(例如对同一资源的轮询请求),那么 首部开销就是零字节。此时所有首部都自动使用之前请求发送的首部。
    • 如果首部发生变化了,那么只需要发送变化了数据在Headers帧里面,新增或修改的首部帧会被追加到“首部表”。首部表在 HTTP 2.0 的连接存续期内始终存在,由客户端和服务器共同渐进地更新 。
    • 本质上,当然是为了减少请求啦,通过多个js或css合并成一个文件,多张小图片拼合成Sprite图,可以让多个HTTP请求减少为一个,减少额外的协议开销,而提升性能。当然,一个HTTP的请求的body太大也是不合理的,有个度。文件的合并也会牺牲模块化和缓存粒度,可以把“稳定”的代码or 小图 合并为一个文件or一张Sprite,让其充分地缓存起来,从而区分开迭代快的文件。
    Demo的性能对比:

    clipboard.png

    Http的那些致命缺陷,并没有完全解决,于是有了 https,也是目前应用最广的协议之一

    HTTP+ 加密 + 认证 + 完整性保护 =HTTPS ?

    可以这样认为~HTTP 加上加密处理和认证以及完整性保护后即是 HTTPS

    • 如果在 HTTP 协议通信过程中使用未经加密的明文,比如在 Web 页面中输入信用卡号,如果这条通信线路遭到窃听,那么信用卡号就暴露了。
    • 另外,对于 HTTP 来说,服务器也好,客户端也好,都是没有办法确认通信方的。

    因为很有可能并不是和原本预想的通信方在实际通信。并且还需要考虑到接收到的报文在通信途中已经遭到篡改这一可能性。

    • 为了统一解决上述这些问题,需要在 HTTP 上再加入加密处理和认证等机制。我们把添加了加密及认证机制的 HTTP 称为 HTTPS
    不加密的重要内容被 wireshark这类工具抓到包,后果很严重~

    HTTPS 是身披 SSL 外壳的 HTTP

    • HTTPS 并非是应用层的一种新协议。只是 HTTP 通信接口部分用 SSL(SecureSocket Layer)和 TLS(Transport Layer Security)协议代替而已。

    通常,HTTP 直接和 TCP 通信。

    • 当使用 SSL 时,则演变成先和 SSL 通信,再由 SSL和 TCP 通信了。简言之,所谓 HTTPS,其实就是身披 SSL 协议这层外壳的HTTP。
    • 在采用 SSL 后,HTTP 就拥有了 HTTPS 的加密、证书和完整性保护这些功能。SSL 是独立于 HTTP 的协议,所以不光是 HTTP 协议,其他运行在应用层的 SMTP和 Telnet 等协议均可配合 SSL 协议使用。可以说 SSL 是当今世界上应用最为广泛的网络安全术。

    clipboard.png

    相互交换密钥的公开密钥加密技术 -----对称加密

    clipboard.png

    • 在对 SSL 进行讲解之前,我们先来了解一下加密方法。SSL 采用一种叫做公开密钥加密(Public-key cryptography)的加密处理方式。
    • 近代的加密方法中加密算法是公开的,而密钥却是保密的。通过这种方式得以保持加密方法的安全性。
    加密和解密都会用到密钥。没有密钥就无法对密码解密,反过来说,任何人只要持有密钥就能解密了。如果密钥被攻击者获得,那加密也就失去了意义。

    HTTPS 采用混合加密机制

    • HTTPS 采用共享密钥加密和公开密钥加密两者并用的混合加密机制。
    • 但是公开密钥加密与共享密钥加密相比,其处理速度要慢。所以应充分利用两者各自的优势,将多种方法组合起来用于通信。在交换密钥环节使用公开密钥加密方式,之后的建立通信交换报文阶段则使用共享密钥加密方式。

    HTTPS虽好,非对称加密虽好,但是不要滥用

    HTTPS 也存在一些问题,那就是当使用 SSL 时,它的处理速度会变慢。

    SSL 的慢分两种。一种是指通信慢。另一种是指由于大量消耗 CPU 及内存等资源,导致处理速度变慢。

    • 和使用 HTTP 相比,网络负载可能会变慢 2 到 100 倍。除去和 TCP 连接、发送 HTTP 请求 ? 响应以外,还必须进行 SSL 通信,因此整体上处理通信量不可避免会增加。
    • 另一点是 SSL 必须进行加密处理。在服务器和客户端都需要进行加密和解密的运算处理。因此从结果上讲,比起 HTTP 会更多地消耗服务器和客户端的硬件资源,导致负载增强。

    针对速度变慢这一问题,并没有根本性的解决方案,我们会使用 SSL 加速器这种(专用服务器)硬件来改善该问题。该硬件为 SSL 通信专用硬件,相对软件来讲,能够提高数倍 SSL 的计算速度。仅在 SSL 处理时发挥 SSL加速器的功效,以分担负载。

    为什么不一直使用 HTTPS

    • 既然 HTTPS 那么安全可靠,那为何所有的 Web 网站不一直使用 HTTPS?

    其中一个原因是,因为与纯文本通信相比,加密通信会消耗更多的 CPU 及内存资源。如果每次通信都加密,会消耗相当多的资源,平摊到一台计算机上时,能够处理的请求数量必定也会随之减少。

    • 因此,如果是非敏感信息则使用 HTTP 通信,只有在包含个人信息等敏感数据时,才利用 HTTPS 加密通信。

    特别是每当那些访问量较多的 Web 网站在进行加密处理时,它们所承担着的负载不容小觑。在进行加密处理时,并非对所有内容都进行加密处理,而是仅在那些需要信息隐藏时才会加密,以节约资源。

    • 除此之外,想要节约购买证书的开销也是原因之一。

    要进行 HTTPS 通信,证书是必不可少的。而使用的证书必须向认证机构(CA)购买。证书价格可能会根据不同的认证机构略有不同。通常,一年的授权需要数万日元(现在一万日元大约折合 600 人民币)。那些购买证书并不合算的服务以及一些个人网站,可能只会选择采用HTTP 的通信方式。

    clipboard.png

    复习完了基本的协议,介绍下报文格式:

    • 请求报文格式

    clipboard.png

    • 响应报文格式

    clipboard.png

    所谓响应头,请求头,其实都可以自己添加字段,只要前后端给对应的处理机制即可

    Node.js代码实现响应头的设置

    
      if (config.cache.expires) {
                            res.setHeader("expries", new Date(Date.now() + (config.cache.maxAge * 1000)))
                        }
                        if (config.cache.lastModified) {
                            res.setHeader("last-modified", stat.mtime.toUTCString())
                        }
                        if (config.cache.etag) {
                            res.setHeader('Etag', etagFn(stat))
                        }
    }

    响应头的详解:

    clipboard.png

    本人的开源项目,手写的 Node.js静态资源服务器, https://github.com/JinJieTan/...,欢迎 star~

    浏览器的缓存策略:

    • 首次请求:

    clipboard.png

    • 非首次请求:

    clipboard.png

    • 用户行为与缓存:

    clipboard.png

    不能缓存的请求:

    无法被浏览器缓存的请求如下:

    • HTTP信息头中包含Cache-Control:no-cache,pragma:no-cache(HTTP1.0),或Cache-Control:max-age=0等告诉浏览器不用缓存的请求
    • 需要根据Cookie,认证信息等决定输入内容的动态请求是不能被缓存的
    • 经过HTTPS安全加密的请求(有人也经过测试发现,ie其实在头部加入Cache-Control:max-age信息,firefox在头部加入Cache-Control:Public之后,能够对HTTPS的资源进行缓寸)
    • 经过HTTPS安全加密的请求(有人也经过测试发现,ie其实在头部加入Cache-Control:max-age信息,firefox在头部加入Cache-Control:Public之后,能够对HTTPS的资源进行缓存,参考《HTTPS的七个误解》)
    • POST请求无法被缓存
    • HTTP响应头中不包含Last-Modified/Etag,也不包含Cache-Control/Expires的请求无法被缓存

    即时通讯协议

    从最初的没有 websocket协议开始:

    传统的协议无法服务端主动 push数据,于是有了这些骚操作:
    • 轮询,在一个定时器中不停向服务端发送请求。
    • 长轮询,发送请求给服务端,直到服务端觉得可以返回数据了再返回响应,否则这个请求一直挂起~
    • 以上两种都有瑕疵,而且比较明显,这里不再描述。

    为了解决实时通讯,数据同步的问题,出现了 webSocket.

    • webSockets的目标是在一个单独的持久连接上提供全双工、双向通信。在Javascript创建了Web Socket之后,会有一个HTTP请求发送到浏览器以发起连接。在取得服务器响应后,建立的连接会将HTTP升级从HTTP协议交换为WebSocket协议。
    • webSocket原理: 在 TCP连接第一次握手的时候,升级为 ws协议。后面的数据交互都复用这个 TCP通道。
    • 客户端代码实现:
      const ws = new WebSocket('ws://localhost:8080');
            ws.onopen = function () {
                ws.send('123')
                console.log('open')
            }
            ws.onmessage = function () {
                console.log('onmessage')
            }
            ws.onerror = function () {
                console.log('onerror')
            }
            ws.onclose = function () {
                console.log('onclose')
            }
    • 服务端使用 Node.js语言实现
    const express = require('express')
    const { Server } = require("ws");
    const app = express()
    const wsServer = new Server({ port: 8080 })
    wsServer.on('connection', (ws) => {
        ws.onopen = function () {
            console.log('open')
        }
        ws.onmessage = function (data) {
            console.log(data)
            ws.send('234')
            console.log('onmessage' + data)
        }
        ws.onerror = function () {
            console.log('onerror')
        }
        ws.onclose = function () {
            console.log('onclose')
        }
    });
    
    app.listen(8000, (err) => {
        if (!err) { console.log('监听OK') } else {
            console.log('监听失败')
        }
    })

    webSocket的报文格式有一些不一样:

    ![图片上传中...]

    • 客户端和服务端进行Websocket消息传递是这样的:

      • 客户端:将消息切割成多个帧,并发送给服务端。
      • 服务端:接收消息帧,并将关联的帧重新组装成完整的消息。

    即时通讯的心跳检测:

    pingand pong

    • 服务端 Go实现:
    package main
    
    import (
        "net/http""time""github.com/gorilla/websocket"
    )
    
    var (
        //完成握手操作
        upgrade = websocket.Upgrader{
           //允许跨域(一般来讲,websocket都是独立部署的)
           CheckOrigin:func(r *http.Request) bool {
                return true
           },
        }
    )
    
    func wsHandler(w http.ResponseWriter, r *http.Request) {
       var (
             conn *websocket.Conn
             err error
             data []byte
       )
       //服务端对客户端的http请求(升级为websocket协议)进行应答,应答之后,协议升级为websocket,http建立连接时的tcp三次握手将保持。
       if conn, err = upgrade.Upgrade(w, r, nil); err != nil {
            return
       }
    
        //启动一个协程,每隔5s向客户端发送一次心跳消息
        go func() {
            var (
                err error
            )
            for {
                if err = conn.WriteMessage(websocket.TextMessage, []byte("heartbeat")); err != nil {
                    return
                }
                time.Sleep(5 * time.Second)
            }
        }()
    
       //得到websocket的长链接之后,就可以对客户端传递的数据进行操作了
       for {
             //通过websocket长链接读到的数据可以是text文本数据,也可以是二进制Binary
            if _, data, err = conn.ReadMessage(); err != nil {
                goto ERR
         }
         if err = conn.WriteMessage(websocket.TextMessage, data); err != nil {
             goto ERR
         }
       }
    ERR:
        //出错之后,关闭socket连接
        conn.Close()
    }
    
    func main() {
        http.HandleFunc("/ws", wsHandler)
        http.ListenAndServe("0.0.0.0:7777", nil)
    }

    客户端的心跳检测( Node.js实现):

    this.heartTimer = setInterval(() => {
          if (this.heartbeatLoss < MAXLOSSTIMES) {
            events.emit('network', 'sendHeart');
            this.heartbeatLoss += 1;
            this.phoneLoss += 1;
          } else {
            events.emit('network', 'offline');
            this.stop();
          }
          if (this.phoneLoss > MAXLOSSTIMES) {
            this.PhoneLive = false;
            events.emit('network', 'phoneDisconnect');
          }
        }, 5000);

    自定义即时通信协议:

    new Socket开始:

    • 目前即时通讯大都使用现有大公司成熟的 SDK接入,但是逼格高些还是自己重写比较好。
    • 打个小广告,我们公司就是自己定义的即时通讯协议~招聘一位高级前端,地点深圳-深南大道,做跨平台 IM桌面应用开发的~
    • 客户端代码实现(Node.js):
    
    const {Socket} = require('net') 
    const tcp = new Socket()
    tcp.setKeepAlive(true);
    tcp.setNoDelay(true);
    //保持底层tcp链接不断,长连接
    指定对应域名端口号链接
    tcp.connect(80,166.166.0.0)
    建立连接后
    根据后端传送的数据类型 使用对应不同的解析
    readUInt8 readUInt16LE readUInt32LE readIntLE等处理后得到myBuf 
    const myBuf = buffer.slice(start);//从对应的指针开始的位置截取buffer
    const header = myBuf.slice(headstart,headend)//截取对应的头部buffer
    const body = JSON.parse(myBuf.slice(headend-headstart,bodylength).tostring())
    //精确截取数据体的buffer,并且转化成js对象
    即时通讯强烈推荐使用 Golang, GRPC, Prob传输数据。

    上面的一些代码,都在我的开源项目中:

    觉得写得不错,可以点个赞支持下,文章也借鉴了一下其他大佬的文章,但是地址都贴上来了~ 欢迎 gitHub点个 star哦~

    Web Components 入门实例教程

    $
    0
    0

    组件是前端的发展方向,现在流行的 React 和 Vue 都是组件框架。

    谷歌公司由于掌握了 Chrome 浏览器,一直在推动浏览器的原生组件,即 Web Components API。相比第三方框架,原生组件简单直接,符合直觉,不用加载任何外部模块,代码量小。目前,它还在不断发展,但已经可用于生产环境。

    Web Components API 内容很多,本文不是全面的教程,只是一个简单演示,让大家看一下怎么用它开发组件。

    一、自定义元素

    下图是一个用户卡片。

    本文演示如何把这个卡片,写成 Web Components 组件,这里是最后的 完整代码

    网页只要插入下面的代码,就会显示用户卡片。

    <user-card></user-card>

    这种自定义的 HTML 标签,称为自定义元素(custom element)。根据规范,自定义元素的名称必须包含连词线,用与区别原生的 HTML 元素。所以, <user-card>不能写成 <usercard>

    二、 customElements.define()

    自定义元素需要使用 JavaScript 定义一个类,所有 <user-card>都会是这个类的实例。

    
    class UserCard extends HTMLElement {
      constructor() {
        super();
      }
    }

    上面代码中, UserCard就是自定义元素的类。注意,这个类的父类是 HTMLElement,因此继承了 HTML 元素的特性。

    接着,使用浏览器原生的 customElements.define()方法,告诉浏览器 <user-card>元素与这个类关联。

    
    window.customElements.define('user-card', UserCard);

    三、自定义元素的内容

    自定义元素 <user-card>目前还是空的,下面在类里面给出这个元素的内容。

    
    class UserCard extends HTMLElement {
      constructor() {
        super();
    
        var image = document.createElement('img');
        image.src = 'https://semantic-ui.com/images/avatar2/large/kristy.png';
        image.classList.add('image');
    
        var container = document.createElement('div');
        container.classList.add('container');
    
        var name = document.createElement('p');
        name.classList.add('name');
        name.innerText = 'User Name';
    
        var email = document.createElement('p');
        email.classList.add('email');
        email.innerText = 'yourmail@some-email.com';
    
        var button = document.createElement('button');
        button.classList.add('button');
        button.innerText = 'Follow';
    
        container.append(name, email, button);
        this.append(image, container);
      }
    }
    

    上面代码最后一行, this.append()this表示自定义元素实例。

    完成这一步以后,自定义元素内部的 DOM 结构就已经生成了。

    四、 <template>标签

    使用 JavaScript 写上一节的 DOM 结构很麻烦,Web Components API 提供了 <template>标签,可以在它里面使用 HTML 定义 DOM。

    <template id="userCardTemplate"><img src="https://semantic-ui.com/images/avatar2/large/kristy.png" class="image"><div class="container"><p class="name">User Name</p><p class="email">yourmail@some-email.com</p><button class="button">Follow</button></div></template>

    然后,改写一下自定义元素的类,为自定义元素加载 <template>

    
    class UserCard extends HTMLElement {
      constructor() {
        super();
    
        var templateElem = document.getElementById('userCardTemplate');
        var content = templateElem.content.cloneNode(true);
        this.appendChild(content);
      }
    }  

    上面代码中,获取 <template>节点以后,克隆了它的所有子元素,这是因为可能有多个自定义元素的实例,这个模板还要留给其他实例使用,所以不能直接移动它的子元素。

    到这一步为止,完整的代码如下。

    <body><user-card></user-card><template>...</template><script>
        class UserCard extends HTMLElement {
          constructor() {
            super();
    
            var templateElem = document.getElementById('userCardTemplate');
            var content = templateElem.content.cloneNode(true);
            this.appendChild(content);
          }
        }
        window.customElements.define('user-card', UserCard);    </script></body>

    五、添加样式

    自定义元素还没有样式,可以给它指定全局样式,比如下面这样。

    
    user-card {
      /* ... */
    }

    但是,组件的样式应该与代码封装在一起,只对自定义元素生效,不影响外部的全局样式。所以,可以把样式写在 <template>里面。

    <template id="userCardTemplate"><style>
       :host {
         display: flex;
         align-items: center;
         width: 450px;
         height: 180px;
         background-color: #d4d4d4;
         border: 1px solid #d5d5d5;
         box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
         border-radius: 3px;
         overflow: hidden;
         padding: 10px;
         box-sizing: border-box;
         font-family: 'Poppins', sans-serif;
       }
       .image {
         flex: 0 0 auto;
         width: 160px;
         height: 160px;
         vertical-align: middle;
         border-radius: 5px;
       }
       .container {
         box-sizing: border-box;
         padding: 20px;
         height: 160px;
       }
       .container > .name {
         font-size: 20px;
         font-weight: 600;
         line-height: 1;
         margin: 0;
         margin-bottom: 5px;
       }
       .container > .email {
         font-size: 12px;
         opacity: 0.75;
         line-height: 1;
         margin: 0;
         margin-bottom: 15px;
       }
       .container > .button {
         padding: 10px 25px;
         font-size: 12px;
         border-radius: 5px;
         text-transform: uppercase;
       }</style><img src="https://semantic-ui.com/images/avatar2/large/kristy.png" class="image"><div class="container"><p class="name">User Name</p><p class="email">yourmail@some-email.com</p><button class="button">Follow</button></div></template>

    上面代码中, <template>样式里面的 :host伪类,指代自定义元素本身。

    六、自定义元素的参数

    <user-card>内容现在是在 <template>里面设定的,为了方便使用,把它改成参数。

    <user-card
      image="https://semantic-ui.com/images/avatar2/large/kristy.png"
      name="User Name"
      email="yourmail@some-email.com"></user-card>

    <template>代码也相应改造。

    <template id="userCardTemplate"><style>...</style><img class="image"><div class="container"><p class="name"></p><p class="email"></p><button class="button">Follow John</button></div></template>

    最后,改一下类的代码,把参数加到自定义元素里面。

    
    class UserCard extends HTMLElement {
      constructor() {
        super();
    
        var templateElem = document.getElementById('userCardTemplate');
        var content = templateElem.content.cloneNode(true);
        content.querySelector('img').setAttribute('src', this.getAttribute('image'));
        content.querySelector('.container>.name').innerText = this.getAttribute('name');
        content.querySelector('.container>.email').innerText = this.getAttribute('email');
        this.appendChild(content);
      }
    }
    window.customElements.define('user-card', UserCard);    

    七、Shadow DOM

    我们不希望用户能够看到 <user-card>的内部代码,Web Component 允许内部代码隐藏起来,这叫做 Shadow DOM,即这部分 DOM 默认是隐藏的,开发者工具里面看不到。

    自定义元素的 this.attachShadow()方法开启 Shadow DOM,详见下面的代码。

    
    class UserCard extends HTMLElement {
      constructor() {
        super();
        var shadow = this.attachShadow( { mode: 'closed' } );
    
        var templateElem = document.getElementById('userCardTemplate');
        var content = templateElem.content.cloneNode(true);
        content.querySelector('img').setAttribute('src', this.getAttribute('image'));
        content.querySelector('.container>.name').innerText = this.getAttribute('name');
        content.querySelector('.container>.email').innerText = this.getAttribute('email');
    
        shadow.appendChild(content);
      }
    }
    window.customElements.define('user-card', UserCard);

    上面代码中, this.attachShadow()方法的参数 { mode: 'closed' },表示 Shadow DOM 是封闭的,不允许外部访问。

    至此,这个 Web Component 组件就完成了,完整代码可以访问 这里。可以看到,整个过程还是很简单的,不像第三方框架那样有复杂的 API。

    八、组件的扩展

    在前面的基础上,可以对组件进行扩展。

    (1)与用户互动

    用户卡片是一个静态组件,如果要与用户互动,也很简单,就是在类里面监听各种事件。

    
    this.$button = shadow.querySelector('button');
    this.$button.addEventListener('click', () => {
      // do something
    });

    (2)组件的封装

    上面的例子中, <template>与网页代码放在一起,其实可以用脚本把 <template>注入网页。这样的话,JavaScript 脚本跟 <template>就能封装成一个 JS 文件,成为独立的组件文件。网页只要加载这个脚本,就能使用 <user-card>组件。

    这里就不展开了,更多 Web Components 的高级用法,可以接着学习下面两篇文章。

    九、参考链接

    (完)

    文档信息

    • 版权声明:自由转载-非商用-非衍生-保持署名( 创意共享3.0许可证
    • 发表日期: 2019年8月 6日

    vue父子组件通信高级用法

    $
    0
    0

    vue项目的一大亮点就是组件化。使用组件可以极大地提高项目中代码的复用率,减少代码量。但是使用组件最大的难点就是父子组件之间的通信。

    子通信父

    父组件

    <template><div class="parent">我是父组件
        <!--父组件监听子组件触发的say方法,调用自己的parentSay方法--><!--通过:msg将父组件的数据传递给子组件--><children :msg="msg" @say="parentSay"></children></div></template><script>
    import Children from './children.vue'
    export default {
      data () {
        return {
          msg: 'hello, children'
        }
      },
      methods: {
          // 参数就是子组件传递出来的数据
          parentSay(msg){
              console.log(msg) // hello, parent
          }
      },
    
      // 引入子组件
      components:{
          children: Children
      }
    }
    </script>

    子组件

    <template><div class="hello"><div class="children" @click="say">我是子组件
          <div>父组件对我说:{{msg}}
          </div></div></div></template><script>
    
      export default {
          //父组件通过props属性传递进来的数据
          props: {
              msg: {
                  type: String,
                  default: () => {
                      return ''
                  }
              }
          },
          data () {
            return {
                childrenSay: 'hello, parent'
            }
          },
    
          methods: {
              // 子组件通过emit方法触发父组件中定义好的函数,从而将子组件中的数据传递给父组件
              say(){
                  this.$emit('say' , this.childrenSay);
              }
          }
      }</script>

    子组件使用$emit方法调用父组件的方法,达到子通信父的目的。

    父通信子

    父组件

    <!--Html--><template><!--父组件触发click方法--><div class="parent" @click="say">我是父组件
        <!--通过ref标记子组件--><children ref="child"></children></div></template><script>
    import Children from './children.vue'
    export default {
      data () {
        return {
            msg: "hello,my son"
        }
      },
      methods: {
          // 通过$refs调用子组件的parentSay方法
          say(){
             this.$refs.child.parentSay(this.msg);
          }
      },
    
      // 引入子组件
      components:{
          children: Children
      }
    }
    </script>

    子组件

    <template><div class="hello"><div class="children" >我是子组件
          <div>父组件对我说:{{msg}}
          </div></div></div></template><script>
    
      export default {
          data () {
            return {
                msg: ''
            }
          },
    
          methods: {
              // 父组件调用的JavaScript方法parentSay
              parentSay(msg){
                  this.msg = msg;
              }
          }
      }
    </script>

    父组件通过 $refs调用子组件的方法。
    以上就是父子组件通信的方式,父子组件传递数据直接用props,或者使用 $emit$refs依靠事件来传递数据。

    父子组件通信提升篇

    上文中,子通信父是在 子中触发点击事件然后调用父组件的方法,父通信子是在 父中触发点击事件调用子组件的方法。但是实际情况中可能存在 子通信父时子组件不允许有点击事件而事件在父中或者 父通信子时点击事件在子组件中。

    子通信父时击事件在父组件

    这种情况其实很常见,例如提交一个表单时表单的内容为子组件,而保存按钮在父组件上。此时点击保存按钮想要获取子组件表单的值。这种情况下已经不单单是子通信父和父通信子了,需要将两者结合在一起使用才能完成整个通信过程。

    实现的思路是在父组件中点击事件时,先通过父子通信调用子组件的方法,然后在子组件中的该方法里使用子父通信调用父组件的另一个方法并将信息传递回来。以下是代码演示:

    父组件

    <template><div class="parent" @click="getData">我是父组件
        <!--父组件监听子组件触发的transData方法,调用自己的transData方法--><!--通过ref标记子组件--><children ref="child" @transData="transData"></children></div></template><script>
    import Children from './children.vue'
    export default {
      data () {
        return {
          msg: 'hello, children'
        }
      },
      methods: {
          getData(){
              // 调用子组件的getData方法
              this.$refs.child.getData();
          },
          // 参数就是子组件传递出来的数据
          transData(msg){
              console.log(msg) // hello, parent
          }
      },
    
      // 引入子组件
      components:{
          children: Children
      }
    }
    </script>

    子组件

    <template><div class="hello"><div class="children" >我是子组件
          <div>子组件的数据:{{childrenSay}}
          </div></div></div></template><script>
    
      export default {
          data () {
            return {
                childrenSay: 'hello, parent'
            }
          },
          methods: {
              // 子组件通过emit方法触发父组件中定义好的函数,从而将子组件中的数据传递给父组件
              getData() {
                  this.$emit('transData' , this.childrenSay);
              }
          }
      }</script>

    另一种情况思路也和这个一样,基础就在与父通信子和子通信父的灵活运用。
    转评赞就是最大的鼓励

    GitHub 上周 JavaScript 趋势榜项目

    $
    0
    0

    header.png

    1. yemount/pose-animator

    项目地址: https://github.com/yemount/pose-animator

    ⭐stars:4237 | forks:354 | 2117 ⭐stars this week

    Pose Animator拍摄2D矢量图,并基于PoseNet和FaceMesh的识别结果实时对其包含的曲线进行动画处理。 它从计算机图形学中借鉴了基于骨骼的动画的思想,并将其应用于矢量字符。

    2. renrenio/renren-fast-vue

    项目地址: https://github.com/renrenio/renren-fast-vue

    ⭐stars:2023 | forks:1062 | 132 ⭐stars this week

    renren-fast-vue基于vue、element-ui构建开发,实现renren-fast后台管理前端功能,提供一套更优的前端解决方案。

    3. zeit/next.js

    项目地址: https://github.com/zeit/next.js

    ⭐stars:48399 | forks:7232 | 483 ⭐stars this week

    React SSR 框架

    4. discordjs/discord.js

    项目地址: https://github.com/discordjs/discord.js

    ⭐stars:5791 | forks:1447 | 84 ⭐stars this week

    强大的 JavaScript 库,可与 Discord API 进行交互

    5. openspug/spug

    项目地址: https://github.com/openspug/spug

    ⭐stars:2383 | forks:534 | 476 ⭐stars this week

    开源运维平台:面向中小型企业设计的轻量级无Agent的自动化运维平台,整合了主机管理、主机批量执行、主机在线终端、应用发布部署、在线任务计划、配置中心、监控、报警等一系列功能。

    6. ccxt/ccxt

    项目地址: https://github.com/ccxt/ccxt

    ⭐stars:13828 | forks:3889 | 88 ⭐stars this week

    JavaScript / Python / PHP加密货币交易API,支持超过120个 比特币 / 数字货币 交换

    7. Advanced-Frontend/Daily-Interview-Question

    项目地址: https://github.com/Advanced-Frontend/Daily-Interview-Question

    ⭐stars:18641 | forks:2284 | 207 ⭐stars this week

    我是木易杨,公众号「高级前端进阶」作者,每天搞定一道前端大厂面试题,祝大家天天进步,一年后会看到不一样的自己。

    8. quasarframework/quasar

    项目地址: https://github.com/quasarframework/quasar

    ⭐stars:14653 | forks:1667 | 77 ⭐stars this week

    Quasar Framework-在极短时间内构建高性能 VueJS 用户界面

    9. denysdovhan/wtfjs

    项目地址: https://github.com/denysdovhan/wtfjs

    ⭐stars:17262 | forks:1140 | 193 ⭐stars this week

    有趣又棘手的 JavaScript 示例列表

    10. YMFE/yapi

    项目地址: https://github.com/YMFE/yapi

    ⭐stars:15486 | forks:2619 | 310 ⭐stars this week

    YApi 是一个可本地部署的、打通前后端及QA的、可视化的接口管理平台

    11. agalwood/Motrix

    项目地址: https://github.com/agalwood/Motrix

    ⭐stars:21052 | forks:2625 | 272 ⭐stars this week

    一个全功能的下载管理器。

    12. jgraph/drawio-desktop

    项目地址: https://github.com/jgraph/drawio-desktop

    ⭐stars:7603 | forks:811 | 322 ⭐stars this week

    drawio 桌面版

    13. facebook/react

    项目地址: https://github.com/facebook/react

    ⭐stars:148832 | forks:28875 | 514 ⭐stars this week

    大名鼎鼎的 React 框架, 用于构建用户界面的声明性,高效且灵活的JavaScript库。

    14. iamadamdev/bypass-paywalls-chrome

    项目地址: https://github.com/iamadamdev/bypass-paywalls-chrome

    ⭐stars:4799 | forks:359 | 198 ⭐stars this week

    绕过付费墙 web 浏览器扩展

    15. cypress-io/cypress

    项目地址: https://github.com/cypress-io/cypress

    ⭐stars:20222 | forks:1213 | 227 ⭐stars this week

    对浏览器中运行的所有内容进行快速,轻松和可靠的测试。

    16. jgraph/drawio

    项目地址: https://github.com/jgraph/drawio

    ⭐stars:16966 | forks:3550 | 296 ⭐stars this week

    diagrams.net(以前为draw.io)是一个在线图表绘制网站,在此项目中提供了源代码。

    17. twbs/bootstrap

    项目地址: https://github.com/twbs/bootstrap

    ⭐stars:140844 | forks:68838 | 281 ⭐stars this week

    最受欢迎的HTML,CSS和JavaScript框架,用于在网络上开发响应式,移动优先项目。

    18. mui-org/material-ui

    项目地址: https://github.com/mui-org/material-ui

    ⭐stars:57433 | forks:16064 | 276 ⭐stars this week

    React UI 库,组件可以更快,更轻松地进行Web开发。 建立自己的设计系统,或从材料设计开始。

    19. gatsbyjs/gatsby

    项目地址: https://github.com/gatsbyjs/gatsby

    ⭐stars:44405 | forks:8044 | 228 ⭐stars this week

    使用React构建快速,现代化的应用程序和网站

    20. microsoft/playwright

    项目地址: https://github.com/microsoft/playwright

    ⭐stars:12773 | forks:370 | 408 ⭐stars this week

    节点库可通过单个API自动化Chromium,Firefox和WebKit

    21. MarkerHub/eblog

    项目地址: https://github.com/MarkerHub/eblog

    ⭐stars:448 | forks:149 | 100 ⭐stars this week

    eblog是一个基于Springboot2.1.2开发的博客学习项目,为了让项目融合更多的知识点,达到学习目的,编写了详细的从0到1开发文档。主要学习包括:自定义Freemarker标签,使用shiro+redis完成了会话共享,redis的zset结构完成本周热议排行榜,t-io+websocket完成即时消息通知和群聊,rabbitmq+elasticsearch完成博客内容搜索引擎等。值得学习的地方很多!

    22. goldbergyoni/nodebestpractices

    项目地址: https://github.com/goldbergyoni/nodebestpractices

    ⭐stars:43891 | forks:3974 | 293 ⭐stars this week

    Node.js最佳实践列表(2020年5月)

    23. grommet/grommet

    项目地址: https://github.com/grommet/grommet

    ⭐stars:6300 | forks:738 | 40 ⭐stars this week

    基于react的框架,可在整齐的程序包中提供可访问性,模块化,响应性和主题化

    24. egonSchiele/grokking_algorithms

    项目地址: https://github.com/egonSchiele/grokking_algorithms

    ⭐stars:4004 | forks:1558 | 49 ⭐stars this week

    《 Grokking算法》源码

    25. jonasschmedtmann/complete-javascript-course

    项目地址: https://github.com/jonasschmedtmann/complete-javascript-course

    ⭐stars:3629 | forks:5226 | 54 ⭐stars this week

    完整 JavaScript 课程的入门文件,最终项目和常见问题解答

    Viewing all 148 articles
    Browse latest View live


    <script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>