计算机网络基础

本篇博客仅以个人学习记录使用

前言

OSI和TCP/IP是很基础但又非常重要的知识,很多知识点都是以它们为基础去串联的,作为底层,掌握得越透彻,理解上层时会越顺畅。这篇网络基础,就是根据OSI层级逐一展开。

计算机网络基础

计算机网络的分类

按照网络的作用范围:广域网(WAN)、地域网(MAN)、局域网(LAN);

按照网络使用者:公用网络、专用网络。

计算机网络的层次结构

TCP/IP四层模型与OSI体系结构对比:

层次结构设计的基本原则

1.各层之间是相互独立的;

2.每一层需要有足够的灵活性;

3.各层之间完全解耦;

计算机网络的性能指标

速率:bps=bit/s;

时延:发送时延、传播时延、排队时延、处理时延;
往返时间RTT:数据报文在端到端通信中的来回一次的时间。

物理层

物理层的作用:

连接不同的物理设备,传输比特流。该层为上层协议提供了一个传输数据的可靠的物理媒体。简单的说,物理层确保原始的数据可在各种物理媒体上传输。

物理层设备:

1、中继器【Repeater,也叫放大器】:同一局域网的再生信号;两端口的网段必须同一协议;5-4-3规程:10BASE-5以太网中,最多串联4个中继器,5段中只能有3个连接主机;

2、集线器:同一局域网的再生、放大信号(多端口的中继器);半双工,不能隔离冲突域也不能隔离广播域。

信道的基本概念:信道是往一个方向传输信息的媒体,一条通信电路包含一个发送信道和一个接受信道。

1.单工通信信道:只能一个方向通信,没有反方向反馈的信道;

2.半双工通信信道:双方都可以发送和接受信息,但不能同时发送也不能同时接收;

3.全双工通信信道:双方都可以同时发送和接收。

数据链路层

数据链路层概述

数据链路层在物理层提供的服务的基础上向网络层提供服务,其最基本的服务是将源自网络层来的数据可靠地传输到相邻节点的目标机网络层。数据链路层在不可靠的物理介质上提供可靠的传输。

该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。

有关数据链路层的重要知识点:

1.数据链路层为网络层提供可靠的数据传输;

2.基本数据单位为帧;

3.主要的协议:以太网协议;

4.两个重要设备名称:网桥和交换机。

封装成帧:“帧”是数据链路层数据的基本单位:

透明传输:“透明”是指即使控制字符在帧数据中,但是要当做不存在去处理。即在控制字符前加上转义字符ESC。

数据链路层的差错检测

差错检测:奇偶校验码、循环冗余校验码CRC

1.奇偶校验码–局限性:当出错两位时,检测不到错误。

2.循环冗余检验码:根据传输或保存的数据而产生固定位数校验码。

最大传输单元MTU

最大传输单元MTU(Maximum Transmission Unit),数据链路层的数据帧不是无限大的,数据帧长度受MTU限制。
路径MTU:由链路中MTU的最小值决定。

以太网协议详解

MAC地址:每一个设备都拥有唯一的MAC地址,共48位,使用十六进制表示。

以太网协议:是一种使用广泛的局域网技术,是一种应用于数据链路层的协议,使用以太网可以完成相邻设备的数据帧传输:

局域网分类:

Ethernet以太网IEEE802.3:

1.以太网第一个广泛部署的高速局域网;

2.以太网数据速率快;

3.以太网硬件价格便宜,网络造价成本低。

以太网帧结构:

1.类型:标识上层协议(2字节);

2.目的地址和源地址:MAC地址(每个6字节);

3.数据:封装的上层协议的分组(46~1500字节);

4.CRC:循环冗余码(4字节);

5.以太网最短帧:以太网帧最短64字节;以太网帧除了数据部分18字节;数据最短46字节。

MAC地址(物理地址、局域网地址):

1.MAC地址长度为6字节,48位;

2.MAC地址具有唯一性,每个网络适配器对应一个MAC地址;

3.通常采用十六进制表示法,每个字节表示一个十六进制数,用 - 或 : 连接起来;

4.MAC广播地址:FF-FF-FF-FF-FF-FF。

网络层

网络层的目的是实现两个端系统之间的数据透明传送,具体功能包括寻址和路由选择、连接的建立、保持和终止等。数据交换技术是报文交换(基本上被分组所替代):采用储存转发方式,数据交换单位是报文。

网络层中涉及众多的协议,其中包括最重要的协议,也是TCP/IP的核心协议——IP协议。IP协议非常简单,仅仅提供不可靠、无连接的传送服务。IP协议的主要功能有:无连接数据报传输、数据报路由选择和差错控制。

与IP协议配套使用实现其功能的还有地址解析协议ARP、逆地址解析协议RARP、因特网报文协议ICMP、因特网组管理协议IGMP。

具体的协议我们会在接下来的部分进行总结,有关网络层的重点为:

1、网络层负责对子网间的数据包进行路由选择。此外,网络层还可以实现拥塞控制、网际互连等功能;

2、基本数据单位为IP数据报;

3、包含的主要协议:
(1).IP协议(Internet Protocol,因特网互联协议);

(2).ICMP协议(Internet Control Message Protocol,因特网控制报文协议);

(3)ARP协议(Address Resolution Protocol,地址解析协议);

(4)RARP协议(Reverse Address Resolution Protocol,逆地址解析协议)。

4、重要的设备:路由器。

路由器相关协议:

协议详解

IP网际协议是 Internet 网络层最核心的协议。

虚拟互联网络的产生:实际的计算机网络错综复杂;物理设备通过使用IP协议,屏蔽了物理网络之间的差异;当网络中主机使用IP协议连接时,无需关注网络细节,于是形成了虚拟网络。

IP协议使得复杂的实际网络变为一个虚拟互联的网络;并且解决了在虚拟网络中数据报传输路径的问题。

其中,版本指IP协议的版本,占4位,如IPv4和IPv6;

首部位长度表示IP首部长度,占4位,最大数值位15;

总长度表示IP数据报总长度,占16位,最大数值位65535;

TTL表示IP数据报文在网络中的寿命,占8位;

协议表明IP数据所携带的具体数据是什么协议的,如TCP、UDP。

IP协议的转发流程

IP地址的子网划分


A类(8网络号+24主机号)、B类(16网络号+16主机号)、C类(24网络号+8主机号)可以用于标识网络中的主机或路由器,D类地址作为组广播地址,E类是地址保留。

网络地址转换NAT技术

用于多个主机通过一个公有IP访问访问互联网的私有网络中,减缓了IP地址的消耗,但是增加了网络通信的复杂度。

NAT 工作原理:

从内网出去的IP数据报,将其IP地址替换为NAT服务器拥有的合法的公共IP地址,并将替换关系记录到NAT转换表中;

从公共互联网返回的IP数据报,依据其目的的IP地址检索NAT转换表,并利用检索到的内部私有IP地址替换目的IP地址,然后将IP数据报转发到内部网络。

ARP协议与RARP协议

地址解析协议 ARP(Address Resolution Protocol):为网卡(网络适配器)的IP地址到对应的硬件地址提供动态映射。可以把网络层32位地址转化为数据链路层MAC48位地址。

ARP 是即插即用的,一个ARP表是自动建立的,不需要系统管理员来配置。

RARP(Reverse Address Resolution Protocol)协议指逆地址解析协议,可以把数据链路层MAC48位地址转化为网络层32位地址。

ICMP协议详解

网际控制报文协议(Internet Control Message Protocol),可以报告错误信息或者异常情况,ICMP报文封装在IP数据报当中。

ICMP协议的应用:

1.Ping应用:网络故障的排查;

2.Traceroute应用:可以探测IP数据报在网络中走过的路径。

网络层的路由概述

关于路由算法的要求:

正确的完整的、在计算上应该尽可能是简单的、可以适应网络中的变化、稳定的公平的。

自治系统AS:
指处于一个管理机构下的网络设备群,AS内部网络自治管理,对外提供一个或多个出入口,其中自治系统内部的路由协议为内部网关协议,如RIP、OSPF等;自治系统外部的路由协议为外部网关协议,如BGP。

静态路由:
人工配置,难度和复杂度高。

动态路由:

1.链路状态路由选择算法LS:向所有隔壁路由发送信息收敛快;全局式路由选择算法,每个路由器计算路由时,需构建整个网络拓扑图;利用Dijkstra算法求源端到目的端网络的最短路径;Dijkstra(迪杰斯特拉)算法;

2.距离-向量路由选择算法DV:向所有隔壁路由发送信息收敛慢、会存在回路;基础是Bellman-Ford方程(简称B-F方程)。

内部网关路由协议之RIP协议

路由信息协议 RIP(Routing Information Protocol)【应用层】,基于距离-向量的路由选择算法,较小的AS(自治系统),适合小型网络;RIP报文,封装进UDP数据报。

RIP协议特性:

1.RIP在度量路径时采用的是跳数(每个路由器维护自身到其他每个路由器的距离记录);
2.RIP的费用定义在源路由器和目的子网之间;

3.RIP被限制的网络直径不超过15跳;

4.和隔壁交换所有的信息,30主动一次(广播)。

内部网关路由协议之RIP协议

开放最短路径优先协议 OSPF(Open Shortest Path First)【网络层】,基于链路状态的路由选择算法(即Dijkstra算法),较大规模的AS ,适合大型网络,直接封装在IP数据报传输。

OSPF协议优点:

1.安全;

2.支持多条相同费用路径;

3.支持区别化费用度量;

4.支持单播路由和多播路由;

5.分层路由。

RIP与OSPF的对比(路由算法决定其性质):

外部网关路由协议之BGP协议

BGP(Border Gateway Protocol)边际网关协议【应用层】:是运行在AS之间的一种协议,寻找一条好路由:首次交换全部信息,以后只交换变化的部分,BGP封装进TCP报文段。

