简单代码热更新

背景

  1. 开发静态页面修改样式时需要手动刷新页面才会在页面上看到效果比较麻烦
  2. 了解了热更新原理需要实践下
  3. 手动实现了websocket 的解析,趁热打铁把 websocket 应用在实际项目中

原理

  1. 服务端监听文件改动
  2. 当文件有改动之后服务端发送事件通知客户端
  3. 客户端接收到事件之后做出对应的处理

实现

服务端监听文件改动

nodejs 在 fs 模块中提供了两个监听文件改动的方法 fs.watchfs.watchFile 两个方法。

fs.watch 参考

监听文件及文件夹下的改动,通过如下方式调用:

1
fs.watch(filename[, options], listener)
  • filename: 文件或目录
  • options: 可选的,如果 options 传入字符串,则它指定 encoding。 否则,options 应传入对象,属性如下:
    • persistent: <boolean> 指示如果文件已正被监视,进程是否应继续运行。默认值: true
    • recursive: <boolean> 指示应该监视所有子目录,还是仅监视当前目录。这适用于监视目录时,并且仅适用于受支持的平台(参见注意事项)。默认值: false。
    • encoding: <string> 指定用于传给监听器的文件名的字符编码。默认值: utf8
  • listener: 监听器回调有两个参数 (eventType, filename)eventTyperenamechangefilename 是触发事件的文件的名称。
fs.watchFile 参考

监听特定文件的改动,通过如下方式调用:

1
fs.watchFile(filename[, options], listener)
  • filename: 文件
  • options: 可选的,options 应传入对象,属性如下:
    • persistent: <boolean> 指示如果文件已正被监视,进程是否应继续运行。默认值: true
    • interval : 指示轮询目标的频率(以毫秒为单位)。
    • bigint: 回调函数的参数对象是否为BigInts类型。
  • listener: 监听器回调有两个参数 (curr, prev)curr 是文件修改后的 fs.Stat 实例, prev 是文件修改前的 fs.Stat 实例。

通知客户端

通过建立一个 websocket 服务的方式,当文件有变更的时候发送更新消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server.on('upgrade', function (req, socket, head) {
const read = new WebsocketRead(req, socket)
const write = new WebsocketWrite(socket)

console.log('websocket connect success')

read.on('text', function (data) {
console.log('接收到响应', data)
})

read.on('ping', function () {
write.pong()
})

watch.on('update', (data) => {
write.send(data)
})
})

客户端处理事件

在客户端插入建立 websocket 链接的代码,接收到更新之后重新刷新页面

1
2
3
4
5
6
7
<script>
const socket = new WebSocket(`ws://\${location.host}`)
socket.onmessage = function (e) {
const data = e.data
location.reload()
}
</script>

其它

自动注入建立 websocket 链接的代码

返回 HTML 页面时通过正则的方式在页面中插入代码

1
2
3
4
5
6
let data = fs.readFileSync(filePath)
if (path.extname(filePath) === '.html') {
data = data
.toString()
.replace(/\<\/\s*body>/, `${websocketTemplateStr}</body>`)
}

对文件修改监听的封装

通过封装成对应的方式,利用事件总线的模式实现事件的派发

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
const fs = require('fs')
const { EventEmitter } = require('events')
const path = require('path')

const { resolveRootPath } = require('./utils')

class Watch extends EventEmitter {
constructor(dir) {
super()
this.watchPath = resolveRootPath(dir)
fs.watch(this.watchPath, { recursive: true }, this.handleCallback)
}

handleCallback = (eventType, filename) => {
try {
const content = fs
.readFileSync(path.resolve(this.watchPath, filename), {
encoding: 'utf-8',
})
.toString()
this.emit('update', {
filename,
content,
})
} catch (e) {
this.emit('remove', {
filename,
content: null,
})
}
}
}

module.exports = Watch

优化

  1. fs.watchfs.watchFile 兼容性考虑,在不兼容的平台上可以通过定时检查文件的方式判断文件是否有改动
  2. websocket 兼容性考虑,不支持的平台上降级为短轮询或长轮询的方式
  3. 文件修改时可以在抛出的事件中提供更多的信息,事件局部文件的热更新而不用刷新整个页面

参考文献

粘贴图片

背景

优化用户编辑体验,实现在编辑内容(markdown 或富文本)时通过复制的方式实现图片的插入

