国内服务器中转流量

firewalld 流量转发

firewalld是CentOS7/8默认的防火墙前端软件,绝大多数主机商提供的镜像都已经安装。

判断防火墙是否已经开启

1
firewall-cmd --state

如果状态不是 running,使用下面命令安装或开启防火墙

1
2
3
yum install -y firewalld         # 安装
systemctl enable firewalld # 开机启动
systemctl start firewalld # 开启防火墙

配置转发。假设你将国内服务器9090端口流量转发到国外vps的9091端口,转发命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.conf
sysctl -p # 使配置生效

# 启用伪装 --permanent 配置永久生效
firewall-cmd --permanent --add-masquerade

# 查看伪装是否生效
firewall-cmd --permanent --query-masquerade

# 开启端口
firewall-cmd --permanent --add-port=8080/tcp
firewall-cmd --permanent --add-port=8080/udp

# 配置转发
firewall-cmd --permanent --add-forward-port=port=9090:proto=tcp:toaddr=xxx.xx.xxx.xx:toport=9091
firewall-cmd --permanent --add-forward-port=port=9090:proto=udp:toaddr=xxx.xx.xxx.xx:toport=9091

# 重启防火墙生效
firewall-cmd --reload

注意:

云服务器需要在控制页面开启端口

ssr 等工具,ip和端口填写为国内转发服务器,链接时间和加密方式,和外网服务器配置相同

inotify + rsync 实现数据本地实时备份

实现过程

inotify 工具监听文件改变,将改变的文件使用 rsync 同步

inotify介绍

inotify 是一种强大的、细粒度的、异步文件系统监控机制,它满足各种各样的文件监控需要,可以监控文件系统的访问属性、读写属性、权限属性、创建删除、移动等操作,也可以监控文件发生的一切变化。

inotify-tools 是一个C库和一组命令行的工作提供Linux下inotify的简单接口。

inotify-tools 中包含 inotifywait 和 inotifywatch 两个命令

inotifywatch 命令用于收集关于被监控的文件系统的统计数据,包括每个inotify事件发生多少次。

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
[root@backup ~]# inotifywait --help
inotifywait 3.14
Wait for a particular event on a file or set of files.
Usage: inotifywait [ options ] file1 [ file2 ] [ file3 ] [ ... ]
Options:
-h|--help Show this help text.
@<file> Exclude the specified file from being watched.
--exclude <pattern>
Exclude all events on files matching the
extended regular expression <pattern>.指定排除部分文件
--excludei <pattern>
Like --exclude but case insensitive.(同上,排除且忽略大小写)
-m|--monitor Keep listening for events forever. Without
this option, inotifywait will exit after one
event is received.(持续监听)
-d|--daemon Same as --monitor, except run in the background
logging events to a file specified by --outfile.
Implies --syslog.(daemon模式)
-r|--recursive Watch directories recursively.(递归子目录)
--fromfile <file>
Read files to watch from <file> or '-' for stdin.
-o|--outfile <file>
Print events to <file> rather than stdout. (将事件输出到文件,而不是屏幕)
-s|--syslog Send errors to syslog rather than stderr.
-q|--quiet Print less (only print events).(打印事件)
-qq Print nothing (not even events).(不打印事件)
--format <fmt> Print using a specified printf-like format
string; read the man page for more details. (设置打印格式%T时间;%w触发事件文件所在绝对路径;%f触发事件文件名称;%e触发的事件名称;)
--timefmt <fmt> strftime-compatible format string for use with
%T in --format string.(指定输出内容,相当于将时间赋值给%T)
-c|--csv Print events in CSV format.
-t|--timeout <seconds>
When listening for a single event, time out after
waiting for an event for <seconds> seconds.
If <seconds> is 0, inotifywait will never time out.
-e|--event <event1> [ -e|--event <event2> ... ]
Listen for specific event(s). If omitted, all events are
listened for.(指定要监听的事件,多个事件用逗号隔开)

Exit status:
0 - An event you asked to watch for was received.
1 - An event you did not ask to watch for was received
(usually delete_self or unmount), or some error occurred.
2 - The --timeout option was given and no events occurred
in the specified interval of time.

Events: (事件)
access file or directory contents were read
modify file or directory contents were written
attrib file or directory attributes changed
close_write file or directory closed, after being opened in
writeable mode
close_nowrite file or directory closed, after being opened in
read-only mode
close file or directory closed, regardless of read/write mode
open file or directory opened
moved_to file or directory moved to watched directory
moved_from file or directory moved from watched directory
move file or directory moved to or from watched directory
create file or directory created within watched directory
delete file or directory deleted within watched directory
delete_self file or directory was deleted
unmount file system containing file or directory unmounted

示例 监听/backup/目录下所有文件和目录的增删改操作

1
inotifywait -mrq -e 'create,delete,close_write,attrib,moved_to' --timefmt '%Y-%m-%d %H:%M' --format '%T %w%f %e' /backup/

rsync介绍

rsync是可以实现增量备份的工具。配合任务计划,rsync能实现定时或间隔同步,配合inotify或sersync,可以实现触发式的实时同步。

Rsync的命令格式可以为以下六种:

1
2
3
4
5
6
rsync [OPTION]... SRC DEST
rsync [OPTION]... SRC [USER@]HOST:DEST
rsync [OPTION]... [USER@]HOST:SRC DEST
rsync [OPTION]... [USER@]HOST::SRC DEST
rsync [OPTION]... SRC [USER@]HOST::DEST
rsync [OPTION]... rsync://[USER@]HOST[:PORT]/SRC [DEST]

对应于以上六种命令格式,rsync有六种不同的工作模式:

  1)拷贝本地文件。当SRC和DES路径信息都不包含有单个冒号”:”分隔符时就启动这种工作模式。如:rsync -a /data /backup

  2)使用一个远程shell程序(如rsh、ssh)来实现将本地机器的内容拷贝到远程机器。当DST路径地址包含单个冒号”:”分隔符时启动该模式。如:rsync -avz *.c foo:src

  3)使用一个远程shell程序(如rsh、ssh)来实现将远程机器的内容拷贝到本地机器。当SRC地址路径包含单个冒号”:”分隔符时启动该模式。如:rsync -avz foo:src/bar /data

  4)从远程rsync服务器中拷贝文件到本地机。当SRC路径信息包含”::”分隔符时启动该模式。如:rsync -av root@172.16.78.192::www /databack

  5)从本地机器拷贝文件到远程rsync服务器中。当DST路径信息包含”::”分隔符时启动该模式。如:rsync -av /databack root@172.16.78.192::www

  6)列远程机的文件列表。这类似于rsync传输,不过只要在命令中省略掉本地机信息即可。如:rsync -v rsync://172.16.78.192/www

rsync参数的具体解释如下:

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
-v, --verbose 详细模式输出
-q, --quiet 精简输出模式
-c, --checksum 打开校验开关,强制对文件传输进行校验
-a, --archive 归档模式,表示以递归方式传输文件,并保持所有文件属性,等于-rlptgoD
-r, --recursive 对子目录以递归模式处理
-R, --relative 使用相对路径信息
-b, --backup 创建备份,也就是对于目的已经存在有同样的文件名时,将老的文件重新命名为~filename。可以使用--suffix选项来指定不同的备份文件前缀。
--backup-dir 将备份文件(如~filename)存放在在目录下。
-suffix=SUFFIX 定义备份文件前缀
-u, --update 仅仅进行更新,也就是跳过所有已经存在于DST,并且文件时间晚于要备份的文件。(不覆盖更新的文件)
-l, --links 保留软链结
-L, --copy-links 想对待常规文件一样处理软链结
--copy-unsafe-links 仅仅拷贝指向SRC路径目录树以外的链结
--safe-links 忽略指向SRC路径目录树以外的链结
-H, --hard-links 保留硬链结
-p, --perms 保持文件权限
-o, --owner 保持文件属主信息
-g, --group 保持文件属组信息
-D, --devices 保持设备文件信息
-t, --times 保持文件时间信息
-S, --sparse 对稀疏文件进行特殊处理以节省DST的空间
-n, --dry-run现实哪些文件将被传输
-W, --whole-file 拷贝文件,不进行增量检测
-x, --one-file-system 不要跨越文件系统边界
-B, --block-size=SIZE 检验算法使用的块尺寸,默认是700字节
-e, --rsh=COMMAND 指定使用rsh、ssh方式进行数据同步
--rsync-path=PATH 指定远程服务器上的rsync命令所在路径信息
-C, --cvs-exclude 使用和CVS一样的方法自动忽略文件,用来排除那些不希望传输的文件
--existing 仅仅更新那些已经存在于DST的文件,而不备份那些新创建的文件
--delete 删除那些DST中SRC没有的文件
--delete-excluded 同样删除接收端那些被该选项指定排除的文件
--delete-after 传输结束以后再删除
--ignore-errors 及时出现IO错误也进行删除
--max-delete=NUM 最多删除NUM个文件
--partial 保留那些因故没有完全传输的文件,以是加快随后的再次传输
--force 强制删除目录,即使不为空
--numeric-ids 不将数字的用户和组ID匹配为用户名和组名
--timeout=TIME IP超时时间,单位为秒
-I, --ignore-times 不跳过那些有同样的时间和长度的文件
--size-only 当决定是否要备份文件时,仅仅察看文件大小而不考虑文件时间
--modify-window=NUM 决定文件是否时间相同时使用的时间戳窗口,默认为0
-T --temp-dir=DIR 在DIR中创建临时文件
--compare-dest=DIR 同样比较DIR中的文件来决定是否需要备份
-P 等同于 --partial
--progress 显示备份过程
-z, --compress 对备份的文件在传输时进行压缩处理
--exclude=PATTERN 指定排除不需要传输的文件模式
--include=PATTERN 指定不排除而需要传输的文件模式
--exclude-from=FILE 排除FILE中指定模式的文件
--include-from=FILE 不排除FILE指定模式匹配的文件
--version 打印版本信息
--address 绑定到特定的地址
--config=FILE 指定其他的配置文件,不使用默认的rsyncd.conf文件
--port=PORT 指定其他的rsync服务端口
--blocking-io 对远程shell使用阻塞IO
-stats 给出某些文件的传输状态
--progress 在传输时现实传输过程
--log-format=formAT 指定日志文件格式
--password-file=FILE 从FILE中得到密码
--bwlimit=KBPS 限制I/O带宽,KBytes per second
-h, --help 显示帮助信息

示例1 将/etc/fstab拷贝到/tmp目录下。

1
rsync /etc/fstab /tmp