传输层

第一个端到端,即主机到主机的层次。传输层负责将上层数据分段并提供端到端的、可靠的或不可靠的传输。

此外,传输层还要处理端到端的差错控制和流量控制问题。

传输层的任务是根据通信子网的特性,最佳的利用网络资源,为两个端系统的会话层之间,提供建立、维护和取消传输连接的功能,负责端到端的可靠数据传输。

在这一层,信息传送的协议数据单元称为段或报文。

网络层只是根据网络地址将源结点发出的数据包传送到目的结点,而传输层则负责将数据可靠地传送到相应的端口。

有关网络层的重点:

1.传输层负责将上层数据分段并提供端到端的、可靠的或不可靠的传输以及端到端的差错控制和流量控制问题;

2.包含的主要协议:TCP协议(Transmission Control Protocol,传输控制协议)、UDP协议(User Datagram Protocol,用户数据报协议);

3.重要设备:网关。

UDP协议详解

UDP(User Datagram Protocol: 用户数据报协议),是一个非常简单的协议。

UDP协议的特点:

1.UDP是无连接协议;

2.UDP不能保证可靠的交付数据;

3.UDP是面向报文传输的;

4.UDP没有拥塞控制;

5.UDP首部开销很小。

UDP数据报结构:

首部:8B,四字段/2B【源端口 | 目的端口 | UDP长度 | 校验和】 数据字段:应用数据。

TCP协议详解

TCP(Transmission Control Protocol: 传输控制协议),是计算机网络中非常复杂的一个协议。

TCP协议的功能:

1.对应用层报文进行分段和重组;

2.面向应用层实现复用与分解;

3.实现端到端的流量控制;

4.拥塞控制;

5.传输层寻址;

6.对收到的报文进行差错检测(首部和数据部分都检错);

7.实现进程间的端到端可靠数据传输控制。

TCP协议的特点:

1.TCP是面向连接的协议;

2.TCP是面向字节流的协议;

3.TCP的一个连接有两端,即点对点通信;

4.TCP提供可靠的传输服务;

5.TCP协议提供全双工通信(每条TCP连接只能一对一)。

TCP报文段结构:

最大报文段长度:报文段中封装的应用层数据的最大长度。

TCP首部:

1.序号字段:TCP的序号是对每个应用层数据的每个字节进行编号;

2.确认序号字段:期望从对方接收数据的字节序号,即该序号对应的字节尚未收到。用ack_seq标识;

3.TCP段的首部长度最短是20B ,最长为60字节。但是长度必须为4B的整数倍。

TCP标记的作用:

可靠传输的基本原理

基本原理:

1.不可靠传输信道在数据传输中可能发生的情况:比特差错、乱序、重传、丢失;

2.基于不可靠信道实现可靠数据传输采取的措施。

差错检测:利用编码实现数据包传输过程中的比特差错检测。

确认:接收方向发送方反馈接收状态。

重传:发送方重新发送接收方没有正确接收的数据。

序号:确保数据按序提交。

计时器:解决数据丢失问题。

停止等待协议:是最简单的可靠传输协议,但是该协议对信道的利用率不高。

连续ARQ(Automatic Repeat reQuest:自动重传请求)协议:滑动窗口+累计确认,大幅提高了信道的利用率。

TCP协议的可靠传输:

基于连续ARQ协议,在某些情况下,重传的效率并不高,会重复传输部分已经成功接收的字节。

TCP协议的流量控制:

流量控制:让发送方发送速率不要太快,TCP协议使用滑动窗口实现流量控制。

TCP协议的拥塞控制

拥塞控制与流量控制的区别:

流量控制考虑点对点的通信量的控制,而拥塞控制考虑整个网络,是全局性的考虑。拥塞控制的方法:慢启动算法+拥塞避免算法。

慢开始和拥塞避免:

1.【慢开始】拥塞窗口从1指数增长;

2.到达阈值时进入【拥塞避免】,变成+1增长;

3.【超时】,阈值变为当前cwnd的一半(不能<2);

4.再从【慢开始】,拥塞窗口从1指数增长。

快重传和快恢复:

1.发送方连续收到3个冗余ACK,执行【快重传】,不必等计时器超时;

2.执行【快恢复】,阈值变为当前cwnd的一半(不能<2),并从此新的ssthresh点进入【拥塞避免】。

TCP连接的三次握手(重要)

TCP三次握手使用指令:

面试常客:为什么需要三次握手?

1.第一次握手:客户发送请求,此时服务器知道客户能发;

2.第二次握手:服务器发送确认,此时客户知道服务器能发能收;

3.第三次握手:客户发送确认,此时服务器知道客户能收。

建立连接(三次握手):

第一次:客户向服务器发送连接请求段,建立连接请求控制段(SYN=1),表示传输的报文段的第一个数据字节的序列号是x,此序列号代表整个报文段的序号(seq=x);客户端进入 SYN_SEND (同步发送状态);

第二次:服务器发回确认报文段,同意建立新连接的确认段(SYN=1),确认序号字段有效(ACK=1),服务器告诉客户端报文段序号是y(seq=y),表示服务器已经收到客户端序号为x的报文段,准备接受客户端序列号为x+1的报文段(ack_seq=x+1);服务器由LISTEN进入SYN_RCVD (同步收到状态);

第三次:客户对服务器的同一连接进行确认.确认序号字段有效(ACK=1),客户此次的报文段的序列号是x+1(seq=x+1),客户期望接受服务器序列号为y+1的报文段(ack_seq=y+1);当客户发送ack时,客户端进入ESTABLISHED 状态;当服务收到客户发送的ack后,也进入ESTABLISHED状态;第三次握手可携带数据。

TCP连接的四次挥手(重要)

释放连接(四次挥手)

第一次:客户向服务器发送释放连接报文段,发送端数据发送完毕,请求释放连接(FIN=1),传输的第一个数据字节的序号是x(seq=x);客户端状态由ESTABLISHED进入FIN_WAIT_1(终止等待1状态);

第二次:服务器向客户发送确认段,确认字号段有效(ACK=1),服务器传输的数据序号是y(seq=y),服务器期望接收客户数据序号为x+1(ack_seq=x+1);服务器状态由ESTABLISHED进入CLOSE_WAIT(关闭等待);客户端收到ACK段后,由FIN_WAIT_1进入FIN_WAIT_2;

第三次:服务器向客户发送释放连接报文段,请求释放连接(FIN=1),确认字号段有效(ACK=1),表示服务器期望接收客户数据序号为x+1(ack_seq=x+1);表示自己传输的第一个字节序号是y+1(seq=y+1);服务器状态由CLOSE_WAIT 进入 LAST_ACK (最后确认状态);

第四次:客户向服务器发送确认段,确认字号段有效(ACK=1),表示客户传输的数据序号是x+1(seq=x+1),表示客户期望接收服务器数据序号为y+1+1(ack_seq=y+1+1);客户端状态由FIN_WAIT_2进入TIME_WAIT,等待2MSL时间,进入CLOSED状态;服务器在收到最后一次ACK后,由LAST_ACK进入CLOSED。

为什么需要等待2MSL?

1.最后一个报文没有确认;

2.确保发送方的ACK可以到达接收方;

3.2MSL时间内没有收到,则接收方会重发;

4.确保当前连接的所有报文都已经过期。

应用层

为操作系统或网络应用程序提供访问网络服务的接口。应用层重点:

1.数据传输基本单位为报文;

2.包含的主要协议:FTP(文件传送协议)、Telnet(远程登录协议)、DNS(域名解析协议)、SMTP(邮件传送协议),POP3协议(邮局协议),HTTP协议(Hyper Text Transfer Protocol)。

DNS详解

DNS(Domain Name System:域名系统)【C/S,UDP,端口53】:解决IP地址复杂难以记忆的问题,存储并完成自己所管辖范围内主机的 域名 到 IP 地址的映射。

域名解析的顺序:
1.浏览器缓存;

2.找本机的hosts文件;

3.路由缓存;

4.找DNS服务器(本地域名、顶级域名、根域名)->迭代解析、递归查询。

IP—>DNS服务—>便于记忆的域名。

域名由点、字母和数字组成,分为顶级域(com,cn,net,gov,org)、二级域(baidu,taobao,qq,alibaba)、三级域(www)(12-2-0852)。

DHCP协议详解

DHCP(Dynamic Configuration Protocol:动态主机设置协议):是一个局域网协议,是应用UDP协议的应用层协议。作用:为临时接入局域网的用户自动分配IP地址。

HTTP协议详解

文件传输协议(FTP):控制连接(端口21):传输控制信息(连接、传输请求),以7位ASCII码的格式。整个会话期间一直打开。

HTTP(HyperText Transfer Protocol:超文本传输协议)【TCP,端口80】:是可靠的数据传输协议,浏览器向服务器发收报文前,先建立TCP连接,HTTP使用TCP连接方式(HTTP自身无连接)。

HTTP请求报文方式:
1.GET:请求指定的页面信息,并返回实体主体;

2.POST:向指定资源提交数据进行处理请求;

3.DELETE:请求服务器删除指定的页面;

4.HEAD:请求读取URL标识的信息的首部,只返回报文头;

5.OPETION:请求一些选项的信息;

6.PUT:在指明的URL下存储一个文档。

(1)HTTP工作的结构

(2) HTTPS协议详解

HTTPS(Secure)是安全的HTTP协议,端口号443。基于HTTP协议,通过SSL或TLS提供加密处理数据、验证对方身份以及数据完整性保护。

VUE 移动端适配方案

本篇博客仅以个人学习记录使用
参照开源项目地址:https://juejin.im/entry/5aa09c3351882555602077ca

搭建步骤

1.可直接使用 cli 搭建项目工程,然后执行以下命令安装需要用的依赖包

