0%

这部分的入门门槛太高了,等我先把基础学好了再研究吧。

现代图形系统

  • 光栅 Raster:几乎所有的现代图形系统都是基于光栅来绘制图形的,光栅就是指构成图像的像素阵列。
  • 像素 Pixel:一个像素对应图像上的一个点,它通常保存图像上的某个具体位置的颜色等信息。
  • 帧缓存 Frame Buffer:在绘图过程中,像素信息被存放于帧缓存中,帧缓存是一块内存地址。
  • CPU (Central Processing Unit):中央处理单元,负责逻辑计算。
  • GPU (Graphics Processing Unit):图形处理单元,负责图形计算。

CPU VS GPU

WebGL Startup

  1. 创建 WebGL 上下文(Canvas)
  2. 创建 WebGL Program(顶点选择器、片源选择器)
  3. 将数据存入缓冲区(比如顶点颜色)
  4. 将缓冲区数据读取到 GPU
  5. GPU 执行 WebGL 程序,输出结果

应用场景

  • 前端工程化
  • 服务端
  • Electron 跨端桌面应用

编写 Http Server

Hello World

1
2
3
4
5
6
7
8
9
10
11
const http = require('http');

const port = 8000;

const server = http.createServer((req, res) => {
res.end('hello world');
})

server.listen(port, () => {
console.log(`Listening on port ${port}`);
})

JSON

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
// 接收请求
const http = require('http');

const port = 8000;

const server = http.createServer((req, res) => {
const bufs = [];
req.on('data', (buf) => {
bufs.push(buf);
})
req.on('end', () => {
const buf = Buffer.concat(bufs).toString('utf-8');
try {
const result = JSON.parse(buf);
const msg = result.msg || 'success';
const responseMsg = {
msg: `receive: ${msg}`
}
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(responseMsg));
} catch (err) {
res.end('invalid json data');
}
})
})

server.listen(port, () => {
console.log(`Listening on port ${port}`);
})
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
// 发请求
const http = require('http');

const body = JSON.stringify({
msg: 'Hello Byte Dance'
})

const req = http.request('http://127.0.0.1:8000', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
}, res => {
const bufs = [];
res.on('data', buf => {
bufs.push(buf);
})
res.on('end', () => {
const buf = Buffer.concat(bufs);
const json = JSON.parse(buf);

console.log('json msg is: ', json.msg);
})
})

req.end(body);

静态文件服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const http = require('http');
const fs = require('fs');
const url = require('url');
const path = require('path');

const port = 8000;

const folderPath = path.resolve(__dirname, './static');

const server = http.createServer((req, res) => {
const info = url.parse(req.url);
const filePath = path.resolve(folderPath, '.' + info.pathname);
const fileStream = fs.createReadStream(filePath);

fileStream.pipe(res);
})

server.listen(port, () => {
console.log(`Listening on port ${port}`);
})

SSR

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
const http = require('http');
const React = require('react');
const ReactDomServer = require('react-dom/server')

const port = 8000;

function App() {
return React.createElement('h1', {
children: 'Hello SSR'
});
}

const server = http.createServer((req, res) => {
res.end(`
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nodejs</title>
</head>

<body>
${ReactDomServer.renderToString(
React.createElement(App)
)}
</body >

</html>
`
)
})

server.listen(port, () => {
console.log(`Listening on port ${port}`);
})

部署

  • 守护进程:当进程退出以后
  • 多进程:通过 cluster 模块便捷地利用多进程
  • 记录进程状态:用于诊断

什么是 HTTP

  • Hyper Text Transfer Protocol 超文本传输协议
  • 应用层协议,基于 TCP 协议
  • 分为两部分:请求 & 响应
  • 可拓展,比如可以自定义 Header
  • 无状态,每个请求之间都是孤立的

HTTP 历史

HTTP/0.9(单行协议)

  • 只有一个请求行,没有请求头和请求体
  • 请求方法只有 GET
  • 响应只有 HTML 文档
  • 文件格式只局限于 ASCII 编码
  • 存在的问题:
    • 只支持 HTML 文件,其余类型文件无法传输
    • 文件格式不再仅仅局限于 ASCII 编码

HTTP/1.0(可拓展性)

  • 引入了请求头和响应头
  • 增加了状态码
  • 支持多种的文档类型
  • 提供了 Cache 机制(If-Modified-Since、Last-Modified、Expires)
  • 请求头加入了 User-Agent
  • 存在的问题:
    • 每次通信都需要经过建立 TCP 连接、传输数据、断开 TCP 连接三个阶段,开销很大
    • 在同一个 TCP 连接里面,数据请求的通信次序是固定的。服务器只有处理完一个请求的响应后,才会进行下一个请求的处理,如果前面请求的响应特别慢的话,就会造成许多请求排队等待的情况,也就是所谓的队头阻塞
    • 每个域名绑定唯一 IP 地址,因此一个服务器只支持一个域名
    • 需要在响应头设置 Content-Length,然后浏览器再根据设置的数据大小来接收数据,对于动态生成的数据无能为力
    • 不支持断点续传(在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载。用户可以节省时间,提高速度。)

HTTP/1.1(标准化协议)

  • 增加了持久连接,默认开启 Connection: Keep-Alive。只要浏览器或服务器没有明确断开连接,那么连接会一直保持
  • 虚拟主机的发展可以让一个 IP 对应多个域名。
    • 请求头增加了 Host 字段,用来表示当前域名地址
    • 域名分片机制:引入 CDN 之后,每个域名可以维护 6 个连接
  • 引入了 cookie 机制和安全机制
  • 新的缓存方案(If-None-Match、ETag)
  • 存在的问题:
    • TCP 的慢启动
    • 同时开启多条 TCP 连接时,连接之间会竞争带宽
    • 队头阻塞问题依然无法解决
    • 由于 HTTP 1.1 协议不带有状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如 Cookie 和 User Agent,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。

HTTP/2 (表现更优异)

  • HTTP/2 是一个二进制协议。在 HTTP/1.1 中,报文的头信息必须是文本(ASCII 编码),数据体可以是文本,也可以是二进制。HTTP/2 则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为
  • 实现了多路复用。HTTP/2 仍然复用 TCP 连接,但是在一个连接里,客户端和服务器都可以同时发送多个请求或回应,而且不用按照顺序一一发送,这样就避免了队头阻塞问题
  • 使用了数据流的概念。因为 HTTP/2 的数据包是不按顺序发送的,同一个连接里面连续的数据包,可能属于不同的请求。因此,必须要对数据包做标记,指出它属于哪个请求。HTTP/2 将每个请求或回应的所有数据包,称为一个数据流。每个数据流都有一个独一无二的编号。数据包发送的时候,都必须标记数据流 ID ,用来区分它属于哪个数据流。
  • 实现了头信息压缩。由于 HTTP 1.1 协议不带有状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如 Cookie 和 User Agent,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。HTTP/2 对这一点做了优化,引入了头信息压缩机制。一方面,头信息使用 gzip 或 compress 压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就能提高速度了。
  • 允许服务器推送。HTTP/2 允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送。使用服务器推送,提前给客户端推送必要的资源,这样就可以相对减少一些延迟时间。
  • 存在的问题:因为 HTTP/2 使用了多路复用,一般来说同一域名下只需要使用一个 TCP 连接。由于多个数据流使用同一个 TCP 连接,遵守同一个流量状态控制和拥塞控制。只要一个数据流遭遇到拥塞,剩下的数据流就没法发出去,这样就导致了后面的所有数据都会被阻塞。这也导致了队头阻塞。HTTP/2 出现的这个问题是由于其使用 TCP 协议的问题,与它本身的实现其实并没有多大关系。

HTTP/3(QUIC 协议)

  • Quick UDP Internet Connection
  • 基于 UDP 实现了类似 TCP 的流量控制、可靠传输机制
  • 集成了 TLS 安全加密
  • 实现了 HTTP/2 多路复用技术,QUIC 实现了在同一个物理连接中可以有多个独立的逻辑数据流,实现了数据流单独传输,解决了 TCP 队头阻塞的问题
  • 实现了快速握手功能(因为是基于 UDP 的)
  • 存在的问题:
    • 服务器和浏览器还没有对其提供较完整的支持
    • 可能存在安全性问题

常用请求方法