示例2 将/etc/cron.d目录拷贝到/tmp下。

1
rsync -r /etc/cron.d /tmp

说明:为了保持文件夹一致,可以将路径写为

1
rsync -avr /src/ /backup/

这样src下面的所有目录会被备份到 backup目录下面,而不会在backup文件夹下面创建src文件夹

创建同步脚本

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
#!/bin/bash

fn() {

src=$1 # 需要同步的源路径
des=$2 # 目标路径

cd ${src} #定位到源文件下面

# 此方法中,由于rsync同步的特性,这里必须要先cd到源目录,inotify再监听 ./ 才能rsync同步后目录结构一致

# 把监控到有发生更改的"文件路径列表"循环
/usr/bin/inotifywait -mrq --format '%Xe %w%f' -e modify,create,delete,attrib,close_write,move ./ | while read file; do
INO_EVENT=$(echo $file | awk '{print $1}') # 把inotify输出切割 把事件类型部分赋值给INO_EVENT
INO_FILE=$(echo $file | awk '{print $2}') # 把inotify输出切割 把文件路径部分赋值给INO_FILE
echo "-------------------------------$(date)------------------------------------"
echo $file
#增加、修改、写入完成、移动进事件
#增、改放在同一个判断,因为他们都肯定是针对文件的操作,即使是新建目录,要同步的也只是一个空目录,不会影响速度。
if [[ $INO_EVENT =~ 'CREATE' ]] || [[ $INO_EVENT =~ 'MODIFY' ]] || [[ $INO_EVENT =~ 'CLOSE_WRITE' ]] || [[ $INO_EVENT =~ 'MOVED_TO' ]]; then # 判断事件类型
echo 'CREATE or MODIFY or CLOSE_WRITE or MOVED_TO'
# INO_FILE变量代表路径 -c校验文件内容
rsync -avzcR $(dirname ${INO_FILE}) ${des}

#上面的rsync同步命令 源是用了$(dirname ${INO_FILE})变量 即每次只针对性的同步发生改变的文件的目录
#只同步目标文件的方法在生产环境的某些极端环境下会漏文件 现在可以在不漏文件下也有不错的速度 做到平衡)
#然后用-R参数把源的目录结构递归到目标后面 保证目录结构一致性
fi
#删除、移动出事件
if [[ $INO_EVENT =~ 'DELETE' ]] || [[ $INO_EVENT =~ 'MOVED_FROM' ]]; then
echo 'DELETE or MOVED_FROM'
#并加上--delete来删除目标上有而源中没有的文件,这里不能做到指定文件删除,如果删除的路径越靠近根,则同步的目录月多,同步删除的操作就越花时间。
rsync -avzr --delete $(dirname ${INO_FILE}) ${des}
fi
#修改属性事件 指 touch chmod chown等操作
if [[ $INO_EVENT =~ 'ATTRIB' ]]; then
echo 'ATTRIB'
if [ ! -d "$INO_FILE" ]; then # 如果修改属性的是目录 则不同步,因为同步目录会发生递归扫描,等此目录下的文件发生同步时,rsync会顺带更新此目录。
rsync -avzcR $(dirname ${INO_FILE}) ${des}
fi
fi
done
}

fn /home/supreme/Workspace/ /media/supreme/yes/Workspace/ & #加上&符号表示在后台执行
fn 源路径2 目标路径2 & #加上&符号表示在后台执行

添加开机启动项

将写好的脚本放到指定目录中

1
sudo cp ./sync.sh /usr/sbin

创建一个服务文件 sync.service

systemd有系统和用户区分;系统(/user/lib/systemd/system/)、用户(/etc/lib/systemd/user/).

一般系统管理员手工创建的单元文件建议存放在/etc/systemd/system/目录下面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Unit]
Description= 服务的简单描述
Documentation= 服务文档
# Before、After:定义启动顺序。Before=xxx.service,代表本服务在xxx.service启动之前启动。After=xxx.service,代表本服务在xxx.service之后启动。
After=network.target

[Service]
# systemd认为当该服务进程fork,且父进程退出后服务启动成功。对于常规的守护进程 daemon
# 除非你确定此启动方式无法满足需求,使用此类型启动即可。使用此启动类型应同时指定 PIDFile=,以便systemd能够跟踪服务的主进程。
Type=forking

ExecStart=/usr/sbin/sync.sh

[Install]
# 单元被允许运行需要的弱依赖性单元,WantBy从Want列表获得依赖信息。
WantedBy=multi-user.target

添加开机启动并重载服务

1
2
3
4
5
6
7
8
sudo systemctl daemon-reload

sudo systemctl enable sync.service

sudo systemctl start sync.service
sudo systemctl stop sync.service
sudo systemctl reload sync.service

Docker 部署Jira + 破解

创建必要目录

1
mkdir -p docker-compose/jira

创建 docker-compose.yml

  • 修改映射路径为当前文件夹下的相对路径
  • JIRA_PROXY_NAME = 域名
  • JIRA_PROXY_PORT = 外部端口
  • JIRA_PROXY_SCHEME = 协议
  • POSTGRES_PASSWORD 修改数据库密码
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
services:
jira_db:
image: mysql:8.0
container_name: jira_mysql
environment:
MYSQL_ROOT_PASSWORD: w.521@@ong.COM
MYSQL_DATABASE: jira
MYSQL_USER: jira
MYSQL_PASSWORD: w.521@@ong.COM
ports:
- 19306:3306
volumes:
- ./data:/var/lib/mysql
networks:
- jira_network # 将 mysql 服务连接到 mynetwork 网络
restart: always
jira:
image: atlassian/jira-software
container_name: jira
ports:
- 19140:8080
volumes:
- ./jiraVolume:/var/atlassian/application-data/jira
- ./lib:/opt/atlassian/jira/lib #驱动的目录,需要映射
networks:
- jira_network # 将 jira 服务连接到 mynetwork 网络
restart: unless-stopped
volumes:
mysql-data:
driver: local
jiraVolume:
driver: local
networks:
jira_network:
# 定义名为 mynetwork 的网络
driver: bridge # 使用默认的 bridge 驱动

破解

  • 下载 atlassian-agent.jar 文件压缩包,并解压

  • atlassian-agent.jar 复制到容器内 docker cp ./atlassian-agent.jar jira容器名称:/opt/jira

  • docker-compose/jira 目录下执行 docker-compose up 启动动容器

  • 进入容器 docker exec -it jira 容器名称 /bin/bash

  • 修改环境变量 cd /opt/jira/bin vi setenv.sh

export JAVA_OPTS 修改为 export JAVA_OPTS=”-javaagent:/opt/jira/atlassian-agent.jar ${JAVA_OPTS}”

  • 重启容器, 在日志中可以看到 ========= agent working ========= 字样表示成功

  • 再次进入容器 执行 java -jar atlassian-agent.jar -p jsm -m aaa@bbb.com -n my_name -o https://zhile.io -s ABCD-1234-EFGH-5678

特别注意 -p 参数设置,通过 java -jar atlassian-agent.jar 查看使用帮助,每种产品有不同的标识

-m 邮箱任意填写
-n 名称任意填写
-o 网址任意填写
-s server id 再安装时查看

复制执行命令之后产生的激活码,复制到激活码的窗口完成激活。

FAQ

  • 连接时报错 Could not find driver with class name: com.mysql.cj.jdbc.Driver

    下载 对应版本的驱动

    将驱动放在 jira 安装目录(或 docker 映射的目录)/lib 下面

    重启 jira 服务

③悄无声息的扩展-装饰者模式

简单说装饰可以让你不用修改底层代码给对象赋予新的职责。

从分离改变,更进一步

看了前两个设计模式,相信你一定感觉到继承只能解决静态时对类的扩展,如果想动态的对类的行为扩展就需要用到组合。

既然我们已经可以用策略模式分离改变的部分,还有什么做不到的么? 看看下面一个问题:

  • 奶茶店有几十种品种的饮料,他们都需要继承自一个抽象类 Beverage,因为每种饮料有自己的产品说明,并且各自实现了一个计算金额的方法 cost。
  • 每种奶茶除了自己特有的配料外,还可以额外付费添加配料,比如加两份的珍珠,加一份椰果,并且需要计算总价。

如果想枚举出店内的每一种产品是不现实的,那时非常庞大的一个排列组合,因为不可能知道客户要加那些配料。并且严重的违反了设计中的两个原则:

1
2
3
4
5
6
7
8
9
abstract class BeverageAbstract {
description = "some description";
cost() {}
}
// 果茶加牛奶
class FruitTeaWithMilk extends BeverageAbstract {}
// 柠檬茶加两份珍珠
class LemonTeaWith2Pearls extends BeverageAbstract {}
//...

既然不能枚举考虑是不是应该有一个统一的 Beverage 类,用于实现抽象类 BeverageAbstract,并且把所有的配料都添加在 Beverage 上,并记录配料的数量。
子类会调用父类 cost 方法计算所有配料,并加上自己品种的价格。

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
abstract class BeverageAbstract {
description = "some description";
cost() {}
}
class Beverage extends BeverageAbstract {
milk = 2;
milkCount = 0;
setMilk(count: number) {
this.milkCount = count;
}
coffee = 5;
coffeeCount = 0;
setCoffee(count: number) {
this.coffeeCount = count;
}
cost() {
let total = 0;
if (this.milkCount > 0) {
total += this.milkCount * this.milk;
}
if (this.coffeeCount > 0) {
total += this.coffeeCount * this.coffee;
}
return total;
}
}

class FruitTea extends Beverage {
cost() {
return 10 + super.cost();
}
}

const fruitTea = new FruitTea();
fruitTea.setMilk(2);
console.log(fruitTea.cost());

const fruitTea2 = new FruitTea();
fruitTea2.setMilk(1);
console.log(fruitTea.cost());

这样的设计还有一些问题:

  • 并不是所有的配料都需要继承,每种饮料都有自己特有的配料。
  • 一但需要新的配料或新的品种或价钱的改变,就需要修改父类。
  • 当有新的品种出现的时候,他可能继承了不必要的方法。

定义装饰者

设计类的一个原则是 ❤‍🔥 类应该对扩展开放,对修改关闭,也就是类设计中提到的开放关闭原则

开放且关闭并不冲突,想想观察中模式中的案例,可以通过调用类的方法添加观察者,而不改变原有的类的方法。并不需要每一个类都遵循开放关闭原则,避免过度设计,只需要针对可能经常会发生变化的类应用开发-关闭原则。

装饰者模式:动态的将责任附加到对象上,若要扩展功能,装饰者提供了比继承更有弹性的替代方案

