0%

背景

虽然我们系统有建设一些监控工具比如Grafana、Prometheus等,但是很多时候还是愿意直接先到服务器上去瞧一眼,特别是客户现场的问题,因为客户现场暂时不会装这些工具。因为我们是做NPMD的,流量大的时候资源消耗就比较大,加上前期先铺功能非功能需求的细节待完善,所以有时候会出现客户现场机器在一段时间后会变得比较慢的情况,这个时候那当然就说劈里啪啦一堆装逼的命令敲上去瞅瞅服务器咋了。

用的比较高频的命令就是ps、grep、top。用的倒是还算一般,不过一直没有深深的了解,所以刚好借着这个机会,深入的学习一下。演示用的是安装centos系统的vps。

ps

这命令应该是我使用的命令中的top one了,该命令用于获取正在运行的进程信息。 我们在看一些服务是否运行的时候通常都是用这个命令。我们可以获取任何用户在当前系统上运行的进程之类的信息,例如进程ID(PID)。

ps命令本身是一个扩展工具,ps –help a一下,会发现有很多的命令选项,说是有80多个。

当然我们只说说常用的。

基本用法

如果在Linux中使用不带任何选项的ps命令,它将显示当前shell中正在运行的进程:

1
ps

只会看到ps和bash。

1
2
3
  PID TTY          TIME CMD
2053 pts/0 00:00:00 ps
31585 pts/0 00:00:00 bash
  • PID是进程的唯一ID
  • TTY是已登录终端用户的类型。pts表示伪终端
  • TIME给您进程运行了多长时间
  • CMD是您运行以启动该过程的命令

很明显,我们并没有得到任何真实,有用的信息。

1.查看所有运行过程

如果要查看自己运行的所有进程,可以将ps命令与选项x一起使用,如下所示:

1
ps -x

x选项将显示所有进程,即使它们与当前tty(终端类型)不相关。

“– ”是可选的,但一般的Linux约定是加上“–”选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  PID TTY      STAT   TIME COMMAND
543 tty1 Ss+ 0:00 /sbin/agetty --noclear tty1 linux
544 ? Ssl 0:01 /usr/bin/python2 -Es /usr/sbin/firewalld --nofork --nopid
546 ? S 0:00 [hwrng]
556 ? I< 0:00 [cryptd]
943 ? Ss 0:00 /sbin/dhclient -1 -q -lf /var/lib/dhclient/dhclient--eth0.lease -pf /var/run/dhclient-eth0.pid -H vultr eth0
1009 ? Ssl 0:53 /usr/bin/python2 -Es /usr/sbin/tuned -l -P
1012 ? Ssl 0:59 /usr/sbin/rsyslogd -n
1052 ? Ss 0:00 nginx: master process /usr/sbin/nginx
1265 ? Ss 0:02 /usr/libexec/postfix/master -w
12385 ? R 0:00 [kworker/u2:0-ev]
12684 ? Ss 0:00 sshd: root@pts/0
12688 ? Ss 0:00 sshd: root@notty
12690 pts/0 Ss 0:00 -bash

上面输出中的STAT表示过程状态代码。有兴趣可以查一下详细的说明。其实很少会看到仅使用选项x的ps命令。通常以这种方式伴随选项u:

1
ps -ux

使用选项u,您将获得有关每个进程的详细信息:

1
2
3
4
5
6
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
abhishek 503 0.0 0.4 681580 37516 pts/0 Sl 18:09 0:00 gedit
abhishek 2245 0.0 0.0 11300 1496 ? S 18:37 0:00 /usr/bin/ssh-agent -D -a /run/user/1000/keyring/.ssh
abhishek 3039 0.0 0.0 77344 3508 ? Ss 10:37 0:00 /lib/systemd/systemd --user
abhishek 3040 0.0 0.0 114632 360 ? S 10:37 0:00 (sd-pam)
abhishek 3054 0.0 0.1 517104 11512 ? SLl 10:37 0:01 /usr/bin/gnome-keyring-daemon --daemonize --login

如您所见,现在您获得了每个进程的用户名以及CPU 使用率内存使用率。RSS显示该进程当前在RAM中有多少内存,而VSZ显示该进程总共有多少虚拟内存。

2.使用ps aux命令查看所有正在运行的进程

您可能会一直在Linux教程和文档中看到ps -aux或看到ps aux它们。

使用添加的-a选项,您可以查看Linux系统上所有用户的运行进程。

1
ps -aux

命令输出与ps -ux相同,但是现在您也具有其他用户的进程。由于使用-u选项,您可以识别哪个进程属于哪个用户。

3.在Linux中使用ps -ef命令查看所有正在运行的进程

除了ps -aux以外,您还可以使用-e命令列出所有正在运行的进程。通常的做法是将与“f”结合使用,以获得用于运行进程的命令的完整列表。

1
ps -ef

还可以加上“H”,显示所有的父进程以及其下的子进程一起:

1
ps -efH
4.查看特定用户的所有正在运行的进程

要获取有关特定用户运行的所有进程的信息,可以将“-U”选项与用户名一起使用:

1
ps -U user_name

例如,我可以看到root用户正在运行的所有进程是这样的:

1
2
3
4
5
6
7
8
9
ps -U root
PID TTY TIME CMD
1 ? 00:00:41 systemd
2 ? 00:00:00 kthreadd
3 ? 00:00:00 rcu_gp
4 ? 00:00:00 rcu_par_gp
8 ? 00:00:00 mm_percpu_wq
9 ? 00:00:03 ksoftirqd/0
10 ? 00:01:22 rcu_sched
5.查看小组运行的所有进程

您还可以通过按组名或组ID来对正在运行的进程进行分类:

1
ps -G group_name_or_id

您可以与选项f结合使用以获取完整列表。

6.获取程序的所有出现次数和PID

ps命令的基本用法之一是获取正在运行的程序的进程ID(PID)。通常,当要终止行为异常的程序时,将搜索该程序的所有实例,获取其PID并使用kill命令终止该过程。

1
ps -C program__name

例如,如果我必须找到apt软件包管理器的所有正在运行的实例:

1
2
3
ps -C apt
PID TTY TIME CMD
11425 pts/1 00:00:00 apt

您也可以使用grep命令获得类似的结果。

1
ps aux | grep program_name
7.获取有关PID的过程信息

好的!您有一个PID,但您不知道它属于哪个进程。您可以通过以下方式使用ps命令从其PID查找过程信息,N即PID:

1
ps -pN

您可以通过使用逗号分隔多个PID,以使用多个PID:

1
ps -pN1,N2,N3

grep

其实上面提到了一点,结合ps命令搜索进程,所以可以看出grep命令就像一个过滤器一样的作用,grep在文件中搜索匹配条件的内容,并显示包含该条件的所有行。

基本用法

1
2
3
4
5
[root@vultr ~]# grep --help
Usage: grep [OPTION]... PATTERN [FILE]...
Search for PATTERN in each FILE or standard input.
PATTERN is, by default, a basic regular expression (BRE).
Example: grep -i 'hello world' menu.h main.c
1
2
3
4
5
$cat > geekfile.txt
unix is great os. unix is opensource. unix is free os.
learn operating system.
Unix linux which one you choose.
uNix is easy to learn.unix is a multiuser os.Learn unix .unix is a powerful.

1.不区分大小写的搜索: -i选项允许在给定文件中不区分大小写地搜索字符串。比如“ UNIX”,“ Unix”,“ unix”等词都可匹配。

1
$grep -i "UNix" geekfile.txt

Output:

1
2
3
unix is great os. unix is opensource. unix is free os.
Unix linux which one you choose.
uNix is easy to learn.unix is a multiuser os.Learn unix .unix is a powerful.

2. 显示与条件匹配的文件名

1
2
3
4
5
$grep -l "unix" *

or

$grep -l "unix" f1.txt f2.txt f3.xt f4.txt

Output:

1
geekfile.txt

3. 检查文件中的整个单词 : 默认情况下,即使grep在文件中找到子字符串,它也只会匹配给定的字符串。 grep的-w选项使其仅匹配整个单词。

1
$ grep -w "unix" geekfile.txt

Output:

4.使用grep -n在显示输出时显示行号:要显示匹配行的文件的行号。

1
$ grep -n "unix" geekfile.txt

Output:

1
2
1:unix is great os. unix is opensource. unix is free os.
4:uNix is easy to learn.unix is a multiuser os.Learn unix .unix is a powerful.

5.反转模式匹配:您可以使用-v选项显示与指定搜索字符串模式不匹配的行。

1
$ grep -v "unix" geekfile.txt

Output:

1
2
learn operating system.
Unix linux which one you choose.

6.匹配以字符串开头的行: ^正则表达式模式指定行的开头。可以在grep中使用它来匹配以给定的字符串开头的行

1
$ grep "^unix" geekfile.txt

Output:

1
unix is great os. unix is opensource. unix is free os.

7.匹配以字符串结尾的行:“ $”正则表达式模式指定行的结尾。可以在grep中使用它来匹配以给定字符串结尾的行。

1
$ grep "os$" geekfile.txt

Output:

1
2
[root@vultr home]# grep "system.$" geekfile.txt
learn operating system.

grep正则有兴趣的可以看下regular-expression-grep。通常我是ps和grep结合在一起用,比如:

1
2
3
[root@vultr home]# ps -ef |grep -i "v2ray"
root 1008 1 0 Apr17 ? 00:01:57 /usr/bin/v2ray/v2ray -config /etc/v2ray/config.json
root 29435 28377 0 11:09 pts/0 00:00:00 grep --color=auto -i v2ray

小结一下

Linux grep command options Description
-i 忽略大小写
-w 强制PATTERN只匹配整个单词
-v 反向匹配
-n 输出匹配的行号
-h 在输出中禁止Unix文件名前缀
-r 在Linux上递归搜索目录
-R 就像-r一样,但是遵循所有符号链接
-l 仅打印具有选定行的文件名称称
-c 每个文件仅打印选定行的数量
–color 颜色显示匹配的内容

top

top命令提供系统信息的快速概述,它提供了正在运行的系统的动态实时视图,每3秒刷新一次(默认情况下)。 视图内容分为两部分:

  1. 系统的摘要信息
  2. 当前由Linux内核管理的进程或线程的列表。

一旦运行此命令,它将打开一个交互式命令模式,其中上半部分将包含进程和资源使用情况的统计信息。 下半部分包含当前正在运行的进程的列表。 按q会退出命令模式。

上半部分

如图,在终端的顶部,我们获得了概览数据,包括当前任务数、内存使用率和cpu负载。

第一行:任务队列信息,同uptime命令的结果一样。

4:06:23 — 当前系统时间