1
yarn add postcss-aspect-ratio-mini postcss-px-to-viewport postcss-write-svg postcss-preset-env postcss-viewport-units cssnano

安装成功后 package.json 文件中就会有这些依赖包

2.接下来在.postcssrc.js 文件对新安装的 PostCSS 插件进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
module.exports = {
"plugins": {
"postcss-import": {},
"postcss-url": {},
"postcss-aspect-ratio-mini": {},
"postcss-write-svg": {
utf8: false
},
"postcss-cssnext": {},
"postcss-px-to-viewport": {
viewportWidth: 750, // (Number) The width of the viewport.
viewportHeight: 1334, // (Number) The height of the viewport.
unitPrecision: 3, // (Number) The decimal numbers to allow the REM units to grow to.
viewportUnit: 'vw', // (String) Expected units.
selectorBlackList: ['.ignore', '.hairlines'], // (Array) The selectors to ignore and leave as px.
minPixelValue: 1, // (Number) Set the minimum pixel value to replace.
mediaQuery: false // (Boolean) Allow px to be converted in media queries.
},
"postcss-viewport-units":{},
"cssnano": {
preset: "advanced",
autoprefixer: false,
"postcss-zindex": false
}
}
}
1
2
3
4
5
6
7
8
9
10
11
postcss-px-to-viewport 插件对应配置

"postcss-px-to-viewport": {
viewportWidth: 750, // 视窗的宽度,对应的是我们设计稿的宽度,一般是750
viewportHeight: 1334, // 视窗的高度,根据750设备的宽度来指定,一般指定1334,也可以不配置
unitPrecision: 3, // 指定`px`转换为视窗单位值的小数位数(很多时候无法整除)
viewportUnit: 'vw', // 指定需要转换成的视窗单位,建议使用vw
selectorBlackList: ['.ignore', '.hairlines'], // 指定不转换为视窗单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名
minPixelValue: 1, // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值
mediaQuery: false // 允许在媒体查询中转换`px`
}

3.到了这一步已经完成了大部分,但是还是会有一些兼容性的问题,但是可借助 viewport 的 polyfill 解决 viewport 的兼容问题,将以下代码引入到 vue 中的 index.html 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
引入polyfill
<script src="//g.alicdn.com/fdilab/lib3rd/viewport-units-buggyfill/0.6.2/??viewport-units-buggyfill.hacks.min.js,viewport-units-buggyfill.min.js"></script>

调用polyfill
<script>
window.onload = function () {
window.viewportUnitsBuggyfill.init({
hacks: window.viewportUnitsBuggyfillHacks
});
}


var winDPI = window.devicePixelRatio;
var uAgent = window.navigator.userAgent;
var screenHeight = window.screen.height;
var screenWidth = window.screen.width;
var winWidth = window.innerWidth;
var winHeight = window.innerHeight;

//测试设备基本属性
alert(
"Windows DPI:" + winDPI +
";\ruAgent:" + uAgent +
";\rScreen Width:" + screenWidth +
";\rScreen Height:" + screenHeight +
";\rWindow Width:" + winWidth +
";\rWindow Height:" + winHeight
)

</script>

具体的使用。在你的 CSS 中,只要使用到了 viewport 的单位(vw、vh、vmin 或 vmax )地方,需要在样式中添加 content:

1
2
3
4
5
6
7
8
9
.my-viewport-units-using-thingie {
width: 50vmin;
height: 50vmax;
top: calc(50vh - 100px);
left: calc(50vw - 100px);

/* hack to engage viewport-units-buggyfill */
content: 'viewport-units-buggyfill; width: 50vmin; height: 50vmax; top: calc(50vh - 100px); left: calc(50vw - 100px);';
}

这可能会令你感到恶心,而且我们不可能每次写 vw 都去人肉的计算。特别是在我们的这个场景中,咱们使用了 postcss-px-to-viewport 这个插件来转换 vw,更无法让我们人肉的去添加 content 内容。
这个时候就需要前面提到的 postcss-viewport-units 插件。这个插件将让你无需关注 content 的内容,插件会自动帮你处理。转换后的代码可以在浏览器中查看到类似 content: ‘viewport-units-buggyfill;这样的标识

4.Viewport Units Buggyfill 还提供了其他的功能。详细的这里不阐述了。但是 content 也会引起一定的副作用。比如 img 和伪元素::before(:before)或::after(:after)。在 img 中 content 会引起部分浏览器下,图片不会显示。这个时候需要全局添加:

1
2
3
img {
content: normal !important;
}

而对于::after 之类的,就算是里面使用了 vw 单位,Viewport Units Buggyfill 对其并不会起作用。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 编译前
.after {
content: 'after content';
display: block;
width: 100px;
height: 20px;
background: green;
}

// 编译后
.after[data-v-469af010] {
content: "after content";
display: block;
width: 13.333vw;
height: 2.667vw;
background: green;
}

相关插件介绍

postcss-import

postcss-import 主要功有是解决@import 引入路径问题。使用这个插件,可以让你很轻易的使用本地文件、node_modules 或者 web_modules 的文件。这个插件配合 postcss-url 让你引入文件变得更轻松

postcss-url

该插件主要用来处理文件,比如图片文件、字体文件等引用路径的处理。

在 Vue 项目中,vue-loader 已具有类似的功能,只需要配置中将 vue-loader 配置进去

autoprefixer

autoprefixer 插件是用来自动处理浏览器前缀的一个插件。如果你配置了 postcss-cssnext,其中就已具备了 autoprefixer 的功能。在配置的时候,未显示的配置相关参数的话,表示使用的是 Browserslist 指定的列表参数,你也可以像这样来指定 last 2 versions 或者 > 5%。

如此一来,你在编码时不再需要考虑任何浏览器前缀的问题,可以专心撸码。这也是 PostCSS 最常用的一个插件之一。

其他插件

Vue-cli 默认配置了上述三个 PostCSS 插件,但我们要完成 vw 的布局兼容方案,或者说让我们能更专心的撸码,还需要配置下面的几个 PostCSS 插件:

1
2
3
4
5
6
postcss-aspect-ratio-mini
postcss-px-to-viewport
postcss-write-svg
postcss-cssnext (这个插件已经被postcss-preset-env所替代)
cssnano
postcss-viewport-units

postcss-preset-env/postcss-cssnext(废弃)

该插件可以让我们使用 CSS 未来的特性,其会对这些特性做相关的兼容性处理

cssnano

cssnano 主要用来压缩和清理 CSS 代码。在 Webpack 中,cssnano 和 css-loader 捆绑在一起,所以不需要自己加载它。不过你也可以使用 postcss-loader 显式的使用 cssnano

在 cssnano 的配置中,使用了 preset: “advanced”,所以我们需要另外安装:

1
yarn add cssnano-preset-advanced

cssnano 集成了一些其他的 PostCSS 插件,如果你想禁用 cssnano 中的某个插件的时候,可以像下面这样操作:

1
2
3
4
"cssnano": {
autoprefixer: false,
"postcss-zindex": false
}

上面的代码把 autoprefixer 和 postcss-zindex 禁掉了。前者是有重复调用,后者是一个讨厌的东东。只要启用了这个插件,z-index 的值就会重置为 1。这是一个天坑,千万记得将 postcss-zindex 设置为 false。

postcss-px-to-viewport

postcss-px-to-viewport 插件主要用来把 px 单位转换为 vw、vh、vmin 或者 vmax 这样的视窗单位,也是 vw 适配方案的核心插件之一。

在配置中需要配置相关的几个关键参数:

1
2
3
4
5
6
7
8
9
"postcss-px-to-viewport": {
viewportWidth: 750, // 视窗的宽度,对应的是我们设计稿的宽度,一般是750
viewportHeight: 1334, // 视窗的高度,根据750设备的宽度来指定,一般指定1334,也可以不配置
unitPrecision: 3, // 指定`px`转换为视窗单位值的小数位数(很多时候无法整除)
viewportUnit: 'vw', // 指定需要转换成的视窗单位,建议使用vw
selectorBlackList: ['.ignore', '.hairlines'], // 指定不转换为视窗单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名
minPixelValue: 1, // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值
mediaQuery: false // 允许在媒体查询中转换`px`
}

目前出视觉设计稿,我们都是使用 750px 宽度的,那么 100vw = 750px,即 1vw = 7.5px。那么我们可以根据设计图上的 px 值直接转换成对应的 vw 值。在实际撸码过程,不需要进行任何的计算,直接在代码中写 px,比如:

1
2
3
4
5
6
7
8
9
10
.test {
border: .5px solid black;
border-bottom-width: 4px;
font-size: 14px;
line-height: 20px;
position: relative;
}
[w-188-246] {
width: 188px;
}

编译出来的

1
2
3
4
5
6
7
8
9
10
.test {
border: .5px solid #000;
border-bottom-width: .533vw;
font-size: 1.867vw;
line-height: 2.667vw;
position: relative;
}
[w-188-246] {
width: 25.067vw;
}

在不想要把 px 转换为 vw 的时候,首先在对应的元素(html)中添加配置中指定的类名.ignore 或.hairlines(.hairlines 一般用于设置 border-width:0.5px 的元素中):

1
<div class="box ignore"></div>

写 CSS 的时候:

1
2
3
4
5
6
7
8
9
10
11
.ignore {
margin: 10px;
background-color: red;
}
.box {
width: 180px;
height: 300px;
}
.hairlines {
border-bottom: 0.5px solid red;
}

编译出来的 CSS:

1
2
3
4
5
6
7
8
9
10
11
.box {
width: 24vw;
height: 40vw;
}
.ignore {
margin: 10px; /*.box元素中带有.ignore类名,在这个类名写的`px`不会被转换*/
background-color: red;
}
.hairlines {
border-bottom: 0.5px solid red;
}