对于上面的问题,按以下的方式思考:

  • 我们已经有一个果茶类 FruitTea
  • 需要加两份额外的牛奶,用两个牛奶配料的类去修饰它
  • 需要加一份额外的咖啡,用一个咖啡配料的类去修饰它
  • 依赖修饰者的 cost 方法计算价格
  • 在编码的时候为了明确修饰者和被修饰者的关系,需要让修饰者和被修饰者继承同样的类或实现相同的接口,因为被修饰的类仍然被视作原有的类,它的属性以及方法的作用不应该发生变化,只是扩展了方法实现。
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
interface BeverageInterface {
description: string;
cost(): number;
getDescription(): string;
}
class MilkDecorator implements BeverageInterface {
beverage: BeverageInterface;
description = "MilkDecorator";
constructor(beverage: BeverageInterface) {
this.beverage = beverage;
}
getDescription() {
return this.description;
}
cost() {
return 2 + this.beverage.cost();
}
}
class CoffeeDecorator implements BeverageInterface {
beverage: BeverageInterface;
description = "CoffeeDecorator";
constructor(beverage: BeverageInterface) {
this.beverage = beverage;
}
getDescription() {
return this.description;
}
cost() {
return 5 + this.beverage.cost();
}
}
class FruitTea implements BeverageInterface {
description = "FruitTea";
getDescription() {
return this.description;
}
cost() {
return 10;
}
}

let fruitTea = new FruitTea();
// 原价fruitTea
console.log(fruitTea.cost());
// 添加配料一份牛奶 一份咖啡
fruitTea = new MilkDecorator(fruitTea);
fruitTea = new CoffeeDecorator(fruitTea);
console.log(fruitTea.cost());

现在已经分离了装饰着对象,但是使用装饰者模式是基于一下几个前提:

  • 被修饰的对象是可以抽象的,也就是说被修饰的对象不会轻易改变,这样针对它的修饰类才有意义
  • 修饰对象是不关心外部状态的,它只关心被修饰的对象,因为你想让他控制修饰链中的每个节点,需要更好的设计。
  • 被装饰的对象可能拥有特定的类型,在使用装饰的时候需要小心,避免功能或类型丢失。

与 ES6 修饰器的区别

ES6 提供修饰器方提案,在 babel 转译后支持,可以修饰类或类的属性和方法,但并不是一种设计模式。

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
interface BeverageInterface {
description: string;
cost(): number;
getDescription(): string;
}
function milkDecorator(target: () => number): () => number {
return () => 2 + target();
}

class FruitTea implements BeverageInterface {
description = "FruitTea";
getDescription() {
return this.description;
}
cost() {
return 10;
}
}

class FruitTeaWithMilk extends FruitTea {
@milkDecorator
cost(): number {
return super.cost();
}
}
let fruitTea = new FruitTeaWithMilk();
console.log(fruitTea.cost());
  • 继承属于扩展形式之一,但不见得是达到弹性设计的最佳方式。
  • 在我们的设计中,应该允许行为可以被扩展,而无须修改现有的代码。
  • 组合和委托可用于在运行时动态地加上新的行为。
  • 除了继承,装饰者模式也可以让我们扩展行为。
  • 装饰者模式意味着一群装饰者类,这些类用来包装具体组件。
  • 开放一关闭原则
  • 装饰者类反映出被装饰的组件类型(事实上,他们具有相同的类型,都经过接口或继承实)
  • 装饰者可以在被装饰者的行为前面与/或后面加上自己的行为,甚至将被装饰者的行为整个取代掉,而达到特定的目的。
  • 你可以用无数个装饰者包装一个组件。
  • 装饰者一般对组件的客户是透明的,除非客户程序依赖于组件的具体类型。
  • 装饰者会导致设计中出现许多小对象,如果过度使用,会让程序变得很复杂。

MySQL DQL查询语言

DQL (Data Query Language)数据查询语言;

基本查询

1、查询的结果集 是一个虚拟表
2、select 查询列表 类似于System.out.println(打印内容);

select后面跟的查询列表,可以有多个部分组成,中间用逗号隔开
例如:select 字段1,字段2,表达式 from 表;

System.out.println()的打印内容,只能有一个。

3、执行顺序

① from子句
② select子句

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

USE myemployees;

#一、查询常量
SELECT 100 ;

#二、查询表达式
SELECT 100%3;

#三、查询单个字段
SELECT `last_name` FROM `employees`;

#四、查询多个字段
SELECT `last_name`,`email`,`employee_id` FROM employees;

#五、查询所有字段
SELECT * FROM `employees`;


#F12:对齐格式
SELECT
`last_name`,
`first_name`,
`last_name`,
`commission_pct`,
`hiredate`,
`salary`
FROM
employees ;

#六、查询函数(调用函数,获取返回值)
SELECT DATABASE();
SELECT VERSION();
SELECT USER();

#七、起别名
#方式一:使用as关键字

SELECT USER() AS 用户名;
SELECT USER() AS "用户名";
SELECT USER() AS '用户名';

SELECT last_name AS "姓 名" FROM employees;


#方式二:使用空格


SELECT USER() 用户名;
SELECT USER() "用户名";
SELECT USER() '用户名';

SELECT last_name "姓 名" FROM employees;


#八、+的作用
-- 需求:查询 first_name 和last_name 拼接成的全名,最终起别名为:姓 名

#方案1:使用+ pass×
SELECT first_name+last_name AS "姓 名"
FROM employees;



#方案2:使用concat拼接函数

SELECT CONCAT(first_name,last_name) AS "姓 名"
FROM employees;



/*

Java中+的作用:
1、加法运算
100+1.5 'a'+2 1.3+'2'

2、拼接符
至少有一个操作数为字符串
"hello"+'a'


mysql中+的作用:
1、加法运算

①两个操作数都是数值型
100+1.5

②其中一个操作数为字符型
将字符型数据强制转换成数值型,如果无法转换,则直接当做0处理

'张无忌'+100===>100


③其中一个操作数为null

null+null====》null

null+100====》 null



*/



#九、distinct的使用 去重

#需求:查询员工涉及到的部门编号有哪些


SELECT DISTINCT department_id FROM employees;


#十、查看表的结构

DESC employees;
SHOW COLUMNS FROM employees;

练习

显示出表 employees 的全部列,各个列之间用逗号连接,列头显示成 OUT_PUT

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT CONCAT(employee_id,',',first_name,',',last_name,',',salary,',',IFNULL(commission_pct,''))  AS "OUT_PUT"
FROM employees;

#ifnull(表达式1,表达式2)
/*
表达式1:可能为null的字段或表达式
表达式2:如果表达式1为null,则最终结果显示的值

功能:如果表达式1为null,则显示表达式2,否则显示表达式1

*/

SELECT commission_pct,IFNULL(commission_pct,'空') FROM employees;

条件查询

select 查询列表
from 表名
where 筛选条件;

执行顺序:
①from子句
②where子句
③select子句

1、按关系表达式筛选

关系运算符:> < >= <= = <>
补充:也可以使用!=,但不建议

2、按逻辑表达式筛选

逻辑运算符:and or not
补充:也可以使用&& || ! ,但不建议

3、模糊查询

like
in
between and
is null

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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
#一、按关系表达式筛选
#案例1:查询部门编号不是100的员工信息
SELECT *
FROM employees
WHERE department_id <> 100;


#案例2:查询工资<15000的姓名、工资
SELECT last_name,salary
FROM employees
WHERE salary<15000;


#二、按逻辑表达式筛选

#案例1:查询部门编号不是 50-100之间员工姓名、部门编号、邮箱
#方式1
SELECT last_name,department_id,email
FROM employees
WHERE department_id <50 OR department_id>100;

#方式2


SELECT last_name,department_id,email
FROM employees
WHERE NOT(department_id>=50 AND department_id<=100);



#案例2:查询奖金率>0.03 或者 员工编号在60-110之间的员工信息
SELECT *
FROM employees
WHERE commission_pct>0.03 OR (employee_id >=60 AND employee_id<=110);


#三、模糊查询

#1like

/*
功能:一般和通配符搭配使用,对字符型数据进行部分匹配查询
常见的通配符:
_ 任意单个字符
% 任意多个字符,支持0-多个
like/not like
*/

#案例1:查询姓名中包含字符a的员工信息
SELECT *
FROM employees
WHERE last_name LIKE '%a%';

#案例2:查询姓名中包含最后一个字符为e的员工信息
SELECT *
FROM employees
WHERE last_name LIKE '%e';

#案例3:查询姓名中包含第一个字符为e的员工信息
SELECT *
FROM employees
WHERE last_name LIKE 'e%';

#案例4:查询姓名中包含第三个字符为x的员工信息
SELECT *
FROM employees
WHERE last_name LIKE '__x%';

#案例5:查询姓名中包含第二个字符为_的员工信息
SELECT *
FROM employees
WHERE last_name LIKE '_\_%';

SELECT *
FROM employees
WHERE last_name LIKE '_$_%' ESCAPE '$';


#2in
/*
功能:查询某字段的值是否属于指定的列表之内

a in(常量值1,常量值2,常量值3,...)
a not in(常量值1,常量值2,常量值3,...)

in/not in
*/

#案例1:查询部门编号是30/50/90的员工名、部门编号


#方式1
SELECT last_name,department_id
FROM employees
WHERE department_id IN(30,50,90);

#方式2

SELECT last_name,department_id
FROM employees
WHERE department_id = 30
OR department_id = 50
OR department_id = 90;


#案例2:查询工种编号不是SH_CLERK或IT_PROG的员工信息
#方式1
SELECT *
FROM employees
WHERE job_id NOT IN('SH_CLERK','IT_PROG');

#方式2
SELECT *
FROM employees
WHERE NOT(job_id ='SH_CLERK'
OR job_id = 'IT_PROG');


#3between and

/*
功能:判断某个字段的值是否介于xx之间

between and/not between and

*/


#案例1:查询部门编号是30-90之间的部门编号、员工姓名

#方式1
SELECT department_id,last_name
FROM employees
WHERE department_id BETWEEN 30 AND 90;

#方式2

SELECT department_id,last_name
FROM employees
WHERE department_id>=30 AND department_id<=90;


#案例2:查询年薪不是100000-200000之间的员工姓名、工资、年薪

SELECT last_name,salary,salary*12*(1+IFNULL(commission_pct,0)) 年薪
FROM employees
WHERE salary*12*(1+IFNULL(commission_pct,0))<100000 OR salary*12*(1+IFNULL(commission_pct,0))>200000;