实现

  1. 监听剪贴板事件中的 paste 事件
  2. 通过事件的回调对象获取粘贴内容,若为文本则执行默认操作,若为图片类型则阻止默认操作并继续处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const cb = (event) => {
const data = event.clipboardData // 获取粘贴内容
let fileContent
let stopFlag = false
// 遍历对象获取粘贴文件
for (let i = 0; data && data.items && i < data.items.length; i++) {
const item = data.items[i]
if (item.kind === 'file' && item.type.match('^image/')) {
stopFlag = true
fileContent = item.getAsFile()
}
}
// 对 fileContent 做处理
}

说明

  • eventClipboardEvent 类型,可以通过 clipboardData 属性拿到数据内容
  • clipboardData 是 [ DataTransfer](https://developer.mozilla.org/zh-CN/docs/Web/API/DataTransfer)对象,在粘贴事件中可以通过 items 属性获取粘贴的内容和数据类型,其是一个 [DataTransferItemList](https://developer.mozilla.org/zh-CN/docs/Web/API/DataTransferItemList) 对象,通过 for 循环的方式遍历每一个元素,每个元素都是 [DataTransferItem`](https://developer.mozilla.org/zh-CN/docs/Web/API/DataTransferItem)对象

DataTransferItem 对象

属性
  • kind: 拖拽项的种类,string 或是 file
  • type:拖拽项的类型,一般是一个 MIME 类型
方法
  • getAsFile():返回一个关联拖拽项的 File 对象 (当拖拽项不是一个文件时返回 null)
  • getAsString():使用拖拽项的字符串作为参数执行指定回调函数。
  • webkitGetAsEntry():返回一个基于 FileSystemEntry 的对象来表示文件系统中选中的项目。通常是返回一个 FileSystemFileEntry 或是 FileSystemDirectoryEntry 对象.

注意

  1. mac 上复制一张图片文件时会产生两个记录,第一个记录是文件名,第二个对象是文件本身
  2. mac 上复制一个非图片类型的文件时,也会产生两个记录,一个是文件名,另一个是文件的缩略图,但是这个文章的缩略图通过 getAsFile 方法无法获取到内容

参考

用Hexo搭建静态博客

环境准备

hexo 环境配置

  • 创建文件夹 blog 作为项目文件夹
  • 初始化项目文件夹
  • 指定文件夹初始化

    hexo init blog
    
  • 或者,进入文件夹再初始化

    cd blog
    hexo init
    
  • 安装插件 deployer

    npm install hexo-deployer-git --save
    
  • 修改根目录下的 _config.yml 文件

    deploy:
    type: git
    repo: git@github.com:JackXuyi/JackXuyi.github.io.git
    branch: master
    
  • 配置域名:在 source 目录下添加 CNAME 文件,并在文件里写入你的域名

    xuyi-emb.win
    

使用说明

跨页面通信

背景

在浏览器中每个页面都运行在单独的进程中,如何实现页面间的通信?

实现方案

BroadCastChannel

BroadcastChannel 接口代理了一个命名频道,可以让指定 origin 下的任意 browsing context 来订阅它。它允许同源的不同浏览器窗口,Tab 页,frame 或者 iframe 下的不同文档之间相互通信。通过触发一个 message 事件,消息可以广播到所有监听了该频道的 BroadcastChannel 对象。

  1. 构建一个实例
1
const bc = new BroadcastChannel('channel')
  1. 调用 postMessage 发送消息
1
bc.postMessage(data)
  1. 分别监听 onmessageonmessageerror 事件来接收数据和错误消息
1
2
3
4
5
6
bc.onmessage = function (e) {
// e.data是发送过来的数据,可以根据数据做一些事情
}
bc.onmessageerror = function (e) {
// 错误消息
}
  1. 调用 close 关闭通道,并销毁 BroadcastChannel 对象
1
bc.close()

localStorage 实现

当前页面使用的 storage 被其他页面修改时会触发 StorageEvent 事件

  1. 在源页面设置值触发事件
1
localStorage.setItem('newValue', 'new')
  1. 在接收页面监听 storage 事件以实现通信
1
2
3
4
window.addEventListener('storage', function (e) {
// e.newValue表示新设置的值
// e.key表示新设置的值的键
})

其它方案

  1. Service Worker: 一个可以长期运行在后台的 Worker,能够实现与页面的双向通信。
  2. Shared Worker: Worker 家族的另一个成员。普通的 Worker 之间是独立运行、数据互不相通;而多个 Tab 注册的 Shared Worker 则可以实现数据共享。
  3. IndexedDB: 数据存储技术
  4. window.open + window.opener: 当我们使用 window.open 打开页面时,方法会返回一个被打开页面 window 的引用。而在未显示指定 no opener 时,被打开的页面可以通过 window.opener 获取到打开它的页面的引用 —— 通过这种方式我们就将这些页面建立起了联系(一种树形结构)。
  5. visibilitychange: 当其选项卡的内容变得可见或被隐藏时,会在文档上触发 visibilitychange (能见度更改)事件。

参考

nodejs中实现websocket服务

建立链接

若要实现 WebSocket 协议,首先需要浏览器主动发起一个 HTTP 请求。

这个请求头包含“Upgrade”字段,内容为“websocket”(注:upgrade 字段用于改变 HTTP 协议版本或换用其他协议,这里显然是换用了 websocket 协议),还有一个最重要的字段“Sec-WebSocket-Key”,这是一个随机的经过 base64 编码的字符串,像密钥一样用于服务器和客户端的握手过程。一旦服务器君接收到来自客户端的 upgrade 请求,便会将请求头中的“Sec-WebSocket-Key”字段提取出来,追加一个固定的“魔串”:258EAFA5-E914-47DA-95CA-C5AB0DC85B11,并进行 SHA-1 加密,然后再次经过 base64 编码生成一个新的 key,作为响应头中的“Sec-WebSocket-Accept”字段的内容返回给浏览器。一旦浏览器接收到来自服务器的响应,便会解析响应中的“Sec-WebSocket-Accept”字段,与自己加密编码后的串进行匹配,一旦匹配成功,便有建立连接的可能了(因为还依赖许多其他因素)。

这是一个基本的 Client 请求头:

1
2
3
4
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: ************==
Sec-WebSocket-Version: **

Server 正确接收后,会返回一个响应头:

1
2
3
Upgrade:websocket
Connnection: Upgrade
Sec-WebSocket-Accept: ******************

这表示双方握手成功了,之后就是全双工的通信。

js 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 构建响应头
function buildHeaders(secWebsocketKey) {
// 计算返回的key
const resKey = crypto
.createHash('sha1')
.update(secWebsocketKey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
.digest('base64')

// 构造响应头
return [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
'Sec-WebSocket-Accept: ' + resKey,
]
.concat('', '')
.join('\r\n')
}

解析数据

Frame(帧)

WebSocket 传输的数据都是以 Frame(帧)的形式实现的,就像 TCP/UDP 协议中的报文段 Segment。下面就是一个 Frame:(以 bit 为单位表示)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
*
1 2 3 4
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
*/
按照 RFC 中的描述:
  • FIN: 1 bit, 0 表示还有后续帧,1 表示最后一帧
  • RSV1、2、3: 没个 1 bit,除非一个扩展经过协商赋予了非零值以某种含义,否则必须为 0,如果没有定义非零值,并且收到了非零的 RSV,则 websocket 链接会失败
  • Opcode: 4 bit,如果收到了未知的 opcode,最后会断开链接, 这四位的值组合结果的含义分别如下:
1
2
3
4
5
6
7
8
0x0 : 代表连续的帧
0x1 : text帧
0x2 binary帧
0x3-0x7 为非控制帧而预留的
0x8 关闭握手帧
0x9 ping帧
0xA : pong
0xB-0xF 为非控制帧而预留的
  • Mask: 1 bit,0 表示数据没有添加掩码,1 表示数据被添加了掩码,如果置 1, “Masking-key”就会被赋值,所有从客户端发往服务器的帧都会被置 1
  • Payload length: 7 bit | 7+16 bit | 7+64 bit,“payload data” 的长度如果在 0~125 bytes 范围内,它就是“payload length”,如果是 126 bytes, 紧随其后的被表示为 16 bits 的 2 bytes 无符号整型就是“payload length”,如果是 127 bytes, 紧随其后的被表示为 64 bits 的 8 bytes 无符号整型就是“payload length”
  • Masking-key: 0 or 4 bytes,所有从客户端发送到服务器的帧都包含一个 32 bits 的掩码(如果“mask bit”被设置成 1),否则为 0 bit。一旦掩码被设置,所有接收到的 payload data 都必须与该值以一种算法做异或运算来获取真实值。
  • Payload data: n bytes,数据内容

读取数据帧

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
class CustomReadSocket extends EventEmitter {
static MAGIC_STRING = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'

constructor(req, socket, head) {
super()
// 保存上下文信息
this.req = req
this.socket = socket
this.head = head

// 内部状态
this.dataType = ''
this.resHeaders = this.buildHeaders(req.headers['sec-websocket-key'])
this.buffer = null

// 接收数据
this.socket.on('data', this.handleData)

// 响应给客户端
this.socket.write(this.resHeaders)
}

// 构建响应头
buildHeaders(secWebsocketKey) {
// 计算返回的key
const resKey = crypto
.createHash('sha1')
.update(secWebsocketKey + CustomReadSocket.MAGIC_STRING)
.digest('base64')

// 构造响应头
return [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
'Sec-WebSocket-Accept: ' + resKey,
]
.concat('', '')
.join('\r\n')
}

// 处理收到的数据
handleData = (data) => {
do {
const type = this.getFrameType(data[0])
if (type && type !== 'continue') {
this.dataType = type
}
if (type === 'continue' || type === 'text' || type === 'binary') {
let maskLen = 0
if (this.hasMask(data[1])) {
maskLen = 4
}
const [start, len] = this.getFrameDataLength(data)

this.getDataFromFrame(
data.slice(start + maskLen),
data.slice(start, start + maskLen),
)
}
} while (!this.isLastFrame(data[0]))
this.handleAllType(this.dataType)
}

// 获取当前帧类型
getFrameType(byte) {
const realType = byte & 0x7f
if (!realType) {
// 代表连续的帧
return 'continue'
} else if (realType === 0x01) {
return 'text'
} else if (realType === 0x09) {
return 'ping'
} else if (realType === 0x0a) {
return 'pong'
} else if (realType === 0x02) {
return 'binary'
} else if (realType === 0x08) {
return 'close'
} else {
return ''
}
}

// 处理所有帧类型
handleAllType(type) {
switch (type) {
case 'continue': {
console.log('continue')
break
}
case 'text': {
this.emit('message', this.buffer.toString())
break
}
case 'binary': {
this.emit('message', this.buffer)
break
}
case 'close': {
this.emit('close')
break
}
case 'ping': {
this.emit('ping')
break
}
case 'pong': {
this.emit('pong')
break
}
default: {
console.log('others')
}
}
// 释放 buffer 和重置数据类型
this.buffer = null
this.dataType = ''
}

// 判断是否是最后一个帧
isLastFrame(byte) {
return !!(byte & 0x80)
}

// 提取帧的长度
getFrameDataLength(buffer) {
// 第二个字节的底 7 位
const firtLen = buffer[1] & 0x7f
if (firtLen < 125) {
return [2, firtLen]
} else if (firtLen === 126) {
const len = buffer.readUInt16BE(2)
return [4, len]
} else {
const len = buffer.readUInt64BE(2)
return [10, len]
}
}

// 判断是否有掩码
hasMask(byte) {
return !!(0x80 & byte)
}

// 从帧里提取数据
getDataFromFrame(buffer, maskBuffer) {
if (buffer && buffer.length) {
const len = buffer.length
if (maskBuffer && maskBuffer.length === 4) {
for (let i = 0; i < len; i++) {
buffer[i] = buffer[i] ^ maskBuffer[i % 4]
}
}
this.buffer = this.buffer ? Buffer.concat([this.buffer, buffer]) : buffer
}
}
}

写入数据帧

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
class CustomWebsocket extends CustomReadSocket {
timer = null
constructor(req, socket, head, options) {
super(req, socket, head)
this.options = options

this.timeout()
this.socket.on('data', () => {
this.timeout()
})
}

send(message) {
const buffer = Buffer.from(message)
const list = this.buildDataFrameList('text', buffer)
list.forEach((frame) => {
this.socket.write(frame)
})
this.timeout()
}

ping() {
const frame = this.buildFrame('ping')
this.socket.write(frame)
this.timeout()
}

pong() {
const frame = this.buildFrame('pong')
this.socket.write(frame)
this.timeout()
}

close() {
const frame = this.buildFrame('close')
this.socket.write(frame)
this.socket.end()
process.nextTick(() => {
this.socket.destroy()
})
}

timeout() {
const { timeout = 10000 } = this.options || {}
clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.ping()
}, timeout)
}

buildDataFrameList(type, buffer) {
const bufferList = []
let tempBuffer = buffer
while (tempBuffer.length) {
bufferList.push(tempBuffer.slice(0, this.MAX_FRAME_SIZE))
tempBuffer = tempBuffer.slice(this.MAX_FRAME_SIZE)
}
const len = bufferList.length
return bufferList.map((buf, index) =>
this.buildFrame(index ? 'continue' : type, len === index + 1, buf),
)
}

buildFrame(dataType, isLast = true, buffer = null) {
let firstByte = isLast ? 0x80 : 0x00
switch (`${dataType || ''}`.toLowerCase()) {
case 'continue': {
firstByte = firstByte | 0x00
break
}
case 'text': {
firstByte = firstByte | 0x01
break
}
case 'binary': {
firstByte = firstByte | 0x02
break
}
case 'close': {
firstByte = firstByte | 0x08
break
}
case 'ping': {
firstByte = firstByte | 0x09
break
}
case 'pong': {
firstByte = firstByte | 0x0a
break
}
default: {
console.log('others')
}
}
let secondByte = 0x00

if (buffer && buffer.length) {
const lenByteList = []
const len = buffer.length
if (len <= 125) {
secondByte = secondByte | len
} else if (len >= 126 && len < 65536) {
secondByte = secondByte | 0x7e
for (let i = 0; i < 2; i++) {
lenByteList.push(0xff & (len >> (i * 8)))
}
} else {
secondByte = secondByte | 0x7f
for (let i = 0; i < 8; i++) {
lenByteList.push(0xff & (len >> (i * 8)))
}
}
lenByteList.reverse()
const prefixBuffer = Buffer.from([firstByte, secondByte, ...lenByteList])
return Buffer.concat([prefixBuffer, buffer])
}
return Buffer.from([firstByte, secondByte])
}
}