上面解决了 px 到 vw 的转换计算。那么在哪些地方可以使用 vw 来适配我们的页面。根据相关的测试:

容器适配,可以使用 vw
文本的适配,可以使用 vw
大于 1px 的边框、圆角、阴影都可以使用 vw
内距和外距,可以使用 vw

postcss-aspect-ratio-mini

postcss-aspect-ratio-mini 主要用来处理元素容器宽高比。在实际使用的时候,具有一个默认的结构

1
2
3
<div aspectratio>
<div aspectratio-content></div>
</div>

在实际使用的时候,你可以把自定义属性 aspectratio 和 aspectratio-content 换成相应的类名,比如:

1
2
3
<div class="aspectratio">
<div class="aspectratio-content"></div>
</div>

也可以用自定义属性,它和类名所起的作用是同等的。结构定义之后,需要在你的样式文件中添加一个统一的宽度比默认属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[aspectratio] {
position: relative;
}
[aspectratio]::before {
content: '';
display: block;
width: 1px;
margin-left: -1px;
height: 0;
}

[aspectratio-content] {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
}

如果我们想要做一个 188:246(188 是容器宽度,246 是容器高度)这样的比例容器,只需要这样使用:

1
2
3
[w-188-246] {
aspect-ratio: '188:246';
}

有一点需要特别注意:aspect-ratio 属性不能和其他属性写在一起,否则编译出来的属性只会留下 aspect-ratio 的值,比如:

1
<div aspectratio w-188-246 class="color"></div>

编译前的 CSS 如下:

1
2
3
4
5
[w-188-246] {
width: 188px;
background-color: red;
aspect-ratio: '188:246';
}

编译之后:

1
2
3
[w-188-246]:before {
padding-top: 130.85106382978725%;
}

主要是因为在插件中做了相应的处理,不在每次调用 aspect-ratio 时,生成前面指定的默认样式代码,这样代码没那么冗余。所以在使用的时候,需要把 width 和 background-color 分开来写:

1
2
3
4
5
6
7
[w-188-246] {
width: 188px;
background-color: red;
}
[w-188-246] {
aspect-ratio: '188:246';
}

这个时候,编译出来的 CSS 就正常了:

1
2
3
4
5
6
7
[w-188-246] {
width: 25.067vw;
background-color: red;
}
[w-188-246]:before {
padding-top: 130.85106382978725%;
}

postcss-write-svg

postcss-write-svg 插件主要用来处理移动端 1px 的解决方案。该插件主要使用的是 border-image 和 background 来做 1px 的相关处理。比如:

1
2
3
4
5
6
7
8
9
10
11
12
@svg 1px-border {
height: 2px;
@rect {
fill: var(--color, black);
width: 100%;
height: 50%;
}
}
.example {
border: 1px solid transparent;
border-image: svg(1px-border param(--color #00b1ff)) 2 2 stretch;
}

编译出来的 CSS:

1
2
3
4
.example {
border: 1px solid transparent;
border-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='2px'%3E%3Crect fill='%2300b1ff' width='100%25' height='50%25'/%3E%3C/svg%3E") 2 2 stretch;
}

上面演示的是使用 border-image 方式,除此之外还可以使用 background-image 来实现。比如:

1
2
3
4
5
6
7
8
9
10
11
@svg square {
@rect {
fill: var(--color, black);
width: 100%;
height: 100%;
}
}

#example {
background: white svg(square param(--color #00b1ff));
}

编译出来就是:

1
2
3
#example {
background: white url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%2300b1ff' width='100%25' height='100%25'/%3E%3C/svg%3E");
}
1
特别声明:由于有一些低端机对border-image支持度不够友好,个人建议你使用background-image的这个方案。

postcss-viewport-units

postcss-viewport-units 插件主要是给 CSS 的属性添加 content 的属性,配合 viewport-units-buggyfill 库给 vw、vh、vmin 和 vmax 做适配的操作。

这是实现 vw 布局必不可少的一个插件,因为少了这个插件,这将是一件痛苦的事情。后面你就清楚。

适用情况

上面解决了 px 到 vw 的转换计算。那么在哪些地方可以使用 vw 来适配我们的页面。根据相关的测试:

容器适配,可以使用 vw
文本的适配,可以使用 vw
大于 1px 的边框、圆角、阴影都可以使用 vw
内距和外距,可以使用 vw

ES6技巧

本篇博客仅以个人学习记录使用
原文链接: https://www.h5jun.com/post/six-nifty-es6-tricks.html

通过参数默认值强制要求传参

ES6 指定默认参数在它们被实际使用的时候才会被执行,这个特性让我们可以强制要求传参:

1
2
3
4
5
6
7
8
9
10
/**
* Called if a parameter is missing and
* the default value is evaluated.
*/
function mandatory() {
throw new Error("Missing parameter");
}
function foo(mustBeProvided = mandatory()) {
return mustBeProvided;
}

函数调用 mandatory() 只有在参数 mustBeProvided 缺失的时候才会被执行。
在控制台测试:

1
2
3
4
> foo()
Error: Missing parameter
> foo(123)
123

更多内容:
段落: “Required parameters”

通过 for-of 循环来遍历数组元素和索引

方法 forEach() 允许你遍历一个数组的元素和索引:

1
2
3
4
5
6
7
8
var arr = ["a", "b", "c"];
arr.forEach(function (elem, index) {
console.log("index = "+index+", elem = "+elem);
});
// Output:
// index = 0, elem = a
// index = 1, elem = b
// index = 2, elem = c

ES6 的 for-of 循环支持 ES6 迭代(通过 iterables 和 iterators)和解构。如果你通过数组的新方法 enteries() 再结合解构,可以达到上面 forEach 同样的效果:

1
2
3
4
const arr = ["a", "b", "c"];
for (const [index, elem] of arr.entries()) {
console.log(`index = ${index}, elem = ${elem}`);
}

arr.enteries() 通过索引-元素配对返回一个可迭代对象。然后通过解构数组 [index, elem] 直接得到每一对元素和索引。console.log() 的参数是 ES6 中的模板字面量特性,这个特性带给字符串解析模板变量的能力。
更多内容:
章节: “Destructuring”
章节: “Iterables and iterators”
段落: “Iterating with a destructuring pattern”
章节: “Template literals”

遍历 Unicode 表示的字符串

一些 Unicode 编码的字由两个 JavaScript 字符组成,例如,emoji 表情:

字符串实现了 ES6 迭代,如果你通过迭代来访问字符串,你可以获得编码过的单个字(每个字用 1 或 2 个 JavaScript 字符表示)。例如

1
2
3
4
5
6
7
for (const ch of "x\uD83D\uDE80y") {
console.log(ch.length);
}
// Output:
// 1
// 2
// 1

这让你能够很方便地得到一个字符串中实际的字数:

1
2
> [..."x\uD83D\uDE80y"].length
3

展开操作符 (…) 将它的操作对象展开并插入数组。
更多内容:
章节: “Unicode in ES6”
段落: “The spread operator (…)”

通过变量解构交换两个变量的值

如果你将一对变量放入一个数组,然后将数组解构赋值相同的变量(顺序不同),你就可以不依赖中间变量交换两个变量的值:

1
[a, b] = [b, a];

可以想象,JavaScript 引擎在未来将会针对这个模式进行特别优化,去掉构造数组的开销。
更多内容:
章节: “Destructuring”

通过模板字面量(template literals)进行简单的模板解析

ES6 的模板字面量与文字模板相比,更接近于字符串字面量。但是,如果你将它们通过函数返回,你可以使用他们来做简单的模板渲染:

1
2
3
4
5
6
7
8
const tmpl = addrs => `
<table>
${addrs.map(addr => `
<tr><td>${addr.first}</td></tr>
<tr><td>${addr.last}</td></tr>
`).join("")}
</table>
`;

tmpl 函数将数组 addrs 用 map(通过箭头函数) join 拼成字符串。tmpl() 可以批量插入数据到表格中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const data = [
{ first: "<Jane>", last: "Bond" },
{ first: "Lars", last: "<Croft>" },
];
console.log(tmpl(data));
// Output:
// <table>
//
// <tr><td><Jane></td></tr>
// <tr><td>Bond</td></tr>
//
// <tr><td>Lars</td></tr>
// <tr><td><Croft></td></tr>
//
// </table>

更多内容:
博客文章: “Handling whitespace in ES6 template literals”
段落: “Text templating via untagged template literals”
章节: “Arrow functions”

通过子类工厂实现简单的合成器

当 ES6 类继承另一个类,被继承的类可以是通过任意表达式创建的动态类:

1
2
3
4
// Function id() simply returns its parameter
const id = x => x;

class Foo extends id(Object) {}

这个特性可以允许你实现一种合成器模式,用一个函数来将一个类 C 映射到一个新的继承了C的类。例如,下面的两个函数 Storage 和 Validation 是合成器:

1
2
3
4
5
6
const Storage = Sup => class extends Sup {
save(database) { ··· }
};
const Validation = Sup => class extends Sup {
validate(schema) { ··· }
};

你可以使用它们去组合生成一个如下的 Employee 类:

1
2
class Person { ··· }
class Employee extends Storage(Validation(Person)) { ··· }

更多内容:
段落: “Simple mixins”

进一步阅读

下面的两个章节提供了很好地概括了 ECMAScript 6 的特性:
“An overview of what’s new in ES6”

函数式编程

本篇博客仅以个人学习记录使用
原文链接: https://www.h5jun.com/post/js-functional-1.html

理解

函数式编程这个概念,对于大多数人而言还是有些陌生的,对于它的评价也是褒贬不一,要说到函数式编程就不得不略提一下面向对象。
面向对象对数据进行抽象,将行为以对象的方式封装到数据实体内部,从而降低系统的耦合度。而函数式编程,选择读过程进行抽象,将数据以输入输出流的方式封装进过程内部,从而也降低系统的耦合度。两者虽是截然不同,然而在系统设计的目标上可以说是殊途同归。

面向对象思想和函数式编程思想也是不矛盾的,因为一个庞大的系统,可能既要对数据进行抽象,又要对过程进行抽象,或者一个局部适合进行数据抽象,另一个局部适合进行过程抽象,这都是可能的。数据抽象不一定以对象实体为形式,同样过程抽象也不是说形式上必然是functional的,比如流式对象(InputStream、OutputStream)、Express 的 middleware,就带有明显的过程抽象的特征。但是在通常情况下,OOP更适合用来做数据抽象,FP更适合用来做过程抽象。

纯函数

根据定义,如果一个函数符合两个条件,它被称为纯函数:
1.此函数在相同的输入值时,总是产生相同的输出。函数的输出和当前运行环境的上下文状态无关。
2.此函数运行过程不影响运行环境,比如不会触发事件、更改环境中的对象、终端输出值等。

简单来说,也就是当一个函数的输出不收外部环境影响,同时也不影响外部环境时,该函数就是纯函数。

JavaScript 内置函数中有不少纯函数,也有不少非纯函数。

比如以下函数是纯函数:
1.String.prototype.toUpperCase
2.Array.prototype.map
3.Function.prototype.bind

以下函数不是纯函数:
1.Math.random
2.Date.now
3.document.body.appendChild
4.Array.prototype.sort

为什么要区分纯函数和非纯函数呢?因为在系统里,纯函数与非纯函数相比,在可测试性、可维护性、可移植性、并行计算和可扩展性方面都有着巨大的优势。

对于纯函数,因为是无状态的,测试的时候不需要构建运行时环境,也不需要用特定的顺序进行测试:

1
2
3
4
test( t => {
t.is(add(10,20), 30); //add(x,y) 是个纯函数,不需要为它构建测试环境
...
});

对于非纯函数,就比较复杂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
test.before(t => {
let list = document.createElement('ul');
list.id = 'xxxxxx';
...
});

test(t => {
let list = document.getElementById('xxxxxx');
t.is(sortList(list).innerHTML, `<ul>
...
</ul>`);
});

test.after(t => {
...
document.removeChild(list);
});

函数式编程能够减少系统中的非纯函数

简述JavaScript设计模式

本篇博客仅以个人学习记录使用

介绍

一个模式就是一个可重用的方案,可应用于在软件设计中的常见问题,通常情况下,
模式是行之有效的解决方法:他们提供固定的解决方法来解决在软件开发中出现的问题
模式可以很容易地重用:一个模式通常反映了一个可以适应自己需要的开箱即用的解决方案。这个特性让它们很健壮。
模式善于表达:当我们看到一个提供某种解决方案的模式时,一般有一组结构和词汇可以非常优雅地帮助表达相当大的解决方案

分类

创建型设计模式

创建型设计模式关注于对象创建的机制方法,通过该方法,对象以适应工作环境的方式被创建。基本的对象创建方法可能会给项目增加额外的复杂性,而这些模式的目的就是为了通过控制创建过程解决这个问题。

属于这一类的一些模式是:构造器模式(Constructor),工厂模式(Factory),抽象工厂模式 (Abstract),原型模式 (Prototype),单例模式 (Singleton)以及 建造者模式(Builder)

创建型设计模式

结构模式关注于对象组成和通常识别的方式实现不同对象之间的关系。该模式有助于在系统的某一部分发生改变的时候,整个系统结构不需要改变。该模式同样有助于对系统中某部分没有达到某一目的的部分进行重组。

在该分类下的模式有:装饰模式,外观模式,享元模式,适配器模式和代理模式。

行为设计模式

行为模式关注改善或精简在系统中不同对象间通信。

行为模式包括:迭代模式,中介者模式,观察者模式和访问者模式。

根据创建对象的概念分类

Factory Method(工厂方法):通过将数据和事件接口化来构建若干个子类。
Abstract Factory(抽象工厂):建立若干族类的一个实例,这个实例不需要具体类的细节信息。(抽象类)
Builder (建造者):将对象的构建方法和其表现形式分离开来,总是构建相同类型的对象。
Prototype(原型):一个完全初始化的实例,用于拷贝或者克隆。
Singleton(单例):一个类只有唯一的一个实例,这个实例在整个程序中有一个全局的访问点。

根据构建对象块的方法分类

Adapter(适配器):将不同类的接口进行匹配,调整,这样尽管内部接口不兼容但是不同的类还是可以协同工作的。
Bridge(桥接模式):将对象的接口从其实现中分离出来,这样对象的实现和接口可以独立的变化。
Composite(组合模式):通过将简单可组合的对象组合起来,构成一个完整的对象,这个对象的能力将会超过这些组成部分的能力的总和,即会有新的能力产生。
Decorator(装饰器):动态给对象增加一些可替换的处理流程。
Facada(外观模式):一个类隐藏了内部子系统的复杂度,只暴露出一些简单的接口。
Flyweight(享元模式):一个细粒度对象,用于将包含在其它地方的信息 在不同对象之间高效地共享。
Proxy(代理模式):一个充当占位符的对象用来代表一个真实的对象。

基于对象间作用方式分类

Interpreter(解释器):将语言元素包含在一个应用中的一种方式,用于匹配目标语言的语法。
Template Method(模板方法):在一个方法中为某个算法建立一层外壳,将算法的具体步骤交付给子类去做。

Chain of Responsibility(响应链):一种将请求在一串对象中传递的方式,寻找可以处理这个请求的对象。
Command(命令):封装命令请求为一个对象,从而使记录日志,队列缓存请求,未处理请求进行错误处理 这些功能称为可能。
Iterator(迭代器):在不需要直到集合内部工作原理的情况下,顺序访问一个集合里面的元素。
Mediator(中介者模式):在类之间定义简化的通信方式,用于避免类之间显式的持有彼此的引用。
Observer(观察者模式):用于将变化通知给多个类的方式,可以保证类之间的一致性。
State(状态):当对象状态改变时,改变对象的行为。
Strategy(策略):将算法封装到类中,将选择和实现分离开来。
Visitor(访问者):为类增加新的操作而不改变类本身。

总结

设计模式有很多,大多数都是遵循设计原则,结合经验和问题所衍生出来的,设计模式也可以很灵活,虽然设计模式在解决软件开发问题当中有很大的作用但是在开发的过程中,不能因为设计模式而去设计模式,要正确认识和理解的去学习,才能更好的发挥作用。

js中的setTimeout与Promise

本篇博客仅以个人学习记录使用

介绍

Promise 是在 ES5 之后出现的,这是属于 JavaScript 引擎本身的能力,由 JavaScript 引擎发起的任务被称作微观任务。Promise 是 JavaScript 语言提供的一种标准化的异步管理方式,当需要 IO、等待或者其他异步操作的函数,不直接返回真实结果,而返回一个 Promise,
函数的调用方可以在合适的时机,选择等待这个 Promise 兑现(通过 Promise 的 then 方法的回调)

Promise 的基本用法示例如下:

1
2
3
4
5
6
7
function sleep(duration) {
return new Promise(function(resolve, reject)) {
setTimeout(resolve,duration);
}
}

sleep(1000).then( () => console.log('完成'))

Promise 的 then 回调是一个异步的执行过程,下面来研究一下 Promise 函数中的执行顺序:

1
2
3
4
5
6
let r = new Promise(function(resolve.reject) {
console.log("a");
resolve();
});
r.then( () => console.log("c"));
console.log("b")

执行这段代码之后,输出的顺序是 a,b,c。在进入 cpnsole.log(b)之前,毫无疑问 r 已经得到了 resolve,但是 Promise 的 resolve 始终是异步操作,所以 c 无法出现在 b 之前。

setTimeout 是浏览器的 api,是在 window 下的,是宿主环境中的,他被称作宏观任务。

那么,如果将 setTimeout 混用 Promise 会怎样呢??

在下面的代码中,设置了两段互不相干的异步操作:通过 setTimeout 执行 console.log(“d”),通过 Promise 执行 console.log(“c”).

1
2
3
4
5
6
7
8
9
let r = new Promise(function(resolve,reject) {
console.log("a");
resolve()
})

setTimeout( () => console.log("d"))

r.then( () => console.log("c"))
console.log("b")

不论代码顺序如何,d 必定发生在 c 之后,因为 Promise 产生的是 JavaScript 引擎内部的微任务,而 setTimeout 是浏览器 api,它产生宏任务,而微任务始终优先于宏任务。举例代码:

1
2
3
4
5
6
7
8
9
10
11
12
setTimeout( () => console.log("d"), 0)
let r = new Promise(function(resolve,reject){
resolve()
});
r.then(() => {
let begin = Date.now();
while(Date.now() - begin < 1000);
console.log("c1")
new Promise(function(resolve,reject) {
resolve();
}).then( () => console.log("c2"));
});

以上代码强制了 1 秒的执行耗时,这样,可以确保任务 c2 是在 d 之后被添加到任务队列。但是即便是这样,先执行了 c1,由执行了 c2,d 仍然是最后执行的,这就很好的解释了微任务优先的原理。

通过以上的分析,总结一下如何分析异步执行的顺序: 1.首先分析有多少个宏观任务; 2.在每个宏观任务中,分析有多少个微观任务; 3.根据调用次序,确定宏任务中的微任务执行次序; 4.根据宏任务的触发规则和调用次序,确定宏任务的执行次序; 5.确定整个顺序。

这是一个稍微复杂的例子;

1
2
3
4
5
6
7
8
function sleep(duration) {
return new Promise(function(resolve,reject) {
console.log("b");
setTimeout(resolve,duration);
})
}
console.log("a");
sleep(5000).then( () => console.log("C"));

这是一段分非常常用的封装方法,利用 Promise 把 setTimeout 封装成可以用于异步的函数。我们首先来看,setTimeout 把整个代码分割成了 2 个宏观任务,这里不论是 5 秒还是 0 秒,都是一样的。第一个宏观任务中,包含了先后同步执行的 console.log(“a”);和 console.log(“b”)
setTimeout 后,第二个宏观任务执行调用了 resolve,然后 then 中的代码异步得到执行,所以调用了 console.log(“c”),最终输出的顺序才是 a,b,c.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
setTimeout(function() {
console.log(1);
})

new Promise(function(resolve, reject) {
console.log(2);
resolve(3)
}).then((res) => {
console.log(res)
})
console.log(4);

2
4
3
1

原因:
首先 setTimeout 会在下一轮时间循环执行,所以不会当时就打印。
Promise 对象在实例的时候其实就已经执行了内部的代码,所以 2 首先打印了。
Promise.then() 在本轮事件循环结束之后执行,所以 3 会在 4 之后打印。
本轮事件循环结束,开始下一循环打印 setTimeout 中的 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");

}
async function async2() {
console.log( 'async2');
}
console.log("script start");
setTimeout(function () {
console.log("settimeout");
},0);
async1();
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log('script end');


script start
async1 start
async2
promise1
script end
promise2
async1 end
settimeout

css 水平垂直居中

本篇博客仅以个人学习记录使用
参考链接:https://yanhaijing.com/css/2018/01/17/horizontal-vertical-center/

介绍

使用css实现水平垂直居中是一个最常见不过的需求了,这里做一下总结,特别区分定宽高和不定宽高的水平垂直居中

仅居中元素定宽高适用

1.absolute + 负margin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div class="wp">
<div class="box size">123123</div>
</div>

wp是父元素的类名,box是子元素的类名,因为有定宽和不定宽的区别,size用来表示指定宽度,下面是所有效果都要用到的公共代码,主要是设置颜色和宽高

/* 公共代码 */
.wp {
border: 1px solid red;
width: 300px;
height: 300px;
}

.box {
background: green;
}

.box.size{
width: 100px;
height: 100px;
}
/* 公共代码 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
绝对定位的百分比是相对于最近已定位父元素的宽高,通过这个特性可以让子元素的居中显示,但绝对定位是基于子元素的左上角,期望的效果是子元素的中心居中显示

为了修正这个问题,可以借助外边距的负值,负的外边距可以让元素向相反方向定位,通过指定子元素的外边距为子元素宽度一半的负值,就可以让子元素居中了,css代码如下

/* 此处引用上面的公共代码 */
/* 此处引用上面的公共代码 */

/* 定位代码 */
.wp {
position: relative;
}
.box {
position: absolute;;
top: 50%;
left: 50%;
margin-left: -50px;
margin-top: -50px;
}


这是比较常用的方式,这种方式比较好理解,兼容性也很好,缺点是需要知道子元素的宽

2.absolute + margin auto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div class="wp">
<div class="box size">123123</div>
</div>

wp是父元素的类名,box是子元素的类名,因为有定宽和不定宽的区别,size用来表示指定宽度,下面是所有效果都要用到的公共代码,主要是设置颜色和宽高

/* 公共代码 */
.wp {
border: 1px solid red;
width: 300px;
height: 300px;
}

.box {
background: green;
}

.box.size{
width: 100px;
height: 100px;
}
/* 公共代码 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这种方式也要求居中元素的宽高必须固定
这种方式通过设置各个方向的距离都是0,此时再讲margin设为auto,就可以在各个方向上居中了

/* 此处引用上面的公共代码 */
/* 此处引用上面的公共代码 */

/* 定位代码 */
.wp {
position: relative;
}
.box {
position: absolute;;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
}

这种方法兼容性也很好,缺点是需要知道子元素的宽高

3.absolute + calc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div class="wp">
<div class="box size">123123</div>
</div>

wp是父元素的类名,box是子元素的类名,因为有定宽和不定宽的区别,size用来表示指定宽度,下面是所有效果都要用到的公共代码,主要是设置颜色和宽高

/* 公共代码 */
.wp {
border: 1px solid red;
width: 300px;
height: 300px;
}

.box {
background: green;
}

.box.size{
width: 100px;
height: 100px;
}
/* 公共代码 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
这种方式也要求居中元素的宽高必须固定

感谢css3带来了计算属性,既然top的百分比是基于元素的左上角,那么在减去宽度的一半就好了,代码如下

/* 此处引用上面的公共代码 */
/* 此处引用上面的公共代码 */

/* 定位代码 */
.wp {
position: relative;
}
.box {
position: absolute;;
top: calc(50% - 50px);
left: calc(50% - 50px);
}

这种方法兼容性依赖calc的兼容性,缺点是需要知道子元素的宽高

居中元素不定宽高

1.absolute + transform

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div class="wp">
<div class="box">123123</div>
</div>

wp是父元素的类名,box是子元素的类名,因为有定宽和不定宽的区别,size用来表示指定宽度,下面是所有效果都要用到的公共代码,主要是设置颜色和宽高

/* 公共代码 */
.wp {
border: 1px solid red;
width: 300px;
height: 300px;
}

.box {
background: green;
}

.box.size{
width: 100px;
height: 100px;
}
/* 公共代码 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
还是绝对定位,但这个方法不需要子元素固定宽高,所以不再需要size类了

修复绝对定位的问题,还可以使用css3新增的transform,transform的translate属性也可以设置百分比,其是相对于自身的宽和高,所以可以讲translate设置为-50%,就可以做到居中了,代码如下

/* 此处引用上面的公共代码 */
/* 此处引用上面的公共代码 */

/* 定位代码 */
.wp {
position: relative;
}
.box {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

这种方法兼容性依赖translate2d的兼容性

2.lineheight

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div class="wp">
<div class="box">123123</div>
</div>

wp是父元素的类名,box是子元素的类名,因为有定宽和不定宽的区别,size用来表示指定宽度,下面是所有效果都要用到的公共代码,主要是设置颜色和宽高

/* 公共代码 */
.wp {
border: 1px solid red;
width: 300px;
height: 300px;
}

.box {
background: green;
}

.box.size{
width: 100px;
height: 100px;
}
/* 公共代码 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
利用行内元素居中属性也可以做到水平垂直居中,不需要size

把box设置为行内元素,通过text-align就可以做到水平居中,但很多同学可能不知道通过通过vertical-align也可以在垂直方向做到居中,代码如下

/* 此处引用上面的公共代码 */
/* 此处引用上面的公共代码 */

/* 定位代码 */
.wp {
line-height: 300px;
text-align: center;
font-size: 0px;
}
.box {
font-size: 16px;
display: inline-block;
vertical-align: middle;
line-height: initial;
text-align: left; /* 修正文字 */
}

这种方法需要在子元素中将文字显示重置为想要的效果

3.writing-mode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div class="wp">
<div class="box">123123</div>
</div>

wp是父元素的类名,box是子元素的类名,因为有定宽和不定宽的区别,size用来表示指定宽度,下面是所有效果都要用到的公共代码,主要是设置颜色和宽高

/* 公共代码 */
.wp {
border: 1px solid red;
width: 300px;
height: 300px;
}

.box {
background: green;
}

.box.size{
width: 100px;
height: 100px;
}
/* 公共代码 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
很多人一定和我一样不知道writing-mode属性,感谢@张鑫旭老师的反馈,简单来说writing-mode可以改变文字的显示方向,比如可以通过writing-mode让文字的显示变为垂直方向

<div class="div1">水平方向</div>
<div class="div2">垂直方向</div>

.div2 {
writing-mode: vertical-lr;
}

显示效果如下:

水平方向





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

更神奇的是所有水平方向上的css属性,都会变为垂直方向上的属性,比如text-align,通过writing-mode和text-align就可以做到水平和垂直方向的居中了,只不过要稍微麻烦一点

<div class="wp">
<div class="wp-inner">
<div class="box">123123</div>
</div>
</div>


/* 此处引用上面的公共代码 */
/* 此处引用上面的公共代码 */

/* 定位代码 */
.wp {
writing-mode: vertical-lr;
text-align: center;
}
.wp-inner {
writing-mode: horizontal-tb;
display: inline-block;
text-align: center;
width: 100%;
}
.box {
display: inline-block;
margin: auto;
text-align: left;
}

4.table

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div class="wp">
<div class="box">123123</div>
</div>

wp是父元素的类名,box是子元素的类名,因为有定宽和不定宽的区别,size用来表示指定宽度,下面是所有效果都要用到的公共代码,主要是设置颜色和宽高

/* 公共代码 */
.wp {
border: 1px solid red;
width: 300px;
height: 300px;
}

.box {
background: green;
}

.box.size{
width: 100px;
height: 100px;
}
/* 公共代码 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
曾经table被用来做页面布局,现在没人这么做了,但table也能够实现水平垂直居中,但是会增加很多冗余代码

<table>
<tbody>
<tr>
<td class="wp">
<div class="box">123123</div>
</td>
</tr>
</tbody>
</table>

tabel单元格中的内容天然就是垂直居中的,只要添加一个水平居中属性就好了

.wp {
text-align: center;
}
.box {
display: inline-block;
}

这种方法就是代码太冗余,而且也不是table的正确用法

5.css-table

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div class="wp">
<div class="box">123123</div>
</div>

wp是父元素的类名,box是子元素的类名,因为有定宽和不定宽的区别,size用来表示指定宽度,下面是所有效果都要用到的公共代码,主要是设置颜色和宽高

/* 公共代码 */
.wp {
border: 1px solid red;
width: 300px;
height: 300px;
}

.box {
background: green;
}

.box.size{
width: 100px;
height: 100px;
}
/* 公共代码 */
1
2
3
4
5
6
7
8
9
10
.wp {
display: table-cell;
text-align: center;
vertical-align: middle;
}
.box {
display: inline-block;
}

这种方法和table一样的原理,但却没有那么多冗余代码,兼容性也还不错

6.flex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div class="wp">
<div class="box">123123</div>
</div>

wp是父元素的类名,box是子元素的类名,因为有定宽和不定宽的区别,size用来表示指定宽度,下面是所有效果都要用到的公共代码,主要是设置颜色和宽高

/* 公共代码 */
.wp {
border: 1px solid red;
width: 300px;
height: 300px;
}

.box {
background: green;
}

.box.size{
width: 100px;
height: 100px;
}
/* 公共代码 */
1
2
3
4
5
6
7
8
9
flex作为现代的布局方案,颠覆了过去的经验,只需几行代码就可以优雅的做到水平垂直居中

.wp {
display: flex;
justify-content: center;
align-items: center;
}

目前在移动端已经完全可以使用flex了,PC端需要看自己业务的兼容性情况

7.grid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div class="wp">
<div class="box">123123</div>
</div>

wp是父元素的类名,box是子元素的类名,因为有定宽和不定宽的区别,size用来表示指定宽度,下面是所有效果都要用到的公共代码,主要是设置颜色和宽高

/* 公共代码 */
.wp {
border: 1px solid red;
width: 300px;
height: 300px;
}

.box {
background: green;
}

.box.size{
width: 100px;
height: 100px;
}
/* 公共代码 */
1
2
3
4
5
6
7
8
9
.wp {
display: grid;
}
.box {
align-self: center;
justify-self: center;
}

代码量也很少,但兼容性不如flex,不是特别推荐使用,不过如果是在现代浏览器当中是完全可以使用的

最后看下来发现当定宽高的时候都是用定位去实现的,不定宽的时候在现代浏览器中使用flex就比较简单了

mapbox 离线

本篇博客仅以个人学习记录使用
参考链接:https://zhuanlan.zhihu.com/p/30967394
参考链接:https://jacelyn.fish/2018/10/19/mapbox-localization/

介绍

mapbox是一款webgis的地图产品,起初作为leaflet的延伸,在2d地图上,有着优雅的Api,相较于arcgis等老牌产品更加轻量(当然arcgis继承的功能也更多)。那么Mapbox GL则是作为基于WebGL技术在3D地图领域的又一产品,虽然此时也还有Cesium做的也已经比较完善,但是Mapbox GL仍然有着她自己的优势,比如她的风格高度可定制化,并且她也是开源的,同时也可以用于移动端,还和Uber的几款开源产品可以相继承,更好的展现出地理信息可视化的魅力。相较于传统的瓦片地图,Mapbox GL是支持矢量数据进行渲染的并且可以实时交互,不需要与服务端通信,便可更改地图的风格、同时也可以按图层配置样式。因为Mapbox GL使用的是pbf编码的格式数据,相比较于图片资源更小,如果是在我们没有合法的底图数据,并且对于地理地形或者行政区要求不严格的情景下,还可以使用OpenStreetMap(OSM)开源地图数据用于Mapbox GL的地图底图原始数据,用于地理信息可视化。这些地图底图的原始数据可以使用sqlite数据库存储,甚至是用于移动端的离线地图数据包,可能是基于这些因素最后Map box GL采用的MBTiles数据,也是sqlite存储导出导出的数据。

基本使用

虽然Mapbox 是开源产品, 但是如果想完整体验所有cool的功能需要配合使用mapbox的云平台,在云平台上可以直接处理地图风格及标记等,并且可以发布成在线服务,当然使用者也必须要注册一个accessToken。产品很好,体验也很好,也很方便,但是总是会因为一些不可描述的原因,并不能让我们这些劳苦大众爽歪歪的去使用她。。在某些场景下我们是只能在内网访问的,所以说一切的云云都将变成过眼云烟,但是这个东西真的很好啊,又可以解决我们现有的问题,这时就需要我们把她搬到我们的内网上。

愚公移山

要问愚公移山拢(总)共分几步?? 我想应该是需要很多步。

首先,既然是要做地理信息可视化,我们就应该先有一份底图的原始数据也就是我们说pbf格式的数据,开篇已经说到了,在我们没有紧张的合法的地图数据时我们可以使用OSM的开源地图数据来实现我们野心的第一步。

获取数据

想要获取OSM数据最直接的办法是去官网下载,当然更多的情况下我们手里是shp格式的数据,这时我们可以使用QGIS等工具将shp导出成geojson格式的文件,我们也可以使用ogr2ogr进行转换,并支持属性选择和坐标系统转换等功能。也可以使用GDAL命令行转换

1
2
3
4
5
6
//Homebrew安装方式
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)”;;

//安装tippecanoe
brew install tippecanoe

1
2
3
4
5
6
7
8
9
10
11
12
/由于工具使用的GeoJson数据,所以先将相关的矢量文件转换为GeoJson文件
ogr2ogr -f GeoJson hechi4326.json hechi4326.shp

//将GeoJson文件转换为mbtiles文件
// -o 输出文件
// -z 裁剪层级
// -f
// -n 图层说明
// -l 输出的图层名称
// xxx.json 原文件
tippecanoe -o hechi4326.mbtiles -z 16 -f -n 河池市各个镇 -l hechi hechi4326.json

数据处理

当我们有了转换成功的geojson数据后,我们可以使用 Mapbox 开源瓦片数据处理工具 tippecanoe 转换 json 数据为 .mbtiles 格式。在这个过程中可进行独立图层合并、设置缩放范围和过滤属性等操作

1
tippecanoe -o geodata.mbtiles -z 18 -Z 13 -f -n geodata ~/road.geojson ~/water.geojson ~/sea.geojson;

搭建服务器

根据官方的介绍我们可以使用nodejs来搭建我们的地图服务器,事实上是官方有支持更好的模块用来解析数据去渲染数据。这个包就是tilelive。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
var express = require('express');
var http = require('http');
var app = express();
var tilelive = require('tilelive');
require('mbtiles').registerProtocols(tilelive);


// 放置的矢量瓦片数据位置
tilelive.load('mbtiles:///Users/lsw/Desktop/mapbox-server/hechi4326.mbtiles', function(err, source) {
if (err) {
throw err;
}

// 设置端口为7777
app.set('port', 7777);

app.use(function(req, res, next) {
// 服务端要支持跨域,否则会出现跨域问题
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});

//访问的url是:http://localhost:7777/tiles/{z}/{x}/{y}.pbf
app.get(/^\/tiles\/(\d+)\/(\d+)\/(\d+).pbf$/, function(req, res) {
var z = req.params[0];
var x = req.params[1];
var y = req.params[2];
console.log('get tile %d, %d, %d', z, x, y);


source.getTile(z, x, y, function(err, tile, headers) {
if (err) {
res.status(404)
res.send(err.message);
console.log(err.message);
} else {
res.set(headers);
res.send(tile);
}
});
});

http.createServer(app).listen(app.get('port'), function() {
console.log('Express server listening on port ' + app.get('port'));
});
});
1
node server.js

这时一个地图服务器就搭建成功了。

PS: 若练此功亦无需自宫 ————– 也有很多开源的支持Mbtiles的服务可以直接使用

基础前端安全

本篇博客仅以个人学习记录使用

XSS

XSS通常是由带有页面可解析内容的数据未经处理直接插入页面上解析导致的。需要注意的是,XSS分为存储型XSS、反射型XSS、MXSS(也叫DOM XSS)三种。这里区分不同类型主要是根据攻击脚本的引入位置:存储型XSS的攻击脚本常常是由前端提交的数据未经处理直接存储到数据库然后从数据库中读取出来后又直接插入到页面所导致的;反射型XSS的攻击可能是在网页URL参数中注入了可解析内容的数据而导致的,如果直接获取URL中不合法的并插入页面中则可能出现页面上的XSS攻击;MXSS则是在渲染DOM属性时将攻击脚本插入DOM属性中被解析而导致的。XSS主要的防范方法是验证输入到页面上所有内容来源数据是否安全,如果可能会含有脚本标签等内容则需要进行必要的转义。一般的做法是将所有可能包含攻击的内容进行HTML字符编码转义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// HTML 字符转译编码
function htmlEncode(str) {
let s = '';
if(str.length == 0 ) return '';
s = str.replace(/&/g, '$amp;')
s = s.replace(/</g, '&lt;');
s = s.replace(/>/g, '&gt;');
s = s.replace(/ /g, '&nbsp;');
s = s.replace(/\'/g, '&#39;');
s = s.replace(/\"/g, '&quot;');
s = s.replace(/\n/g, '<br>');
return s;
}

//HTML 字符转译编码
function htmlDecode(str) {
let s = '';
if(str.length == 0 ) return '';
s = str.replace(/&amp;/g, '&');
s = s.replace(/&lt;/g, '<');
s = s.replace(/&gt;/g, '>');
s = s.replace(/&nbsp;/g, '');
s = s.replace(/&#39;/g, '\');
s = s.replace(/&quot;/g, '\"');
s = s.replace(/<br>/g, ''/n);
return s;
}

SQL注入攻击

SQL注入攻击主要是因为页面提交数据到服务器端后,在服务器端未进行数据验证就将数据直接拼接到SQL语句中执行,因此产生执行与预期不同的现象。主要防范措施是对前端网页提交的数据内容进行严格的检查校验。

1
2
3
4
5
6
let di = req.query['id'];
let sql = `select * from user_table where id=${id}`;

let data = exec(sql);
this.body = data;

例如以上实例,如果前端传入的id内容为”100 or name=%user%”, 那么查询出来的结果就不只是id=100的用户了,包含user字符用户名的用户内容也都会被查询出来,并且这些用户信息可能被输出,导致SQL注入的发生。所以这是我们需要对传入的id内容进行校验,检查是否包含非法内容。

CSRF

CSRF是指非源站点按照源站点额数据请求格式提交非法数据给源站点服务器的一种攻击方法。非源站点在取到用户登陆验证信息的情况下,可以直接对源站点的某个数据接口进行提交,如果源站点对该提交请求的数据未经验证,该请求可能被成功执行,这其实并不合理。通常比较安全的是通过页面token(令牌)提交验证的方式来验证请求是否为源站点页面提交的,来阻止跨站伪造请求的发生。

用户通过源站点页面可以正常访问源站点服务器接口,但是也有肯能被钓鱼进入伪站点来访问源服务器,如果伪站点通过第三方或用户信息拼接等方式获取到了用户的信息、直接访问源站点的服务器接口进行关键性操作(例如支付扣款或返回用户隐私信息等操作),此时如果源站点服务器未做校验防护,伪站点的请求操作就可以被成功执行。另一种情况则可能是盗刷源站点的登录等接口来暴力破解用户密码的情况,如果源站点不添加防护措施,用户信息就极可能被盗取,所以我们需要进行安全性验证。

我们在源站点服务请求调用时添加了对源站点的验证,使用服务器端实时返回加密的验证Token给源站点页面,在源站点页面提交时将Token一起带给服务器验证,而Token是不会被其他伪站点利用的。而非法的伪站点和盗刷的行为就可以被直接拒绝掉,这样就大大降低了CSRF发生的概率。所以在Web后端,我们常常会进行Token验证,其中一种形式是将页面提交到后台的验证Token与session临时保存额Token进行比较就可以实现了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//生成随机的 csrf验证Token,并返回给前端页面
this.session.csrf = md5(Math.random(0,1).toString()).slice(5,15);
this.body = yield render('user/login',{
scrf: ctx.session.csrf
})

//提交时验证Token是否与源站点的Token相同
let csrf = this.request.body['csrf'];
if(csrf !== this.session.csrf) {
res = {
code:403,
msg: '不明网站来源提交'
}
}else {
//正常提交后的逻辑处理
}
1
目前解决CSRF的最佳方式就是通过加密计算的Token验证,而Token除了通过session也可以使用HTTP请求头中Authorization的特定认证字段来传递。当然并不是说使用了Token,网站调用服务就安全了,单纯的Token验证防止CSRF的方式理论上也是可以被破解的,例如可以通过域名伪造和拉取源站点实时Token信息的方式来进行提交。另外,任何所谓的安全都是相对的,只是说理论的破解时间变长了,而不容易被攻击。很多时候要使用多种方法结合的方式来一起增加网站的安全性,可以结合验证码等手段大大减少盗刷网站用户信息的频率等,进一步增强网站内容的安全性。

请求劫持与HTTPS

现在除了正常的前后端脚本安全问题,网络请求劫持的发生也越来越频繁。网络劫持一般指网站资源请求在请求过程中因为认为的攻击导致没有加载到预期的资源内容。网络请求劫持目前主要分为两种:DNS劫持与HTTP劫持。

DNS劫持

DNS劫持通常是指攻击者劫持了DNS服务器,通过某些手段取得某域名的解析记录控制权,进而修改此域名的解析结果,导致用户对该域名地址的访问由原IP地址转入到修改后的指定IP地址的现象,其结果就是让正确的网址不能解析或被解析指向令一网站IP,实现获取用户资料或者破坏原有网站正常服务的目的。DNS劫持一般通过篡改DNS服务器上的域名解析记录,来返回给用户一个错误的DNS查询结果实现。

HTTP劫持

HTTP劫持是指,在用户浏览器与访问的目的服务器之间所建立的网络数据传输通道中从网关或防火墙层上监视特定数据信息,当满足一定的条件时,就会在正常的数据包中插入或修改成为攻击者设计的网络数据包,目的是让用户浏览器解释“错误”的数据,或者以弹出新窗口的形式在使用者浏览器界面上展示宣传性广告或者直接显示某块其他的内容。而发生HTTP劫持时网站开发者一般无法通过修改网站代码程序等手段去进行防范。请求劫持的唯一可行的预防方法就是尽量使用HTTPS协议来访问目标网站。但是即使是使用了HTTPS也不是百分百的,仍然可以通过某些手段降低HTTPS至HTTP,然后进行HTTP劫持。

HTTPS协议通信过程

HTTPS协议是通过加入SSL(Secure Sockets Layer)层来加密HTTP数据进行安全传输的HTTP协议,同时启用默认的443端口进行数据传输。那么使用HTTPS是怎样保证浏览器和服务器之间数据安全传输的呢?我们需要先理解两个概念:公钥和私钥。

公钥(Public key)与私钥(Private key)是通过一种加密算法得到的密钥对(即一个公钥和一个与之匹配的私钥),公钥是密钥对中公开的部分,私钥则是非公开的部分。公钥常常用于会话加密、验证数字签名或者加密可以用相应私钥解密的数据。通过这种算法得到的密钥对保证是唯一的。使用这个密钥对的时候,如果用其中一个密钥加密一段数据,则必须用另一个密钥解密。比如用公钥加密数据就必须用私钥解密,如果用私钥加密也必须用公钥解密,否则解密将不会成功。我们以公钥加密方式为例,来看看HTTPS进行消息安全通信的整个过程。

客户端在需要使用HTTPS请求数据时,首先会发起连接请求,告诉服务器将建立HTTPS连接;服务器收到通知后首先自己先生成一个公钥并将它返回给客户端,如果是第一次请求,同时还要告诉客户端需要进行连接验证;如果需要验证,客户端接收到服务器公钥后开始发送验证请求,将一个特定的验证串使用服务器返回的公钥加密后形成密文发送给服务器,同时客户端也将自己生成的公钥发送给服务器;服务器获取到加密的报文和客户端公钥,先使用服务器私钥解密报文获得验证串,然后将验证串通过接收到的客户端公钥加密后返回给客户端,客户端再通过私钥解密验证串,判断是否为自己开始发送的验证串;如果正确,说明双方的连接是安全的,连接验证成功,客户端开始将后面的数据通过服务器返回的公钥不断加密发送给服务器,服务器也不断解密获取报文,并通过客户端的公钥加密响应的报文内容返回给客户端验证。这样就建立了HTTPS双向的加密传输连接。

在这种情况下,传输层传输的内容不会以明文的方式显示,而且HTTPS的请求只能被添加了对应数字证书的应用层代理拦截,因此第三方攻击者就无计可施了。通常我们要创建HTTPS服务,在服务端可以使用对应的模块来实现,例如在Node端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//引入HTTPS模块
const httpsModule = require('https');
const fs = require('fs');

//加载网站HTTPS 服务证书文件,证书一般需要注册申请
const https = httpsModule.Server({
key: fs.readFileSync('/path/to/server.key'),
cert: fs.readFileSync('/path/to/server.crt')
},function(req,res){
res.writeHead(200);
res.end("hello world")
})

//HTTPS 默认监听443端口
https.listen(443, function(err){
console.log("https listening on port:443");
})

当然如果使用Web框架,也可以通过更简单的方式创建一个HTTPS服务器。

const koa = require('koa');
const app = koa();

//同时监听多个端口
app.listen(80);
app.listen(443);

浏览器Web安全控制

除了HTTPS外,前端浏览器设置的安全性的控制还有很多,通过某些特定的head头配置,就可以完成浏览器端的安全性设置。下面看几个典型的安全消息头域设置。

X-XSS-Protection

这个head消息头设置主要是用来防止浏览器中的反射性XSS问题的发生,通过这种方式可以在浏览器层面增加前端网页的安全性

1
2
3
X-XSS-Protection:1;
mode=block 0 -关闭对浏览器的xss防护;1 -开始xss防护
mode=block 可以开启XSS防护并通知浏览器组织而不是过滤用户注入的XSS脚本
Strict-Transport-Security

Strict Transport Security(STS)是一种用来配置浏览器和服务器之间安全通信的机制,主要用来防止中间者攻击,因为它强制所有的通信都是用HTTPS,在普通的HTTP报文请求中配置STS是没有作用的,而且攻击者也能更改这些值。为了防止这样的现象发生,很多浏览器内置了一个配置STS的站点列表,在Chrome浏览器下可以通过访问Chrome://net-internals/#hsts查看浏览器中站点的STS列表,一般STS的配置实现如下。

1
2
3
4
max-age = 31536000;
includeSubDomains;
preload; -告诉浏览器将域名缓存到STS列表里面并且包含所有的子域名,并可支持预加载,时间是一年
max-age = 0 -告诉浏览器移除在STS缓存里的域名,或者不保存当前域名
Content-Security-Policy

我们简称它为CSP,这是一种由开发者定义的安全策略性声明,通过CSP所约束的规则设定,浏览器只可以加载指定可信的域名来源的内容(这里的内容可以是脚本、图片、iframe、font、style等等远程资源)。通过CSP协定,Web只能加载指定安全域名下的资源文件,保证运行时的内容总处于一个安全的环境中。

Access-Control-Allow-Origin

Access-Control-Allow-Origin是从Cross Origin Resource Sharing(CORS)中分离出来的。这个头部设置是决定一个通配符或域名来决定是单一的网站还是所有网站可以访问服务器的资源。需要注意的是,如果服务器端定义了通配符“ * ”,那么服务端的Access-Control-Allow-Credentials(是否允许请求时携带验证信息)选项就无效了,此时用户浏览器中的不同域Cookie信息将默认不会再服务器请求里发送(即如果需要实现带Cookie进行跨域请求,则要明确的配置允许来源的域,使用任意域的配置是不合法的)

1
Access-Control-Allow-Origin常常作为跨域共享设置的一种实现方式,其他常用的跨域手段还有:JSONP(JSON with Padding)、script标签跨域、window.postMessage、修改document.domain跨子域、window.name跨域和WebSocket跨越等。