SELECT last_name,salary,salary*12*(1+IFNULL(commission_pct,0)) 年薪
FROM employees
WHERE salary*12*(1+IFNULL(commission_pct,0)) NOT BETWEEN 100000 AND 200000;



#4is null/is not null

#案例1:查询没有奖金的员工信息
SELECT *
FROM employees
WHERE commission_pct IS NULL;



#案例2:查询有奖金的员工信息
SELECT *
FROM employees
WHERE commission_pct IS NOT NULL;


SELECT *
FROM employees
WHERE salary IS 10000;

#----------------对比------------------------------------

= 只能判断普通的内容

IS 只能判断NULL

<=> 安全等于,既能判断普通内容,又能判断NULL




SELECT *
FROM employees
WHERE salary <=> 10000;

SELECT *
FROM employees
WHERE commission_pct <=> NULL;

排序查询

select 查询列表
from 表名
【where 筛选条件】
order by 排序列表

执行顺序:

①from子句
②where子句
③select子句
④order by 子句

特点:

1、排序列表可以是单个字段、多个字段、表达式、函数、列数、以及以上的组合
2、升序 ,通过 asc ,默认行为
降序 ,通过 desc

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
#一、按单个字段排序

#案例1:将员工编号>120的员工信息进行工资的升序
SELECT *
FROM employees

ORDER BY salary ;

#案例1:将员工编号>120的员工信息进行工资的降序
SELECT *
FROM employees
WHERE employee_id>120
ORDER BY salary DESC;

#二、按表达式排序
#案例1:对有奖金的员工,按年薪降序

SELECT *,salary*12*(1+IFNULL(commission_pct,0)) 年薪
FROM employees
WHERE commission_pct IS NOT NULL
ORDER BY salary*12*(1+IFNULL(commission_pct,0)) DESC;


#三、按别名排序
#案例1:对有奖金的员工,按年薪降序

SELECT *,salary*12*(1+IFNULL(commission_pct,0)) 年薪
FROM employees

ORDER BY 年薪 DESC;

#四、按函数的结果排序

#案例1:按姓名的字数长度进行升序


SELECT last_name
FROM employees
ORDER BY LENGTH(last_name);


#五、按多个字段排序

#案例1:查询员工的姓名、工资、部门编号,先按工资升序,再按部门编号降序

SELECT last_name,salary,department_id
FROM employees
ORDER BY salary ASC,department_id DESC;


#六、补充选学:按列数排序


SELECT * FROM employees
ORDER BY 2 DESC;


SELECT * FROM employees
ORDER BY first_name;

函数

函数:类似于java中学过的“方法”,
为了解决某个问题,将编写的一系列的命令集合封装在一起,对外仅仅暴露方法名,供外部调用

1、自定义方法(函数)
2、调用方法(函数)★
叫什么 :函数名
干什么 :函数功能

单行函数
  • 字符函数
    concat
    substr
    length(str)
    char_length
    upper
    lower
    trim
    left
    right
    lpad
    rpad
    instr
    strcmp

  • 数学函数
    abs
    ceil
    floor
    round
    truncate
    mod

  • 日期函数
    now
    curtime
    curdate
    datediff
    date_format
    str_to_date

  • 流程控制函数
    if
    case

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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
1、CONCAT 拼接字符

SELECT CONCAT('hello,',first_name,last_name) 备注 FROM employees;

2、LENGTH 获取字节长度

SELECT LENGTH('hello,郭襄');

3CHAR_LENGTH 获取字符个数
SELECT CHAR_LENGTH('hello,郭襄');

4、SUBSTRING 截取子串
/*
注意:起始索引从1开始!!!
substr(str,起始索引,截取的字符长度)
substr(str,起始索引)
*/
SELECT SUBSTR('张三丰爱上了郭襄',1,3);
SELECT SUBSTR('张三丰爱上了郭襄',7);

5、INSTR获取字符第一次出现的索引

SELECT INSTR('三打白骨精aaa白骨精bb白骨精','白骨精');

6、TRIM去前后指定的字符,默认是去空格


SELECT TRIM(' 虚 竹 ') AS a;
SELECT TRIM('x' FROM 'xxxxxx虚xxx竹xxxxxxxxxxxxxxxxxx') AS a;

7、LPAD/RPAD 左填充/右填充
SELECT LPAD('木婉清',10,'a');
SELECT RPAD('木婉清',10,'a');

8、UPPER/LOWER 变大写/变小写

#案例:查询员工表的姓名,要求格式:姓首字符大写,其他字符小写,名所有字符大写,且姓和名之间用_分割,最后起别名“OUTPUT”


SELECT UPPER(SUBSTR(first_name,1,1)),first_name FROM employees;
SELECT LOWER(SUBSTR(first_name,2)),first_name FROM employees;
SELECT UPPER(last_name) FROM employees;

SELECT CONCAT(UPPER(SUBSTR(first_name,1,1)),LOWER(SUBSTR(first_name,2)),'_',UPPER(last_name)) "OUTPUT"
FROM employees;

9、STRCMP 比较两个字符大小

SELECT STRCMP('aec','aec');


10LEFT/RIGHT 截取子串
SELECT LEFT('鸠摩智',1);
SELECT RIGHT('鸠摩智',1);


#二、数学函数

1、ABS 绝对值
SELECT ABS(-2.4);
2、CEIL 向上取整 返回>=该参数的最小整数
SELECT CEIL(-1.09);
SELECT CEIL(0.09);
SELECT CEIL(1.00);

3、FLOOR 向下取整,返回<=该参数的最大整数
SELECT FLOOR(-1.09);
SELECT FLOOR(0.09);
SELECT FLOOR(1.00);

4、ROUND 四舍五入
SELECT ROUND(1.8712345);
SELECT ROUND(1.8712345,2);

5TRUNCATE 截断

SELECT TRUNCATE(1.8712345,1);

6、MOD 取余

SELECT MOD(-10,3);
a%b = a-(INT)a/b*b
-10%3 = -10 - (-10)/3*3 = -1

SELECT -10%3;
SELECT 10%3;
SELECT -10%-3;
SELECT 10%-3;


#三、日期函数


1、NOW
SELECT NOW();

2、CURDATE

SELECT CURDATE();

3、CURTIME
SELECT CURTIME();


4、DATEDIFF
SELECT DATEDIFF('1998-7-16','2019-7-13');

5、DATE_FORMAT

SELECT DATE_FORMAT('1998-7-16','%Y年%M月%d日 %H小时%i分钟%s秒') 出生日期;



SELECT DATE_FORMAT(hiredate,'%Y年%M月%d日 %H小时%i分钟%s秒')入职日期
FROM employees;



6、STR_TO_DATE 按指定格式解析字符串为日期类型
SELECT * FROM employees
WHERE hiredate<STR_TO_DATE('3/15 1998','%m/%d %Y');


#四、流程控制函数


1、IF函数

SELECT IF(100>9,'好','坏');


#需求:如果有奖金,则显示最终奖金,如果没有,则显示0
SELECT IF(commission_pct IS NULL,0,salary*12*commission_pct) 奖金,commission_pct
FROM employees;



2CASE函数

①情况1 :类似于switch语句,可以实现等值判断
CASE 表达式
WHEN1 THEN 结果1
WHEN2 THEN 结果2
...
ELSE 结果n
END


案例:
部门编号是30,工资显示为2
部门编号是50,工资显示为3
部门编号是60,工资显示为4
否则不变

显示 部门编号,新工资,旧工资

SELECT department_id,salary,
CASE department_id
WHEN 30 THEN salary*2
WHEN 50 THEN salary*3
WHEN 60 THEN salary*4
ELSE salary
END newSalary
FROM employees;


②情况2:类似于多重IF语句,实现区间判断
CASE
WHEN 条件1 THEN 结果1
WHEN 条件2 THEN 结果2
...

ELSE 结果n

END



案例:如果工资>20000,显示级别A
工资>15000,显示级别B
工资>10000,显示级别C
否则,显示D

SELECT salary,
CASE
WHEN salary>20000 THEN 'A'
WHEN salary>15000 THEN 'B'
WHEN salary>10000 THEN 'C'
ELSE 'D'
END
AS a
FROM employees;
分组函数

说明:分组函数往往用于实现将一组数据进行统计计算,最终得到一个值,又称为聚合函数或统计函数

分组函数清单:

sum(字段名):求和
avg(字段名):求平均数
max(字段名):求最大值
min(字段名):求最小值
count(字段名):计算非空字段值的个数

特点:

sum,avg 一般处理数值类型,max,min,count 可以处理任意类型,以上分组函数都忽略null值。

可以和 distinct 搭配实现去重的运算

count函数一般使用count(*)用作统计行数

和分组函数一同查询的字段有限制,只能是 group by 后面的函数

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
#案例1 :查询员工信息表中,所有员工的工资和、工资平均值、最低工资、最高工资、有工资的个数

SELECT SUM(salary),AVG(salary),MIN(salary),MAX(salary),COUNT(salary) FROM employees;

#案例2:添加筛选条件
#①查询emp表中记录数:
SELECT COUNT(employee_id) FROM employees;

#②查询emp表中有佣金的人数:

SELECT COUNT(salary) FROM employees;


#③查询emp表中月薪大于2500的人数:
SELECT COUNT(salary) FROM employees WHERE salary>2500;


#④查询有领导的人数:
SELECT COUNT(manager_id) FROM employees;


#count的补充介绍★


#1、统计结果集的行数,推荐使用count(*)

SELECT COUNT(*) FROM employees;
SELECT COUNT(*) FROM employees WHERE department_id = 30;


SELECT COUNT(1) FROM employees;
SELECT COUNT(1) FROM employees WHERE department_id = 30;


#2、搭配distinct实现去重的统计

#需求:查询有员工的部门个数

SELECT COUNT(DISTINCT department_id) FROM employees;


#思考:每个部门的总工资、平均工资?

SELECT SUM(salary) FROM employees WHERE department_id = 30;
SELECT SUM(salary) FROM employees WHERE department_id = 50;


SELECT SUM(salary) ,department_id
FROM employees
GROUP BY department_id;

分组查询

select 查询列表
from 表名
where 筛选条件
group by 分组列表
having 分组后筛选
order by 排序列表;

执行顺序:
①from子句
②where子句
③group by 子句
④having子句
⑤select子句
⑥order by子句

特点:
①查询列表往往是 分组函数和被分组的字段 ★
②分组查询中的筛选分为两类
筛选的基表 使用的关键词 位置
分组前筛选 原始表 where group by 的前面

分组后筛选 分组后的结果集 having group by的后面

where——group by ——having