参考

记一次项目打包优化

由于项目功能越来复杂,打包发布的速度越来越慢,严重影响了开发速度,所以决定优化下打包发布速度

分析打包流程及耗时

打包流程

  • 代码 clone 到打包服务器上
  • 编译代码,项目采用 nextjs 进行服务端渲染,所以编译编译时分为两个部分,分别为构建服务端 js 和客户端 js,每次编译都要先安装项目依赖,再执行打包构建命令
  • 构建 docker 容器,项目运行在 docker 容器中,每次打包发布都是构建一个新的容器去替换对应环境的容器
  • 部署静态资源,把客户端对应的 js 推送到 CDN 上
  • 替换容器,用之前构建的容器去替换当前环境的容器

耗时分析

  • clone 代码速度由网络和项目大小决定(网络无法控制,源代码不大,优化效果不明显)—— 通常在 10 秒左右
  • 采用 webpack 进行编译,可以通过插件进行构建优化 —— 通常在 2.5 分钟左右
  • 构建 docker 容器时会把 node_modules 下的依赖按照生产模式的方式放入容器中 —— 通常在 4.5 分钟左右
  • 静态资源的上传由上传资源大小及网络决定,上传由基础组控制 —— 2 分钟左右
  • 替换容器由集群控制,业务方无法控制 —— 通常在 1 分左右