请求方法说明
GET请求指定的资源,并返回实体
POST向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST 请求可能会导致新的资源的建立和已有资源的修改
PUT从客户端向服务器传送的数据取代指定的内容
DELETE请求服务器删除指定的数据
OPTIONS在采取具体资源的请求之前,决定对该资源采取何种必要措施,或者了解服务器的性能
CONNECTHTTP/1.1 协议中预留于能够将连接改为管道方式
HEAD类似于 GET 请求,只不过返回的响应中没有请求体

报文

请求方法

  • 安全的方法:不好修改服务器数据,如 GET、HEAD、OPTIONS
  • 幂等的方法:同样的请求执行一次,与连续执行多次效果相同。包括上面所有的安全方法,还包括 PUT、DELETE 这两个不安全但是幂等的方法

状态码

状态码概览:

  • 1XX:请求已接受,正在继续处理
  • 2XX:请求成功,处理完毕
  • 3XX:重定向
  • 4XX:客户端错误
  • 5XX:服务端错误

常见状态码总结:

状态码 & 英文描述详细说明
100 Continue服务器收到了请求的一部分,并且希望客户端继续发送其余部分
101 Switching Protocols切换协议,服务端根据客户端请求的头信息切换协议
200 OK请求成功,且被服务端成功处理
201 Created成功请求,且创建了新的资源
202 Accepted服务器已接受请求,但未处理完成
204 No Content请求成功处理,但是没有资源可以返回
206 Partial Content服务器成功处理了部分 GET 请求
301 Moved Permanently永久重定向。请求的资源被分配了新的 URL,之后应使用更改的URL
302 Found临时重定向。表示请求的资源被分配了新的 URL,希望本次访问使用新的 URL
304 Not Modified缓存相关的状态码。自从上次请求后,请求的资源未被修改过。 服务器返回此响应时,不会返回任何资源。客户端的请求中带有 If-Modified-Since 或者 If-None-Match
307 Temporary Redirect类似 302,但是 307 会遵照浏览器标准,请求方法不会从 POST 变成 GET
400 Bad Request请求报文中存在语法错误
401 Unauthorized用户未授权
403 Forbidden服务器拒绝该次访问
404 Not Found服务器上无法找到请求的资源
408 Request Time-out服务器等待客户端发送的请求时间过长,超时
500 Internal Server Error服务器内部错误,无法完成请求
502 Bad Gateway服务器作为网关或者代理时,为了完成请求访问下一个服务器,但该服务器返回了非法的应答
503 Service Unavailable服务器超负载或正在进行停机维护,无法处理请求

RESTful API

一种 API 设计风格,有以下特点:

  • 每个 URI 代表一种资源
  • URI 只用于表示资源的名称,而不包括资源的操作
  • 接口应该使用标准的 HTTP 方法如 GET,PUT 和 POST,并遵循这些方法的语义

CRUD 应该遵循以下语义:

示例:

常用请求头

请求头说明
Accept浏览器可接受的 MIME 类型
Content-Type资源属于什么 MIME 类型
Cache-Control用于指定缓存机制。常见的值有 no-cache(不直接使用缓存,要向服务器发起请求确认资源是否更改,也就是我们常说的协商缓存),no-store(不使用任何缓存),max-age=xxx(缓存内容在 xxx 秒后失效)
If-Modified-Since对应于服务端的 Last-Modified,若所请求的内容在指定的日期之后没有修改过,则返回 304 Not Modified,精度达到秒
If-None-Match对应于服务端的 ETag,若所请求的内容在指定的日期之后没有修改过,则返回 304 Not Modified,精度非常准确
Cookie有 cookie 而且同域的时候会自动带上
User-Agent浏览器类型
Connection若为 Keep-Alive,或者协议是 HTTP/1.1,则开启持久连接
Location一般用来表示重定向的地址
Expires缓存过期时间,在此时间内不需要发起请求,可以直接使用缓存
Authorization授权信息
Referer说明该页面的来源 URL。用处:防止盗链;避免 CSRF 攻击
Origin类似于 Referer,把 URI 剥离成 {协议,域名,端口} 的三元组,用于指明当前请求来自于哪个站点。Origin 的出现就是为了实现跨域。

常用响应头

响应头说明
Content-Type资源属于什么 MIME 类型
Cache-Control用于指定缓存机制。常见的值有 no-cache(不直接使用缓存,要向服务器发起请求确认资源是否更改,也就是我们常说的协商缓存),no-store(不使用任何缓存),max-age=xxx(缓存内容在 xxx 秒后失效)
Last-Modified最后修改时间。客户端可以通过 If-Modified-Since 请求头提供一个日期,只有改动时间迟于指定时间,才会返回新的资源,否则返回一个 304 Not Modified
ETag资源特定的标识符。客户端可以通过 If-None-Match 请求头提供一个标识符,如果客户端标识符与服务端不同才会返回新的资源,否则返回一个 304 Not Modified
Set-Cookie设置页面相关的 Cookie
Access-Control-Allow-Origin服务器允许请求的 Origin,如果设置为 * 则表示允许所有的 Origin
Expires缓存过期时间。在此时间内不需要发起请求,可以直接使用缓存
Max-age本地缓存应该缓存多久,开启了 Cache-Control 后才生效

缓存

强缓存

强缓存是利用 http 头中的 Expires 和 Cache-Control 两个字段来控制的。强缓存中,当请求再次发出时,浏览器会根据 Expires 和 Cache-control 判断目标资源是否命中强缓存,若命中则直接从缓存中获取资源,不会再与服务端发生通信

Expires VS max-age

  • Expires 是一个时间戳,接下来如果我们试图再次向服务器请求资源,浏览器就会先对比本地时间和 Expires 的时间戳,如果本地时间小于 Expires 设定的过期时间,那么就直接去缓存中取这个资源。由于时间戳是服务器来定义的,而本地时间的取值却来自客户端,因此 Expires 的工作机制对客户端时间与服务器时间之间的一致性提出了极高的要求,若服务器与客户端存在时差,将带来意料之外的结果
  • max-age 是一个相对时间,这就意味着它有能力规避掉 Expires 可能会带来的时差问题。客户端会记录请求到资源的时间点,以此作为相对时间的起点,从而确保参与计算的起始时间和当前时间都来源于客户端,因此能够实现更加精准的判断。
  • Cache-Control 的 max-age 配置项相对于 Expires 的优先级更高。当 Cache-Control 与 Expires 同时出现时,我们以 Cache-Control 为准。

协商缓存

浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求,还是从本地获取缓存的资源。如果服务端提示缓存资源未改动,资源会被重定向到浏览器缓存(解释了为什么它是 3XX 开头),这种情况下网络请求对应的状态码是 304。

Last-Modified / If-Modified-Since

含义:指最后一次修改资源的时间。开启了协商缓存之后,我们的每次请求都会带上 If-Modified-Since 的时间戳字段,它的值正是上一次 response 返回给它的 Last-Modified 值。服务器接收到这个时间戳后,会比对该时间戳和资源在服务器上的最后修改时间是否一致,从而判断资源是否发生了变化。如果发生了变化,就会返回一个完整的响应内容,并在 Response Headers 中添加新的 Last-Modified 值,否则返回 304 Not Modified。

弊端:

  • 有时候可能我们编辑了文件,但文件的内容没有改变。服务端并不清楚我们是否真正改变了文件,它仍然通过最后编辑时间进行判断。因此这个资源在再次被请求时,会被当做新资源,进而引发一次完整的响应——不该重新请求的时候,也会重新请求。
  • 当我们修改文件的速度过快时(比如花了 100ms 完成了改动),由于 If-Modified-Since 只能检查到以秒为最小计量单位的时间差,所以它是感知不到这个改动的——该重新请求的时候,反而没有重新请求了。

Etag / If-None-Match

Etag 是由服务器为每个资源生成的唯一的标识字符串,这个标识字符串是基于文件内容编码的,只要文件内容不同,它们对应的 Etag 就是不同的;相反,只要文件内容相同,ETag 就是相同的。因此 Etag 能够精准地感知文件的变化。它的作用原理和 If-Modified-Since 类似,都是客户端带上然后去跟服务端进行比较,不同就返回资源以及新的 ETag,相同就返回 304 Not Modified。

弊端:Etag 的生成过程需要服务器额外付出开销,会影响服务端的性能,这是它的弊端。Etag 并不能替代 Last-Modified,它只能作为 Last-Modified 的补充存在。 Etag 在感知文件变化上比 Last-Modified 更加准确,优先级也更高。当 Etag 和 Last-Modified 同时存在时,以 Etag 为准。

示例图:

简单来说就是:

  • 有强缓存且新鲜,就用,否则,就看有没有协商缓存
  • 协商缓存 ETag 优先级高于 If-Modified-Since
  • 什么缓存都没有,或者缓存不新鲜,就去重新请求
属性说明
<cookie-name>=<cookie-value>cookie 的键值对。
Path指定一个 URL 路径,这个路径必须出现在要请求的资源的路径中才可以发送 cookie。
Domain指定 cookie 可以送达的主机名。与之前的规范不同的是,域名之前的点号会被忽略。假如指定了域名,那么相当于各个子域名也包含在内了。
Sec规定必须通过安全的 HTTPS 连接来传输 cookie。
HttpOnlyJS 脚本将无法读取到 cookie 信息,这样能有效的防止 XSS 攻击。
Expires规定 cookie 的最长有效时间。形式为符合 HTTP-Date 规范的时间戳。
Max-Age在 cookie 失效之前需要经过的秒数。假如 Expires 和 Max-Age 均存在,那么 Max-Age 优先级更高。
SameSite=[Strict, Lax]允许服务器设定 cookie 不随着跨域请求一起发送,这样可以在一定程度上防范 CSRF 攻击。

HTTPS

HTTP 存在的问题

  • HTTP 报文使用明文方式发送,可能被第三方窃听。
  • HTTP 报文可能被第三方拦截后修改通信内容,接收方没有办法发现报文内容的修改。
  • HTTP 还存在认证的问题,第三方可以冒充他人参与通信。

HTTPS 的解决方案

HTTPS 指的是超文本传输安全协议,HTTPS 是基于 HTTP 协议的,不过它会使用 TLS/SSL 来对数据加密。优点有:

  • 所有的信息都是加密的,第三方没有办法窃听。
  • 它提供了一种校验机制,信息一旦被篡改,通信的双方会立刻发现。
  • 配备了身份证书,防止身份被冒充的情况出现。

TLS 握手安全的原理

  • 对称加密:双方使用同一个秘钥对数据进行加密和解密。但是对称加密的存在一个问题,就是如何保证秘钥传输的安全性,因为秘钥还是会通过网络传输的,一旦秘钥被其他人获取到,那么整个加密过程就毫无作用了。因此还需要非对称加密。
  • 非对称加密:我们拥有两个秘钥,一个公钥,一个私钥。公钥是公开的,私钥是保密的。用私钥加密的数据,只有对应的公钥才能解密;用公钥加密的数据,只有对应的私钥才能解密。我们可以将公钥公布出去,任何想和我们通信的客户,都可以使用我们提供的公钥对数据进行加密,然后我们就可以对应的私钥进行解密,这样就能保证数据的安全了。但是非对称加密有一个缺点就是加密速度很慢,因此如果每次通信都使用非对称加密的方式的话,反而会造成等待时间过长的问题。
  • 非对称与对称的权衡:因为对称加密的方式的缺点是无法保证秘钥的安全传输,非对称加密的缺点是加密速度很慢,因此我们可以用非对称加密的方式来对对称加密的密钥进行传输,然后以后的通信使用对称加密的方式来加密,这样就解决了两个方法各自存在的问题。注意,只有传输这个加密的密钥的时候我们才需要使用非对称的加密。
  • 数字证书:但是这样依然无法确保安全性。因为我们没有办法确定我们得到的公钥就一定是安全的公钥。可能存在一个中间人,拦截了对方发给我们的公钥,然后将他自己的公钥发送给我们,当我们使用他的公钥加密后发送信息,就可以被他用自己的私钥解密。然后他伪装成我们以同样的方法向对方发送信息,这样我们的信息就被窃取了,然而我们自己还不知道。为了解决这样的问题,我们可以使用数字证书的方式,首先我们使用一种 Hash 算法来对我们的公钥和其他信息进行加密生成一个信息摘要,然后让有公信力的认证中心(简称 CA )用它的私钥对信息摘要加密,形成签名。最后将原始的信息和签名合在一起,称为数字证书。当接收方收到数字证书的时候,先根据原始信息使用同样的 Hash 算法生成一个摘要,然后使用公证处的公钥来对数字证书中的摘要进行解密,最后将解密的摘要和我们生成的摘要进行对比,就能发现我们得到的信息是否被更改了。这个方法最重要的是认证中心的可靠性,一般浏览器里会内置一些顶层的认证中心的证书,相当于我们自动信任了他们,只有这样我们才能保证数据的安全。

示例图:

静态资源

静态资源方案:缓存 + CDN + 文件名 hash(确保用户能拿到最新的文件)

CDN

Content Delivery Network,内容分发网络,是由分布在不同区域的边缘节点服务器组成的分布式网络。开启 CDN 之后,用户的请求并不是直接发送给源网站,而是发送给 CDN 服务器,由 CND 服务器将请求定位到最近的含有该资源的服务器上去请求。这样有利于提高网站的访问速度,同时也分担了源服务器的访问压力。

是否开启 CDN 的区别

不开启 CDN:
用户输入域名 -> DNS 解析获取 IP -> 向该 IP 对应服务器发送访问请求 -> 返回资源

开启 CDN:
用户输入域名 -> 智能 DNS 解析 -> 获取缓存服务器 IP -> 若缓存有目标资源,返回资源;若没有就向源服务器发起请求,把获取的资源保存到缓存服务器,再把资源返回给用户

SSO 单点登录

在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。这种方式减少了由登录产生的时间消耗,辅助了用户管理。

用户在授权访问后会获得一个凭证,之后访问相关的应用时也会带上这个凭证,所以用户就可以在不需要再次登录的情况下直接登录访问。

跨域解决方案

常用的跨域解决方案主要有五种:

  • JSONP
  • CORS
  • WebSocket
  • Node 正向代理
  • Nginx 反向代理

Ajax

Ajax 五部曲

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 第一步:创建 xhr 对象
const xhr = new XMLHttpRequest();
// 第二步:初始化,设置请求方法和 url,注意此处 url 必须写完整
xhr.open('get', 'http://127.0.0.1:8000');
// 第三步:发送请求
xhr.send();
// 第四步:绑定事件
xhr.onreadystatechange = function () {
// readState
// 0 表示未初始化
// 1 表示 open 完毕
// 2 表示 send 完毕
// 3 表示服务端返回了部分结果
// 4 表示服务端返回了所有结果
if (xhr.readyState === 4 && xhr.status === 200) {
// 第五步:处理结果
console.log(xhr.responseText);
}
}

状态码 & 英文描述详细说明
100 Continue服务器收到了请求的一部分,并且希望客户端继续发送其余部分
101 Switching Protocols切换协议,服务端根据客户端请求的头信息切换协议
200 OK请求成功,且被服务端成功处理
201 Created成功请求,且创建了新的资源
202 Accepted服务器已接受请求,但未处理完成
204 No Content请求成功处理,但是没有资源可以返回
206 Partial Content服务器成功处理了部分 GET 请求
301 Moved Permanently永久重定向。请求的资源被分配了新的 URL,之后应使用更改的URL
302 Found临时重定向。表示请求的资源被分配了新的 URL,希望本次访问使用新的 URL
304 Not Modified缓存相关的状态码。自从上次请求后,请求的资源未被修改过。 服务器返回此响应时,不会返回任何资源。客户端的请求中带有 If-Modified-Since 或者 if-Match
307 Temporary Redirect类似 302,但是 307 会遵照浏览器标准,请求方法不会从 POST 变成 GET
400 Bad Request请求报文中存在语法错误
401 Unauthorized用户未授权
403 Forbidden服务器拒绝该次访问
404 Not Found服务器上无法找到请求的资源
408 Request Time-out服务器等待客户端发送的请求时间过长,超时
500 Internal Server Error服务器内部错误,无法完成请求
502 Bad Gateway服务器作为网关或者代理时,为了完成请求访问下一个服务器,但该服务器返回了非法的应答
503 Service Unavailable服务器超负载或正在进行停机维护,无法处理请求

动画基本原理

插值

无论动画多么简单,始终需要定义两个基本状态,即开始状态和结束状态。没有它们,我们将无法定义插值状态,从而填补两者之间的空白。
插值可能是颜色,也可能是位置等属性,可以理解为是对某个上下文中一个值或多个值的估计。当图形的变化可以由线性方程表示,就是线性插值。

  • 帧:连续变换的多张画面,其中的每一幅画面都是一帧。
  • 帧率:用于度量一定时间段内的帧数,通常的测量单位是 FPS (frame per second)。
  • 帧率与人眼:一般每秒 10-12 帧人会认为画面是连贯的,这个现象称为视觉暂留。对于一些电脑动画和游戏来说低于 30FPS 会感受到明显卡顿,目前主流的屏幕、显卡输出为 60FPS,效果会明显更流畅。