问题:分组函数做条件只可能放在having后面!!!

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
#1)简单的分组
#案例1:查询每个工种的员工平均工资

SELECT AVG(salary),job_id
FROM employees
GROUP BY job_id;

#案例2:查询每个领导的手下人数

SELECT COUNT(*),manager_id
FROM employees
WHERE manager_id IS NOT NULL
GROUP BY manager_id;





#2)可以实现分组前的筛选
#案例1:查询邮箱中包含a字符的 每个部门的最高工资
SELECT MAX(salary) 最高工资,department_id
FROM employees
WHERE email LIKE '%a%'
GROUP BY department_id;


#案例2:查询每个领导手下有奖金的员工的平均工资
SELECT AVG(salary) 平均工资,manager_id
FROM employees
WHERE commission_pct IS NOT NULL
GROUP BY manager_id;


#3)可以实现分组后的筛选
#案例1:查询哪个部门的员工个数>5
#分析1:查询每个部门的员工个数
SELECT COUNT(*) 员工个数,department_id
FROM employees
GROUP BY department_id

#分析2:在刚才的结果基础上,筛选哪个部门的员工个数>5

SELECT COUNT(*) 员工个数,department_id
FROM employees

GROUP BY department_id
HAVING COUNT(*)>5;


#案例2:每个工种有奖金的员工的最高工资>12000的工种编号和最高工资

SELECT job_id,MAX(salary)
FROM employees
WHERE commission_pct IS NOT NULL
GROUP BY job_id
HAVING MAX(salary)>12000;


#案例3:领导编号>102的 每个领导手下的最低工资大于5000的最低工资
#分析1:查询每个领导手下员工的最低工资
SELECT MIN(salary) 最低工资,manager_id
FROM employees
GROUP BY manager_id;

#分析2:筛选刚才1的结果
SELECT MIN(salary) 最低工资,manager_id
FROM employees
WHERE manager_id>102
GROUP BY manager_id
HAVING MIN(salary)>5000 ;




#4)可以实现排序
#案例:查询没有奖金的员工的最高工资>6000的工种编号和最高工资,按最高工资升序
#分析1:按工种分组,查询每个工种有奖金的员工的最高工资
SELECT MAX(salary) 最高工资,job_id
FROM employees
WHERE commission_pct IS NULL
GROUP BY job_id


#分析2:筛选刚才的结果,看哪个最高工资>6000
SELECT MAX(salary) 最高工资,job_id
FROM employees
WHERE commission_pct IS NULL
GROUP BY job_id
HAVING MAX(salary)>6000


#分析3:按最高工资升序
SELECT MAX(salary) 最高工资,job_id
FROM employees
WHERE commission_pct IS NULL
GROUP BY job_id
HAVING MAX(salary)>6000
ORDER BY MAX(salary) ASC;


#5)按多个字段分组
#案例:查询每个工种每个部门的最低工资,并按最低工资降序
#提示:工种和部门都一样,才是一组

工种 部门 工资
1 10 10000
1 20 2000
2 20
3 20
1 10
2 30
2 20


SELECT MIN(salary) 最低工资,job_id,department_id
FROM employees
GROUP BY job_id,department_id;

链接查询

说明:又称多表查询,当查询语句涉及到的字段来自于多个表时,就会用到连接查询

笛卡尔乘积现象:表1 有m行,表2有n行,结果=m*n行

发生原因:没有有效的连接条件
如何避免:添加有效的连接条件

  • 分类:

    • 按年代分类:
      • sql92标准:仅仅支持内连接
        • 内连接:
          • 等值连接
          • 非等值连接
          • 自连接
      • sql99标准【推荐】:支持内连接+外连接(左外和右外)+交叉连接
    • 按功能分类:
      • 内连接:
        • 等值连接
        • 非等值连接
        • 自连接
      • 外连接:
        • 左外连接
        • 右外连接
        • 全外连接
      • 交叉连接
内链接

语法:
select 查询列表
from 表1 别名,表2 别名
where 连接条件
and 筛选条件
group by 分组列表
having 分组后筛选
order by 排序列表

执行顺序:

1、from子句
2、where子句
3、and子句
4、group by子句
5、having子句
6、select子句
7、order by子句

SQL92和SQL99的区别:
SQL99,使用JOIN关键字代替了之前的逗号,并且将连接条件和筛选条件进行了分离,提高阅读性!!!

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
语法:
SELECT 查询列表
FROM 表名1 别名
INNERJOIN 表名2 别名
ON 连接条件
WHERE 筛选条件
GROUP BY 分组列表
HAVING 分组后筛选
ORDER BY 排序列表;

#一)等值连接
#①简单连接
#案例:查询员工名和部门名

SELECT last_name,department_name
FROM departments d
JOIN employees e
ON e.department_id =d.department_id;



#②添加筛选条件
#案例1:查询部门编号>100的部门名和所在的城市名
SELECT department_name,city
FROM departments d
JOIN locations l
ON d.`location_id` = l.`location_id`
WHERE d.`department_id`>100;


#③添加分组+筛选
#案例1:查询每个城市的部门个数

SELECT COUNT(*) 部门个数,l.`city`
FROM departments d
JOIN locations l
ON d.`location_id`=l.`location_id`
GROUP BY l.`city`;




#④添加分组+筛选+排序
#案例1:查询部门中员工个数>10的部门名,并按员工个数降序

SELECT COUNT(*) 员工个数,d.department_name
FROM employees e
JOIN departments d
ON e.`department_id`=d.`department_id`
GROUP BY d.`department_id`
HAVING 员工个数>10
ORDER BY 员工个数 DESC;


#二)非等值连接

#案例:查询部门编号在10-90之间的员工的工资级别,并按级别进行分组
SELECT * FROM sal_grade;


SELECT COUNT(*) 个数,grade
FROM employees e
JOIN sal_grade g
ON e.`salary` BETWEEN g.`min_salary` AND g.`max_salary`
WHERE e.`department_id` BETWEEN 10 AND 90
GROUP BY g.grade;




#三)自连接

#案例:查询员工名和对应的领导名

SELECT e.`last_name`,m.`last_name`
FROM employees e
JOIN employees m
ON e.`manager_id`=m.`employee_id`;
外连接

说明:查询结果为主表中所有的记录,如果从表有匹配项,则显示匹配项;如果从表没有匹配项,则显示 null

应用场景:一般用于查询主表中有但从表没有的记录

特点:

1、外连接分主从表,两表的顺序不能任意调换
2、左连接的话,left join 左边为主表
右连接的话,right join 右边为主表

语法:

select 查询列表
from 表 1 别名
left|right|full 【outer】 join 表 2 别名
on 连接条件
where 筛选条件;

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
#案例1:查询所有女神记录,以及对应的男神名,如果没有对应的男神,则显示为null

#左连接
SELECT b.*,bo.*
FROM beauty b
LEFT JOIN boys bo ON b.`boyfriend_id` = bo.`id`;

#右连接
SELECT b.*,bo.*
FROM boys bo
RIGHT JOIN beauty b ON b.`boyfriend_id` = bo.`id`;



#案例2:查哪个女生没有男朋友

#左连接
SELECT b.`name`
FROM beauty b
LEFT JOIN boys bo ON b.`boyfriend_id` = bo.`id`
WHERE bo.`id` IS NULL;

#右连接
SELECT b.*,bo.*
FROM boys bo
RIGHT JOIN beauty b ON b.`boyfriend_id` = bo.`id`
WHERE bo.`id` IS NULL;


#案例3:查询哪个部门没有员工,并显示其部门编号和部门名

SELECT COUNT(*) 部门个数
FROM departments d
LEFT JOIN employees e ON d.`department_id` = e.`department_id`
WHERE e.`employee_id` IS NULL;

子查询

说明:当一个查询语句中又嵌套了另一个完整的 select 语句,则被嵌套的 select 语句称为子查询或内查询
外面的 select 语句称为主查询或外查询。

分类:

按子查询出现的位置进行分类:

1、select 后面
要求:子查询的结果为单行单列(标量子查询)
2、from 后面
要求:子查询的结果可以为多行多列
3、where 或 having 后面 ★
要求:子查询的结果必须为单列
单行子查询
多行子查询
4、exists 后面
要求:子查询结果必须为单列(相关子查询)
特点:
1、子查询放在条件中,要求必须放在条件的右侧
2、子查询一般放在小括号中
3、子查询的执行优先于主查询
4、单行子查询对应了 单行操作符:> < >= <= = <>
多行子查询对应了 多行操作符:any/some all in

单行子查询
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
#一)单行子查询

#案例1:谁的工资比 Abel 高?


#①查询Abel的工资
SELECT salary
FROM employees
WHERE last_name = 'Abel'
#②查询salary>①的员工信息
SELECT last_name,salary
FROM employees
WHERE salary>(
SELECT salary
FROM employees
WHERE last_name <> 'Abel'

);

#案例2:返回job_id与141号员工相同,salary比143号员工多的员工姓名,job_id 和工资
#①查询141号员工的job_id
SELECT job_id
FROM employees
WHERE employee_id = 141

#②查询143号员工的salary

SELECT salary
FROM employees
WHERE employee_id = 143

#③查询job_id=① and salary>②的信息
SELECT last_name,job_id,salary
FROM employees
WHERE job_id = (
SELECT job_id
FROM employees
WHERE employee_id = 141
) AND salary>(

SELECT salary
FROM employees
WHERE employee_id = 143

);
多行子查询
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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
/*
in:判断某字段是否在指定列表内
x in(10,30,50)


any/some:判断某字段的值是否满足其中任意一个

x>any(10,30,50)
x>min()

x=any(10,30,50)
x in(10,30,50)

all:判断某字段的值是否满足里面所有的

x >all(10,30,50)
x >max()

*/


#案例1:返回location_id是1400或1700的部门中的所有员工姓名

#①查询location_id是1400或1700的部门
SELECT department_id
FROM departments
WHERE location_id IN(1400,1700)


#②查询department_id = ①的姓名
SELECT last_name
FROM employees
WHERE department_id IN(
SELECT DISTINCT department_id
FROM departments
WHERE location_id IN(1400,1700)

);



#题目:返回其它部门中比job_id为‘IT_PROG’部门任一工资低的员工的员工号、姓名、job_id 以及salary

#①查询job_id为‘IT_PROG’部门的工资
SELECT DISTINCT salary
FROM employees
WHERE job_id = 'IT_PROG'


#②查询其他部门的工资<任意一个①的结果

SELECT employee_id,last_name,job_id,salary
FROM employees
WHERE salary<ANY(

SELECT DISTINCT salary
FROM employees
WHERE job_id = 'IT_PROG'


);