优化

通过上面的分析发现,制作 docker 容器和 打包过程最耗费时间,且这两块也在业务方控制之中,所以优化从这两方面着手

docker 容器优化

通过查看打包日志发现制作 docker 容器时发现制作容器时发送的上下文达 1G 多,对比其它项目明显偏大

优化方向分析

  • 发送的上下文是在生产模式下安装的依赖,即只会安装 dependencies 下的依赖 —— 考虑把所有非服务端必须的依赖安装到 devDependencies 中以减小上下文大小
  • 由于需要 SEO 优化,优化不能减少服务端渲染时的页面内容 —— 只能优化对页面内容没有影响但是需要客户端交互及样式操作相关的库

优化操作

  • 通过 webpack 插件提取项目中使用的依赖,结合 package.json 的配置,筛选出项目中无用的依赖
  • 筛选出 dependencies 中的依赖是否可以只在客户端渲染时引入,例如 react-copy-to-clipboard 只会在客户端执行复制操作就可以放入 devDependencies 中以便只在客户端引入

优化效果

构建 docker 容器的时间从优化前的 4.5 分钟左右下降到 2.5 分钟左右,优化了 50%

webpack 打包优化

此项目是基于 webpack 4.x 进行打包的,可以通过插件和 webpack 配置及 loader 的形式进行打包优化