空白补全

空白的补全方式有以下两种

  • 补间动画:传统动画,主画师绘制关键帧,交给清稿部门,清稿部门的补间动画师补充关键帧进行交付。(类比到这里, 补间动画师由浏览器来担任,如 keyframe,transition)
  • 逐帧动画(Frame By Frame):从词语来说意味着全片每一帧逐帧都是纯手绘。(如 css 的 steps 实现精灵动画)

前端动画分类

CSS 动画

优点:

  • 浏览器会对 CSS3 动画做一些优化,导致 CSS3 动画性能上稍有优势。(新建一个图层来跑动画)
  • CSS3 动画的代码相对简单。

缺点:

  • 动画控制上不够灵活。
  • 兼容性不佳。
  • 部分动画无法实现。(视差效果、滚动动画)

主要属性:

  • transform:可以用于图形的旋转、移动、缩放等
  • transition:处理开始状态到结束状态的过渡效果
  • animation:通过 keyframe 定义开始状态、结束状态以及多个中间态,相比于 transition 可以处理更复杂的动画

SVG 动画

优点:通过矢量元素实现动画,不同的屏幕下均可获得较好的清晰度。可以用于实现一些特殊的效果,如:描字,形变,墨水扩散等。

缺点:使用方式较为复杂,过多使用可能会带来性能问题。

实现 SVG 动画通常有三种方式,SMIL、JS、CSS

SMIL

兼容性不理想,因此不过多讨论

JS 操作 SVG

  • Snap.svg
  • anime.js
  • HTML 原生的 Web Animation

例一:SVG 文字变形

CSS 属性 filter:可以将模糊或颜色偏移等效果应用于元素,它的属性 url 可以传入一个 svg

1
2
3
4
5
6
elts.text2.style.filter = `blur(${Math.min(8 / fraction - 8, 100)}px)`;
elts.text2.style.opacity = `${Math.pow(fraction, 0.4) * 100}%`;

fraction = 1 - fraction;
elts.text1.style.filter = `blur(${Math.min(8 / fraction - 8, 100)}px)`;
elts.text1.style.opacity = `${Math.pow(fraction, 0.4) * 100}%`;

通过上面的代码控制 bluropacity 不断地变化。每次切换单词的时候,当它的模糊程度快没有,就直接通过透明度把它隐藏掉,造成一种文字溶解的错觉。

例二:JS 画笔

JS 画笔的原理
stroke-dashoffsetstroke-dasharray 配合使用实现笔画效果。

  • 属性 stroke-dasharray 可控制用来描边的点划线的图案范式。它是一个数列,数与数之间用逗号或者空白隔开,指定短划线和缺口的长度。如果提供了奇数个值,则这个值的数列重复一次,从而变成偶数个值。因此,5,3,2 等同于 5,3,2,5,3,2。
  • stroke-dashoffset 属性指定了 dash 模式到路径开始的距离。(当使用了 stroke-dasharray,就进入了 dash 模式)
1
2
<line stroke-dasharray="5, 5" x1="10" y1="10" x2="190" y2="10" />
<line stroke-dasharray="5, 10" x1="10" y1="30" x2="190" y2="30" />

上面第一个表示先走 5 像素实线,再走 5 像素的空白;第二个表示先走 5 像素实线,再走 10 像素空白。

计算 path 的长度:path.getTotalLength(),然后将 stroke-dashoffset 的值设置为该的长度,就能实现类似画画的效果。

JS 动画

JS 可以通过操作 SVG、CSS、Canvas 等实现动画。

优点:

  • 使用灵活,同样在定义一个动画的 keyframe 序列时,可以根据不同的条件调节若干参数(JS 动画函数)改变动画方式。(CSS 会有非常多的代码冗余),对比于 CSS 的 keyframe 粒度更粗,CSS 本身的时间函数是有限的,这块 JS 可以弥补。
  • CSS 很难做到两个以上的状态转化。(要么使用关键帧,要么需要多个动画延时触发,再想到要对动画循环播放或暂停倒序等,复杂度极高)

缺点:

  • 使用到 JS 运行时,调优方面不如 CSS 简单,CSS 调优方式固定。
  • 对于性能和兼容性较差的浏览器,CSS 可以做到优雅降级,而 JS 需要额外的代码兼容。

对比与结论

  • 当 UI 元素采用较小的独立状态时,使用 CSS。
  • 在需要对动画进行大量控制时,使用 JavaScript。
  • 在特定的场景下可以使用 SVG,可以使用 CSS 或 JS 去操作 SVG 变化。

实现前端动画

animate 函数的实现

requestAnimationFrame VS setTimeout VS setInterval

JavaScript 动画应该通过 requestAnimationFrame 实现。该内置方法允许设置回调函数以在浏览器准备重绘时运行,因此不容易丢帧,但是 setTimeoutsetInterval 容易丢帧。通常这很快,确切的时间取决于浏览器。另外,当页面在后台时,根本没有重绘,所以回调不会运行,此时动画将被暂停并且不会消耗资源,这也比 setTimeoutsetInterval 更优。

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
function animate({ easing, draw, duration }) {
// 为什么不使用 Date.new()?
// 因为 performance.now() 会以恒定速度自增,精确到微秒级别,而 Date.now() 容易被篡改
const start = performance.now();
// 因为动画是连续的,执行完这个动画以后可能还有别的动画
// 所以我们返回 Promise 以支持后续的顺序调用
return new Promise((resolve) => {
requestAnimationFrame(function animate(time) {
// timeFraction 是当前已经执行的时间与动画要持续的总时间的比值
// progress 是一个介于 0 到 1 的值,表示执行绘画的进度

// 如果 timeFraction 大于或等于 1,说明时间已经超过 duration,执行完成
// 直接把 progress 的最终值 1 传过去就行了
// 否则的话,继续执行 requestAnimationFrame
let timeFraction = (time - start) / duration;
if (timeFraction > 1) {
timeFraction = 1;
}

const progress = easing(timeFraction);
draw(progress);

if (timeFraction < 1) {
requestAnimationFrame(animate);
} else {
resolve();
}
});
});
}

// draw 绘制函数
// draw 是一支画笔,它会被反复调用
// 传入的参数是当前执行的进度,是一个介于 0 到 1 之间的值
const ball = document.querySelector(".ball");
const draw = (progress) => {
ball.style.transfrom = `translate(${progress}px, 0)`;
};

// easing 缓动函数
// 修改动画执行的节奏
const easing = (timeFraction) => {
return timeFraction ** 2;
};

JS 动画的核心思想

这里 r 是距离,v 是速度,t 是时间。动画就是在开始状态和结束状态之间插值,这里的 r 可以简单理解为插值。

另外我们还需要通过比例尺实现物体的缩放,使得动画可以在显示屏中正常显示。

简单动画

动画演示

  • 匀速直线
  • 重力
  • 摩擦力
  • 平抛
  • 旋转 + 平抛

贝塞尔曲线

贝塞尔曲线生成网站:cubic-bezier.com

React 的应用场景

  • 网页应用,Facebok,Instagram,Netflix 网页版
  • 移动原生应用:Instagram,Discord,Oculus
  • 桌面应用:结合 Electron
  • 3D:react-three-fiber

React 的设计思路

UI 编程的痛点

  • 状态更新,UI 不会自动更新,需要手动调用 dom
  • 缺少代码层面的封装与隔离,没有组件化
  • UI 之间存在数据依赖关系,需要手动维护;如果数据依赖关系链太长,会出现 callback hell

转换式系统 & 响应式系统

转换式系统:给定输入,求解输出,如编译器,计算器
响应式系统:监听事件,由消息驱动

前端代码并不需要大量的计算,更多的是需要去处理一些事件(比如用户的点击);另外当事件发生时,我们需要进行一些响应(比如改变界面)。这两个特点决定了转换式系统对于前端写起来是很难受的,我们需要一种新的方式。

依据上面的分析,我们对于 React 设计的期望也很容易得出:

  • 状态更新,UI 自动更新
  • 前端代码组件化,可以复用,可以封装
  • 状态之间

组件化

  • 组件是组件的组合 / 原子组件
  • 组件内拥有状态,且外部不可见
  • 父组件可以把状态传递给子组件