等价于

SELECT employee_id,last_name,job_id,salary
FROM employees
WHERE salary<(

SELECT MAX(salary)
FROM employees
WHERE job_id = 'IT_PROG'


);




#案例3:返回其它部门中比job_id为‘IT_PROG’部门所有工资都低的员工 的员工号、姓名、job_id 以及salary

#①查询job_id为‘IT_PROG’部门的工资
SELECT DISTINCT salary
FROM employees
WHERE job_id = 'IT_PROG'


#②查询其他部门的工资<所有①的结果

SELECT employee_id,last_name,job_id,salary
FROM employees
WHERE salary<ALL(

SELECT DISTINCT salary
FROM employees
WHERE job_id = 'IT_PROG'


);



等价于

SELECT employee_id,last_name,job_id,salary
FROM employees
WHERE salary<(

SELECT MIN(salary)
FROM employees
WHERE job_id = 'IT_PROG'


);


#二、放在select后面

#案例;查询部门编号是50的员工个数

SELECT
(
SELECT COUNT(*)
FROM employees
WHERE department_id = 50
) 个数;


#三、放在from后面

#案例:查询每个部门的平均工资的工资级别
#①查询每个部门的平均工资

SELECT AVG(salary),department_id
FROM employees
GROUP BY department_id



#②将①和sal_grade两表连接查询

SELECT dep_ag.department_id,dep_ag.ag,g.grade
FROM sal_grade g
JOIN (

SELECT AVG(salary) ag,department_id
FROM employees
GROUP BY department_id

) dep_ag ON dep_ag.ag BETWEEN g.min_salary AND g.max_salary;


#四、放在exists后面

#案例1 :查询有无名字叫“张三丰”的员工信息
SELECT EXISTS(
SELECT *
FROM employees
WHERE last_name = 'Abel'

) 有无Abel;


#案例2:查询没有女朋友的男神信息

USE girls;

SELECT bo.*
FROM boys bo
WHERE bo.`id` NOT IN(
SELECT boyfriend_id
FROM beauty b
)



SELECT bo.*
FROM boys bo
WHERE NOT EXISTS(
SELECT boyfriend_id
FROM beauty b
WHERE bo.id = b.boyfriend_id
);

React v16 源码分析 ② 设计理念

状态渲染 UI

1
UI = react(state);

React 程序设计哲学

  • 将设计好的 UI 划分为组件层级
  • 确定 UI state 的最小(且完整)表示
  • 确定 state 放置的位置
  • 添加反向数据流,低层层级组件更新高层级组件状态

使用组合而不是继承

Props 和组合为你提供了清晰而安全地定制组件外观和行为的灵活方式。注意:组件可以接受任意 props,包括基本数据类型,React 元素以及函数。

如果你想要在组件间复用非 UI 的功能,我们建议将其提取为一个单独的 JavaScript 模块,如函数、对象或者类。组件可以直接引入(import)而无需通过 extend 继承它们。

Fiber

Fiber 其实就是 Virtual DOM 的一种实现,相比于通过 React.createElement 创建的 Virtual DOM,Fiber 在此基础上添加了更多的属性,例如 return, current 等指针,用于将 Fiber 对象链接为链表。最终形成一颗树状结构,也就是 Fiber 树,他对应着真实 DOM 树的结构。

而 Fiber 对象上的属性还不止这些,还有像 updateQueue 更新队列等属性,但到目前位置知道 Fiber 是对 DOM 树的一种描述,已经足够了。而让 React 设计 Fiber 的原因,则是因为下面的协调过程。

协调 reconciler

这一概念应该是当我们对 React 执行过程深入思考的时候最容易想到的一部分,通过 JSX 创建的 Virtual DOM 如何与真实的 DOM 同步,真实 DOM 属性改变的时候,又如何被记录到 Virtual DOM 上,这个过程就叫做协调

reconciler 模块,用于处理协调相关的事务。Diff 算法也在这个期间发生。

React15 之前的协调过程是同步的,也叫 stack reconciler。

JS 的执行是单线程的,由于浏览器器触发的事件(用户交互触发的事件回调)是一个宏任务,所以会等待同步任务执行完成,在更新比较耗时的任务时,会阻塞用户的交互。

也许会考虑将耗时任务放到异步任务中执行,但最终还是会回到主线程中执行,所以比较好的解决办法就是任务分割,当其他优先级比较高的任务到来时,将正在执行的任务打断让出执行权。之后再从中断的部分开始异步执行剩下的计算。

为了将老的同步更新的架构变为异步可中断更新,所以需要一套数据结构让它既能对应真实的 dom 又能作为分隔的单元,这就是 Fiber。

Scheduler

有了 Fiber,就需要用浏览器的时间片异步执行这些 Fiber 的工作单元,浏览器有一个 api 叫做 requestIdleCallback,它可以在浏览器空闲的时候执行一些任务,我们用这个 api 执行 react 的更新,让高优先级的任务优先响应不就可以了吗,但事实是 requestIdleCallback 存在着浏览器的兼容性和触发不稳定的问题,所以我们需要用 js 实现一套时间片运行的机制,在 react 中这部分叫做 scheduler。

下面用伪代码理解一下 分割,异步执行,让出执行权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let firstFiber; // 代表Fiber树的头节点
let nextFiber = firstFiber; // 用于遍历子节点

function performUnitOfWork() {
// 处理节点相关逻辑
return nextFiber.next; // 返回下一个节点
}

function workLoop(deadline) {
while (nextFiber && !shouldYield) {
nextFiber = performUnitOfWork();
// 如果没有剩余时间处理下一个节点
// 则暂停执行,让出主线程,给优先级更高的任务
shouldYield = deadline < 1;
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);

为什么不使用统的异步控制:

  • setTimeout setTimeout 在嵌套超过 5 层之后有默认 4ms 的延时
  • requestFrameAnimation 执行时机不确定,chrome 和 firefox 是在渲染前执行,safari 是在渲染之后执行。
  • promise 微任务会在主进程执行结束后释放掉有所得微任务,不能控制什么时候需要执行。

Lane

有了异步调度,我们还需要细粒度的管理各个任务的优先级,让高优先级的任务优先执行,各个 Fiber 工作单元还能比较优先级,相同优先级的任务可以一起更新。

代数效应

(algebraic effects) 可能翻译成 可以当做参数传递的副作用 更容易理解。 它是函数式编程中的一个概念,用于将副作用从函数调用中分离。

从实用的角度上举例,假如我们有这样一段代码,其主要目的是进行一大段精妙的运算:

1
2
3
4
5
6
7
async function biz(id) {
const infoId = /* do some calc */ id; // 这里可以理解为是一大段计算逻辑
const info = await getInfo(infoId); // 副作用,与 server 通信
const dataId = /* do some calc */ info.dataId; // 这里可以理解为是一大段计算逻辑
const data = getData(dataId); // 副作用,非幂等操作
return /* do some calc */ data.finalCalcData; // 这里可以理解为是一大段计算逻辑
}

尽管运算逻辑很优美,但美中不足的是有两段副作用,导致它不能成为一个干净的纯函数被单元测试。而且这里会导致严重的逻辑耦合:『做什么』与『怎么做』没有拆的很干净:你的一大段计算逻辑是在处理做什么;两个副作用更关心怎么做:比如线上是接口调用,单测里是 mock 数据;但是由于这两块副作用代码,导致整个糅杂的逻辑都无法复用。直接把两个副作用传进来不就行了?

1
2
3
4
5
6
7
async function biz(id, getInfo, getData) {
const infoId = /* do some calc */ id; // 这里可以理解为是一大段计算逻辑
const info = await getInfo(infoId); // 副作用,与 server 通信
const dataId = /* do some calc */ info.dataId; // 这里可以理解为是一大段计算逻辑
const data = getData(dataId); // 副作用,非幂等操作
return /* do some calc */ data.finalCalcData; // 这里可以理解为是一大段计算逻辑
}

是的,这样确实可以复用,但还有一个叫函数染色的问题没有解决:明明是一大段干净的同步运算逻辑,因为 getInfo 是异步的,导致整个函数都得加个 async。而且很有可能在我单元测试里,这个 getInfo 是直接同步取内存数据,还得因此弄个 Promise……这时候如果 JS 里有这样一种语法就好了:

当函数执行到perform的时候,会被暂停,并被handle捕获,当异步执行的结果被返回,函数在继续执行

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

function biz(id) {
const infoId = /* do some calc */ id; // 这里可以理解为是一大段计算逻辑
const info = perform { type: 'getInfo', payload: infoId };
const dataId = /* do some calc */ info.dataId; // 这里可以理解为是一大段计算逻辑
const data = perform { type: 'getData', payload: dataId };
return /* do some calc */ data.finalCalcData; // 这里可以理解为是一大段计算逻辑
}

// 正常业务逻辑
async function runBiz() {
try {
biz();
} handle(effect) {
if (effect.type === 'getInfo') {
resume await getInfo(effect.payload);
} else if (effect.type === 'getData') {
resume await getData(effect.payload)
}
}
}

// 单元测试逻辑
function testBiz() {
try {
biz();
} handle(effect) {
if (effect.type === 'getInfo') {
resume testInfo;
} else if (effect.type === 'getData') {
resume testData;
}
}
}

分离副作用在函数编程中非常常见,redux-saga也会将副作用分离出来,只负责发起请求

1
2
3
4
5
6
7
8
function* fetchUser(action) {
try {
const user = yield call(Api.fetchUser, action.payload);
yield put({ type: "SUCCESS", user: user });
} catch (err) {
yield put({ type: "ERROR" });
}
}

这样业务逻辑代码即摆脱了副作用,完成了做什么与怎么做的解耦;又完全不必担心异步副作用带来的染色问题,可以愉快的单测和复用了。Suspense 也是这种概念的延伸:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const ProductResource = createResource(fetchProduct);

const Product = (props) => {
const p = ProductResource.read(
// 用同步的方式来编写异步代码!
props.id
);
return <h3>{p.price}</h3>;
};

function App() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<Product id={123} />
</Suspense>
</div>
);
}

可以看到 ProductResource.read 完全是同步的写法,把获取数据的部分完全分离出了 Product 组件之外。在源码中, ProductResource.read 会在获取数据之前会 throw 一个特殊的 Promise, 由于 scheduler 的存在, scheduler 可以捕获这个 promise,暂停更新等数据获取之后交还执行权。ProductResource 可以是 localStorage 甚至是 redismysql 等数据库,也就是组件即服务,可能以后会有 server Component 的出现。