up 4 days, 22:36 — 系统已经运行了4天22小时36分钟(在这期间系统没有重启过的吆!)

2 users — 当前有2个用户登录系统

load average: 0.00, 0.00, 0.00 — load average后面的三个数分别是1分钟、5分钟、15分钟的负载情况。例如,负载为1.0表示当前负荷为100%。

load average:平均负载部分表示一分钟,五分钟和十五分钟的平均“负载”,数据是每隔5秒钟检查一次活跃的进程数,然后按特定算法计算出的数值。。 “负载”是系统执行的计算工作量的度量。 在Linux上,负载是在任何给定时刻处于R和D状态的进程数。 “平均负载”值为您提供了一个等待时间,可以用来衡量您需要等待多长时间才能完成工作。

在多核系统上,您应该首先将平均负载除以CPU核数以得到类似的度量。

第二行:Tasks — 任务(进程)

系统进程也称为任务。进程可以以许多不同的方式运行,并使用各种算法确定优先级。 这有助于优化计算机执行任务的方式和时间。

1
Tasks:  83 total,   1 running,  48 sleeping,   0 stopped,   0 zombie

对状态进行简单的说明:

State Description
Running 运行中/待处理(Active / in Queue to be Processed)
Sleeping 休眠(Waiting for a Process to Complete)
Stopped 停止(Interrupted by Job Control Signal )
Zombie 僵尸进程(Made up of “Orphaned” Child Tasks / No Longer Running)

第三行:cpu状态信息

1
%Cpu(s):  0.3 us,  0.0 sy,  0.0 ni, 99.7 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st

CPU使用率部分显示了在各种任务上花费的CPU时间的百分比。

Abbreviation Description
us CPU在用户空间(各个进程使用)中花费在执行进程上的时间
sy 运行内核空间(内核使用)进程所花费的时间
ni 优先级
id CPU保持空闲的时间
wa CPU等待I / O完成所花费的时间
hi 处理硬件中断花费的时间(中断是向处理器发出有关需要立即关注的事件的信号)
si 处理软件中断花费的时间(中断是向处理器发出有关需要立即关注的事件的信号)
st CPU在虚拟机上花费的时间

第四行:内存状态信息

“内存”部分显示有关系统内存使用情况的信息。 标记为“ Mem”和“ Swap”的行分别显示有关RAM和交换空间的信息。 简而言之,交换空间是硬盘的一部分,就像RAM一样使用。 当RAM使用率接近满时,RAM的不常用区域将写入交换空间,以备以后需要时检索。 但是,由于访问磁盘的速度很慢,因此过多地依赖交换会损害系统性能。

1
2
KiB Mem :  1006744 total,   200872 free,   146212 used,   659660 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 660908 avail Mem

“avail Mem”值是可以分配给进程而不会引起更多交换的内存量。

Linux内核还尝试以各种方式减少磁盘访问时间。 它在RAM中维护“磁盘缓存”,在RAM中存储磁盘的常用区域。 另外,磁盘写操作存储在“磁盘缓冲区”中,内核最终将其写出到磁盘。 它们消耗的总内存为“ buff / cache”值。缓存使用的内存将在需要时分配给进程。

下半部分

在终端的下部,我们有一个任务信息的表格,其中包含许多详细信息,先简单解释一下表头。

Abbreviation Description
PID 进程ID(唯一正整数)
USER 用户名
PR 代表任务的优先级
NI 代表任务的价值。 负值表示优先级较高,正值表示优先级较低
VIRT 任务使用的虚拟内存
RES 任务使用的物理内存
SHR 任务使用的共享内存
S 进程状态(正在运行,已停止等)
%CPU CPU 负载
%MEM 物理内存/总内存的百分比
TIME + 自启动以来该进程使用的总CPU时间,精确到百分之一秒
COMMAND 进程名称

基本用法

杀进程

如果您想终止进程,只需在top运行时按“ k”。 这将弹出提示,提示您输入进程的进程ID,然后按Enter。

如下图中的:PID to signal/kill [default pid = 1]

1
2
3
4
5
6

Tasks: 81 total, 2 running, 46 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.0 us, 0.3 sy, 0.0 ni, 99.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 1006744 total, 202708 free, 144288 used, 659748 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 662828 avail Mem
<span style=color:red>PID to signal/kill [default pid = 1]</span>

进程列表排序

使用诸如top之类的工具的最常见原因之一就是找出哪个进程消耗最多的资源。 您可以按以下键对列表进行排序(记住是大写哦):

  • “ M”按内存使用量排序
  • “ P”按CPU使用率排序
  • “ N”按进程ID排序
  • “ T”按运行时间排序

默认情况下,top按降序显示所有结果。 但是,您可以通过按’R’切换到升序。

您也可以使用-o+表头的名称对列表进行排序。 例如,如果要按CPU使用率对进程进行排序,可以使用以下方法:

1
top -o %CPU

查看线程

1
2
3
top -H
//出现的视图中的Tasks会变为Threads
Threads: 103 total, 3 running, 68 sleeping, 0 stopped, 0 zombie

显示完整路径(绝对路径)

默认情况下,top不显示程序的完整路径,也不区分内核空间进程和用户空间进程。 如果您需要此信息,请在top运行时按“ c”,再次按“ c”返回默认设置。内核空间进程周围带有方括号标记。

1
2
3
4
5
2036 root      20   0  158820   8852   7532 S  0.3  0.9   0:00.01 sshd: root [priv]
1 root 20 0 46124 8060 5616 S 0.0 0.8 0:15.58 /usr/lib/systemd/systemd --switched-root --system --deserialize 22
2 root 20 0 0 0 0 S 0.0 0.0 0:00.13 [kthreadd]
3 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 [rcu_gp]
4 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 [rcu_par_gp]

或者运行以下命令

1
top -c

树状显示

如果希望查看进程的子级父级。 按“ V”。比如下图所示systemd为系统启动的第一个进程,该进程又创建了sshd等其它进程…。

1
2
3
4
5
6
7
8
9
10
11
12
13
    1 root      20   0   46124   8060   5616 S  0.0  0.8   0:15.58 systemd
