0%

1. Nullish coalescing operator.

MDN参考

可选链操作符( ?. )允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?. 操作符的功能类似于 . 链式操作符,不同之处在于,在引用为空(nullish ) (null 或者 undefined) 的情况下不会引起错误,该表达式短路返回值是 undefined。与函数调用一起使用时,如果给定的函数不存在,则返回 undefined。

当尝试访问可能不存在的对象属性时,可选链操作符将会使表达式更短、更简明。在探索一个对象的内容时,如果不能确定哪些属性必定存在,可选链操作符也是很有帮助的。

在变量赋值过程中、经常需要做判断空值操作,例如:

1
var value = someValue || -1

利用短路求值方式、当someValue为空时候,就需要设定一个默认值-1,但这种写法有问题,因为||操作符为布尔判断、当someValue为0的时候,value仍然会被赋值成-1,所以新标准中提供了空值合并运算符:

1
var value = someValue ?? -1

以上仅有当someValue为undefined或者null时候,value才被赋值为-1,接下来就自己简单实现一个函数完成以上需求:

1
2
3
4
5
6
7
function nullish() {
for (var i = 0; i < arguments.length; i++) {
if (typeof arguments[i] != null) {
return arguments[i]
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

const valueA = nullish(0, -1) // 结果:0
const _valueA = 0 || -1 // 结果:-1
const __valueA = 0 ?? -1 // 结果:0


const valueB = nullish('', 'hello world') // 结果: ''
const _valueB = '' || 'hello world' // 结果: 'hello world'
const __valueB = '' ?? 'hello world' // 结果: ''


const valueC = nullish(undefined, null, 0, -1) // 结果:0
const _valueC = undefined || null || 0 || -1 // 结果:-1
const __valueC = undefined ?? null ?? 0 ?? -1 // 结果: 0

2. Optional chaining.

MDN参考

空值合并操作符(??)是一个逻辑操作符,当左侧的操作数为 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数。

与逻辑或操作符(||)不同,逻辑或操作符会在左侧操作数为假值时返回右侧操作数。也就是说,如果使用 || 来为某些变量设置默认值,可能会遇到意料之外的行为。比如为假值(例如,’’ 或 0)时。见下面的例子。

实现代码如下:

1
2
3
4
5
6
7
8
9
function optionalChain(target, propsChain) {
var props = propsChain.split(/\??\./)
var result
while (props.length) {
target = result = target[props.shift()]
if (result == null) break
}
return result
}
1
2
3
4
5
6
7
8
9
10
11
12
const a = {
b: {
c: 0
}
}

const value = a && a.b && a.b.c && a.b.c.value // 结果: 0

const _value = a?.b?.c?.value // 结果: undefined

const __value = optionalChain(a, 'b.c.value') // 结果: undefined
const ___value = optionalChain(a, 'b?.c.value') // 结果: undefined

以上测试中,value为undefined才是预期的结果值.

目录

  • aria2简介
  • 下载源码
  • 修改TCP连接数
  • 编译&安装
    • 安装依赖
    • 编译
    • 安装
  • 创建配置文件
  • 使用launchctl添加Mac自启动

aria2简介

aria2是一个基于C++编写的开源下载工具、具有HTTP、HTTPS、FTP、BT等协议支持,具有多线程下载、断点续传、具有RPC服务器等许多实用特性,详细内容可以访问官方网站进行了解。

aria2 is a lightweight multi-protocol & multi-source command-line download utility. It supports HTTP/HTTPS, FTP, SFTP, BitTorrent and Metalink. aria2 can be manipulated via built-in JSON-RPC and XML-RPC interfaces.

下载源码

你可以使用git clone的形式将对应版本的分支下载到本地、也可以下载使用release页面中提供的源码包,这边以下载源码包形式进行演示说明:

前往release-1.35.0页面,找到对应的源码包Source code(tar.gz)或者Source code(zip)进行下载、并解压。

修改TCP连接数

进入刚解压源码项目目录、打开文件src/OptionHandlerFactory.cc,进行以下两处的修改:

  • 修改最大单服务器连接数
1
2
3
4
5
// ...
OptionHandler* op(new NumberOptionHandler(PREF_MAX_CONNECTION_PER_SERVER,
TEXT_MAX_CONNECTION_PER_SERVER,
"1", 1, 16, 'x'));
// ...

将其中的连接数16修改为128:

1
2
3
4
5
// ...
OptionHandler* op(new NumberOptionHandler(PREF_MAX_CONNECTION_PER_SERVER,
TEXT_MAX_CONNECTION_PER_SERVER,
"1", 1, 128, 'x'));
// ...
  • 修改最小数据分片大小
1
2
3
4
// ...
OptionHandler* op(new UnitNumberOptionHandler(
PREF_MIN_SPLIT_SIZE, TEXT_MIN_SPLIT_SIZE, "20M", 1_m, 1_g, 'k'));
// ...

将其中的最小数据分片大小1_m修改为256_k,最小触发值"20M"修改为"1M"

1
2
OptionHandler* op(new UnitNumberOptionHandler(
PREF_MIN_SPLIT_SIZE, TEXT_MIN_SPLIT_SIZE, "1M", 256_k, 1_g, 'k'));

编译&安装

安装依赖

1
brew install libtool automake autoconf-archive pkg-config gettext cppunit

软链gettext/usr/local/bin

1
brew link gettext

或者强制链接:

1
brew link gettext --force

编译

初始化编译配置文件

1
autoreconf -i

* 如果第一次autoreconf -i出现错误,则在执行一次 autoreconf -i

更新配置编译文件configure.ac

1
autoconf configure.ac

配置makefile

1
./configure

编译

1
make check

或者多线程进行编译

1
make check -j4

安装

1
sudo make install

安装结束后可在/usr/local/bin/aria2c找到

测试是否安装成功:

1
aria2c --version

输出版本信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
aria2 version 1.35.0
Copyright (C) 2006, 2019 Tatsuhiro Tsujikawa

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

** Configuration **
Enabled Features: Async DNS, BitTorrent, Firefox3 Cookie, GZip, HTTPS, Message Digest, Metalink, XML-RPC
Hash Algorithms: sha-1, sha-224, sha-256, sha-384, sha-512, md5, adler32
Libraries: zlib/1.2.11 libxml2/2.9.4 sqlite3/3.28.0 AppleTLS GMP/6.1.2 c-ares/1.14.0
Compiler: Apple LLVM 11.0.3 (clang-1103.0.32.62)
built by x86_64-apple-darwin19.6.0
on Oct 24 2020 18:19:17
System: Darwin 19.6.0 Darwin Kernel Version 19.6.0: Thu Jun 18 20:49:00 PDT 2020; root:xnu-6153.141.1~1/RELEASE_X86_64 x86_64

Report bugs to https://github.com/aria2/aria2/issues
Visit https://aria2.github.io/

创建配置文件

aria2默认读取的配置文件路径为$HOME/.aria2/aria2.conf

创建配置文件

1
touch ~/.aria2/aira2.conf

配置文件常用配置内容及说明:

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
#用户名
#rpc-user=user
#密码
#rpc-passwd=passwd
#设置加密的密钥
#rpc-secret=secret
#允许rpc
enable-rpc=true
#允许所有来源, web界面跨域权限需要
rpc-allow-origin-all=true
#是否启用https加密,启用之后要设置公钥,私钥的文件路径
#rpc-secure=true
#启用加密设置公钥
#rpc-certificate=/home/name/.config/aria2/example.crt
#启用加密设置私钥
#rpc-private-key=/home/name/.config/aria2/example.key
#支持GZip
http-accept-gzip=true
#自定义请求头
#header=
#允许外部访问,false的话只监听本地端口
rpc-listen-all=true
#RPC端口, 仅当默认端口被占用时修改
rpc-listen-port=6800
#最大同时下载数(任务数), 路由建议值: 9
max-concurrent-downloads=6
#检查完整性
check-integrity=true
#断点续传
continue=false
#同服务器连接数
max-connection-per-server=32
#最小文件分片大小, 下载线程数上限取决于能分出多少片, 对于小文件重要
min-split-size=1M
#单文件最大线程数, 路由建议值: 5
split=84
#下载速度限制
max-overall-download-limit=0
#单文件速度限制
max-download-limit=0
#上传速度限制
max-overall-upload-limit=0
#单文件速度限制
max-upload-limit=0
#断开速度过慢的连接
#lowest-speed-limit=0
#验证用,需要1.16.1之后的release版本
#referer=*
#文件保存路径, 默认为当前启动位置
dir=$HOME/Downloads
#文件缓存, 使用内置的文件缓存, 如果你不相信Linux内核文件缓存和磁盘内置缓存时使用, 需要1.16及以上版本
disk-cache=32M
#另一种Linux文件缓存方式, 使用前确保您使用的内核支持此选项, 需要1.15及以上版本(?)
#enable-mmap=true
#会话信息
# input-file=$HOME/.aria2/aria2.session
# save-session=$HOME/.aria2/aria2.session
# save-session-interval=60
#下载完成后强制保存下载信息、在BT做种时候适用
# force-save=false
#文件预分配, 能有效降低文件碎片, 提高磁盘性能. 缺点是预分配时间较长
#所需时间 none < falloc ? trunc << prealloc, falloc和trunc需要文件系统和内核支持
file-allocation=none
#不进行证书校验
check-certificate=false
#第一次请求使用head方法
use-head=true
#设置用户代理信息
user-agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36"
# 所有代理
# all-proxy=127.0.0.1:8070

按照自己的实际情况与需求、配置以上内容并保存到~/.aria2/aira2.conf配置文件中、通常只要修改dir来设置下载目录。

添加系统启动服务

配置文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>aria2</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<false/>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/aria2c</string>
</array>
</dict>
</plist>

保存文件到~/Library/LaunchAgents/mxctl.aria2.plist

加载配置:

1
launchctl load ~/Library/LaunchAgents/mxctl.aria2.plist

启动服务:

1
launchctl start aria2

完成!

prompt-command.js文件位于webapck-cli源码包的bin/utils/目录下,该文件里面的内容主要为运行npm命令相关的一些操作

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
// based on https://github.com/webpack/webpack/blob/master/bin/webpack.js

/**
* @param {string} command 命令
* @param {string[]} args 命令行参数
* @returns {Promise<void>} promise 返回结果的Promise实例
*/
const runCommand = (command, args) => {
// 使用child_process模块,进行shell命令的执行
// 由于js为单线程、额外的shell命令执行与交互
// 需要在另外子线程中执行。
const cp = require("child_process");

// 构建Promise
return new Promise((resolve, reject) => {
// 利用child_process模块的spawn方法
// 该方法实现创建出一个子线程实例
const executedCommand = cp.spawn(command, args, {
stdio: "inherit",
shell: true // 通过系统shell进行执行命令
});

// 注册child_process实例错误事件
// 对应resolve与reject
executedCommand.on("error", error => {
reject(error);
});

// 注册child_process实例退出事件
// 对应resolve与reject
executedCommand.on("exit", code => {
/// 退出状态吗为0,则正常退出
/// shell中的退出状态吗0未正常
/// 非0为对应的其它非正常退出
if (code === 0) {
resolve();
} else {
reject();
}
});
});
};

关于exec与spwan的区别,在于:

  1. exec返回buffer实例

  2. spawn返回stream实例

所以在需要长时间、大量处理数据输入输出,则选择spawn,其它短平快则方式选择exec等

区别参考:https://www.hacksparrow.com/nodejs/difference-between-spawn-and-exec-of-node-js-child-rocess.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 获取npm的全局包安装路径
// 主要是对命令`npm root -g`的封装
const npmGlobalRoot = () => {
const cp = require("child_process");
return new Promise((resolve, reject) => {
// 通过spawn方法创建对应命令的子线程实例
const command = cp.spawn("npm", ["root", "-g"]);

command.on("error", error => reject(error)); // 注册error事件并reject

// 由于这边的命令没有做持续输出、所以,对于data就简单的调用一下toString()
// 获取终端的执行结果
// 如果在运行其它具有持续输出的命令时候、需要对data事件所返回的数据分片进行整合,
// 并且在close事件中进行resolve,这样可防止丢失返回结果。
command.stdout.on("data", data => resolve(data.toString()));

// stderr的输出数据则为执行的错误消息、该事件非底层的error错误
// 处理方式与stdout的data事件类似、不过在语义上,是reject
command.stderr.on("data", data => reject(data));
});
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 运行`@webpack-cli`相关已安装的包、或者命令
* @param packages 包名称
* @param pathForCmd 包的执行命令路径
* @param args 参数
*/
const runWhenInstalled = (packages, pathForCmd, ...args) => {

// 引入运行命令对应的模块
const currentPackage = require(pathForCmd);

// 约定包默认导出为执行函数
const func = currentPackage.default;

if (typeof func !== "function") {
// 非函数则直接中断
throw new Error(`@webpack-cli/${packages} failed to export a default function`);
}

return func(...args);
};
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
/**
* 运行`@webpack-cli`相关已安装的包、或者命令,如果未安装则提示执行安装
* @param packages 包名称
* @param args 参数
*/
module.exports = function promptForInstallation(packages, ...args) {

// 组合目标包路径
const nameOfPackage = "@webpack-cli/" + packages;

// 默认标志该包是未安装状态、从后面去检测判断
let packageIsInstalled = false;

// 包对应的路径
let pathForCmd;

try {
const path = require("path");
const fs = require("fs");

// 在当前项目node_modules文件夹中的查找对应包
pathForCmd = path.resolve(process.cwd(), "node_modules", "@webpack-cli", packages);

// 当前项目未安装、则进一步找全局
if (!fs.existsSync(pathForCmd)) {
// 获取全局的node_modules路径
const globalModules = require("global-modules");

// 拼接新的路径
pathForCmd = globalModules + "/@webpack-cli/" + packages;

// 利用require.resolve来验证包是否存在
require.resolve(pathForCmd);

// 存在文件
} else {
require.resolve(pathForCmd);
}

// require.resolve正常执行,则包已安装
packageIsInstalled = true;
} catch (err) {
// require.resolve异常,则包未安装
packageIsInstalled = false;
}

/// 如果包未安装则进一步提示安装
if (!packageIsInstalled) {
const path = require("path");
const fs = require("fs");

// 利用原生的readline模块进行询问
const readLine = require("readline");

// 判断是否要通过yarn进行安装,通过yarn.lock来判断
const isYarn = fs.existsSync(path.resolve(process.cwd(), "yarn.lock"));

// 确定npm或者yarn作为安装器
const packageManager = isYarn ? "yarn" : "npm";

// 构建安装命令需要的参数列表,这边默认为npm的参数形式
const options = ["install", "-D", nameOfPackage];

if (isYarn) {
// yarn模式的话,修改参数列表 `install` -> `add`
// --> `yarn add -D`
options[0] = "add";
}

// 初始化安装webapck-cli
if (packages === "init") {
if (isYarn) {
options.splice(1, 1); // remove '-D'
options.splice(0, 0, "global");
// --> `yarn global add`
} else {
options[1] = "-g";
// --> `npm install -g`
}
}


// 拼接需要运行的参数
const commandToBeRun = `${packageManager} ${options.join(" ")}`;

// 问题
const question = `Would you like to install ${packages}? (That will run ${commandToBeRun}) (yes/NO) : `;

console.error(`The command moved into a separate package: ${nameOfPackage}`);

// 通过readline创建终端交互接口
const questionInterface = readLine.createInterface({
input: process.stdin,
output: process.stdout
});

// 执行提示询问
questionInterface.question(question, answer => {
// 关闭接口
questionInterface.close();

// 根据不同输入执行
switch (answer.toLowerCase()) {
case "y":
case "yes":
case "1": { // 按回车
// 执行安装命令
runCommand(packageManager, options)
.then(_ => {
// 初始化webpack-cli
if (packages === "init") {
npmGlobalRoot()
.then(root => {
const pathtoInit = path.resolve(root.trim(), "@webpack-cli", "init");
return pathtoInit;
})
.then(pathForInit => {
// 找到init相关脚本模块、并执行
return require(pathForInit).default(...args);
})
.catch(error => {
console.error(error);
process.exitCode = 1;
});
return;
}

// 安装完对应的包后、通过runWhenInstalled执行命令
pathForCmd = path.resolve(process.cwd(), "node_modules", "@webpack-cli", packages);
return runWhenInstalled(packages, pathForCmd, ...args);
})
.catch(error => {
console.error(error);
process.exitCode = 1;
});
break;
}
default: {
// 放弃安装、提示需要依赖包,退出程序
console.error(`${nameOfPackage} needs to be installed in order to run the command.`);
process.exitCode = 1;
break;
}
}
});
} else {
// 已经对应包已安装、则直接执行
return runWhenInstalled(packages, pathForCmd, ...args);
}
};

1. 文件路径格式

1
2
3
4
5
const files = [
'/Users/god/my_project/a.js',
'/Users/god/my_project/b.js',
'/Users/god/my_project/others/c.js',
]

2. 定义文件树节点

1
2
3
4
5
type FileTreeNode = {
label: string
path: string
children: Array<FileTreeNode>
}

在处理过程中为了方便索引与记录,所以添加一个childMap字段,用于记录children数组中对应的子节点

1
2
3
type CacheFileTreeNode = {
childMap: Record<string, CacheFileTreeNode>
} & FileTreeNode

3. 分割路径、并按节点顺序进行归并

3.1 定义路径分割方法

1
2
3
4
function SplitPath(pathStr: string, sep = '/'): string[] {
/** 根据分隔符sep进行路径字符串分割, 过滤掉空字符串 */
return pathStr.split(sep).filter(Boolean)
}

3.2 定义目录树根节点root

1
2
3
4
5
6
const root: CacheFileTreeNode = {
label: 'root',
path: '',
childMap: {},
children: []
}

3.3 定义文件归并处理

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
function ReducePathParts(parts: string[], root: CacheFileTreeNode): CacheFileTreeNode {

// 进行归并关联
parts.reduce((parent, name) => {

// 查找上一级目录节点中是否存在与当前路径名相同的子节点节点
const hit = parent.childMap[name]
if (hit) {
return hit
}

// 定义当前目录对应的节点
const current = {
label: name,
path: `${parent.path}/${name}`,
childMap: {},
children: []
}

// 将当前目录树节点关联到上一级
parent.childMap[name] = current
parent.children.push(current)

// 返回当前节点作为下一级的父节点
return current

}, root)

return root // 返回根节点
}

4. 合并逻辑

1
2
3
4
5
6
7
8
9
10
11
function generateFileTree(files: string[]): CacheFileTreeNode {

// ...

files.forEach(filePath => {
const parts = SplitPath(filePath) // 分割
ReducePathParts(parts, root) // 关联
})

return root
}

获得root节点内容:

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
{
label: 'root',
path: '',
cildren: [
{
label: 'Users',
path: '/Users',
children: [
{
label: 'god',
path: '/Users/god',
children: [
{
label: 'my_project',
path: '/Users/god/my_project',
children: [
{
label: 'a.js',
path: '/Users/god/my_project/a.js',
children: []
},
{
label: 'b.js',
path: '/Users/god/my_project/b.js',
children: []
},
{
label: 'others',
path: '/Users/god/my_project/others',
children: [
{
label: 'c.js',
path: '/Users/god/my_project/others/c.js',
children: []
}
]
}
]
}
]
}
]
}
]
}

1. 菜单数据结构

通常,对于菜单的配置,都是单条记录,利用pid等字段进行父级关联,这边默认定义菜单配置项目的数据结构

1
2
3
4
5
type MenuOpt = {
id: nubmer
pid: number
name: string
}

包含最主要的三个字段id,pid,name,其中完成关系嵌套的两个字段为id和pid,这边pid是一对一关系,也就是说一个菜单项目只能归属到一个父级菜单下面,而接下来,我们需要为菜单添加一个额外的字段children用来存放子菜单,这里定义一个新的数据结构:

1
2
3
4
5
6
type MenuItem = {
id: number
pid: number
name: string
children: MenuItem[]
}

由于具有相同的字段id,pid,name,所以我们复用MenuOpt类型进行联合:

1
2
3
type MenuItem = {
children: MenuItem[]
} & MenuOpt

2. 原始菜单配置项数据

通过以上的类型定义,我来生成一个菜单的配置项数据,通常这些数据:

1
2
3
4
5
6
7
8
const menuOpts: MenuOpt[] = [
{ id: 1, pid: 0, name: '节点1' },
{ id: 2, pid: 1, name: '节点1-1' },
{ id: 3, pid: 0, name: '节点2' },
{ id: 4, pid: 3, name: '节点2-1' },
{ id: 5, pid: 2, name: '节点1-1-1' },
{ id: 6, pid: 1, name: '节点1-2' },
]

这边定义pid如果为0则为顶级的菜单项目,则直接添加到最终的顶级菜单数组中。

3. 嵌套菜单

为了方便进行管理,我们需要遍历并记录对应id与配置项的映射,通过Object的引用特性,即可快速完成嵌套关系,具体的步骤为:

  1. 遍历配置项目,拷贝配置项并完成id => opt的映射索引
  2. 遍历配置项目(拷贝完成),根据pid去查找映射并添加到对应的父级配置项中
  3. 完成对菜单项目的排序,根据自定义排序规则进行
  4. 完成菜单关系嵌套

3.1 遍历并建立索引

为了防止引用特性带来的副作用,我们将配置项进行深拷贝,先定义一个深拷贝函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function cloneDeep<
C extends { origin: any, copied: any }
>(origin: any, cache: C[] = []) {

if (typeof origin !== 'object' || origin === null) {
return origin
}

const hit = cache.find(c => c.origin === origin)

if (hit) { return hit.copied }

const copied = Array.isArray(origin) ? [] : {}

cache.push({ origin, copied } as C)

Object.keys(origin).forEach(key => {
copied[key] = cloneDeep(origin[key], cache)
})

return copied
}

定义嵌套处理方法

1
2
3
4
function nestingMenu(menuOpts: MenuOpt[]) {
const menu: MenuItem[] = []
const marked: Record<number, MenuItem> = {}
}

建立索引

1
2
3
4
5
6
7
8
9
10
11
12
13
// ...

// building index mark
const menuItems = menuOpts.map(opt => {
const menuItem: MenuItem = cloneDeep(opt)
// init children
menuItem.children = []
// mark
marked[menuItem.id] = menuItem
return menuItem
})

// ...

3.2 完成嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ...

// nesting
menuItems.forEach(item => {
const { pid } = item
const parent = marked[pid]

// nested
if (parent) {
parent.children.push(item)
}
// top level menu item
else {
menu.push(item)
}
})

// ...

3.3 对菜单进行排序保证最终顺序

定义排序方法并完成排序

1
2
3
4
5
6
7
8
9
10
11
// ...

// sorting menu items
function sortMenuItems(menuItems: MenuItem[]) => {
menuItems.sort((a, b) => a.name > b.name ? 1 : -1)
menuItems.forEach(item => sorting(item.children))
}

sortMenuItems(menu)

// ...

4 完整代码

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
type MenuOpt = {
id: number
pid: number
name: string
}

type MenuItem = {
children: MenuItem[]
} & MenuOpt

const menuOpts: MenuOpt[] = [
{ id: 1, pid: 0, name: '节点1' },
{ id: 2, pid: 1, name: '节点1-1' },
{ id: 3, pid: 0, name: '节点2' },
{ id: 4, pid: 3, name: '节点2-1' },
{ id: 5, pid: 2, name: '节点1-1-1' },
{ id: 6, pid: 1, name: '节点1-2' },
]

function cloneDeep<
C extends { origin: any, copied: any }
>(origin: any, cache: C[] = []) {

if (typeof origin !== 'object' || origin === null) {
return origin
}

const hit = cache.find(c => c.origin === origin)

if (hit) { return hit.copied }

const copied = Array.isArray(origin) ? [] : {}

cache.push({ origin, copied } as C)

Object.keys(origin).forEach(key => {
copied[key] = cloneDeep(origin[key], cache)
})

return copied
}

function nestingMenu(menuOpts: MenuOpt[]) {
const menu: MenuItem[] = []

const marked: Record<number, MenuItem> = {}

// sorting menu items
const sorting = (menuItems: MenuItem[]) => {
menuItems.sort((a, b) => a.name > b.name ? 1 : -1)
menuItems.forEach(item => sorting(item.children))
}

// building index mark
const menuItems = menuOpts.map(opt => {
const menuItem: MenuItem = cloneDeep(opt)
// init children
menuItem.children = []
// mark
marked[menuItem.id] = menuItem
return menuItem
})

// nesting
menuItems.forEach(item => {
const { pid } = item
const parent = marked[pid]

// nested
if (parent) {
parent.children.push(item)
}
// top level menu item
else {
menu.push(item)
}
})

sorting(menu) // sorting

return menu
}

测试结果:

1. 基本概念

  • 插件也是JavaScript脚本、改变脚本文件的标示即可
  • 插件的加载与解析是优先于其他普通脚本
  • 插件不提供转码,为实现兼容性,推荐使用ES5进行编写
  • 插件能够获取到cc的全局对象、并且在Web环境中能获取window对象

2. 实现一个简单的Router功能的插件

2.1 功能需求

  • navigateTo
  • back
  • setInitScene

2.2 创建一个普通脚本文件

在资源管理器下,在路径为assets/scripts/plugins下创建router.js文件

然后点击刚刚创建的router.js文件,可以在属性面板中看到刚刚默认创建的脚本内容:

可以在面板找到,一个开关Import As Plugin, 勾选这个开关,即可将该文件更改为插件脚本。勾选后,面板多出来三个可选项,分别确认插件在三个环境中是否加载,对于通用插件,可以勾选native和web,按需选择即可。

2.3 编写脚本内容

由于脚本加载解析是在全局作用域下执行的,为了防止脚本加载造成的变量污染,推荐使用IIFE结构体和’use strict’关键字进行

1
2
3
4
5
6
7
/** 定义基础的IIFE结构体 */
!(function () {
'use strict'

// 代码内容

})()

定义一个globalThis全局上下文变量,方便后面使用:

1
2
3
4
5
6
// ...

/** 定义全局上下文变量globalThis */
var globalThis = this || globalThis|| window || self || cc

// ...

利用ES5语法定义CCRouter类,声明并初始化实例变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ...

/**
* @class CCRotuer
* @property {string} initScene
* @property {string[]} routes
*
* @constructor
* @param {string} [initScene]
*/
function CCRotuer(initScene) {
this.initScene = initScene
this.routes = initScene ? [initScene] : []
}

// ...

定义原型方法back,navigateTo

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
// ...

/**
* Router navigate to
* @param {string} scene
* @param {Function} [onLaunched]
*/
CCRotuer.prototype.navigateTo = function (scene, onLaunched) {
this.routes.push(scene)
cc.director.loadScene(scene, onLaunched)
}

/**
* Rotuer back
* @param {Function} [onLaunched]
*/
CCRotuer.prototype.back = function (onLaunched) {
// pop current
var _scene = this.routes.pop()

// pop previous or get init scene or current scene
var previousScene = this.routes.pop() || this.initScene || _scene

this.navigateTo(previousScene, onLaunched)
}

/**
* Router setInitScene
* @param {string} scene
*/
CCRotuer.prototype.setInitScene = function (scene) {
// get original init scene
var _initScene = this.initScene
// reassign init scene
this.initScene = scene
// replace all previous init scene in rotues stack
if (_initScene) {
for (var i = 0; i < this.routes.length; i++) {
if (this.routes[i] === _initScene) {
this.router[i] = scene
}
}
}
}

// ...

初始化router实例并注入到cc全局对象中

1
2
3
4
5
6
7
// ...
// 假设约定默认场景名为index
const router = new CCRouter('index')
// 注入到cc中
cc.router = router

// ...

完成脚本编写后,重新运行游戏,就可以在其他普通脚本中使用cc.router

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// in some component script
{
// ...
onLoad: function () {
this.node.on(cc.Node.EventType.TOUCH_START, function () {
cc.router.navigateTo('someSceneName', function () {
console.log('Scene `someSceneName` launched.')
})
})

this.someNode.on(cc.Node.EventType.TOUCH_START, function () {
cc.router.back(function () {
console.log('Back From `someComponentName` component.')
})
})
},
// ...
}

3. 完整插件代码

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
/**
* CCRotuer Plugin
*/
!(function () {
'use strict'

// define globalThis context variable
var globalThis = this || globalThis|| window || self || cc

/**
* CCRotuer
* @param {string} initScene
*/
function CCRotuer(initScene) {
this.initScene = initScene
this.routes = initScene ? [initScene] : []
}

/**
* Router setInitScene
* @param {string} scene
*/
CCRotuer.prototype.setInitScene = function (scene) {
// get original init scene
var _initScene = this.initScene
// reassign init scene
this.initScene = scene
// replace all previous init scene in rotues stack
if (_initScene) {
if (this.routes.length) {
for (var i = 0; i < this.routes.length; i++) {
if (this.routes[i] === _initScene) {
this.router[i] = scene
}
}
}
else {
this.routes = [scene]
}
}
}

/**
* Router navigate to
* @param {string} scene
* @param {Function} [onLaunched]
*/
CCRotuer.prototype.navigateTo = function (scene, onLaunched) {
this.routes.push(scene)
cc.director.loadScene(scene, onLaunched)
}


/**
* Rotuer back
* @param {Function} [onLaunched]
*/
CCRotuer.prototype.back = function (onLaunched) {
// pop current
var _scene = this.routes.pop()

// pop previous or get init scene or current scene
var previousScene = this.routes.pop() || this.initScene || _scene

this.navigateTo(previousScene, onLaunched)
}

// create router instance and inject to cc scope
cc.router = new CCRotuer('index')
})()

Simulating Event Loops

基本要素概念

  1. 宏任务(macro task)
  2. 微任务(micro task)
  3. 栈(stack)

宏任务类型

  • setTimeout
  • setInterval
  • setImmediate

微任务类型

  • Promise
  • Object.observe
  • MutationObserver

Node中的特殊类别

  • process.nextTick

利用代码模拟EventLoops的核心特性

1. 定义队列容器

1
2
3
4
5
6
7
8
9
/**
* 宏任务队列
*/
const macroTaskQueue = []

/**
* 微任务队列
*/
const microTaskQueue = []

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
/**
* 宏任务入列
* @param {Function} callback 回调方法
*/
function enqueueMacroTask(callback) {
macroTaskQueue.push(callback)
}

/**
* 宏任务出列
* @returns {Function}
*/
function dequeueMacroTask() {
return macroTaskQueue.shift()
}

/**
* 微任务入列
* @param {Function} callback 回调方法
*/
function enqueueMicroTask(callback) {
microTaskQueue.push(callback)
}

/**
* 微任务出列
* @returns {Function}
*/
function dequeueMicroTask() {
return microTaskQueue.shift()
}

4. 定义nextTick的执行方法

4.1 分别定义任务的执行方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 宏任务nextTick
*/
function nextTickMacro() {
const callback = dequeueMacroTask()
callback()
}

/**
* 微任务nextTick
*/
function nextTickMicro() {
const callback = dequeueMicroTask()
callback()
}

4.2 定义总体nextTick执行方法

由于micro task优先级高于macro task,所以在nextTick时候是要优先判断与处理micro task队列信息。

1
2
3
4
5
6
7
8
9
10
11
function nextTick() {
/* 优先判断micro task */
if (microTaskQueue.length) {
nextTickMicro()
return nextTick()
}
if (macroTaskQueue.length) {
nextTickMacro()
return nextTick()
}
}

执行测试

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
// 推入微任务
enqueueMacroTask(() => {

console.log('macro task A', 2)

// 推入微任务
enqueueMicroTask(() => {
console.log('micro task A', 3)
})

// 推入宏任务
enqueueMacroTask(() => {
console.log('macro task B', 5)
})

})

// 推入微任务
enqueueMicroTask(() => {

console.log('micro task B', 1)

// 推入宏任务
enqueueMacroTask(() => {

console.log('macro task C', 4)

})
})

// 模拟主栈为空,进行下一个事件轮训周期
nextTick()

执行的结果为:

1
2
3
4
5
micro task B 1
macro task A 2
micro task A 3
macro task C 4
macro task B 5

对比setTimeout、Promise的执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
setTimeout(() => {
console.log('setTimeout A', 2)
Promise.resolve().then(() => {
console.log('Promise A', 3)
})
setTimeout(() => {
console.log('setTimeout B', 5)
})
})

Promise.resolve().then(() => {
console.log('Promise B', 1)
setTimeout(() => {
console.log('setTimeout C', 4)
})
})

// 主栈为空,执行原生的event-loop

原生执行结果:

1
2
3
4
5
Promise B 1
setTimeout A 2
Promise A 3
setTimeout C 4
setTimeout B 5

1. 数据类型的拷贝

基本数据类型在heap中是独份存在的,所有拷贝的方式可以直接以赋值形式完成:

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
// number
let a = 1
let aa = a
a = 0
// aa === 1 -> true

// string
let b = 'abc'
let bb = b
b = 'aaa'
// bb === 'abc' -> true

// boolean
let isChecked = true
let checked = isChecked
isChecked = false
// checked === true -> true

// symbol
let s1 = Symbol('S1')
let s2 = s1
s1 = Symbol('SS')
// s2.toString() -> 'S1'

// BigInt
let n1 = BigInt(9999999)
let n2 = n1
n1 = BigInt(12312312312323)
// n2 === 9999999 -> true

// undefined
let u1 = undefined
let u2 = u1
u1 = 0
// u2 === undefined -> true

而对于引用类型如:Object、Array等,其值实际上为引用指针,所以当你以赋值方式进行复制操作时候,修改原始值,则会同步修改,让我们来了解下:

1.1 Array内容存在形式

我们来初始化一个数组arr:

1
const arr = [1, 2, 3]

假设内存中的值形式:

1
2
3
4
|- arr -| - 0x0000
|- 1 -| - 0x0001
|- 2 -| - 0x0002
|- 3 -| - 0x0003

从上面可知,arr变量指针存放在0x0000的地址中, 其余的基本数据都存放于0x0001-0x0003中,那么,arr数组的内容则变为:

1
2
3
4
5
6
7
8
9
10
11
12
13
arr -> 0x0000 = [e1, e2, e3]

// 等价于
[
e1 -> 0x0001,
e2 -> 0x0002,
e3 -> 0x0003,
]

// 等价于
[1, 2, 3]

// 最终就是我们表达中的 arr = [1, 2, 3]

好,接下来我们定义个新数组,并且将arr复制于新数组变量:

1
const arr2 = arr

此时发生的其实是将arr的引用地址(0x0000)复制与arr2,假设新的内存形式:

1
2
3
4
5
|- arr  -| - 0x0000
|- 1 -| - 0x0001
|- 2 -| - 0x0002
|- 3 -| - 0x0003
|- arr2 -| - 0x0003

那么,其实arr2所对应的内存值是arr的空间地址(0x0000),也就是:

1
2
3
4
5
6
7
8
9
arr2 -> 0x0003 -> 0x0000

// 等价于

arr2 -> 0x0000

// 等价于

arr2 === arr
1
2
3
4
5
|- arr  -| - 0x0000
|- 1 -| - 0x0001
|- 2 -| - 0x0002
|- 3 -| - 0x0003
|- 0x0000 -| - 0x0003 // 指针形式

所以可以知道arr和arr2都是同时指向的0x0000这块地址,所以对arr修改内部元素,则arr2也发生了变化,因为是同一个地址内容:

1
2
arr[0] = -1
arr2[0] === -1 // true

经过上面的修改后,新的内存值形式为:

1
2
3
4
5
|- arr  -| - 0x0000
|- -1 -| - 0x0001
|- 2 -| - 0x0002
|- 3 -| - 0x0003
|- 0x0000 -| - 0x0003 // 指针形式

由上面的分析可以知道引用类型内部子元素其实都为指针形式,所以并并不能直接复制进行拷贝,而是需要新的容器内部重新去添加值:

1
2
3
4
5
const arr2 = []

for(let i = 0; i < arr.length; i++) {
arr2[i] = arr[i]
}

对应的内存形式为:

1
2
3
4
5
|- arr  -| - 0x0000
|- 1 -| - 0x0001
|- 2 -| - 0x0002
|- 3 -| - 0x0003
|- arr2 -| - 0x0004

其中arr1的内存形式为:

1
2
3
4
5
6
7
arr -> 0x0000 -> [0x0001, 0x0002, 0x0003]
// 等价于
arr: [1, 2, 3]

arr2 -> 0x0003 -> [0x0001, 0x0002, 0x0003]
// 等价于
arr2: [1, 2, 3]

当我们对arr,arr2对任意元素进行修改时候,由于arr和arr2值并非为同一块地址空间,所以互不影响:

1
2
arr[0] = 5
arr2[2] = 5

内存形式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
|- arr  -| - 0x0000
|- 1 -| - 0x0001
|- 2 -| - 0x0002
|- 3 -| - 0x0003
|- arr2 -| - 0x0004
|- 5 -| - 0x0005

arr -> 0x0000 -> [0x0005, 0x0002, 0x0003 ]
arr2 -> 0x0004 -> [0x0001, 0x0002, 0x0005 ]

// 等价于

arr ==> [5, 1, 2]
arr2 ==> [1, 2, 5]

2.2 Object内容存在形式

对于Object类型其实道理和Array相同,毕竟Array是特殊的Object,Object中的属性值也是指向地址

1
2
3
4
const o = {
a: 1,
b; 2
}

假设内存形式:

1
2
3
|-  o   -| - 0x0000
|- 1 -| - 0x0001
|- 2 -| - 0x0002

所o的指针形式可以这样来理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
o -> 0x0000 = {}
a -> 0x0001 = 1
b -> 0x0002 = 2

// 等价于
o = {
a: -> 0x0001,
b: -> 0x0002,
}

// 等价于
o = {
a: 1,
b: 2,
}

对于引用类型的拷贝基本原则是:

  1. 需要定义新的同类型值来充当容器,从而分离操作地址空间
  2. 将新拷贝的值属性完全指待拷贝的值(仅假设其值为基本数据类型)

2 Array的常用拷贝方式

1
2
const origin = [1, 2, 3]
const copied = []

2.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
// for
for (let i = 0; i < origin.length; i++) {
copied.push(origin[i])
}

// for-in
for (let idx in origin) {
copied.push(origin[idx])
}

// for-of
for (let item of origin) {
copied.push(item)
}

// forEach
origin.forEach(item => {
copied.push(item)
})

// map
copied = origin.map(item => item)

// filter
copied = origin.filter(() => true)

// 扩展操作符
copied = [...origin]

2.2 快速创建副本

1
2
3
4
5
6
7
8
// concat
copied = origin.concat()

// slice
copied = origin.slice()

// Array.from
copied = Array.form(origin)

3 Object常用拷贝方式

1
2
const origin = { a: 1, b: 'hello' }
const copied = {}

3.1 偷懒的parse、stringify

1
copied = JSON.parse(JSON.stringify(origin))

这种方式虽然简单快速,但是有许多的问题:

  1. 属性必须为enumerable,否则stringify将忽略
  2. 属性必须为非undefined,否则stringify将忽略
  3. 属性必须为非function,否则stringify将忽略
  4. 不支持高级属性名类型,例如Symbol等,stringify将忽略
  5. 不支持循环引用,stringify将报错
  6. 性能问题,parse的解析过程

这个方式在简单、数据结构单纯简单的时候可以轻量使用,从而快速实现对象拷贝

3.2 遍历属性拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// for-in
for (let key in origin) {
copied[key] = origin[key]
}

// for-of
for (pair of origin.entries()) {
const [key, value] = pair
copied[key] = value
}

// Object.keys
Object.keys(origin).forEach(key => copied[key] = origin[key])

// Object.entries
Object.entries(origin).forEach(pair => {
const [key, value] = pair
copied[key] = value
})

// 展开操作符
copied = { ...origin }

注意:以上的遍历方式都将忽略不可枚举及继承的属性

3.3 Object.assign方法进行拷贝

通过Object.assign方式,将origin属性重新赋值到新的对象中,完成拷贝效果,而该方式也会和以上方式一样,忽略不可枚举的属性:

1
copied = Object.assign({}, origin)

3.4 Object.getOwnPropertyNames进行拷贝

对于不可枚举的属性,有时候我们也想要完全拷贝,则使用该方法来实现拷贝:

1
2
3
Object.getOwnPropertyNames(origin).forEach(key => {
copied[key] = origin[key]
})

但该方式会使得原来origin中不可枚举的属性配置丢失,所以需要结合Object.getOwnPropertyDescriptorObject.defineProperty来进行拷贝,对上面代码进行修改:

1
2
3
4
5
6
Object.getOwnPropertyNames(origin).forEach(key => {
// 获取当前key在origin中对属性配置描述
const descriptor = Object.getOwnPropertyDescriptor(origin, key)
// 定义copied的新值
Object.defineProperty(copied, key, descriptor)
})

3.5 利用defineProperties,getOwnPropertyDescriptors拷贝

对于以上的方式,我们可以再利用Object.definePropertiesObject.getOwnPropertyDescriptors进行批量属性拷贝,从而精简代码:

1
2
const originPropDescs = Object.getOwnPropertyDescriptors(origin)
Object.defineProperties(copied, originPropDescs)

该方式于3.4中的效果相同,也是支持获取不可枚举属性的变量名,但此方法去除了遍历添加的步骤,直接批量完成拷贝动作

4 多层级的引用类型拷贝

以上的Array,Object拷贝方式主要实现为仅一层属性的浅拷贝,而当属性的值为引用类型的时候,需要递归以上拷贝步骤进行深层拷贝,所以这边主要是2个关键点:

  1. 基本数据类型、null直接赋值拷贝
  2. Object, Array等数据类型进行递归拷贝

4.1 Array的多层级拷贝

这边先假设Array中存在的引用类型仍为Array,后期对于其他的引用类型拷贝如Object则整合进去就行

1
2
3
4
5
6
7
8
9
10
function deepCloneArray(origin) {
const copied = []
origin.forEach(item => {
copied.push(Array.isArray(item)
? deepCloneArray(item) // 递归
: item
)
})
return copied
}

4.2 Object的多层级拷贝

这边进行4.1一样的假设,先假设Object属性中的引用类型任为Object

1
2
3
4
5
6
7
8
9
10
11
12
const isObject = o => Object.prototype.toString.call(o) === '[object Object]'
function deepCloneObject(origin) {
const res = {}
for (const key in origin) {
const value = origin[key]
res[key] = isObject(value)
? deepCloneObject(value)
: value
}

return res
}

4.3 整合Object和Array深拷贝逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 深拷贝函数
function deepClone(origin) {
if (typeof origin !== 'object' || origin === null)) {
return origin
}

const copied = Array.isArray(origin) ? [] : {}

Object.keys(origin).forEach(key => {
copied[key] = deepClone(origin[key])
})

return copied
}

4.4 关于循环引用的处理

4.3中的深拷贝函数中,当出现循环引用则会发生无限的递归调用,从而发生栈溢出错误,需要添加对循环引用的判断和处理,处理的关键逻辑为:

  1. 创建一个cache数组用来存放引用类型值
  2. 每次递归处理前先查找缓存
  3. 发现缓存直接赋值、未发现缓存则添加缓存,并递归

整理代码为:

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
// 深拷贝函数
function deepClone(origin, cache = []) {
if (typeof origin !== 'object' || origin === null) {
return origin
}

// 查找缓存
const hit = cache.find(c => c.origin === origin)

// 返回缓存中的copied
if (hit) { return hit.copied }

// 根据数据类别创建副本
const copied = Array.isArray(origin) ? [] : {}

// 存入缓存
cache.push({ origin, copied })

// 拷贝属性
Object.keys(origin).forEach(key => {
copied[key] = deepClone(origin[key], cache)
})

return copied
}

1.更新Homebrew源信息:

1
brew update

2.搜索MySQL相关的Homebrew包信息

1
brew search mysql

搜索结果如下:

1
2
3
4
5
6
7
8
9
10
==> Formulae
automysqlbackup mysql-client@5.7 mysql-search-replace mysqltuner
mysql mysql-connector-c++ mysql-utilities
mysql++ mysql-connector-c++@1.1 mysql@5.6
mysql-client mysql-sandbox mysql@5.7

==> Casks
homebrew/cask/mysql-connector-python homebrew/cask/navicat-for-mysql
homebrew/cask/mysql-shell homebrew/cask/sqlpro-for-mysql
homebrew/cask/mysql-utilities

3.安装Homebrew默认的最新版本MySQL

1
brew install mysql

以上命令则直接安装默认的版本,为最新的MySQL

4.安装指定版本,这边以5.7为例:

1
brew install mysql@5.7

在安装非默认的版本时候,在环境变量中是找不到mysql命令的,这边需要手动加入包的bin目录到PATH中,有两种方式实现:

方式1:利用brew link

1
brew link mysql@5.7 --force

方式2:利用.bashrc等方式进行PATH内容增补,这边以zsh为例

1
echo 'export PATH="/usr/local/opt/mysql@5.7/bin:$PATH"' >> ~/.zshrc

5.启动MySQL服务

1
brew services start mysql

最终可以按需配置MySQL,包括用户名、密码、端口等信息。

1. 替换清华update源:

https://mirror.tuna.tsinghua.edu.cn/help/homebrew/

替换:

1
2
3
4
5
6
7
git -C "$(brew --repo)" remote set-url origin https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git

git -C "$(brew --repo homebrew/core)" remote set-url origin https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-core.git

git -C "$(brew --repo homebrew/cask)" remote set-url origin https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-cask.git

brew update

复原:

1
2
3
4
5
6
7
git -C "$(brew --repo)" remote set-url origin https://github.com/Homebrew/brew.git

git -C "$(brew --repo homebrew/core)" remote set-url origin https://github.com/Homebrew/homebrew-core.git

git -C "$(brew --repo homebrew/cask)" remote set-url origin https://github.com/Homebrew/homebrew-cask.git

brew update

2. 替换二进制源

https://mirrors.tuna.tsinghua.edu.cn/help/homebrew-bottles/

临时替换:

1
export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles

长期替换:

1
2
3
echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles' >> ~/.bash_profile

source ~/.bash_profile