MySQl 服务基础

MySQL 服务启动和停止

查看系统内是否有 mysql 服务

1
ps aux | grep mysql

停止 mysql 服务

1
sudo service mysql stop

启动 mysql 服务

1
sudo service mysql start

重启 mysql 服务

1
sudo service mysql restart

登录与退出

登录命令,属性和属性值之间可以省略空格

1
mysql -h主机名 -P端口号 -u有户名 -p密码

退出 exit

常见命令

查看当前所有数据库

1
show databases;

打开指定的库

1
use 库名;

查看当前所在库

1
select database();

查看所有表

1
show tables;

查看其他库的所有表

1
show tables from 库名;

创建表

1
2
3
4
create table 表名 {
列名 列类型,
...
}

查看表结构

1
desc 表名;

查看表结构

1
2
3
4
5
6
7

// 进入mysql服务器中
select version();

// 在命令行中
mysql --version
mysql -V

语法规范

  • 不区分大小写,建议关键字大写,表名,列名小写
  • 每条命令;结尾
  • 命令可以换行
  • 注释
    单行注释: #注释文字
    单行注释: -- 注释文字
    多行注释: /* 注释文字 */

⑤ ReactSSR node中间层代理请求

代理请求

由于客户端和服务端公用一套请求的接口,所以需要接口同时适应客户端和移动端,这里可以选用 cross-fetchaxios

当在异步 action 中请求数据时,我们希望请求的是同域的服务

1
2
3
export const loadData =
() => (dispatch: Dispatch, getState: any, request: AxiosInstance) =>
axios("/api/products").then(({ data }) => dispatch(loadDataAction(data)));

而不是直接请求后端服务器。所以需要将 api 开头的请求转发到后端服务器请求。用到了一个 koa 的中间件 koa-proxies

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
import Koa from 'koa';
import koaBody from 'koa-body';
import koaStatic from 'koa-static';
import proxy from 'koa-proxies';
import path from 'path';
import router from '../router'

import errorHandle from './errorHandle'

const app = new Koa();

app
.use(async (ctx:any, next:()=>Promise<any>) => {
try {
await next();
} catch (err) {
ctx.app.emit('error', err, ctx);
}
})
.use(proxy('/api',{
target: 'https://fakestoreapi.com',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/,''),
}))
.use(koaStatic(path.join((process.env as any).PWD,'./static')))
.use(koaBody())
.use(router.routes())
.use(router.allowedMethods())

app.on('error', errorHandle);

export default app;

处理请求

现在客户端正常访问是可以的,但是服务端会报出错误,因为当刷新页面的时候服务端会做服务端渲染,这时直接调用了组件中获取数据的方法。

由于组件中的路径是以 /api 开头的绝对路径,所以会尝试在服务器中查找根路径下api文件夹,因为找不到报错错误。

一个思路是,区分服务端的请求和客户端的请求,分别为其创建不同的 axios 实例用于请求,但是为了避免像上一章中,每个请求分两种写,可以考虑在项目初始化的时候创建不同的 axios 实例,并通过参数传递到请求方法中,从而避免业务逻辑太多冗余。

src/util/request.ts

定义一个请求方法,为服务端和客户端创建不同实例

由于后端服务并不是api开头的接口,所以后端访问时,需要为其重写url路径.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import axios from "axios";

const serverInstance = axios.create({
baseURL: "https://fakestoreapi.com",
adapter: function (config) {
/* ... */
config.url = config.url?.replace(/^\/api/, "");
delete config.adapter;
return new Promise((resolve) => {
resolve(axios(config));
});
},
});

const clientInstance = axios.create({
baseURL: "/",
});

export { serverInstance, clientInstance };

在初始化 store 的时候,通过中间件把 axios 实例传入,让所用的异步 action 在请求前可以通过第三个参数拿到 axios 实例

src/store/index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Provider } from 'react-redux'
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk'
import reducers from './reducers';
import { StoreType as HelloStoreType } from '../components/hello';
import {clientInstance,serverInstance} from '../util/request'

const browserStore = ()=> createStore(reducers, (window as any).__HYDRATE_DATA__, applyMiddleware(thunk.withExtraArgument(clientInstance)));

const serverStore = () => createStore(reducers, applyMiddleware(thunk.withExtraArgument(serverInstance)));

export type StoreType = {
hello: HelloStoreType
}

export {
Provider,
browserStore,
serverStore
}

src/components/hello/action.ts

修改 action 方法,通过 axios 实例请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Dispatch, ActionCreator } from "redux";
import { AxiosInstance } from "axios";
import {} from "redux";

export const LOAD_DATA = "LOAD_DATA";

export type LOAD_DATA_TYPE = typeof LOAD_DATA;

const loadDataAction: ActionCreator<{ type: LOAD_DATA_TYPE }> = (payload) => ({
type: LOAD_DATA,
payload,
});

export type ActionTypes = LOAD_DATA_TYPE;
export const loadData =
() => (dispatch: Dispatch, getState: any, request: AxiosInstance) =>
request("/api/products").then(({ data }) => dispatch(loadDataAction(data)));

export const serverLoadData = loadData;

最后一步,由于我们统一了调用方法,现在服务端也会通过异步 action 方法调用接口

所以需要让服务端调用方法的时候,也像客户端一样通过bindActionCreators传入dispatch方法

通过服务端创建的store传入了dispatch方法,并且让中间件的参数生效。这时也不需要再组合不同接口返回的state,通过异步action方法,在拿到返回值之后,dispatch会触发并更新store

当所有的组件异步数据请求之后,在通过getState获取最新的store渲染页面

src/router/index.tsx

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 router = new Router();

router.get("/(.*)", async (ctx) => {
const store = serverStore();
const promises: Array<any> = matchRoutes(routes, ctx.request.path).map(
({ route, match }) => {
return route.loadData
? bindActionCreators(route.loadData, store.dispatch)()
: Promise.resolve();
}
);
await Promise.all(promises);

ctx.body = `
<!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>Document</title>
</head>
<body>
<div id='root'>${ReactDOMServer.renderToString(
<App {...{ ctx, store }} />
)}</div>
<script>
window.__HYDRATE_DATA__ = ${JSON.stringify(
store.getState()
)}
</script>
<script src='/index.js' defer></script>
</body>
</html>
`;
});

export default router;

④ ReactSSR 接入redux

创建Action

/src/components/hello/action.ts

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
import { Dispatch, ActionCreator } from "redux";
import axios, { AxiosResponse } from 'axios'

export const LOAD_DATA = 'LOAD_DATA';

export type LOAD_DATA_TYPE = typeof LOAD_DATA;

interface FetchDataInterface {
(match?: any): Promise<AxiosResponse>
}

const fetchData: FetchDataInterface = (match) => axios('https://fakestoreapi.com/products');

interface ServerLoadDataInterface {
(match: any): Promise<any>
}

export const serverLoadData: ServerLoadDataInterface = (match) => fetchData().then(({ data }) => ({ hello: { shopData: data } }));

const loadDataAction: ActionCreator<{ type: LOAD_DATA_TYPE }> = (payload) => ({
type: LOAD_DATA,
payload,
})


export type ActionTypes = LOAD_DATA_TYPE;
export const loadData = () => (dispatch: Dispatch) => fetchData().then(({ data }) => dispatch(loadDataAction(data)));

创建reducer

/src/components/hello/reducer.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import {AnyAction,Reducer} from 'redux';
import {
LOAD_DATA,
} from './action';

import { StoreType } from '.';

const initStore = {
shopData:[]
}


const hello:Reducer<StoreType,AnyAction> = (state=initStore, action) => {
switch (action.type) {
case LOAD_DATA:
return ({...state,shopData:action.payload})
default:
return state;
}
}

export default hello;

connect Hello组件

/src/components/hello/hello.ts

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
import React from 'react';
import { bindActionCreators,Dispatch } from 'redux';
import {connect} from 'react-redux';
import { StoreType } from '../../store';
import Header from '../header';
import {loadData} from './action';

type HelloComponentProps = {
loadData:()=>void,
shopData:Array<any>,
};

const Hello:React.FC<HelloComponentProps> = (props) =>{
const {
loadData,
shopData
} = props;


React.useEffect(()=>{
loadData();
},[])

return <div>
<Header/>
{
shopData.map(item=> <h6 onClick={()=>{alert("hello")}} key={item.id}>{item.title}</h6>)
}
</div>
}



const mapStateToProps = (state:StoreType) => {
return {
shopData:state.hello.shopData
}
}

const mapDispatchToProps = (dispatch:Dispatch) => {
return {
loadData: bindActionCreators(loadData,dispatch)
}
}


export default connect(mapStateToProps,mapDispatchToProps)(Hello) ;

修改 Hello 组件入口文件 index.ts

  • 暴露服务端渲染时需要的数据请求方法

  • 添加store类型,暴露到外部的 store/index.js 统一描述store类型

1
2
3
4
5
6
7
8
9
10
11
12
13
import Hello from "./hello";
import {serverLoadData} from './action';


export interface StoreType {
shopData:Array<any>
}

export {
serverLoadData
};

export default Hello;

创建全局的store

src/store/index.ts

用于生成store对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Provider } from 'react-redux'
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk'
import reducers from './reducers';
import { StoreType as HelloStoreType } from '../components/hello';

const browserStore = ()=> createStore(reducers, (window as any).__HYDRATE_DATA__, applyMiddleware(thunk));

const serverStores = (__HYDRATE_DATA__: any) => createStore(reducers, __HYDRATE_DATA__, applyMiddleware(thunk));

export type StoreType = {
hello: HelloStoreType
}

export {
Provider,
browserStore,
serverStores
}

src/store/reducers.ts

合并所有组件中的reducer

1
2
3
4
5
6
import { combineReducers } from 'redux';
import hello from '../components/hello/reducer';

export default combineReducers({
hello
})

服务端预加载数据,改造routers

之前的routers是一个JSX元素,现在想调用组件暴露出的服务端请求数据的方法,并在拿到结果重新渲染组件,生成html字符串,并返回给浏览器

所以第一步:改造routers让我们可以拿到数据请求的方法

src/components/routes.tsx

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
import Hello, { serverLoadData as helloServerLoadData } from "./hello";
import Login from "./login";

const routes = [

{
path: "/hello",
exact: true,
component: Hello,
loadData: (match:any) => helloServerLoadData(match)
},
{
path: "/login",
exact: true,
component: Login,
},
{
path: "/",
component: Hello,
exact: true,
loadData: (match:any) => helloServerLoadData(match)
},
];

export default routes;

src/components/router.tsx