webpack 4 使用 v8 引擎带来的优化

  • for of 替代 forEach
  • Map 和 Set 替代 Object
  • includes 替代 indexOf()
  • 默认使用更快的 md4 hash 算法 替代 md5 算法,md4 较 md5 速度更快
  • webpack AST 可以直接从 loader 传递给 AST,从而减少解析时间
  • 使用字符串方法替代正则表达式

优化方向分析

通过 speed-measure-webpack-plugin 插件进行打包耗时分析

  • 通过配置 exclude / include 和忽略第三方包指定目录及通过 externals 缩小打包文件范围
  • 多进程并行打包和代码压缩
  • 缓存编译结果
  • babel 配置的优化

优化操作

通过研究框架 webpack 配置发现项目已经引入 cache-loaderthread-loaderloader 和插件及配置优化代码打包速度,所以未做 webpack 打包优化

  • 引入 speed-measure-webpack-plugin 查看打包耗时

相关知识点

ts之type、interface和class区别

type

任意类型的别名

1
2
We’ve been using object types and union types by writing them directly in type annotations. This is convenient, but it’s common to want to use the same type more than once and refer to it by a single name.
A type alias is exactly that - a name for any type.

示例

1
2
3
4
type test = number

// error: 标识符“test”重复。
// type test = string