状态归属

状态提升:当多个组件需要共享一个状态的时候,我们需要不断地提升状态,所以我们需要状态管理库。

注意 React 不是双向数据流,是单向的。子组件只是执行了父组件传递过来的函数,而没有把任何的状态传递回去给父组件。(函数在 Js 中是一等公民,所以可以作为一个变量传递)

组件设计

  • 组件声明了状态以及 UI 的映射
  • 组件有 state(内部状态)和 props(外部状态)两种状态
  • 组件可以由其它组件拼装而成

状态管理库

使用状态管理库的弊端:组件的复用性降低了,一般使用于业务代码

常用的状态管理库有:

  • redux
  • mobx
  • xstate
  • recoil
  • reduck(来源于 modern.js)

什么东西应该放到状态管理库?
如果你觉得某个东西整个 APP 有多处可能用到的,就放进去,比如说用户头像,这样也可以减少我们发送的请求数。

React 组件什么时候被渲染?

  • 首次渲染
  • props 变化
  • state 变化
  • context 变化

应用级框架科普

  • Next.js
  • Modern.js
  • Blitz

本文为React Hooks 核心原理与实战阅读笔记

React 组件的本质

我们可以把 UI 的展现看成一个函数的执行过程。其中,Model 是输入参数,函数的执行结果是 DOM 树,也就是 View。而 React 要保证的,就是每当 Model 发生变化时,函数会重新执行,并且生成新的 DOM 树,然后 React 再把新的 DOM 树以最优的方式更新到浏览器。

所以我们是否真的有必要使用 class 来作为组件呢?
事实上使用 class 是一种很牵强的做法,React 根本没有用到类的两个重要特性:

  • React 组件之间是不会互相继承的。比如说,你不会创建一个 Button 组件,然后再创建一个 DropdownButton 来继承 Button。所以说,React 中其实是没有利用到 Class 的继承特性的。
  • 所有 UI 都是由状态驱动的,因此很少会在外部去调用一个类实例(即组件)的方法。要知道,组件的所有方法都是在内部调用,或者作为生命周期方法被自动调用的。在使用类组件的时候,你从不需要去 new 一个对象来实现任何功能。

换句话说,class 并不是最好的组件表现形式,function 才是。

Hooks 的诞生

但是当时有一个局限是,函数组件无法存在内部状态,必须是纯函数,而且也无法提供完整的生命周期机制。

因此我们需要一个机制,能够把一个外部的数据绑定到函数的执行。当数据变化时,函数能够自动重新执行

于是有了 Hooks。

在 React 中,Hooks 就是把某个目标结果钩到某个可能会变化的数据源或者事件源上,那么当被钩到的数据或事件发生变化时,产生这个目标结果的代码会重新执行,产生更新后的结果。

对于函数组件,这个结果是最终的 DOM 树;对于 useCallback、useMemo 这样与缓存相关的组件,则是在依赖项发生变化时去更新缓存。

另外,有一点需要特别注意,Hooks 中被钩的对象,不仅可以是某个独立的数据源,也可以是另一个 Hook 执行的结果,这就带来了 Hooks 的最大好处:实现逻辑的复用

Hooks 实现逻辑复用

在之前的 React 使用中,有一点经常被大家诟病,就是非常难以实现逻辑的复用,必须借助于高阶组件等非常复杂的设计模式。但是高阶组件会产生冗余的组件节点,让调试变得困难。不过这些问题可以通过 Hooks 得到了很好的解决。所以如果有人问你 Hooks 有什么好处,那么最关键的答案就是简化了逻辑复用

在 Hooks 出现之前,高阶组件几乎是 Class 组件中实现代码逻辑复用的唯一方式,其缺点其实比较显然:

  • 代码难理解,不直观,很多人甚至宁愿重复代码,也不愿用高阶组件。
  • 会增加很多额外的组件节点。每一个高阶组件都会多一层节点,这就会给调试带来很大的负担。

但现在我们可以通过 Hooks 的方式对外部数据进行封装,从而将其变成一个可绑定的数据源。

Hooks 帮助实现关注点的分离

除了逻辑复用之外,Hooks 能够带来的另外一大好处就是有助于关注分离,意思是说 Hooks 能够让针对同一个业务逻辑的代码尽可能聚合在一块儿。这是过去在 Class 组件中很难做到的。因为在 Class 组件中,你不得不把同一个业务逻辑的代码分散在类组件的不同生命周期的方法中。

在过去的 Class 组件中,我们需要在 componentDidMount 中监听事件,在 componentWillUnmount 中解绑事件。而在函数组件中,我们可以把所有逻辑写在一起。

浏览器同源策略

浏览器同源策略是一个安全策略,其中同源指的是 协议 + 域名 + 端口号 三者相同,即使有两个不同的域名指向同一个 IP 地址,也不是同源的。同源策略可以一定程度上防止 XSS、CSRF 攻击。

一个域名的组成包括:

在默认情况下 http 可以省略端口 80, https 可以省略端口 443。也就是说,https://www.baidu.comhttps://www.baidu.com:443 显然也是同源的,因为它们是一回事。

不符合同源策略导致的后果有:

  • localStorage、sessionStorage、Cookie 等浏览器的内存无法跨域访问
  • DOM 节点无法进行跨域操作
  • Ajax 请求无法跨域请求

但是有一些标签是允许跨域加载资源:

  • <img>
  • <link>
  • <script>

值得注意的几个要点有:

  • 如果是协议和端口造成的跨域问题,前端是无能为力的
  • 跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了

代码示例

1
2
3
4
5
6
7
8
9
10
// 服务端 http://127.0.0.1:8000
const http = require('http');

const port = 8000;