循环生成路由组件

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
import React from "react";
import Koa from 'koa';
import {
BrowserRouter,
Switch,
Route
} from "react-router-dom";

import routes from './routes'

interface RouterProps {
ctx?: Koa.BaseContext
}

const Router: React.FC<RouterProps> = () => {
return (
<BrowserRouter>
<Switch>
{routes.map(route => (
<Route {...route} key={route.path} />
))}
</Switch>
</BrowserRouter>
)
}

export default Router;

匹配路由对应的组件

react-router-config 用于匹配包括子路由在内的所有路由配置对应的组件

在拿到所有的匹配项之后,循环调用所有组件的数据请求方法,并把返回的promise对象放到一个数组中

当所有的返回值拿到之后,组合所有的state,初始化React组件,并渲染成字符串返回给浏览器

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
import Router from '@koa/router';
import ReactDOMServer from 'react-dom/server';
import React from 'react';
import App from './router';
import routes from '../components/routes'
import { matchRoutes } from "react-router-config";


const router = new Router();

router.get("/(.*)",async (ctx,next)=>{
const promises:Array<any> = matchRoutes(routes,ctx.request.path).map(({route,match})=> route.loadData?route.loadData(match):Promise.resolve());

const preloadData = await Promise.all(promises);
const __HYDRATE_DATA__ = preloadData.reduce((res,data)=>Object.assign({},res,data) ,{});

ctx.body=`
<!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>Document</title>
</head>
<body>
<div id='root'>${ReactDOMServer.renderToString(<App {...{ctx,__HYDRATE_DATA__}}/>)}</div>
<script>
window.__HYDRATE_DATA__ = ${JSON.stringify(__HYDRATE_DATA__)}
</script>
<script src='/index.js'></script>
</body>
</html>
`
})

export default router;

数据脱水和注水

  • 服务端渲染的时候需要把整合之后的store传入到初始化函数中,用于渲染Html字符串

  • 客户端渲染的时候,由于第一次渲染是并没有数据,会覆盖掉服务端渲染的结构,并重新请求后在渲染,这中间的过程就会白屏
    所以会在服务端直接把数据以字符串的方式插入到html界面中,在客户端解析的时候会变成window下的一个store对象,这个过程就叫做数据注水

  • 当客户端初始化时,会尝试查找window下有没有服务端插入的数据,如果有就用这个数据作为初始化数据,从而防止两边状态不统一造成的白屏,这一过程也叫做数据脱水

React v16 源码分析 ① 从入口开始认识React

render 方法

当我们打开 React 的官方文档看到的第一个例子便是 Hello World:

1
ReactDOM.render(<h1>Hello, world!</h1>, document.getElementById("root"));

render函数将h1标签和Hello World文本渲染在了页面的root元素中,但很显然这段代码没有看到的那么简单,js并不会认识html元素,这个标签最终会被转换成 js 对象,然后通过render方法后面的一系列函数调用,最终被插入到页面中。

JSX

刚才看到的 h1 标签,严格说来并不是html,因为他写在 js 语法中,可以把它叫做标签语法,也就是 JSX。

为什么 JSX 的出现好像又让页面开发回到了刀耕火种的jquery时代,jshtml混合在一起,UI(视图)与逻辑耦合。

但实际上 React 认为,渲染逻辑与 UI 的逻辑是耦合的,比如需要在 UI 中绑定事件,数据改变时通知 UI 发生改变,而 React 并没有将这两点分离到不同的文件中,而是通过组件的概念,实现关注点分离,也就是设计原则的分离,每一部分都有自己的关注点。

我们可以在Babel中,看一下 JSX 最终变成了什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const CompA = (
<div>
<p>p</p>div
</div>
);

const CompB = () => <a>a</a>;
const App = () => {
return (
<>
<CompA />
<CompB />
</>
);
};

转化为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"use strict";

const CompA = /*#__PURE__*/ React.createElement(
"div",
null,
/*#__PURE__*/ React.createElement("p", null, "p"),
"div"
);

const CompB = () => /*#__PURE__*/ React.createElement("a", null, "a");

const App = () => {
return /*#__PURE__*/ React.createElement(
React.Fragment,
null,
/*#__PURE__*/ React.createElement(CompA, null),
/*#__PURE__*/ React.createElement(CompB, null)
);
};

可以注意到组件被转化为React.createElement方法的第一个参数,这也是为什么组件的第一个字母需要大写的原因,Babel 会通过大小写来区分原生组件和 React 组件

而正因为 JSX 语法被转换成了React.createElement的函数调用,因此在写 JSX 的时候必须要import React from react

而 React 17-RC 以及之后的版本将采用新的 JSX 转换,从而无需在引入 React

Virtual DOM

通过 React.createElement 处理的节点,会被转换成 Virtual DOM 也可以称为虚拟 DOM

Virtual DOM 是一种编程概念,它与真是的 DOM 一一对应,而且他是保存在内存中的,当 DOM 的属性发生改变的时候,React 会在内存中把改变映射成 Virtual DOM,在把最终状态渲染在页面中,从而保证最小的 DOM 操作

实际上 React 中的 Fiber,也是属于 Virtual DOM 概念的一部分,在 React.createElement 创建的对象上添加了,更多的属性,例如优先级,副作用,更新队列等。

React.createElement /react/packages/react/src/React.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
70
71
72
73
74
75
76
77
78
79
80
81
export function createElement(type, config, children) {
let propName;

// 用于提取保留字段
const props = {};

let key = null;
let ref = null;
let self = null;
let source = null;

if (config != null) {
if (hasValidRef(config)) {
ref = config.ref;
}
if (hasValidKey(config)) {
// 转换为字符串
key = "" + config.key;
}

self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;

// 拷贝外部传入的props属性(这里的形参是config)到props对象上
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}

// 子元素的个数
const childrenLength = arguments.length - 2;

// 如果只有一个子元素直接赋值
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
// 如果大于一个则插入到一个数组中
props.children = childArray;
}

// 如果定义了默认属性,则用默认属性覆盖掉,
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
return ReactElement(
type, // 组件类型 可能是元素标签,也可能是类组件或函数组件的引用
key, // 字符串key
ref, // ref对象
self, // 内部属性初始化null
source, // 内部属性初始化null
ReactCurrentOwner.current,// 用于跟踪拥有当前正在被构建组件的组件
props // 属性集合
);
}

const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
_owner: owner,
};

return element;
};

当type是一个react组件的时候,他会被赋值到type属性上,最终被实例化。现在看一下,这个组件被定义时的样子

1
class App extends React.Component {}

React.Component /react/packages/react/src/ReactBaseClasses.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

// 基本类,用于帮助更新组件的状态
function Component(props, context, updater) {
this.props = props;
this.context = context;
// 如果refs是一个已经过时的string类型,晚一点的时候会重新分配一个不同的对象
this.refs = emptyObject;
// 初始化一个默认updater对象,但真实的对象会在渲染的时候被插入
this.updater = updater || ReactNoopUpdateQueue;
}

Component.prototype.isReactComponent = {};

/**
* 设置一个状态的子集,必须使用这个方法,你应该确保'this.state'是不可变的,也就是不能修改,每次修改必须返回一个新的传入一个新的state
*
* setState没有确保立即更新,所以在调用这个方法后使用数据可能是旧的。也没有确保setState会立即执行,实际上最终可能会一起执行
* 你可以提供一个回调函数,他将在setState真正完成后执行。
*
* 当setState传入一个函数,他将在未来的某一时间执行,并不是同步,它将使用最新的组件参数(state, props, context)被调用
* 这些参数与this对象上的可以是不同的,因为他可能在receiveProps之后但在shouldComponentUpdate之前,这些新的state, props, and context还没来得及合并到this对象
*/
Component.prototype.setState = function(partialState, callback) {
if (
typeof partialState !== 'object' &&
typeof partialState !== 'function' &&
partialState != null
) {
throw new Error(
'setState(...): takes an object of state variables to update or a ' +
'function which returns an object of state variables.',
);
}

// 更新会被添加到队列,在未来的某一时间更新
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

/**
* 强制更新,它应该只当明确知道我们不在一个DOM事务中才被使用,你可能会想,当你知道一些深层组件的状态已经改变但是setState没有调用的时候去调用它。
* 它将不会触发shouldComponentUpdate,但会触发 `componentWillUpdate` 和 `componentDidUpdate`
*/
Component.prototype.forceUpdate = function(callback) {
this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};

执行流程

现在已经知道了render执行时的大部分必要信息,那render背后的逻辑又是怎样的,这显然是一个复杂的调用过程,但是我们可以通过浏览器的性能分析去查看render函数的调用那个过程

现在你只需要大概了解调用了哪些方法,这些方法会组成后面的调用流程图,后面会详细的描述

初始化事件相关对象

  • registerSimpleEvents 创建对象相关对象
变量名称 变量对象 说明
allNativeEvents Set 集合
保存所有原生事件的名称 例如 0:"cancel"
eventPriorities Map 集
保存事件名称和事件优先级对应关系 例如 click=>0
topLevelEventsToReactNames Map 集
保存原始事件名称和 React 事件的对应关系 例如 "cancel" => "onCancel"
registrationNameDependencies Object
保存 React 事件和原生事件的对应关系 例如 onClick:(1) ['click'] 每个 React 事件对应一个数组用于保存合成事件对应关系
possibleRegistrationNames Object
保存小写的 React 事件名称和正确的驼峰命名事件的对应关系,用于校验用户输入 例如 onclick:onClick
入口

render : ReactDom.render()
createRootImpl : 创建 FiberRootNode 根节点
listenToAllSupportedEvents : 绑定所有原生事件在 root 节点上

render 阶段

unbatchedUpdates : 非批量更新,让用户尽早看见页面内容,如果是 batchedUpdates 会以异步执行
scheduleUpdateOnFiber : 调度 Fiber 节点更新优先级
performUnitOfWork : 以 Fiber 节点为单位,深度优先递归遍历每一个节点
reconcileChildren : 创建对比 Fiber 节点,标记有副作用的节点 (添加,删除,移动,更新)
completeUnitOfWork : 从下至上遍历节点,创建相应的 DOM 节点,并创建 Effects 链表,交给 commit 阶段使用

commit 阶段

commitBeforeMutationEffects: 操作真实节点前执行,会执行getSnapshotBeforeUpdate
commitMutationEffects: 执行节点操作
commitLayoutEffects: 执行副作用函数,包括 componentDidUpdateeffect回调函数

如此复杂的调用栈,是为了解决哪些问题。下一章,让我们感受一下react的设计理念

  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信