特点

  • 任意类型的别名(包含基本类型)
  • 只可以定义一次(多次定义报 ’标识符“test”重复‘ 错误)

interface

接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。

1
One of TypeScript’s core principles is that type checking focuses on the shape that values have. This is sometimes called “duck typing” or “structural subtyping”. In TypeScript, interfaces fill the role of naming these types, and are a powerful way of defining contracts within your code as well as contracts with code outside of your project.

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface Shape {
color: string
}

interface Shape {
size?: number
}

interface PenStroke {
penWidth: number
}

interface Square extends Shape, PenStroke {
sideLength: number
}

let square = <Square>{}
square.color = 'blue'
square.sideLength = 10
square.penWidth = 5.0
square.size = 100

class

类是“特殊的函数”,就像你能够定义的函数表达式和函数声明一样,类语法有两个组成部分:类表达式和类声明。

1
TypeScript offers full support for the class keyword introduced in ES2015. As with other JavaScript language features, TypeScript adds type annotations and other syntax to allow you to express relationships between classes and other types.

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let passcode = 'secret passcode'

class Employee {
private _fullName: string

get fullName(): string {
return this._fullName
}

set fullName(newName: string) {
if (passcode && passcode == 'secret passcode') {
this._fullName = newName
} else {
console.log('Error: Unauthorized update of employee!')
}
}
}

let employee = new Employee()
employee.fullName = 'Bob Smith'
if (employee.fullName) {
alert(employee.fullName)
}

abstract class

抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 不同于接口,抽象类可以包含成员的实现细节。 abstract 关键字是用于定义抽象类和在抽象类内部定义抽象方法。

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
abstract class Department {
constructor(public name: string) {}

printName(): void {
console.log('Department name: ' + this.name)
}

abstract printMeeting(): void // 必须在派生类中实现
}

class AccountingDepartment extends Department {
constructor() {
super('Accounting and Auditing') // 在派生类的构造函数中必须调用 super()
}

printMeeting(): void {
console.log('The Accounting Department meets each Monday at 10am.')
}

generateReports(): void {
console.log('Generating accounting reports...')
}
}

let department: Department // 允许创建一个对抽象类型的引用
department = new Department() // 错误: 不能创建一个抽象类的实例
department = new AccountingDepartment() // 允许对一个抽象子类进行实例化和赋值
department.printName()
department.printMeeting()
department.generateReports() // 错误: 方法在声明的抽象类中不存在