http.createServer((req, res) => {
res.end(JSON.stringify('hello world'));
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- 客户端 http://127.0.0.1:5500/index.html -->
<script>
// 第一步:创建 xhr 对象
var xhr = new XMLHttpRequest();
// 第二步:初始化,设置请求方法和 url,注意此处 url 必须写完整
xhr.open('get', 'http://127.0.0.1:8000');
// 第三步:发送请求
xhr.send();
// 第四步:绑定事件
xhr.onreadystatechange = function () {
// readState
// 0 表示未初始化
// 1 表示 open 完毕
// 2 表示 send 完毕
// 3 表示服务端返回了部分结果
// 4 表示服务端返回了所有结果
if (xhr.readyState === 4 && xhr.status === 200) {
// 第五步:处理结果
console.log(xhr.responseText);
}
}
</script>

果不其然报错了:

JSONP

JSONP,即 JSON with Padding,是一个非官方的跨域解决方案,纯粹凭借程序员的聪明才智开发出来,只支持 get 请求。
JSONP 工作原理:在网页有一些标签天生就具有跨域能力,比如 img link script 等。JSONP 就是利用 script 标签的跨域能力来发送请求的。


举个例子,客户端传入 a 和 b,服务端传回 a + b 的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 服务端 http://127.0.0.1:8000
const http = require('http');
const url = require('url');

const port = 8000;

http.createServer((req, res) => {
const { query } = url.parse(req.url, true);
const { a, b, callback } = query;
const ans = parseInt(a) + parseInt(b);
res.end(`${callback}('${ans}')`);
}).listen(port, () => {
console.log('server is listening on port ' + port);
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 客户端 http://127.0.0.1:5500/index.html -->
<body></body>
<script>
function add(ans) {
console.log('a + b =', ans);
}

const jsonp = (a, b, callback) => {
const script = document.createElement('script');
script.src = `http://127.0.0.1:8000?a=${a}&b=${b}&callback=${callback}`;
document.body.appendChild(script);
}

jsonp(1, 2, 'add');
</script>

缺点:需要前后端配合,只支持 get 请求。

CORS

CORS 全称是 Cross-Orgin Resource Sharing,跨域资源共享。CORS 由后端开启,开启后前端就可以跨域访问后端。

服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。该属性表示哪些域名可以访问资源,如果设置为通配符 *,则表示所有网站都可以访问资源。类似的还有 Access-Control-Allow-Methods,表示允许的请求方法,Access-Control-Allow-Headers,表示允许的请求头类型。

设置 CORS 本身和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求复杂请求

简单请求

只要同时满足以下条件的就是简单请求:

  1. 请求方法是以下三者之一:
  • GET
  • POST
  • HEAD
  1. 允许人为设置的字段仅限以下几种:
  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type(有额外限制)
  1. Content-Type 取值为以下三者之一:
  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded
  1. 请求中的任意 XMLHttpRequest 对象均没有注册任何事件监听器;XMLHttpRequest 对象可以使用 XMLHttpRequest.upload 属性访问。

  2. 请求中没有使用 ReadableStream 对象。

复杂请求

不是简单请求的请求就是复杂请求。复杂请求必须首先使用 OPTIONS 请求方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。预检请求的使用,可以避免跨域请求对服务器的用户数据产生预期之外的影响。

代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- 客户端 http://127.0.0.1:5500/index.html -->
<script>
// 第一步:创建 xhr 对象
var xhr = new XMLHttpRequest();
// 第二步:初始化,设置请求方法和 url,注意此处 url 必须写完整
xhr.open('get', 'http://127.0.0.1:8000?a=1&b=2');
// 第三步:发送请求
xhr.send();
// 第四步:绑定事件
xhr.onreadystatechange = function () {
// readState
// 0 表示未初始化
// 1 表示 open 完毕
// 2 表示 send 完毕
// 3 表示服务端返回了部分结果
// 4 表示服务端返回了所有结果
if (xhr.readyState === 4 && xhr.status === 200) {
// 第五步:处理结果
console.log(xhr.responseText);
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 服务端 http://127.0.0.1:8000
const http = require('http');
const url = require('url');

const port = 8000;

http.createServer((req, res) => {
// 开启 CORS
res.writeHead(200, {
//设置允许跨域的域名,也可设置 * 表示允许所有域名
'Access-Control-Allow-Origin': 'http://127.0.0.1:5500',
//跨域允许的请求方法,也可设置 * 表示允许所有方法
"Access-Control-Allow-Methods": "DELETE,PUT,POST,GET,OPTIONS",
//允许的请求头类型
'Access-Control-Allow-Headers': 'Content-Type'
})
const { query: { a, b } } = url.parse(req.url, true);
res.end(`${a} + ${b} = ${parseInt(a) + parseInt(b)}`);
}).listen(port, function () {
console.log('server is listening on port ' + port);
})

WebSocket

Websocket 属于应用层协议,它基于 TCP 传输协议,并复用 http 的握手通道。
相比于 http 协议,它的优点是:

  • 支持双向通信,客户端和服务器之间存在持久的连接,而且双方都可以随时开始发送数据
  • 有更好的二进制支持
  • 支持拓展

因为这种方式本质没有使用 http 的响应头, 因此也没有跨域的限制。

那么为什么 WebSocket 可以跨域呢?
因为 WebSocket 根本不属于同源策略,而且它本身就有意被设计成可以跨域的一个手段。由于历史原因,跨域检测一直是由浏览器端来做,但是 WebSocket 出现以后,对于 WebSocket 的跨域检测工作就交给了服务端,浏览器仍然会带上一个 Origin 跨域请求头,服务端则根据这个请求头判断此次跨域 WebSocket 请求是否合法。

依然以 a + b 问题举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 客户端 http://127.0.0.1:5500/index.html -->
<script>
function ws(a, b) {
const socket = new WebSocket('ws://127.0.0.1:8000');
socket.onopen = () => {
socket.send(JSON.stringify({ a, b }));
};
socket.onmessage = e => {
console.log(e.data);
}
}

ws(1, 2);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
// 服务端 http://127.0.0.1:8000
const WebSocket = require('ws');

const port = 8000;

const ws = new WebSocket.Server({ port });
ws.on('connection', obj => {
obj.on('message', data => {
data = JSON.parse(data.toString());
const { a, b } = data;
obj.send(`${a} + ${b} = ${parseInt(a) + parseInt(b)}`);
})
})

Node 接口代理

同源策略只在浏览器存在,无法限制后端。也就是说前端与后端之间会受同源策略影响,但是后端与后端之间不会受到限制。所以可以通过 Node 做一层接口代理,先访问已经设置了 CORS 的后端 1,再让后端 1 去访问后端 2,获取数据后传给后端 1,最后再让后端 1 把数据传回给前端。

客户端代码同上,把请求端口改成 8888 即可。

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
// 后端 1 http://127.0.0.1:8888
const http = require('http');
const url = require('url');
const querystring = require('querystring');

const port = 8888;

http.createServer((req, res) => {
// 开启 CORS
res.writeHead(200, {
//设置允许跨域的域名,也可设置 * 表示允许所有域名
'Access-Control-Allow-Origin': 'http://127.0.0.1:5500',
//跨域允许的请求方法,也可设置 * 表示允许所有方法
"Access-Control-Allow-Methods": "DELETE,PUT,POST,GET,OPTIONS",
//允许的请求头类型
'Access-Control-Allow-Headers': 'Content-Type'
})
const { query } = url.parse(req.url, true);
const { methods = 'GET', headers } = req;

// 给后端 2 发送请求
http.request({
host: '127.0.0.1',
port: '8000',
path: `/?${querystring.stringify(query)}`,
methods,
headers
}, proxyRes => {
// 把从后端 2 获取的数据传回给前端
proxyRes.on('data', chunk => {
console.log(chunk.toString());
res.end(chunk.toString());
})
}).end()
}).listen(port, () => {
console.log('server is listening on port ' + port);
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 后端 2 http://127.0.0.1:8000
const http = require('http');
const url = require('url');
const port = 8000;

http.createServer(function (req, res) {
// 开启 CORS
res.writeHead(200, {
//设置允许跨域的域名,也可设置 * 表示允许所有域名
'Access-Control-Allow-Origin': 'http://127.0.0.1:5500',
//跨域允许的请求方法,也可设置 * 表示允许所有方法
"Access-Control-Allow-Methods": "DELETE,PUT,POST,GET,OPTIONS",
//允许的请求头类型
'Access-Control-Allow-Headers': 'Content-Type'
})
const { query: { a, b } } = url.parse(req.url, true);
res.end(`${a} + ${b} = ${parseInt(a) + parseInt(b)}`);
}).listen(port, function () {
console.log('server is listening on port ' + port);
})

Nginx 反向代理

实现原理类似于上面提到的 Node 接口代理,需要你搭建一个中转 Nginx 服务器,用于转发请求。使用 Nginx 反向代理实现跨域,是最简单的跨域方式。只需要修改 Nginx 的配置即可解决跨域问题,支持所有浏览器,支持 session,不需要修改任何代码,并且不会影响服务器的性能。

先根据Nginx安装教程进行 Nginx 的安装。
然后修改 conf 目录下的 nginx.conf 文件:

1
2
3
4
5
6
7
8
server{
listen 8888;
server_name 127.0.0.1;

location /{
proxy_pass 127.0.0.1:8000;
}
}

输入命令行

1
nginx -s reload

此时客户端请求 8888 端口,就不会跨域了。

参考资料:
https://juejin.cn/post/7017614708832206878
https://juejin.cn/post/6844904126246027278
https://juejin.cn/post/6844903767226351623
https://www.jianshu.com/p/9a8d793ec52a
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS
公众号前端点线面

本文题目列表来源于政采云前端团队,题源来源于力扣

字符串

整数反转

  • 方法一:使用 reverse
1
2
3
4
5
6
7
8
9
/**
* @param {number} x
* @return {number}
*/
var reverse = function (x) {
let num = parseInt(x.toString().split("").reverse().join(""));
num = x < 0 ? -num : num;
return num < -Math.pow(2, 31) || num > Math.pow(2, 31) - 1 ? 0 : num;
};
  • 方法二:借鉴欧几里得算法,可以把空间复杂度降到 O(1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @param {number} x
* @return {number}
*/
var reverse = function (x) {
let temp = Math.abs(x);

let num = 0;
while (temp !== 0) {
num = num * 10 + (temp % 10);
temp = Math.floor(temp / 10);
}

num = x < 0 ? -num : num;
return num < -Math.pow(2, 31) || num > Math.pow(2, 31) - 1 ? 0 : num;
};

回文数

这题就是上面那题的变式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @param {number} x
* @return {boolean}
*/
var isPalindrome = function (x) {
if (x < 0) {
return false;
}
return x === reverse(x);

// 反转数字
function reverse(x) {
let num = 0;
while (x !== 0) {
num = num * 10 + (x % 10);
x = Math.floor(x / 10);
}
return num;
}
};

有效的字母异位词

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
/**
* @param {string} s
* @param {string} t
* @return {boolean}
*/
var isAnagram = function (s, t) {
if (s.length !== t.length) {
return false;
}

const map = new Map();
for (const ch of s) {
map.set(ch, (map.get(ch) || 0) + 1);
}

for (const ch of t) {
if (map.has(ch) && map.get(ch) > 0) {
map.set(ch, map.get(ch) - 1);
} else {
return false;
}
}

return true;
};

字符串转换整数

  • 方法一:正则
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
/**
* @param {string} s
* @return {number}
*/
var myAtoi = function (s) {
const maxVal = Math.pow(2, 31) - 1,
minVal = -Math.pow(2, 31);

// 书写正则
// (-|\+)? 表示 - 或 + 或什么都没有
// \d+ 为匹配数字
const reg = /^(-|\+)?\d+/g;

const groups = s.trim().match(reg);

let res = 0;

// 若匹配到了,取其第一项
if (groups) {
res = +groups[0];
}

if (res > maxVal) res = maxVal;
if (res < minVal) res = minVal;

return res;
};
  • 方法二:使用 parseInt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @param {string} s
* @return {number}
*/
var myAtoi = function (s) {
const maxVal = Math.pow(2, 31) - 1,
minVal = -Math.pow(2, 31);

let res = parseInt(s);
if (isNaN(res)) {
return 0;
}

if (res > maxVal) res = maxVal;
if (res < minVal) res = minVal;

return res;
};

报数

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
/**
* @param {number} n
* @return {string}
*/
var countAndSay = function (n) {
let str = "1";

for (let i = 2; i <= n; i++) {
const sb = [];
let start = 0,
pos = 0;

while (pos < str.length) {
while (pos < str.length && str[pos] === str[start]) {
pos++;
}
sb.push("" + (pos - start) + str[start]);
start = pos;
}

str = sb.join("");
}

return str;
};

反转字符串

1
2
3
4
5
6
7
8
9
/**
* @param {character[]} s
* @return {void} Do not return anything, modify s in-place instead.
*/
var reverseString = function (s) {
for (let i = 0; i < s.length / 2; i++) {
[s[i], s[s.length - 1 - i]] = [s[s.length - 1 - i], s[i]];
}
};

字符串中的第一个唯一字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @param {string} s
* @return {number}
*/
var firstUniqChar = function (s) {
const map = new Map();

for (const ch of s) {
map.set(ch, (map.get(ch) || 0) + 1);
}

for (let i = 0; i < s.length; i++) {
if (map.get(s[i]) === 1) {
return i;
}
}

return -1;
};

验证回文串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @param {string} s
* @return {boolean}
*/
var isPalindrome = function (s) {
let real = [];
for (let i = 0; i < s.length; i++) {
if (
(s[i] >= "a" && s[i] <= "z") ||
(s[i] >= "A" && s[i] <= "Z") ||
(s[i] >= "0" && s[i] <= "9")
)
real.push(s[i].toLowerCase());
}

const realString = real.join("");
for (let i = 0; i < realString.length / 2; i++) {
if (realString[i] !== realString[realString.length - i - 1]) {
return false;
}
}
return true;
};

实现 strStr()

  • 暴力
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @param {string} haystack
* @param {string} needle
* @return {number}
*/
var strStr = function (haystack, needle) {
if (needle === "") {
return 0;
}
for (let i = 0; i < haystack.length; i++) {
let count = 0;
for (let j = 0; j < needle.length && j + i < haystack.length; j++) {
if (haystack[i + j] === needle[j]) {
count++;
}
if (j === needle.length - 1 && count === needle.length) {
return i;
}
}
}

return -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
/**
* @param {string} haystack
* @param {string} needle
* @return {number}
*/
var strStr = function (haystack, needle) {
// 剪枝
if (needle === "") {
return 0;
}
if (needle.length > haystack.length) {
return -1;
}
if (needle.length === haystack.length) {
return needle === haystack ? 0 : -1;
}

for (let i = 0; i <= haystack.length - needle.length; i++) {
if (haystack[i] !== needle[0]) {
continue;
}
if (haystack.substring(i, i + needle.length) === needle) {
return i;
}
}

return -1;
};

最长公共前缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @param {string[]} strs
* @return {string}
*/
var longestCommonPrefix = function (strs) {
// 假设第一个元素是最长前缀
let prefix = strs[0];
for (let i = 1; i < strs.length; i++) {
while (!strs[i].startsWith(prefix)) {
prefix = prefix.substring(0, prefix.length - 1);
}
// 判断是否存在公共前缀
if (prefix === '') {
return '';
}
}
return prefix;
};

最长回文子串

  • dp
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
48
49
50
/**
* @param {string} s
* @return {string}
*/
var longestPalindrome = function (s) {
// 长度为 1,一定回文
// 长度为 2 或 3,判断首尾是否相同:s[i] === s[j]
// 长度大于 3, 首尾字符相同,且去掉首尾之后的子串仍为回文:(s[i] === s[j]) && dp[i + 1][j - 1]
const len = s.length;
// dp[i][j] 表示 s[i..j] 是否是回文串
const dp = new Array(len).fill(false).map(() => new Array(len).fill(false));

// 边界,长度为 1 必定为回文
for (let i = 0; i < len; i++) {
dp[i][i] = true;
}

// 记录起点和长度,即可得到字符串
let maxLen = 1,
begin = 0;
// 第一层枚举为子串长度
for (let l = 2; l <= len; l++) {
// 第二层枚举为左边界
for (let i = 0; i < len; i++) {
// j - i + 1 = l
const j = l + i - 1;
if (j >= len) {
// 如果右边界越界,就可以退出当前循环
break;
}
if (s[i] !== s[j]) {
dp[i][j] = false;
} else {
if (l <= 3) {
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
}

// 只要 dp[i][j] 为 true,就表示子串 s[i..j] 是回文
if (dp[i][j] && l > maxLen) {
maxLen = l;
begin = i;
}
}
}

return s.substr(begin, maxLen);
};
  • 暴力,需要注意的是这题暴力的解比 dp 更优
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
/**
* @param {string} s
* @return {string}
*/
var longestPalindrome = function (s) {
let res = "";
for (let i = 0; i < s.length; i++) {
// 返回以 i 为中心的回文串
const a = palindrome(i, i);
// 返回以 i 和 i + 1 为中心的回文串
const b = palindrome(i, i + 1);
if (a.length > res.length) {
res = a;
}
if (b.length > res.length) {
res = b;
}
}

return res;

// 设置左右两个指针,是为了可以同时处理回文串长度为奇数和偶数的情况
function palindrome(left, right) {
while (left >= 0 && right < s.length && s[left] === s[right]) {
left--;
right++;
}

// 返回以 s[left] 和 s[right] 为中心的最长回文串
// 此时 left 和 right 已经不符合要求了,所以返回的区间是 [left + 1, right - 1]
return s.substring(left + 1, right);
}
};

数学

罗马数字转整数

先遍历所有罗马数字进行累加,对于特殊数字的循环,比如:5 + 1 = 6,而实际是 4,相差 2,所以需要在结果上减去 2。

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
/**
* @param {string} s
* @return {number}
*/
var romanToInt = function (s) {
let res = 0;
for (const ch of s) {
switch (ch) {
case "I":
res += 1;
break;
case "V":
res += 5;
break;
case "X":
res += 10;
break;
case "L":
res += 50;
break;
case "C":
res += 100;
break;
case "D":
res += 500;
break;
case "M":
res += 1000;
break;
default:
break;
}
}

if (s.includes("IV") || s.includes("IX")) res -= 2;
if (s.includes("XL") || s.includes("XC")) res -= 20;
if (s.includes("CD") || s.includes("CM")) res -= 200;

return res;
};

Fizz Buzz

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @param {number} n
* @return {string[]}
*/
var fizzBuzz = function (n) {
const arr = [];
for (let i = 1; i <= n; i += 1) {
let str = "";
if (i % 3 === 0) {
str += "Fizz";
}
if (i % 5 === 0) {
str += "Buzz";
}
if (i % 3 !== 0 && i % 5 !== 0) {
str += i;
}
arr.push(str);
}
return arr;
};

计数质数

  • 枚举,超出时间限制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @param {number} n
* @return {number}
*/
var countPrimes = function (n) {
let count = 0;
for (let i = 2; i < n; i++) {
if (isPrime(i)) {
count++;
}
}
return count;

function isPrime(num) {
for (let i = 2; i * i <= num; i++) {
if (num % i === 0) {
return false;
}
}
return true;
}
};
  • 埃拉筛
    这个算法的推导由来可以看labuladong。这个算法的复杂度是 O(N * loglogN)
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
/**
* @param {number} n
* @return {number}
*/
var countPrimes = function (n) {
const isPrime = new Array(n).fill(true);
for (let i = 2; i * i < n; i++) {
if (isPrime[i]) {
// i 的倍数不可能是素数,每次 j += i
// 从 i * i 开始,因为前面的数字已经算过了
for (let j = i * i; j < n; j += i) {
isPrime[j] = false;
}
}
}

let count = 0;
for (let i = 2; i < n; i++) {
if (isPrime[i]) {
count++;
}
}

return count;
};

3 的幂

循环和递归都很简单,不说了,还有一种骚操作。
在题目给定的 32 位有符号整数的范围内,最大的 3 的幂为 3 ^ 19 = 1162261467。我们只需要判断 n 是否是这个数的约数即可。

1
2
3
4
5
6
7
/**
* @param {number} n
* @return {boolean}
*/
var isPowerOfThree = function (n) {
return n > 0 && 1162261467 % n === 0;
};

Excel 表列名称

和正常 0 ~ 25 的 26 进制相比,本质上就是每一位多加了 1,所以只要在处理每一位的时候先减 1,就可以按照正常的 26 进制来处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @param {number} columnNumber
* @return {string}
*/
var convertToTitle = function (columnNumber) {
const res = [];

while (columnNumber) {
columnNumber--;
const temp = String.fromCharCode((columnNumber % 26) + 65); // 65 是 A 对应的 Char Code
res.push(temp);
columnNumber = Math.floor(columnNumber / 26);
}

return res.reverse().join("");
};

Excel 表列序号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @param {string} columnTitle
* @return {number}
*/
var titleToNumber = function (columnTitle) {
let sum = 0;
let i = columnTitle.length - 1;

let carry = 1; // 进制
while (i >= 0) {
// 因为 A 表示 1,所以减法后需要每个数加 1,相当于减去 65,再加上 1
const cur = columnTitle[i].charCodeAt(0) - 64;
sum += cur * carry;
carry *= 26;
i--;
}

return sum;
};

快乐数

首先我们需要清楚,快乐数的计算是可能会导致死循环出现的。遇到判断某个可能的死循环是否满足一定的条件,我们可以使用快慢指针,比如链表的经典题目判断链表是否有环

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
/**
* @param {number} n
* @return {boolean}
*/
var isHappy = function (n) {
let slow = n,
fast = n;

do {
// slow 执行一遍, fast 连续执行两遍
slow = calculateSum(slow);
fast = calculateSum(fast);
fast = calculateSum(fast);
} while (slow !== fast);

return slow === 1;

function calculateSum(num) {
let sum = 0;
while (num) {
const bit = num % 10;
sum += bit * bit;
num = Math.floor(num / 10);
}
return sum;
}
};

阶乘后的零

  • 暴力
  1. 尾数中有 0 必定是是 10 的倍数
  2. 尾数中有多少个 0 就就是整个数能有多少个因子 10
  3. 因子 10 又可以拆成 ,因此就是找整个数字可以拆分成多少了
    因为在因子中 2 的数量一定比 5 多,所以实际上我们只要找到因子 5 的个数就可以找到尾数中 0 的个数了,所以这个问题就可以转换成找因子 5 的个数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @param {number} n
* @return {number}
*/
var trailingZeroes = function (n) {
let ans = 0;
for (let i = 1; i <= n; i++) {
let x = i;
while (x !== 0 && x % 5 === 0) {
ans++;
x = Math.floor(x / 5);
}
}

return ans;
};
  • 优化版本
  1. n! 这些乘数中,每隔 5 个数,肯定会有一个数至少能拆出一个 5 因子。所以 n / 5 = 至少会出现的 5 的个数
  2. 因为 n / 5 并不能完全算出 5 因子的个数,比如若某个数 25 = 5 * 5,分解后得到的 5 也算一个,所以能被 25 因式分解相当于会出现 2 个 5 因子
  3. 依此类推,能被 25 _ 5 = 125 因式分解的相当于比之前按 25 因式分解的时候又多出一个 5 因子。能被 125 _ 5 = 625 因式分解的相当于比按 125 因式分解时又多出一个 5 因子。还有 625 * 5 …
    所以 n! 的结果可以拆分为多少个 5 因子呢?
    显然就是 n/5 + n/25 + n/125 + n/625 + …
1
2
3
4
5
6
7
8
9
10
11
12
/**
* @param {number} n
* @return {number}
*/
var trailingZeroes = function (n) {
let ans = 0;
while (n > 0) {
n = Math.floor(n / 5);
ans += n;
}
return ans;
};

Pow(x, n)

  • 二分
    使用折半计算,每次把 n 缩小一半,通过递归,最终获取 x 的 n 次幂。比如说要计算 x32,推算过程为:
    x32 -> x16 -> x8 -> x4 -> x2 -> x
    但是具体的操作还要根据数字的奇偶决定,如要计算 x77,推算过程为:
    x77 -> x38 -> x19 -> x9 -> x4 -> x2 -> x
    当我们要计算 xn 时,我们可以先递归地计算出 y = xn/2,接着:

    • 如果 n 为偶数,那么 xn = y2
    • 如果 n 为奇数,那么 xn = y2 * x

    边界:n = 0,返回 1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
    * @param {number} x
    * @param {number} n
    * @return {number}
    */
    var myPow = function (x, n) {
    return n >= 0 ? quickPower(x, n) : 1 / quickPower(x, -n);

    function quickPow(x, n) {
    if (n === 0) {
    return 1;
    }
    const y = quickPow(x, Math.floor(n / 2));
    return n % 2 === 0 ? y * y : y * y * x;
    }
    };
  • 快速幂
    若 n = a1 + a2 + a3 + … + am,则
    xn = xa1 * xa2 * xa3 * … * xam
    依然以 x77,为例,x -> x2 -> x4 -> x9 -> x19 -> x38 -> x77

    • x38 -> x77 额外乘的 x 在 x77 中贡献了 x
    • x9 -> x19 额外乘的 x 在 后面被平方了 2 次,所以贡献是 x2 ^ 2
    • x4 -> x9 额外乘的 x 在 后面被平方了 3 次,所以贡献是 x2 ^ 3
    • 最初的 x 在之后被平方了 6 次,因此最后贡献的是 x2 ^ 6
    • 上面的贡献相乘, x * x4 * x8 * x64 = x77,这些指数 1、4、8、64,对应了 77 的二进制表示 (1001101)2 中的每一个 1

    因此,我们从 x 开始不断平方,得到 x2、x4、x8、x16,… ,如果 n 的第 k 位(从右往左数)二进制为 1,那么我们就将对应的贡献 x2 ^ k 计入答案。

    说实话不是很懂为什么快速幂居然会超时,这测试用例有点那啥(

    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
      /**
    * @param {number} x
    * @param {number} n
    * @return {number}
    */
    var myPow = function (x, n) {
    // 判断边界,否则会超时
    if (n === 0 || n <= 1 << 31 && Math.abs(x) === 1) {
    return 1;
    }
    if (n <= 1 << 3) {
    return 0;
    }

    return n >= 0 ? quickPow(x, n) : 1 / quickPow(x, -n);

    function quickPow(a, b) {
    let result = 1;
    while (b) {
    if (b & 1) {
    // 计算贡献
    result = result * a;
    }
    // 扩大它的贡献
    a = a * a;
    // 去除最后一位
    b >>= 1;
    }
    return result;
    }
    };

今天大多数的内容都属于是实用性的,没什么笔记记录。

浏览器内核

  • IE:Trident
  • Firefox:Gecko
  • Chrome:Webkit / Blink
  • Safari:Webkit
  • Opera:Presto

Web 标准组织

  • W3C: HTML、XML、CSS、DOM、Web API 等
  • ECMA: ECMAScript
  • WHATWG: 与 W3C 合作制定 HTML 和 DOM
  • ITEF: TCP/IP、FTP 等网络协议

console

  • console.warn
  • console.error
  • console.table:格式化打印数组 / JSON 数据
  • console.dir:按照树形结构打印 DOM 节点

SourceMap 原理

关于 SourceMap 的原理,推荐这篇文章

跨域解决方案

关于跨域,推荐这篇文章

Bug 终极解决方案

传说中程序大师随身携带只小黄鸭,在调试代码的时候会在桌上放上这只小黄鸭,然后详细地向鸭子解释每行代码,然后很快就将问题定位修复了。 ——《程序员修炼之道》