439 root 20 0 37204 3708 3396 S 0.0 0.4 2:00.59 `- systemd-journal
1012 root 20 0 258612 9696 8084 S 0.0 1.0 1:14.57 `- rsyslogd
1052 root 20 0 131160 2188 60 S 0.0 0.2 0:00.00 `- nginx
1053 nginx 20 0 132160 10100 7252 S 0.0 1.0 0:46.42 `- nginx
1265 root 20 0 89716 4836 3804 S 0.0 0.5 0:02.67 `- master
1268 postfix 20 0 89888 6600 5596 S 0.0 0.7 0:00.65 `- qmgr
1608 postfix 20 0 89820 6628 5624 S 0.0 0.7 0:00.01 `- pickup
13138 root 20 0 112936 7732 6708 S 0.0 0.8 0:16.48 `- sshd
28367 root 20 0 159228 10264 8572 S 0.0 1.0 0:03.73 `- sshd
28377 root 20 0 115464 3624 3208 S 0.0 0.4 0:00.13 `- bash
2053 root 20 0 161908 4444 3804 R 0.3 0.4 0:00.21 `- top

列出指定用户的进程

要列出某个用户的进程,请在top运行时按“ u”。

或者用以下命令:

1
top -u root

过滤进程

如果只想查看某些进程,可以使用top的过滤。 要激活此模式,请按“ o” /“ O”。 顶部会出现一个提示,您可以在此处键入过滤器表达式。如下图中的add filter #1 (ignoring case) as: [!]FLD?VAL

1
2
3
4
5
6
7
KiB Mem :  1006744 total,   198520 free,   147308 used,   660916 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 659616 avail Mem
add filter #1 (ignoring case) as: [!]FLD?VAL
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 20 0 46124 8060 5616 S 0.0 0.8 0:15.74 systemd
2 root 20 0 0 0 0 S 0.0 0.0 0:00.13 kthreadd
3 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 rcu_gp

过滤器表达式是指定属性和值之间关系的语句。 过滤器的一些示例是:

  • COMMAND = getty:过滤在COMMAND属性中包含“ getty”的进程。
  • !COMMAND = getty:过滤在COMMAND属性中没有“ getty”的进程。
  • %CPU> 3.0:筛选CPU利用率超过3%的进程。

添加过滤器后,您可以通过添加更多过滤器来进一步简化操作。 要清除添加的所有过滤器,请按“ =”。

更改CPU和内存统计信息的默认外观

如果觉得top显示CPU和内存统计信息的默认方式不喜欢。 可以按“ t”和“ m”来更改CPU和内存统计信息的样式。

保存设置

如果您对top的输出进行了任何更改,则可以按“ W”将其保存以备后用。 top将其配置写入主目录中的.toprc文件。

感谢

最近两周被平台组指名道姓拉去当了两周的苦力,写业务层代码,因为逻辑比较复杂数据输入比较多样,所以导致使用集合的概率很高,且常常伴随着过滤、排序等操作,继而用到了很多Streams提供的方法,遂做个简单记录。

Streams

Stream是Java 8中引入的新的抽象层,它提供了一些类似SQL语句的声明性方式处理数据。

流操作分为中间操作和最终操作,元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。 流管道由一个源(例如Collection,数组,生成器函数或I / O通道)组成; 随后是零个或多个中间操作,例如Stream.filter或Stream.map; 以及诸如Stream.forEach或Stream.reduce之类的终端操作。

即将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道中插入节点上进行处理, 比如筛选, 排序,聚合等。

当然为什么喜欢用它还是因为Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。会让代码看起来更加简洁当然通常也会更加高效。

API

如上图api很多,其中又可以按照最开始说的分为中间操作、最终操作两类,中间(Intermediate)操作是可以零个或者多个但是最终(Terminal)操作只能有一个,能力有限我就列举一下我常用的。

可能我们需要注意的一个概念:因为一个 Stream 可以进行多次中间操作,那是不是就会对 Stream 的每个元素进行转换多次,即时间复杂度就是 N(转换次数)个 ?其实不是这样的,转换操作都是 lazy 的,多个转换操作只会在 Terminal 操作的时候融合起来,一次循环完成。

中间操作(intermediate operation)

API 说明
filter 用于按指定条件过滤元素
map map方法将每个元素映射到其相应的结果,通常用于list转换为map
limit limit返回流中的前N个元素,同SQL的limit
sorted 对Stream中的元素进行排序
distinct 删除重复项

最终操作(terminal operation)

API 说明
forEach 迭代Stream中的元素
sum 对Stream中的元素求和
collect 可以接受各种参数并将流元素累加成集合
reduce 这个方法的主要作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。其实sum等这类也可以说是reduce。
max 获取Stream中符合条件的最大值
findAny 这是一个 termimal 操作,它总是返回 Stream 的符合条件的元素,或者空。注意它的返回值类型是Optional(为了避免空指针)。
anyMatch Stream 中只要有一个元素符合传入的 条件。

示例

列出几个工作中实践的例子

1
2
3
// 用于按指定条件过滤元素并且把符合条件的添加到指定的集合
List<CiStrategy> sorted = new ArrayList<>(cis.size());
cis.stream().filter(it -> it.getType() == StrategyType.GLOBAL).forEach(sorted::add);
1
2
3
4
5
6
7
8
9
10
11
// 拿输入的id到stream中比较是否存在,如果不存在则返回null
final List<String> agentEnableIds = getEnableAgenIdsByApp(query.getAppId());

String enableId = agentEnableIds.stream()
.filter(id -> agentId.equals(id))
.findAny()
.orElse(null);
//未开启xx
if (isNullOrEmpty(enableId)) {
return Collections.emptyList();
}
1
2
// 两个集合中,集合B中找到符合集合A中的数据,最终得到符合条件的元素集合
agentCmsList.stream().filter(ag -> agentEnableIds.contains(ag.getId())).collect(Collectors.toList());
1
2
3
4
5
6
// checkList的元素作为IpV4Ranges中toRange方法的参数,最终把toRange返回值转换为集合
List<String> checkList = splitter.splitToList(scopesToCheck);
List<IpRange> rangesToCheck = checkList.stream().map(IpV4Ranges::toRange).collect(toList());

// failed集合中,去重后的类型失败的有哪些
List<String> types = failed.stream().map(Quality.Metric::getType).distinct().collect(toList());
1
2
3
4
// Alarm的list集合,转换为map,key为Alarm的appId,value为Alarm
List<Alarm> dealingAlarms = dealingAlarmPage.getList();
Map<String, Alarm> dealingAlarmMap = dealingAlarms.stream().collect(Collectors.
toMap(Alarm::getAppId, Function.identity()));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 根据状态排序,如果状态一样按照名称排序
public IBoardAppDataList sortByAppStatus(String[] statusOrder) {
return new BoardAppDataList(this.stream().sorted(new Comparator<BoardAppData>() {

private int findStatus(String[] a, BoardAppData target) {
return IntStream.range(0, a.length)
.filter(i -> String.valueOf(target.getStatus()).equals(a[i]))
.findFirst()
.orElse(-1); // return -1 if target is not found
}

@Override
public int compare(BoardAppData o1, BoardAppData o2) {
int c = findStatus(statusOrder, o1) - findStatus(statusOrder, o2);
if (c == 0) {
return o1.getAppName().compareTo(o2.getAppName());
} else {
return c;
}
}
}).collect(Collectors.toList()));
}
1
2
3
// 判断输入参数里是否存在任意一个满足haveXssCondition
Set<String> keys = paramsObj.keySet();
return keys.stream().anyMatch(key -> haveXssCondition(uri, paramsObj, key));

最后

很显然我这篇仅仅是一个简单的记录文档,如果需要深入了解,还是系统的看相关的文档和源码。而且我主要用的是stream其实还有parallelStream,有兴趣的大家可以看看。

感谢:

搬运DevOps实施手册

  1. 建立愿景与方向
  2. 度量:组织、系统现状
  3. 准入条件。查看是否满足实施 DevOps 的准入条件。
  4. 探索可行方案。即 MVP 尝试
  5. MVP。一次快速的 DevOps 过程和结果的 showcase。
  6. 精细化 DevOps 实施
  7. 回顾优化
  8. 规模化 DevOps 落地

对于技术债务,它的利息表现为系统的不稳定性,以及由于临时性手段和缺乏合适的设计、文档工作和测试带来的不断攀升的维护成本。 —— 《架构师应该知道的 97 件事》

是的这又是一篇搬运文章,谁让我放荡不羁爱打野呢。作者博客地址Preethi Kasireddy

之前有聊过一篇关于Modules的文章,主要讲什么是模块以及为什么需要模块。这篇主要讲怎么捆绑(打包)模块也就是模块使用的深一步探讨。

什么是模块捆绑?

从高层次上讲,模块捆绑只是将一组模块(及其依赖项)按正确顺序拼接到单个文件(或一组文件)中的过程。

为什么需要模块捆绑?

如果项目中使用的模块过多,那么你就需要一个一个的使用script 标签引入到html中去,然后html运行的时候会一个一个的去加载你引入的模块。

像上图那样 one by one 如果用的模块一多,页面的延迟就会过久,用户体验不好。

为了解决这个问题,我们把所有文件捆绑,或“拼接”到一个文件(有时也是一组文件)中,正是为了减少请求数

另一个加速构建操作的常用方法是,“缩减”捆绑后的代码。缩减,是把源代码中不需要的字符(如空格、评论、换行符等等)移除,从而减小了代码的总体积,却不改变其功能。

数据更少,浏览器处理的时间就更短,比如经常见到的”xxxmin.js”,相比完整版,缩减版小了好多。

捆绑模块有哪些不同的方式?

如果你用的是一种标准模块模式(在 第一部分 讨论过)来定义模块,比如IIFE、全局导入等,其实就是直接拼接和缩减几堆纯 JavaScript 代码,通常则不用借助工具。

但如果你用的是非原生模块系统,浏览器不能像 CommonJS、AMD、甚至原生 ES6 模块格式那样解析,你就需要用专门工具先转化成排列正确、浏览器可识别的代码。这正是 Browserify、RequireJS、Webpack 和其他模块捆绑工具,或模块加载工具粉墨登场的时候。

下面就来过一遍常用的模块捆绑方法:

Bundling CommonJS

第一部分所知,CommonJS同步加载模块,它对于浏览器是不适用的。 解决此问题其中一个方法是用Browserify。 Browserify是一个为浏览器编译CommonJS模块的工具。

例如,假设您有一个main.js文件,该文件导入一个模块来计算数字数组的平均值:

1
2
3
var myDependency = require('myDependency');
var myGrades = [93,95,88,0,91];
var myAverageGrade = myDependency.average(myGrades);

因此,在这种情况下,我们只有一个依赖项(myDependency)。 使用以下命令,Browserify将递归所有必需的模块(从main.js开始)并把它们捆绑到一个名为bundle.js的文件中:

1
browserify main.js -o bundle.js

Browserify通过跳入每个require 调用的AST解析来遍历项目的整个依赖关系图来实现此目的。 一旦确定了依存关系的结构,便会将它们按正确的顺序捆绑到一个文件中。当需要使用时,则将“bundle.js” 文件的<script>标记插入到html中,以确保所有源代码都在一个HTTP请求中下载。

同样,如果您有多个具有多个依赖项的文件,则只需告诉Browserify您的条目文件是什么,然后静静等待Browserify进行文件捆绑。

最后,可以使用Minify-JS之类的工具来缩小捆绑代码。

Bundling AMD

如果是AMD,则需要使用诸如RequireJS或Curl之类的AMD加载器。 模块加载器(相对于捆绑器)动态加载程序需要的模块。

AMD与CommonJS的主要区别之一是它加载模块的方式为异步。 所以对于AMD,从技术上讲,实际上不需要构建步骤,因为是异步加载模块的,因此无需将模块捆绑到一个文件中。这意味着运行程序时仅会逐步下载执行该命令所必需的那些文件,而不是在用户首次访问该页面时立即下载所有文件。

但是,实际上,随着时间的推移,随着产品复杂性的增加,随着程序依赖的模块越来越多, 大多数情况下仍然使用构建工具来捆绑和缩小其AMD模块,以实现最佳性能,例如使用诸如RequireJS优化器,r.js之类的工具。

总的来说,AMD与CommonJS之间的区别在于:AMD是异步加载,CommonJS时同步加载。

有关CommonJS与AMD的有趣讨论,请查看Tom Dale博客中的这篇文章。

ES6 modules

接下来谈谈ES6模块,它在某些方面可以减少将来对捆绑器的需求。

当前的JS模块格式(CommonJS,AMD)和ES6模块之间最重要的区别是ES6模块在设计时考虑了静态分析。 这意味着在导入模块时,将在编译时(即在脚本开始执行之前)解决导入问题。 这使我们可以在运行程序之前删除其他模块未使用的导出。 删除未使用的导出可以节省大量空间,从而减轻浏览器的压力。

一个常见的问题是:与使用UglifyJS之类的代码来缩小代码时所产生的死代码消除有什么不同?这得分情况 。

有时,清除死代码可能在UglifyJS和ES6模块之间完全相同,而有时则不行。

使ES6模块与众不同的是消除死代码的不同方法,称为tree shaken。 tree shaken本质上是消除死代码的反向操作。 它仅包含捆绑软件需要运行的代码,而不排除捆绑软件不需要的代码。 让我们看一个例子:

假设我们有一个utils.js文件,其中包含以下函数,我们使用ES6语法导出每个函数::

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
export function each(collection, iterator) {
if (Array.isArray(collection)) {
for (var i = 0; i < collection.length; i++) {
iterator(collection[i], i, collection);
}
} else {
for (var key in collection) {
iterator(collection[key], key, collection);
}
}
}


export function filter(collection, test) {
var filtered = [];
each(collection, function(item) {
if (test(item)) {
filtered.push(item);
}
});
return filtered;
}


export function map(collection, iterator) {
var mapped = [];
each(collection, function(value, key, collection) {
mapped.push(iterator(value));
});
return mapped;
}


export function reduce(collection, iterator, accumulator) {
var startingValueMissing = accumulator === undefined;


each(collection, function(item) {
if(startingValueMissing) {
accumulator = item;
startingValueMissing = false;
} else {
accumulator = iterator(accumulator, item);
}
});


return accumulator;
}

接下来,假设我们不知道要在程序中使用哪些utils函数,因此我们继续将所有模块导入main.js中,如下所示:

1
import * as Utils from ‘./utils.js’;

然后我们最终只使用each函数:

1
2
3
4
import * as Utils from ‘./utils.js’;


Utils.each([1, 2, 3], function(x) { console.log(x) });

模块加载后,main.js文件的“tree shaken”版本将如下所示::

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function each(collection, iterator) {
if (Array.isArray(collection)) {
for (var i = 0; i < collection.length; i++) {
iterator(collection[i], i, collection);
}
} else {
for (var key in collection) {
iterator(collection[key], key, collection);
}
}
};


each([1, 2, 3], function(x) { console.log(x) });

所以你会发现除了 each函数其他函数都没有了.

Meanwhile, if we decide to use the filter function instead of the each function, we wind up looking at something like this:

1
2
3
4
import * as Utils from ‘./utils.js’;


Utils.filter([1, 2, 3], function(x) { return x === 2 });

The tree shaken version looks like:

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 each(collection, iterator) {
if (Array.isArray(collection)) {
for (var i = 0; i < collection.length; i++) {
iterator(collection[i], i, collection);
}
} else {
for (var key in collection) {
iterator(collection[key], key, collection);
}
}
};


function filter(collection, test) {
var filtered = [];
each(collection, function(item) {
if (test(item)) {
filtered.push(item);
}
});
return filtered;
};


filter([1, 2, 3], function(x) { return x === 2 });

注意这次是如何同时包含filtereach的。 这是filter使用each,因此我们需要导出这两个函数模块才能正常工作.

有兴趣可以试试Rollup.js live demo and editor。看看它对Tree-Shaking的应用。

Webpack

就打包器而言,Webpack是新手。 它的设计与您使用的模块系统无关,允许开发人员酌情使用CommonJS,AMD或ES6。

您可能想知道为什么当我们已经有其他捆绑器(如Browserify和RequireJS)完成工作并做得很好时,为什么我们需要Webpack。 其中一个原因是,Webpack提供了一些有用的功能,例如“代码拆分”-一种将您的代码库拆分为“块”(按需加载)的方法。

例如,如果您的Web应用程序仅在某些情况下需要使用代码块,则将整个代码库放入单个大型捆绑文件中可能没有效率。 在这种情况下,您可以使用代码拆分将代码提取为可按需加载的捆绑块,从而避免了在大多数用户只需要应用程序核心的情况下使用较大的前期有效负载的麻烦。

代码拆分只是Webpack提供的众多引人注目的功能之一,并且Internet上对于Webpack还是Browserify更好是有很多意见。 以下是一些我认为有助于解决问题的更高级的讨论::

小结

这篇文章比较老了,但是它对我来说是在于扫盲,毕竟概念性的东西是不过时的。

  • CommonJs主要针对服务端,AMD主要针对浏览器端。(顺便提一下,针对服务器端和针对浏览器端有什么本质的区别呢?服务器端一般采用同步加载文件,也就是说需要某个模块,服务器端便停下来,等待它加载再执行。而浏览器端要保证效率,需要采用异步加载,这就需要一个预处理,提前将所需要的模块文件并行加载好。)

  • AMD是预加载,在并行加载js文件同时,还会解析执行该模块(因为还需要执行,所以在加载某个模块前,这个模块的依赖模块需要先加载完成),即不能懒加载。不过因为AMD并行解析,加载快速,所以同一时间可以解析多个文件。当然也因为并行加载,异步处理,加载顺序不一定,所以不注意的情况下可能会造成程序会出现一些问题。

  • ES6 module的静态分析,配合tree sharking特性,做到用那些导哪些,也不用像AMD那样需要用define包装函数,当然这篇文章比较老了啊,现在其实ES6 module已经是普及了。

是这样的,最近呢在捣鼓一系列升级的问题,刚好就遇到了Gulp从3升到4的过程,所以借机呢稍微深入的了解了一下Gulp,在此做个简单的记录。

Gulp我一开始理解的他就是个样式处理工具,但实际了解下来,才发现它原来跟Grunt一样是个构建工具。

那就开整吧,我按我的节奏来哈:

  1. 了解一些底层,知道是怎么来的。
  2. 了解基本概念,不但要知道是怎么来的,还要知道是干啥的。
  3. 了解语法 ,3->4升级实操

一些底层

Gulp 是基于 Node.js 的项目,它的核心使用的是Node.js四种流中的Transform - 在读写过程中可以修改或转换数据的 Duplex流 。即可读又可写的流,它会对传给它的对象做一些转换的操作。

因为基于Streams,而Streams和其它数据处理方法相比最大的优点:

  1. 内存效率:我们无需先将大量数据加载到内存中即可进行处理。它就像往河里(Pipe)倒水一样,一桶水你可以分很多次倒。
  2. 时间效率:拥有数据后立即开始处理数据所需的时间大大减少,而不必等到整个有效负载都传输完成才开始处理。还是倒水的例子,你倒入河里(Pipe)马上就流走了不会等着一桶水倒完之后才流走。

Streams的cheatsheet

Duplex:

Streams内部实现流程图:

想了解细节的推荐去看stream-handbookVideo introduction to node.js streams。通过学习之后我真的认为Node的Streams是正儿八经牛X的存在,对我来说简直是一颗遗珠啊。

基本概念

Task

每个 gulp 任务(task)都是一个异步的 JavaScript 函数,此函数是一个可以接收 callback 作为参数的函数,或者是一个返回 stream、promise、event emitter、child process 或 observable (后面会详细讲解) 类型值的函数。

任务(tasks)可以是 public(公开)private(私有) 类型的。

  • 公开任务(Public tasks) 从 gulpfile 中被导出(export),可以通过 gulp 命令直接调用。
  • 私有任务(Private tasks) 被设计为在内部使用,通常作为 series()parallel() 组合的组成部分。

Gulpfile

Gulp 允许你使用现有 JavaScript 知识来书写 gulpfile 文,gulpfile 是项目目录下名为 gulpfile.js (或者首字母大写 Gulpfile.js,就像 Makefile 一样命名)的文件,在运行 gulp 命令时会被自动加载。在这个文件中,你经常会看到类似 src()dest()series()parallel() 函数之类的 gulp API,除此之外,纯 JavaScript 代码或 Node 模块也会被使用。任何导出(export)的函数都将注册到 gulp 的任务(task)系统中。

处理文件

gulp 暴露了 src()dest() 方法用于处理计算机上存放的文件。

src() 接受 glob 参数,并从文件系统中读取文件然后生成一个 Node 流(stream)。它将所有匹配的文件读取到内存中并通过流(stream)进行处理。

流(stream)所提供的主要的 API 是 .pipe() 方法,用于连接转换流(Transform streams)或可写流(Writable streams)。

dest() 接受一个输出目录作为参数,并且它还会产生一个 Node 流(stream),通常作为终止流(terminator stream)。当它接收到通过管道(pipeline)传输的文件时,它会将文件内容及文件属性写入到指定的目录中。gulp 还提供了 symlink() 方法,其操作方式类似 dest(),但是创建的是链接而不是文件( 详情请参阅 symlink() )。

大多数情况下,利用 .pipe() 方法将插件放置在 src()dest() 之间,并转换流(stream)中的文件。

Glob

glob 是由普通字符和/或通配字符组成的字符串,用于匹配文件路径。可以利用一个或多个 glob 在文件系统中定位文件。

*(一个星号):在一个字符串片段中匹配任意数量的字符,包括零个匹配。对于匹配单级目录下的文件很有用。

** (两个星号):在多个字符串片段中匹配任意数量的字符,包括零个匹配。 对于匹配嵌套目录下的文件很有用。请确保适当地限制带有两个星号的 glob 的使用,以避免匹配大量不必要的目录。

! (取反):由于 glob 匹配时是按照每个 glob 在数组中的位置依次进行匹配操作的,所以 glob 数组中的取反(negative)glob 必须跟在一个非取反(non-negative)的 glob 后面。第一个 glob 匹配到一组匹配项,然后后面的取反 glob 删除这些匹配项中的一部分。如果取反 glob 只是由普通字符组成的字符串,则执行效率是最高的。

插件

Gulp 插件实质上是 Node 转换流(Transform Streams),它封装了通过管道(pipeline)转换文件的常见功能,通常是使用 .pipe() 方法并放在 src()dest() 之间。他们可以更改经过流(stream)的每个文件的文件名、元数据或文件内容。

Gulp

Gulp的cheatsheet

Gulp 3 - > 4 实操

3其实算是废弃了,当然还是可以用的,只是不维护了。所以升一升还是极好的。

  1. 现卸载现有的
1
2
npm uninstall gulp --save-dev
npm uninstall gulp -g

你以为升级完之后是这样色儿的

但是一执行是这样的:

1
AssertionError [ERR_ASSERTION]: Task function must be specified

这是因为Gulp4只支持 2 个参数的 gulp.task了,所以就意味着咱们以前的代码得改了,那咱们就有疑问了怎么改呢?先了解下改了什么。

series、parallel

官网说了:Gulp.js 4.0引入了series()和parallel()方法来组合任务:

  • series(…)按指定的顺序一次运行一个任务,并返回一个按给定的任务 / 函数的顺序执行的函数。

  • parallel(…)以任何顺序同时运行任务,并返回一个能并行执行给定的任务/函数的函数

    可见Gulp做出了很大的努力来实现对任务运行方式的更多控制,提供了选择顺序或并行执行任务的能力,避免之前需要添加别的依赖(传统上是使用 run-sequence)或者丧心病狂地手动分配任务执行的依赖。。

所以,如果之前你有这样一个任务:

1
2
3
gulp.task('copy_css', ['clean_temp'], function() {
...
});

它将会变为:

1
2
3
gulp.task('copy_css', gulp.series('clean_temp', function() {
...
}));

当做出这个改变时,不要忘了你的任务函数现在在 gulp.series 的回调函数里。所以你需要在尾部多出来的那个括号。这很容易被忽略。

注意到 gulp.sereisgulp.parallel 会返回函数,所以它们可以被嵌套。当你的任务有多个依赖时,你可能需要经常地嵌套它们。

例如,这个常见的模式:

1
2
3
gulp.task('default', ['copy_css', 'copy_image'], function() {
...
});

将会变为:

1
2
3
gulp.task('default', gulp.series(gulp.parallel('copy_css', 'copy_image'), function() {
...
}));

依赖问题

这是一个坑点,在 Gulp 3 中,如果你为多个任务指定了同一个依赖,并且它们都在运行时,Gulp 会意识到它们都依赖相同的任务,然后只执行一次这个被依赖的任务。而在 Gulp 4 中,我们不再指定”依赖”,而是使用 seriesparallel 来组合函数,这导致 Gulp 不能判断哪些任务在当它只应运行一次时会被多次运行。所以我们需要改变我们对依赖的处理方式。我们需要把依赖从任务中抽离出来,并在一个更大的“父级”任务中把依赖指明成一个 series

1
2
3
4
5
6
7
// 这些任务不再有任何依赖
gulp.task('copy_css', function() {...});
gulp.task('copy_image', function() {...});
gulp.task('clean_temp', function() {...});

// default 依赖于 copy_css 与 copy_image
gulp.task('default', gulp.series('clean_temp', gulp.parallel('copy_css', 'copy_image')));

使用普通函数

因为现在每一个任务实际上都只是一个函数,也并没有依赖或者其他特别的东西。所以我们不必每个任务都用 gulp.task 来完成。开始拥抱独立的函数而不用再像之前通过传入 gulp.task 的回调函数来写代码:

1
2
3
4
5
6
gulp.task('copyGlobalImage', function() {...});
gulp.task('copyCss', function() {...});
gulp.task('copyGlobalFont', function() {...});
gulp.task('copyAllImage', function() {...});

gulp.task('copy', series(['copy-static'], gulp.parallel('copyGlobalImage', 'copyCss','copyGlobalFont','copyAllImage')));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//改成以下这样
function themeResourcesCopy(srcPath, destPath, infoMsg) {
let res = null;
for (let i = 0; i < theme.length; i += 1) {
for (let j = 0; j < color.length; j += 1) {
res = gulp
.src(srcPath)
.pipe(info(infoMsg))
.pipe(gulp.dest(`${buildTargetPath + color[j]}/${theme[i]}${destPath}`));
}
}
return res;
}

function copyGlobalImage() {
// 目前不考虑同步情况
const g_image = '_global/image/';
info(`${targetTemp + g_image}**`);
return themeResourcesCopy(`${targetTemp + g_image}**`, '/image/', DIS.image_copy_g);
}
...

// 记得去掉引号
gulp.task('copy', series(['copy-static'], parallel(copyGlobalImage, copyCss, copyGlobalFont, copyAllImage)));

最后

第一次接触所以就这样吧,后续会对这块进行优化,因为我发现之前的同事这块写的太复杂了且没有用一些压缩之类的插件,也是一开始都是大家都是开荒,可能都不是专业的,反正团队里比较open,只要不影响原有的功能,优化没人管。

感谢

整理自团队内部的分享,因为从17年底启动的产品线,所以用的当时的最新版本16.3.1,由于种种原因一直没有升级,特别是自从出了Hooks之后,我是一直觉得应该进行版本升级了,因为升级这个事是避免不了的,除非你不再接收新的变化。

去年其实呼吁过一次不过被按下来了,犹不死心,所以就借着分享的机会,再团队内部普及一遍。

当然我说了只是普及不是教程,所以要点就两个:

  1. Hooks的特性
  2. 在项目中的实践

Before

在介绍Hooks之前先说说我在开发中的一些痛点

  1. 类组件没办法写的比较轻巧,毕竟好几个生命周期在那儿摆着,有时候不得不冷静一下想想用哪个生命周期合适。
  2. 本来是个函数组件,就因为需要添加一个变量(state),所以必须改成class组件。
  3. 有关状态管理的逻辑代码很难在组件之间复用、且该业务逻辑的实现代码很多时候被分分散到了不同的生命周期内,当能提组件的时候还好,如果不能提组件那这套代码如果其他地方有用只能重复造轮子
  4. class组件this的指向问题。

Hooks

什么是 Hooks?

我的理解啊,是这样,就是当你要完成一个动作(事情…),必然就需要一个过程的,有过程就可以分阶段,而在某个阶段,你可以在其前后插入事项从而实现对整个过程的扩展以及把控,这就是hook做的事情,挤进去搞事情。类似的比如Spring里的AOP。

而没有React Hooks之前想要实现上述效果,只能用class+生命周期函数,比如下图。

而React Hooks就是允许你在不编写 class 的情况下使用状态(state)和其他 React 特性。 你还可以构建自己的 Hooks, 跨组件共享可重用的有状态逻辑。React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和在整个渲染过程中进行功能扩展,就用钩子把外部代码”钩”进来。

从图1到图2的进化,忘掉class抱紧hooks。

主要应用的Hook

列举使用频度较高的几个hook。

useRef 代替之前的 ref并且更加强大,不仅用于DOM引用。 “ ref”对象是一个通用容器,其当前属性是可变的,并且可以保存任何值,类似于类的实例属性。

useState 代替之前的 state

useReducer可实现redux类似的功能,其实state就算基于useReducer实现的

useEffect 则可以代替某些生命周期钩子函数,如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。

实践

结合项目中的代码实操一下,感受一下hooks的魅力。

示例一

一个最简单的例子,只是为了加一个变量。

场景:实现弹出窗体的效果,需要一个变量visible控制窗口的显示和隐藏,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class AlarmCards extends React.Component {
constructor(props) {
super(props);
this.state = { visible: false };
}

updateVisible = (visible) => {
this.setState({
visible
});
};

render() {
return {...};
}
}

切换到Hooks代码如下:

  1. class变为函数组件
  2. 用useState
1
2
3
4
5
6
7
8
9
import React, { useState } from 'react';
const AlarmCards = ({ ...props }) => {
// useState 直接声明变量visible,同时声明方法setVisible来更新visible
// false 初始值
const [visible, setVisible] = useState(false);

return {...页面内容...};
}
}

示例二

看一个稍微复杂一点点的例子,只涉及到两个生命周期函数。

场景:从后端获取数据使其更新组件内容,并在该组件卸载时,更新重置状态(变量)到初始值,代码如下:

变量声明

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

export const initState = {
loading: false,
data: [],
header: [],
// 探测结果弹出层是否打开
visible: false,
// 探测结果弹出层参数对象
drawerParams: {
appId: null,
appName: '',
record: 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
/**
* 排查对比表
* @param params 参数对象
* @returns {Function}
*/
export const getComparisonTable = ({ ...params }) => {
/**
* alarmRule 告警规则(类型)
* updateTime 告警更新时间
* onDotClick 小圆点的点击事件
* appId 当前告警的应用id
* span 告警计算时间跨度
*/
const { alarmId, alarmRule, updateTime, onDotClick, appId, span } = params;
// 开始请求
setState({ loading: true });
const errorCallback = () => {
setState({ loading: false });
};
req(BASE_WEB_API.GET_ALARM_DETAIL_COMPARISON, { alarmId, alarmRule, updateTime }, null, {
errorCallback
}).then(result => {
if (!isAvailableArray(result)) {
setState({ loading: false });
return;
}
// 生成表格需要的表头和数据
const data = generateTableObjs(result, onDotClick, appId);
setState({ ...data, loading: false });
});
};

展示组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class AlarmTable extends Component {
componentDidMount() {
const { alarmId, alarmRule, updateTime, appId, getComparisonTable, span,
onDotClick } = this.props;
// 获取对比表数据
getComparisonTable({ alarmId, alarmRule, updateTime, onDotClick, appId, span });
}

componentWillUnmount() {
const { setState } = this.props;
// 重置state避免脏数据影响折叠面板展开和关闭
setState(model.initState);
}

render() {
....
return {...页面内容...};
}
}

Hooks切换,代码如下:

  1. class变为函数组件
  2. 用useState+useEffect
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
import React, { useEffect, useState } from 'react';
const AlarmTable = ({ ...props }) => {

const { alarmId, alarmRule, updateTime, appId, closeLoading, span, onDotClick } = props;
// 变量声明
const [tableData, setTableData] = useState({ data: [], header: [] });

// 开始请求, useEffect可当作componentDidMount,componentDidUpdate 和
// componentWillUnmount三个生命周期的组合

useEffect(() => {
const { alarmId, alarmRule, updateTime, appId, onDotClick, closeLoading } = props;
const errorCallback = () => {
closeLoading(false);
};
req(BASE_WEB_API.GET_ALARM_DETAIL_COMPARISON, { alarmId, alarmRule, updateTime }, null, {
errorCallback
}).then(result => {
if (!isAvailableArray(result)) {
errorCallback();
return;
}
// 生成表格需要的表头和数据
const data = generateTableObjs(result, onDotClick, appId);
setTableData({ ...data });
closeLoading(false);
});

// 卸载函数
return function cleanup() {
// 重置state避免脏数据影响页面呈现
setTableData(({ data: [], header: [] });
};
});

....
return {...页面内容...};
}

继续优化:

自定义钩子封装

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
function useCpmparisonTable(closeLoading, alarmId, alarmRule, updateTime, onDotClick, appId) {
const [drawer, setDrawer] = useState({
visible: false,
drawerParams: { appId: null, appName: null, record: null }
});
const [tableData, setTableData] = useState({ data: [], header: [] });

useEffect(() => {
// 开始请求
const errorCallback = () => {
closeLoading(false);
};
req(BASE_WEB_API.GET_ALARM_DETAIL_COMPARISON, { alarmId, alarmRule, updateTime }, null, {
errorCallback
}).then(result => {
if (!isAvailableArray(result)) {
errorCallback();
return;
}
// 生成表格需要的表头和数据
const data = generateTableObjs(result, onDotClick, appId);
setTableData({ ...data });
closeLoading(false);
});

// 功能等同componentWillUnmount
return ()=> {
// 重置state避免脏数据影响页面呈现
setTableData(({ data: [], header: [] });
};
});
return [drawer, setDrawer, tableData];
}

页面组件

1
2
3
4
5
6
7
const AlarmTable = ({ ...props }) => {
const { alarmId, alarmRule, updateTime, appId, closeLoading, onDotClick } = props;
// 自定义钩子,通常用use开头于官方的钩子呼应,使其能一眼看出这是一个hook
const [ tableData ] = useComparisonTable(closeLoading, alarmId, alarmRule, updateTime, onDotClick, appId);
....
return {...页面内容...};
}

小结

从示例中可以看出Hooks的带来的一些变化,当然篇幅有限只写了两个Hook,useState和useEffect。

简单总结一下Hooks带来的优势

  1. 干掉了生命周期(夸张了一点点),不用在多个生命周期函数中徘徊

  2. 为后面的第三方组件等升级做铺垫。

    1
    2
    3
    特别是基于React的三方库,比如我们用的Ant Design,
    官方前几天发布了4.0有重大升级,假设我们有一天要升级到AntD 4.0,
    它会告诉你先要把React升到16.8以后也就是支持Hooks之后的版本。
  3. 减少代码量,且从面向函数编程细化到面向业务逻辑块编程

    1. 比如不用再bind方法或者不用再写方法体来改变state可用自定的hook封装业务逻辑 使业务逻辑内聚,便于整套业务逻辑能够在不同的组件间复用,组件在使用的时候也不需要关注其内部的实现。

    2. Hook能够在传统的类组件基础上上,实现细化到逻辑层面的代码复用,而不仅仅是停留在组件级别, 而且Hook的复用并不是停留在将某些常用的逻辑方法代码抽成一个公共方法,而是可以将之前散落在类组件中各个生命周期中的用于实现某个业务的逻辑代码合并在一起封装成一个自定义的Hook,其他地方随用随调。

      1
      2
      3
      比如我们的各种CRUD的表单...
      比如我们的表格一些通用交互,过滤、刷新、排序、查询...
      比如我们的图表的一些通用交互,框选、点选...
  4. 更简洁易测的组件。

    1
    2
    比如后续期望分享的前端单元测试的工具,如果我们要把单元测试用起来,
    你会发现class和函数写单元测试的差别真的很大

Hooks不足:

当然不能吹爆React的Hooks,虽然业界公认包括官方规划都指出,Hooks是React的未来,未来需要一个过程。

  1. 现在的Hooks还不能完全替代class

  2. 使用的Hooks必须保证顺序,即内部是通过两个数组来管理的,所以不要在循环,条件判断,嵌套函数里面调用 Hooks。使其下标对不上从而导致state发生混乱,这在前期可能很容易发生bug。

    下图可简单理解一下内部的原理:

  3. 使用hook后,代码归类不会像之前class组件时代的一样有语法的强制规划了,什么意思呢?在class组件时代,redux的有关的代码是放到connect里的,state生命是放constructor里的,其他逻辑是放每个有关的生命周期里的。而在hook的时代,没有这些东西了,一切都直接放在函数组件内部,如果写得混乱,看起来就是一锅粥,所以,制定组件的书写规范和通过注释来归类不同功能的逻辑显得尤为重要。这有助于后期的维护,也有助于保持一个团队在代码书写风格上的一致性。

最后

Peace & Love,没有银弹。

单独记录一下ES Module,听得太多用的也多,但是一直没深刻的认识一下它。

同样的节奏先Google找是否有大佬写这方面的文章。

幸运的是被我找到了Lin Clark(之前我学习浏览器方面的知识就是读的她的文章)写的,很喜欢她的文章,讲的很生动和具体。

开始

来来来,搬运一下这一篇ES modules: A cartoon deep-dive

模块如何提供帮助?

模块为您提供了更好的方式来组织这些变量和函数。使用模块,您可以将有意义的变量和函数组合在一起。

这会将这些函数和变量放入模块范围。模块作用域可用于在模块中的功能之间共享变量。

但是与函数作用域不同,模块作用域具有一种使其变量也可用于其他模块的方式。他们可以明确地说出模块中的哪些变量,类或函数应该可用。

当其他模块可以使用某些东西时,这称为导出。导出后,其他模块可以明确地说它们依赖于该变量,类或函数。

因为这是一种明确的关系,所以您可以知道如果删除另一个模块,哪个模块将中断。

一旦能够在模块之间导出和导入变量,就可以更轻松地将代码分解为可以相互独立工作的小块。然后,您可以组合并重组这些块(类似于积木),以从同一组模块创建所有不同种类的应用程序。

ES模块如何工作

在使用模块进行开发时,您将建立一个依赖关系图。不同依赖项之间的连接来自您使用的任何导入语句。

这些导入语句是浏览器或Node如何确切知道其需要加载哪些代码的方式。您给它一个文件,以用作图形的入口点。从那里开始,它紧随任何import语句以查找其余代码。

具有两个依赖关系的模块。 顶部模块是条目。 另外两个使用import语句关联

但是文件本身不是浏览器可以使用的东西。它需要解析所有这些文件,以将它们转换为称为模块记录的数据结构。这样,它实际上知道文件中正在发生什么。

具有各种字段的模块记录,包括RequestedModules和ImportEntries

之后,需要将模块记录转换为模块实例。实例结合了两件事:代码和状态。

该代码基本上是一组指令。这就像如何做某食物的食谱。但是就其本身而言,您不能使用该代码执行任何操作。您需要原材料才能与这些代码一起使用。

什么是状态?就像做食物的原材料。状态是变量在任何时间点的实际值。当然,这些变量只是内存中保存值的空间的昵称。

因此,模块实例将代码(指令列表)与状态(所有变量的值)组合在一起。

结合了代码和状态的模块实例

我们需要的是每个模块的模块实例。模块加载的过程将从此入口点文件变为具有模块实例的完整图。

对于ES模块,这分为三个步骤。

  1. 构造—查找,下载所有文件并将其解析为模块记录。
  2. 实例化—查找内存中的空间以放置所有导出的值(但尚未用值填充它们)。然后使导出和导入都指向内存中的那些空间的位置(地址)。这称为链接(引用)。
  3. 求值—运行代码以将变量的实际值填写在对应的内存空间。

三个阶段。 构建从单个JS文件到多个模块记录。 实例化链接那些记录。 评估执行代码。

人们谈论ES模块是异步的。您可以将其视为异步的,因为工作分为三个不同的阶段(加载,实例化和评估),并且这些阶段可以分别完成。

这意味着规范确实引入了CommonJS中不存在的一种异步。我将在后面解释,但是在CommonJS中,模块及其下面的依赖项一次全部被加载,实例化和求值,而中间没有任何中断。

但是,步骤本身不一定是异步的。它们可以以同步方式完成。这取决于正在执行的加载。这是因为并非所有内容都由ES模块规范控制。实际上有两部分工作,分别由不同的规范涵盖。

ES模块规范说,你应该如何解析文件到模块的记录,你应该如何实例化和评估模块。但是,它并没有说明如何首先获取文件。

加载程序将获取文件。加载程序在其他规范中指定。对于浏览器,该规范是HTML规范。但是您可以根据所使用的平台使用不同的装载程序。

两个卡通人物。 一个代表说明如何加载模块的规范(即HTML规范)。 另一个代表ES模块规范。

加载程序还精确控制模块的加载方式。它调用ES模块的方法- ParseModuleModule.InstantiateModule.Evaluate。有点像操纵JS引擎的字符串的p。

加载程序图形充当ES模块规范图形的伪造者。

现在,让我们详细介绍每个步骤。

构建

在构建阶段,每个模块发生三件事。

  1. 找出从哪里下载包含模块的文件
  2. 提取文件(通过从URL下载文件或从文件系统加载文件)
  3. 将文件解析为模块记录

查找并获取文件

加载程序将负责查找文件并下载。首先,它需要找到入口点文件。在HTML中,您可以通过脚本标记告诉加载程序在哪里找到它。

具有type = module属性和src URL的脚本标记。 src URL有一个文件,它是条目

但是,如何找到下一组模块- main.js直接依赖的模块呢?

这就是导入语句的来源。导入语句的一部分称为模块说明符。它告诉加载程序可以在哪里找到每个下一个模块。

有关模块说明符的一件事:在浏览器和Node之间有时需要对它们进行不同的处理。每个主机都有自己的解释模块说明符字符串的方式。为此,它使用一种称为模块解析算法的模块,该算法在平台之间有所不同。当前,某些可在Node中工作的模块说明符在浏览器中将无法工作,但仍在进行修复

在此问题修复之前,浏览器仅接受URL作为模块说明符。他们将从该URL加载模块文件。但这不会同时出现在整个图表上。在解析文件之前,您不知道模块需要获取哪些依赖项,并且在获取文件之前无法解析文件。

这意味着我们必须逐层遍历该树,解析一个文件,然后找出其依赖项,然后查找并加载这些依赖项。

如果主线程要等待这些文件中的每一个下载,则许多其他任务将堆积在其队列中。

这样阻塞主线程会使使用模块的应用程序使用起来太慢。这是ES模块规范将算法分为多个阶段的原因之一。将构造分为自己的阶段,使浏览器可以在开始实例化的同步工作之前获取文件并增强对模块图的理解。

这种方法(算法分为多个阶段)是ES模块和CommonJS模块之间的主要区别之一。

CommonJS可以做不同的事情,因为从文件系统加载文件比通过Internet下载花费的时间少得多。这意味着Node可以在加载文件时阻止主线程。并且由于文件已经加载,因此仅实例化和求值(在CommonJS中不是单独的阶段)是有意义的。这也意味着在返回模块实例之前,您要遍历整棵树,加载,实例化和评估任何依赖项。

CommonJS方法有一些含义,我将在后面详细解释。但是,这意味着一件事,就是在带有CommonJS模块的Node中,可以在模块说明符中使用变量。require在寻找下一个模块之前,您正在执行该模块中的所有代码(直到语句)。这意味着当您进行模块解析时,变量将具有一个值。

但是,使用ES模块,您可以在进行任何计算(求值)之前预先建立整个模块图。这意味着您不能在模块说明符中包含变量,因为这些变量尚无值。

但是有时将变量用于模块路径确实很有用。例如,您可能想根据代码在做什么或在什么环境中运行来切换加载的模块。

为了使ES模块成为可能,有一个建议叫做动态导入。有了它,您可以使用类似的导入语句import(${path}/foo.js)

动态导入的工作原理是,任何使用import()来导入的文件,都会作为一个入口文件从而创建一棵单独的依赖树,被单独处理。

但是要注意一件事–这两棵树中的任何模块都将共享一个模块实例。这是因为加载程序会缓存模块实例。对于特定全局范围内的每个模块,将只有一个模块实例。

这意味着浏览器的工作量更少。例如,这意味着即使多个模块依赖该模块文件,该模块文件也只会被提取一次。(这是缓存模块的一个原因。我们将在评估部分中看到另一个原因。)

加载程序使用称为模块映射的内容来管理此缓存。每个全局变量在单独的模块图中跟踪其模块。

当加载程序获取一个URL时,它将把该URL放入模块映射中,并记下它当前正在获取文件。然后它将发出请求并继续以开始获取下一个文件。

如果另一个模块依赖于同一文件会怎样?加载程序将在模块映射中查找每个URL。如果在其中看到fetching,它将继续前进到下一个URL。

但是模块图不仅跟踪正在获取的文件。模块映射还充当模块的缓存,如下所示。

解析

现在我们已经获取了该文件,我们需要将其解析为模块记录。这有助于浏览器了解模块的不同部分。

该图显示了被解析成模块记录的main.js文件

创建模块记录后,将其放置在模块映射中。这意味着无论何时从此处请求,加载程序都可以将其从该映射中拉出。

模块映射图中的“获取”占位符被模块记录填充

解析有一个细节看似微不足道,但实际上有很大的含义。所有模块都像严格模式来解析的。也还有其他的小细节,比如,关键字 await 在模块的最顶层是保留字, this 的值为 undefinded

这种不同的解析方式称为“解析目标”。如果您解析相同的文件但使用不同的目标,那么最终将得到不同的结果。因此,在开始解析之前就需要知道要解析的文件类型,不管是否是模块。

在浏览器中,这非常简单。您只需放入type="module"script标签。这告诉浏览器应将此文件解析为模块。并且由于只能导入模块,因此浏览器知道任何导入也是模块。

加载程序确定main.js是一个模块,因为script标签上的type属性表明是这样,而counter.js必须是一个模块,因为它已导入

但是在Node中,您不使用HTML标记,因此无法选择使用type属性。社区尝试解决此问题的一种方法是使用 .mjs扩展。使用该扩展名告诉Node,“此文件是一个模块”。您会看到人们将其视为解析目标的信号。目前讨论仍在进行中,因此尚不清楚Node社区最终决定使用什么信号。

无论哪种方式,加载程序都将确定是否将文件解析为模块。如果它是一个模块并且有imports,它将重新开始该过程,直到提取并解析了所有文件。

在加载过程的最后,您已经从只有一个入口点文件变成了拥有许多模块记录。

建设阶段的结果,左侧为JS文件,右侧为3个已解析的模块记录

下一步是实例化此模块并将所有实例连接在一起。

实例化

就像我之前提到的,实例化是将代码与状态结合在一起。该状态存在于内存中,因此实例化步骤就是将所有状态链接到内存。

首先,JS引擎创建一个模块环境记录(Module Environment Record)。它是管理所有模块记录的变量。然后,它会在内存中找到所有export对应的的地址。模块环境记录将跟踪内存中与每个export相关联的地址。

内存中的这些地址对应的空间尚无法获取其值。只有在运行之后,它们的实际值才会被填写。需要注意的一点是:在此阶段中将初始化所有导出的函数声明。这将使后面的执行阶段变得更加容易。

为了实例化模块关系图,引擎会采用深度优先的后序遍历方式。这意味着它将到达关系图的最底部(底部不依赖于其他任何东西),并设置其导出。

最终,引擎会把模块下的所有依赖导出连接到当前模块。接着回到上一层把模块的导入连接起来。

请注意,导出和导入均指向内存中的同一位置。

这不同于CommonJS模块。在CommonJS中,整个导出对象在导出时被复制。这意味着导出的任何值(如数字)都是副本,所以在CommonJS如果导出模块以后更改了该值,则导入模块将看不到该更改。

这意味着,如果导出模块以后更改了该值,则导入模块将看不到该更改。

相反,ES模块使用实时绑定(Live Binding)。两个模块都指向内存中的相同位置(引用)。这意味着,当导出模块更改值时,该更改将显示在导入模块中。

导出值的模块可以随时更改这些值,但是导入模块不能更改其导入的值,因为是导入的是只读引用。不过如果模块导入了一个对象,则它可以更改该对象上的属性值。

之所以ESM采用实时绑定,是因为可以在不运行任何代码的情况下链接到所有模块。这有助于解决循环依赖的问题,在后面的运行(evaluation)阶段会细说。

OK,当实例化结束时,我们得到了所有模块实例,并知道了已完成链接的导出/导入变量的内存地址。

现在我们可以开始评估代码,并使用它们的值填充这些内存位置。

运行

最后一步是往解析阶段获取的内存地址所在的空间里填充值。JS 引擎通过运行顶层代码(函数外的代码)来完成填充。

除了填充值以外,运行代码还可能引发副作用。例如,一个模块可能会请求服务器。

模块将在功能之外进行编码,标记为顶级代码

因为这些潜在副作用的存在,所以模块代码只能运行一次
前面我们看到,实例化阶段中发生的链接过程可以多次进行,并且每次的结果都一样。但是,如果运行阶段进行多次的话,则可能会每次都得到不一样的结果。

这正是为什么需要有模块映射的原因之一。模块映射通过规范URL,缓存模块,因此每个模块只有一个模块记录。这样可以确保每个模块仅执行一次。与实例化一样,这是深度优先的后遍历。

那我们之前谈到的循环依赖怎么处理呢?

在循环依赖关系中,您最终会在模块关系图中出现循环。你依赖我我依赖你,通常,这会变成很大的循环。

为了解释这个问题,我举个例子。

首先让我们看一下如果时CommonJS模块会时什么样的。首先,main模块将执行到require语句。然后它将去加载counter模块。

然后,counter模块将尝试从访问导出的对象message。但是由于尚未在main模块中执行,因此它将返回undefined。JS引擎将在内存中为局部变量分配空间,并将该值设置为undefined。

中间的内存,main.js和内存之间没有连接,但是从counter.js到未定义的内存位置的导入链接

此时会一直运行持续到counter模块顶级代码的末尾。我们想看看是否最终将获得正确的message(在执行main.js之后),因此我们设置了超时时间。然后继续运行到main.js

message变量将被初始化并添加到内存中。但是由于两者之间没有连接,因此message在counter模块中仍然是时undefined。

如果使用实时绑定处理导出,则counter模块最终将看到正确的值。到超时运行时,main.js的执行就已经完成并填充了值。

支持循环依赖是 ESM 设计之初就考虑到的一大因素。也正是这种分(三)阶段设计使其成为可能。

原文地址:

模块话,模块化说了那么多次,以为了解个大概就行了,然而在一次面试经历中被问到AMD和ES Module,所以特此来一篇,汇总一下吸收的模块方面的内容。

又是一篇搬运文章,谁让我放荡不羁爱打野呢。作者博客地址Preethi Kasireddy

为什么模块很重要?

如果没有模块,你能想象在复杂场景下你得js代码是个什么鬼模样吗?模块解决了名称空间和可维护性等变得越来越难以处理的问题。

好的模块是高度独立的,具有独特的功能,可以根据需要对它们进行改组,删除或添加,而不会破坏整个系统。

优势:

1)可维护性:根据定义,模块是独立的。精心设计的模块旨在尽可能减少对代码库各部分的依赖,从而使其能够独立增长和改进。当模块与其他代码解耦时,更新单个模块要容易得多。

2)命名空间:在JavaScript中,顶级函数范围之外的变量是全局变量(意味着每个人都可以访问它们)。因此,普遍存在“命名空间污染”,其中完全不相关的代码共享全局变量。

在不相关的代码之间共享全局变量在开发中是一个很大的禁忌。模块允许我们通过为变量创建私有空间来避免名称空间污染。

3)可重用性:抽取通用部分,哪里需要就拿去,不用重复写,当然也对应的第一点,当有修改时只需要该一份。

早期的时候为了达到“模块模式”,也有很多方式,不过我看了下基本上都是基于匿名闭包的基础上而来的。

这些方式有一个共同点:使用单个全局变量将其代码包装在函数中,从而使用闭包作用域为其自身创建私有名称空间,同时自定义公开哪些方法、变量

大家可以看下jQuery的源码,就是这样的实现方式,如下面的代码。

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
var myGradesCalculate = (function () {

// Keep this variable private inside this closure scope
var myGrades = [93, 95, 88, 0, 55, 91];

var average = function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item;
}, 0);

return'Your average grade is ' + total / myGrades.length + '.';
};

var failing = function() {
var failingGrades = myGrades.filter(function(item) {
return item < 70;
});

return 'You failed ' + failingGrades.length + ' times.';
};

// Explicitly reveal public pointers to the private functions
// that we want to reveal publicly

return {
average: average,
failing: failing
}
})();

myGradesCalculate.failing(); // 'You failed 2 times.'
myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'

如您所见,这种方法使我们可以决定将哪些变量/方法设为私有(例如*myGrades*),以及通过将它们放在return语句中(例如*average**failing***)来公开哪些变量/方法。

所以大家可以看出,早期关于模块的写法,尽管每种方法都以其自己的方式有效,但它们也有缺点:

  1. 大家各自发挥的,这样有个最大的问题就是到最后就是乱的,就像为什么需要TC39一样,让大家有一套标准,才能从而写出更友好通用的模块。
  2. 依赖管理是个问题,需要我们开发人员自己管理依赖,记得刚开始用jQuery这种第三方库的时候,如果没注意到顺序就会报错,所以想象一下如果引用较多时,管理依赖关系并正确解决这些问题会让人头疼。
  3. 全局作用域被污染,上述的方式创建的变量(比如myGradesCalculate)都在全局范围内,所以该全局范围内的代码的每个部分都可以更改该变量。恶意代码可以有意更改该变量,以使您的代码执行您不希望这样做的事情,或者非恶意代码可能会无意间破坏了您的变量。

所以基于上述原因,这个时候我们就需要一套规范了来解决这些个问题:我们能否设计一种无需遍历全局范围即可请求模块接口的方法

规范

现在比较通行得规范有两种:CommonJSAMD

CommonJS

CommonJS是一个旨在定义一系列规范以帮助开发服务器端JavaScript应用程序的项目。CommonJS团队尝试解决的领域之一就是模块,负责设计和实现用于声明模块的JavaScript API。我听说CommonJS,最早是在15年写Node应用的时候接触的,Node.js最开始就是遵循这套规范弄得模块化,但是据说后来不用该规范了。

一个CommonJS的模块本质上是一种可重复使用的一段JavaScript代码其中出口特定对象,使它们可用于其他模块需要在他们的计划。

使用CommonJS,每个JavaScript文件都将模块存储在其自己的唯一模块上下文中(就像将其包装在闭包中一样)。在此范围内,我们使用module.exports对象公开模块,并要求将其导入。

当您定义CommonJS模块时,它可能看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
function myModule() {
this.hello = function() {
return 'hello!';
}

this.goodbye = function() {
return 'goodbye!';
}
}

module.exports = myModule;

我们使用特殊对象模块,并将我们函数的引用放入module.exports中。这使CommonJS模块系统知道我们要公开的内容,以便其他文件可以使用它。

然后,当某人想要使用myModule时,他们可以在其文件中要求它,如下所示:

1
2
3
4
5
var myModule = require('myModule');

var myModuleInstance = new myModule();
myModuleInstance.hello(); // 'hello!'
myModuleInstance.goodbye(); // 'goodbye!'

与我们之前讨论的模块模式相比,这种方法有两个明显的好处:

  1. 避免全局命名空间污染
  2. 明确我们的依赖关系

要注意的另一件事是,CommonJS采用服务器优先的方法并同步加载模块。这很重要,因为如果我们有我们需要的其他三个模块需要,它就会加载它们一个接一个。

现在,它可以在服务器上很好地工作,但是不幸的是,这使得为浏览器编写JavaScript时更难使用,因为服务器端通常是从磁盘读取,而浏览器需要网络请求,所以只要加载模块的脚本一直在运行(JavaScript线程将停止直到代码被加载),它就会阻止浏览器运行其他任何东西,直到加载完成。

AMD

从上面我们知道CommonJS是同步的,所以很显然不适用浏览器端,那我们就需要异步模块定义的规范,即AMD

使用AMD加载模块如下所示:

1
2
3
define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) {
console.log(myModule.hello());
});

这里发生的是,define函数将每个模块依赖项的数组作为第一个参数。这些依赖项在后台加载(以非阻塞方式),并且一旦加载了define,便调用回调函数。

接下来,回调函数将加载的依赖项作为参数(在本例中为myModule*myOtherModule),以允许函数使用这些依赖项。最后,还必须使用**define***关键字定义依赖项本身。

例如,***myModule***可能看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
define([], function() {

return {
hello: function() {
console.log('hello');
},
goodbye: function() {
console.log('goodbye');
}
};
});

与CommonJS不同,AMD采用了浏览器优先的方法以及异步行为来完成工作。

除了异步之外,AMD的另一个好处是您的模块可以是对象,函数,构造函数,字符串,JSON和许多其他类型,而CommonJS仅支持将对象作为模块。

AMD与CommonJS相比,其提供的io,文件系统和其他面向服务器的功能不兼容。

UMD

对于需要同时支持AMD和CommonJS功能的项目,还有另一种格式:通用模块定义(UMD)。

UMD本质上创建了一种使用这两种方法之一的方式,同时还支持全局变量定义。结果,UMD模块能够在客户端和服务器上工作。

以下是UMD如何开展业务的快速体验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['myModule', 'myOtherModule'], factory);
} else if (typeof exports === 'object') {
// CommonJS
module.exports = factory(require('myModule'), require('myOtherModule'));
} else {
// Browser globals (Note: root is window)
root.returnExports = factory(root.myModule, root.myOtherModule);
}
}(this, function (myModule, myOtherModule) {
// Methods
function notHelloOrGoodbye(){}; // A private method
function hello(){}; // A public method because it's returned (see below)
function goodbye(){}; // A public method because it's returned (see below)

// Exposed public methods
return {
hello: hello,
goodbye: goodbye
}
}));

ES Module

是我做前端开始听的最多的了,当然也因为无时无刻都在用它。

上面咱们所说的,都不是JavaScript固有的。不过幸运的是,TC39(定义ECMAScript语法和语义的标准机构)引入了ECMAScript 6(ES6)内置模块。

ES6提供了多种导入和导出模块的可能性,其他人则做了很好的解释-以下是其中的一些资源:

与CommonJS或AMD相比,ES6模块最大的优点是它能够提供两全其美的优势:紧凑和声明性语法以及异步加载,以及诸如更好地支持依赖项等附加优点。

ES6模块最让人兴奋的应该是导入是导出的实时只读视图,即是只读引用,不过却可以改写属性。所以你猜到了当你模块里的值属性发生变化时,导入的地方获取的值是一样的。ES Module具体的后面还有一篇文档单讲,不然这文章就太长了。

最后我们对比一下两种方式吧

ES Module与CommonJS:

  • CommonJS规范通常适用于Node这类服务器端的
  • CommonJS模块是对象,是运行时加载,运行时才把模块挂载在exports之上(加载整个模块的所有),加载模块其实就是查找对象属性。
  • ES Module不是对象,是使用export显示指定输出(函数、对象、变量等),再通过import导入。为编译时加载,编译时遇到import就会生成一个只读引用。等到运行时就会根据此引用去被加载的模块取值。所以不会加载模块所有方法,仅取所需。
  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

说明:

全局范围(作用域)

  1. 写在script标签中的JS代码,都在全局作用域 。
  2. 全局作用域在页面打开时创建,在页面关闭时销毁。
  3. 在全局作用域中有一个全局对象window,它代表的是一个浏览器的窗口,它由浏览器创建我们可以直接使用 。
  4. 全局作用域中,创建变量都会作为window对象的属性保存
  5. 创建的函数都会作为window对象的方法保存
  6. 全局作用域中的变量都是全局变量,在页面的任何部分都可以访问的到并可以修改它

来源:

话说从接触Linux那一天起,就有一种独领风骚的傲气,命令走天下的这种霸气,那时候感觉精通Linux的连女朋友都带光环。

我最开始接触Linux是从Ubuntu开始,当时笔记本装了双系统,我就鼓捣了一个Ubuntu,现在用的较多的CentOS,首先入门都是先从了解root开始,我其实一直对root很好奇,刚好今天看到twitter上ruanyf大佬推了一篇文章Root User in Ubuntu: Important Things You Should Know讲基于Ubuntu的root的文章,基础好用,我们现在做的产品的安装包刚好也是基于Ubuntu做的,所以更有必要在此做个中文版记录。

开始

文章主要讲了以下四块:

  1. 为什么在Ubuntu中禁用root用户
  2. 以root身份使用命令
  3. 切换到root用户
  4. 解锁root用户

什么是root用户?为什么将其锁定在Ubuntu中?

根用户Ubuntu

稍微了解Linux的都知道,在Linux中,有一个称为root的超级用户。这是超级管理员帐户,可以使用系统执行任何操作。它可以访问任何文件并在Linux系统上运行任何命令。

拥有权利的同时也被赋予了重大的责任。超级用户可以为您提供对系统的完全控制权,因此也格外谨慎。超级用户可以访问系统文件并运行命令来更改系统配置。因此,错误的命令可能会造成无法挽回的损失,比如网上老说的一个梗:当rm -rf 之后我跑路了。所以一般情况下都不会给root权限,管理员会分配部分权限建对应的用户供对应的人使用。

这也说明了为什么Ubuntu默认情况下锁定了root用户,就是为了避免意外灾难。

您无需具有root特权即可执行日常任务,例如将文件移动到主目录中,从Internet下载文件,创建文档等。

*以此类比更好地理解它。如果必须切水果,可以使用菜刀。如果必须砍伐树木,则必须使用锯。现在,您可以使用锯切水果,但这不明智,是吗?*

这是否意味着您不能成为Ubuntu的root用户或无法使用具有root用户特权的系统?不,您仍然可以在“ sudo”的帮助下获得root用户访问权限(在下一节中说明)。

重点:

用户功能强大,无法用于常规任务。这就是为什么不建议始终使用root的原因。您仍然可以使用root运行特定命令。

如何在Ubuntu中以root用户身份运行命令?

须藤三明治xkcd图片来源:xkcd

当你需要某些系统特定任务的root特权。例如,如果要通过命令行更新Ubuntu,则不能以常规用户身份运行该命令。会有以下类似的错误。

1
2
3
4
5
6
apt update
Reading package lists... Done
E: Could not open lock file /var/lib/apt/lists/lock - open (13: Permission denied)
E: Unable to lock directory /var/lib/apt/lists/
W: Problem unlinking the file /var/cache/apt/pkgcache.bin - RemoveCaches (13: Permission denied)
W: Problem unlinking the file /var/cache/apt/srcpkgcache.bin - RemoveCaches (13: Permission denied)

那么,这个时候怎么做呢?简单的答案是在需要以root身份运行的命令之前添加sudo。

1
sudo apt update

Ubuntu和许多其他Linux发行版使用一种称为sudo的特殊机制。Sudo是一个程序,用于以root(或其他用户)身份控制对运行命令的访问。

Sudo实际上是一个多功能的工具。可以将其配置为允许用户以root用户身份运行所有命令。您可以配置它仅以root身份运行选定的几个命令。您也可以配置为不带密码运行sudo

在安装Ubuntu时,必须创建一个用户帐户。该用户帐户在您的系统上以管理员身份运行,并且按照Ubuntu中的默认sudo策略,它可以使用root特权在系统上运行任何命令。

运行sudo不需要root密码,但需要用户自己的password

这就是为什么当使用sudo运行命令时,总会一开始就询问密码:

1
2
gamehu@nuc:~$ sudo apt update
[sudo] password for gamehu:

如您在上面的示例中看到的,用户gamehu试图使用sudo运行’apt update’命令,系统要求输入gamehu的密码。

要注意当您开始在终端中输入密码时,屏幕上什么都没有发生不会有任何显示,也没有所谓的删除键、退格键…这里会记录你按下的所有键。因为作为默认安全功能,屏幕上不显示任何内容。甚至没有星号(*)。您输入密码,然后按Enter。*

划重点:

要在Ubuntu中以root身份运行命令,请在命令前添加sudo。
当要求输入密码时,输入您的帐户密码。
在屏幕上键入密码时,看不到任何内容。只需继续正确的输入密码,然后按Enter。

如何在Ubuntu中成为root用户?

你可以使用sudo以root身份运行命令。但是,在某些情况下,您必须以root用户身份运行多个命令,则可以临时切换为root用户。

sudo命令允许您使用以下命令模拟root登录shell:

1
2
3
4
5
6
sudo -i
gamehu@nuc:~$ sudo -i
[sudo] password for gamehu:
root@nuc:~# whoami
root
root@nuc:~#

您会注意到,切换到root用户时,shell命令提示符将从$(美元键符号)更改为#(磅键符号)。

*尽管已向您展示了如何成为root用户,但我必须警告你,应避免将系统用作root用户。毕竟出于某种原因,我们不建议这样做。*

临时切换到root用户的另一种方法是使用su命令:

1
sudo su

如果您尝试在不使用sudo的情况下使用su命令,则会遇到“ su身份验证失败”错误。

您可以使用exit命令恢复为普通用户。

1
exit

如何在Ubuntu中启用root用户?

到目前为止,您已经知道默认情况下,root用户在基于Ubuntu的发行版中被锁定。

Linux使您可以自由地对系统进行任何操作。解锁root用户是这些自由之一。

如果出于某种原因决定启用root用户,则可以通过为它设置密码来启用它:

1
sudo passwd root

同样,不建议这样做,我也不鼓励您在桌面上执行此操作。如果忘记了密码,将无法再次在Ubuntu中更改root密码

您可以通过删除密码来再次锁定root用户:

1
sudo passwd -dl root

记录近两年各种机会下与事业部头部三剑客交流收获的一些真知灼见。

关于合作

目标一致

看上去的完美搭档其实是一个很磨人的过程,一开始恨不得一见面就呼巴掌作为打招呼的方式,最终能达成默契或者说合作,目标一致最重要的前提。

不管你怎么看不惯一个人,三观有多么不合,但是当你发现大家目标都是一致的时候,静下来的时候大家总会想一想,是不是我有问题,慢慢的就变得能够互相包容,所以要做成事首要条件不是考虑要找多要好的人多默契的人,关键在于你们的目标是否一致,只要目标一致你会发现你们很难分割彼此。

所以有目标是一件重要紧急的事。

关于做事

干一行,爱一行,不然干不了大事。

最深刻的一句话,我记得当时是我谈起对我现在做的事好像也没到喜欢的程度的时候,领导对我说了这句话,听后我记得当时醍醐灌顶,然后手脚冰凉,内心很奔溃,至此以后我逐渐爱上了我现在做的事,虽然还是有很厌烦的时候,不过总能爱回去。

勇于释放自己,学会利用资源,学会影响上级。

这句话是我最亲近的老大对我说的,当我在抱怨做事的一些困惑时。这是最有挑战的不过收获也是最大的,这句话不是教我怎么在领导面前表现,而是告诉我领导在意的是你的结果,当你结果好的时候,才会在意过程,从而改变对你的一些印象。当你做出结果时,他们会关注到的。

关于自信

当你进入了公司就不要再因为自己的出身、学历等感到自卑,因为能进来表示公司已经认可了你,接下来你要想的是,怎么做到比别人牛逼。

否定自己是很耗能量的一件事,积极一点。