区别

  • 接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名。
  • 类型别名不能被 extends 和 implements(自己也不能 extends 和 implements 其它类型)。
  • 接口只能用来声明对象,而不能重新命名基本数据类型。
  • 接口名称将始终以原始形式出现在错误消息中,当按名称使用时才显示。
  • 接口可以进行声明合并
  • 抽象类和接口一样不可以被实例化,但是抽象类可以包含部分属性方法的实现

for...in和for...of的区别

Iterator(遍历器)的概念

遍历器(Iterator)一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

Iterator 的作用

  • 为各种数据结构,提供一个统一的、简便的访问接口。
  • 使得数据结构的成员能够按某种次序排列。
  • ES6 创造了一种新的遍历命令 for…of 循环,Iterator 接口主要供 for…of 消费。

Iterator 的遍历

  1. 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
  2. 第一次调用指针对象的 next 方法,可以将指针指向数据结构的第一个成员。
  3. 第二次调用指针对象的 next 方法,指针就指向数据结构的第二个成员。
  4. 不断调用指针对象的 next 方法,直到它指向数据结构的结束位置。

Iterator 接口

一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)。ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,或者说,一个数据结构只要具有 Symbol.iterator 属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator 属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。

ES6 原生具备 Iterator 接口的数据结构
  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

调用 Iterator 接口的场合

  • 解构赋值
  • 扩展运算符
  • yield*
  • for…of
  • Array.from()
  • Map(), Set(), WeakMap(), WeakSet()
  • Promise.all()
  • Promise.race()

遍历器对象的 return(),throw()

遍历器对象除了具有 next()方法,还可以具有 return()方法和 throw()方法。

  • 如果 for…of 循环提前退出(通常是因为出错,或者有 break 语句),就会调用 return()方法。
  • throw()方法主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法。

for…in 和 for…of 的区别

  • 对于普通的对象,for…in 循环可以遍历键名,for…of 循环会报错。

for…in 遍历数组的缺点

  • 数组的键名是数字,但是 for…in 循环是以字符串作为键名“0”、“1”、“2”等等。
  • for…in 循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
  • 某些情况下,for…in 循环会以任意顺序遍历键名。

for…of 遍历数组的优点

  • 有着同 for…in 一样的简洁语法,但是没有 for…in 那些缺点
  • 不同于 forEach 方法,它可以与 break、continue 和 return 配合使用。
  • 提供了遍历所有数据结构的统一操作接口。

异步任务按顺序分组执行

问题描述

有一个异步请求列表需要按照顺序分多次执行

分析

任务可以放入一个队列中,每次从队列中获取若干任务异步执行

实现

一次执行的任务放在 Promise.all 中执行,但是如果执行任务中有一个任务花费时间比较长,其它任务消耗时间短就浪费了大量时间,所以采用计数的方式判断是否可以把任务加入任务队列

代码流程

  1. 构建任务队列
  2. 需要执行的任务放入队列
  3. 执行任务,判断是否有任务可以执行,若有任务可以执行,则正在执行的任务队列加一
  4. 任务执行完毕之后,再回到第 3 部,直至待执行任务队列和当前执行的任务为空时退出程序

具体 js 代码如下

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class Queue {
constructor(maxTask) {
this.maxTask = maxTask
this.runningTask = 0
this.taskQueue = []
}

push(task) {
if (Array.isArray(task)) {
task.forEach((t) => {
this.taskQueue.push(t)
})
} else {
this.taskQueue.push(task)
}

this.run()
}

async runTask(task) {
try {
this.runningTask++
await task()
} catch (e) {
console.error(e)
} finally {
this.run()
this.runningTask--
}
}

run() {
if (this.canRunTask()) {
const task = this.taskQueue.shift()
this.runTask(task)
this.run()
}
}

canRunTask() {
return !this.isEmpty() && this.runningTask < this.maxTask
}

isEmpty() {
return !this.taskQueue.length
}
}

const taskQueue = new Queue(3)

const timeList = [100, 300, 500, 900, 600]
const taskList = new Array(20).fill(0).map((item, index) => {
return () =>
new Promise((resolve, reject) => {
const time = index % timeList.length
console.log('task ', index, 'time ', timeList[time], 'start')
const timer = setTimeout(() => {
clearTimeout(timer)
console.log('task ', index, 'time ', timeList[time], 'finished')
if (index % 5 === 0) {
reject('error')
} else {
resolve('success')
}
}, timeList[time])
})
})

taskQueue.push(taskList)

附件