0%

背景

因为团队要用fusion design,所以按我的习惯我得先知道他是什么、为什么、能做什么,我才好下手。

开始

先不负责任的下一个结论,fusion design是个撒子?

答案:

一个平台: fusion.design

两个工具:

  1. 开发者工具 Iceworks
  2. 设计师工具 FusionCool

可以看作是下图这样纠缠:

Fusion Design

fusion design = 一套基础组件库 @alifd/next + 主题定制平台 https://fusion.design + 设计师工具 FusionCool+ 物料中心 。

所以更确切的说 Fusion Design算是一套体系,是一种旨在提升设计与开发之间 UI 构建效率的工作方式 ,我认为理解这点很重要,不然可能咱们用半天还以为他就是另一个库就像我们之前用的antd一样,其实完全不是一回事。

Fusion Design能解决哪些痛点:

  1. 【协作成本】内部(UCD和研发)协作问题,不再需要因为对概念、规范、复用性等问题UCD和研发不断沟通。

  2. 用户体验一致性问题 ,不同业务功能或者不同迭代功能,同样功能的交互和组件用户体验不一致。

  3. 【时间成本】重复工作问题,比如不断的review还原度不断的修正、UCD每次都需要对高保真进行规范说明以及关键内容的标注。

    旧模式如下图:红框部分是我们经常重复的内容。

Fusion Design提供了哪些能力来解决上诉的痛点:

  • 物料中心:各种组件、区块、模板(包含官方(Next等)+其它第三方+咱们自研的)

  • UI的可定制能力,设计师根据物料中心的内容定制UI,还可沉淀设计模板。

  • 研发都能配合前端工具(iceworks等),开发模块模板更高效,沉淀业务模板,后续可直接套用模板不用再开发。

  • 快速定制、切换主题。

    应用fusion design之后,产出过程应该就会像下图:

@alifd/next

  • Next 是基于 Alibaba Fusion Design 的设计理念实现的一套骨架DPL(Design Pattern Library)类似于咱们之前使用的antd。配合 fusion.design 使用可以实现换肤的能力。
  • 基于React的组件库。 可以理解**Next**是fusion design的技术实现。

小结一下

所以综上所述,引人fusion design后,理想状态下设计师和研发产出页面(功能)的过程应该会如下面两张图所示:

物料中心

可以理解成一个仓库,类似maven仓库或者npm仓库,里面可包含用开发好的物料(区块、模板、组件),该物料中心与sketch、iceworks是互通的,相互间可上传可下载。

FusionCool

FusionCool:组件分发工具,主要面向所有设计师。当组件构建者完成组件设计发布组件后,每位设计师手上的Fusion Cool都会“自动”接收到构建者的发布的组件样式,确保无缝衔接组件更新。

FusionCool也可以简单理解为是设计端使用的sketch 插件,达到sketch 既能设计页面,又能沉淀已经设计完成的模板。即设计师使用的同一套规范的组件,产出的设计稿都是同一套规范。

IceWorks

飞冰(ICE) 是一套基于 React 的中后台应用解决方案,ICE 包含了一条从设计端到开发端的完整链路,帮助用户快速搭建属于自己的中后台应用,开发者无须关注环境的问题,并且有海量物料可用。目前已经和 Fusion 的物料体系打通,可以轻松使用 Fusion 站点的物料。

Iceworks 是淘宝飞冰团队开发的面向前端开发者的 GUI 工具,开发者无须关注环境的问题,并且有海量物料可用。目前已经和 Fusion 的物料体系打通,可以轻松使用 Fusion 站点的物料。

fusion design的御用开发者工具,基于其开发各种组件丰富fusion design站点的物料中心,当然iceworks也能轻松使用 Fusion 站点的物料,两者互通。

总之

我个人认为fusion design的价值在于提升工作效率,因为它改造了前端(设计师和研发)的工作方式,减少了重复工作的内容,减少了沟通以及甩锅的成本,通过fusion design这个平台,让设计师和研发都能深度参与产品中来且这种参与是互补共赢的,它让设计师和研发之间的一些壁垒或者冲突点慢慢的消失了。

另外

对于角色(设计、研发)来说:可能最大变化就是对于通用性的、沉淀下来的物料,UCD才是老板,这块的样式布局等需要UCD统一把关收口,研发只需要更新包就行。总体上就是UCD的工作内容会增加但是研发时间会减少,协作时间也会减少,同时体验一致性也能达到要求。我估计能达到这种状态应该就可以要自行车了吧?

但是这玩意都是线上的,不知道能不能支持本地搭一套(如果不能搭是不是又不能要自行车了)?

题外篇:iceworks server

如需使用iceworks提供的一些快捷能力,比如新建项目(基于fusion design、react、typescript)、项目管理等。

用于练手刚好。

1.安装iceworks

npm install iceworks -g –registry=https://registry.npm.taobao.org

每个命令大家都可以玩一玩,我下面只介绍start的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
D:\baymax_projects\fusion-design-one>iceworks -h
Usage: iceworks <command> [options]

Options:
-V, --version output the version number
-h, --help output usage information

Commands:
start start and open the iceworks
init [type] [npmName] init project/material/component by template
add [options] [materialType] [npmName] add block to current directory
generate generate material collection data(material.json)
sync [options] sync materials data to Fusion Material Center
use <version> specify the iceworks-core version
config [type] [key] [value] operate iceworks global config

Run iceworks <command> --help for detailed usage of given command.
2.启动 安装iceworks-server

windows:

1
iceworks  start

linux:

1
2
3
#!/bin/sh
# iceworks start
iceworks # open http://localhost:8000/

3.若提示是否安装iceworks-server 直接Enter 默认是 稍等几分钟 自动启动浏览器

iceworks server使用方法

1.打开项目,(首先你要有项目包)

2.安装依赖

如果要切换cnpm源,设置包管理工具cnpm(前提是现状了cnpm),如不需要则跳过此步。

img

img

3.启动服务

当然你也可以本地运行

4.当页面变成这样说明已经启动成功:(会自动跳转到项目页面)

5.打开编辑器

img

感谢:

背景

其实老早就想做监控,之前整理的前端埋点(一)里头有说一些相关的内容。

不痛就不慌,所以说说我们痛的地方:

  1. 因为我们的产品都是部署在客户内网的,所以对于debug及其不友好,客户现场问题排查前端几乎没有任何输入,难弄。
  2. 产品迭代了多个版本,但是没有任何客户现场的用户行为等数据,产品优化少了一些输入。

本来计划是自研,但是由于业务压力突然来袭,所以就搁置了,不过我犹不放弃,觉得自研既然短期不现实,那可以站在巨人肩膀上搞一搞。

找了两个工具Sentry+rrweb,基于两个工具做一些定制化,手里不就有东西了吗。

Sentry

Sentry的应用程序监视平台可为每位开发人员提供帮助诊断,修复和优化其代码的性能。

config

config for JavaScript

React

features

package

@sentry/tracing 性能监控

1
2
# Using npm
$ npm install --save @sentry/react @sentry/tracing

config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Sentry.init({
dsn: 'http://cb4e9b434f004c53a51af8ab45346635@172.17.162.101:9100/2',
integrations: [new Integrations.BrowserTracing()],//性能监控配置
// beforeSend (event, hint) {
// // Check if it is an exception, and if so, show the report dialog 错误弹窗
// if (event.exception) {
// Sentry.showReportDialog({ eventId: event.event_id });
// }
// return event;
// },
debug: false,// 调试模式
attachStacktrace: false,//附上堆栈信息
tracesSampleRate: 1,// Be sure to lower this in production
environment: 'development',
// logLevel: 2,
release: 'sentryTest-0.1.0'
});
1
2
3
4
5
6
7
8
9
10
11
// 创建一个 全局scope ,可以理解为上报数据的附加信息
Sentry.configureScope((scope) => {
//标签,可用于筛选
scope.setTag("first-tag", "Guide");
//绑定用户信息
scope.setUser({
id: 1,
name: "gamehu",
email: "gamehu@yeah.net",
});
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//局部:自定义上下文,补充信息
Sentry.setContext("zhangsan", {
name: "zhangsan",
age: 19,
attack_type: "zhangsan",
});
// 创建一个零时到 scope ,配置到 context 上面
const scope = new Sentry.Scope();
scope.setTag("section", "articles");
scope.setUser({
id: 2,
name: "zhangsan",
email: "zhangsan@yeah.net",
});

sourceMap

sentry-cli releases -o 组织 -p 项目 files staging@1.0.1 upload-sourcemaps js文件所在目录 –url-prefix 线上资源URI

sentry-cli releases files sentryTest-0.1.0 upload-sourcemaps –url-prefixhttp://172.17.162.101:9100/organizations/sentry/issues/61/?project=2&query=is%3Aunresolved' ‘./dist/static/js’

添加一个 EventProcessor 对全局生效

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

const attachmentUrlFromDsn = (dsn, eventId) => {
const { host, path, projectId, port, protocol, user } = dsn;
return `${protocol}://${host}${port !== '' ? `:${port}` : ''}${path !== '' ? `/${path}` : ''
}/api/${projectId}/events/${eventId}/attachments/?sentry_key=${user}&sentry_version=7&sentry_client=custom-javascript`;
}

//添加一个 EventProcessor 对全局生效
Sentry.addGlobalEventProcessor((event) => {
try {
const client = Sentry.getCurrentHub().getClient();
const endpoint = attachmentUrlFromDsn(
client.getDsn(),
event.event_id
);
const formData = new FormData();
const data = JSON.stringify({ logEntries: ["sentryTest"], message: event.message, logger: event.logger });
formData.append(
'my-attachment',
new Blob([data], {
type: 'application/json',
}),
event.event_id + '.json'
);
fetch(endpoint, {
method: 'POST',
body: formData,
}).catch((ex) => {
// we have to catch this otherwise it throws an infinite loop in Sentry
console.error(ex);
});
return event;
} catch (ex) {
console.error(ex);
}
});

RRWEB/TimeCat

录制回放工具,可单独使用也可搭配Sentry使用,可对用户操作录屏,针对一些现场问题可作为排查问题得输入.

rrweb使用指南

preview

监控理论的记录

要做监控先做设计,根据产品、研发、测试等的输入,整理出监控数据类别:

  • JS 的异常错误;
  • 资源测速;
  • 接口的成功率、失败率;
  • 性能。

img

img

img

本文引用的内容,如有侵权请联系我删除,给您带来的不便我很抱歉。

背景

前面的文章已经说过很多了,因为经常会到服务器上操作,所以记录几个常用的Linux命令。

ls命令

ls命令应该说是我接触的linux的第一批命令之一,属于没事就敲个ls装逼的程度,但是用下来发现也是一个宝藏命令,实用性很强。

ls

直接输入ls,不带任何其它选项,该ls命令提供有关由命令行上给定路径指向的每个文件对象的信息:

  • 只显示路径指向的文件对象的名称;
  • 如果该路径指向目录(或指向目录的符号链接),则该ls命令还会列出该目录的内容;
  • 如果显示多个条目,则将按文件名的字母顺序对其进行排序。

最后,当在命令行上未给出路径时,这些ls命令将采用./–即当前目录。

ls -l: 长格式显示

除文件名称外,亦将日期和时间、权限、拥有者、文件大小等资讯详细列出,我们经常使用的ll其实就是它的别名。

ls -a: 显示所有文件

使用该-a选项时,在显示目录内容时ls包括隐藏文件。但是什么是隐藏文件?

隐藏文件:名称以点开头的文件被视为隐藏文件。此外,每个目录还包含两个特殊的,通常为隐藏的条目:...

在每个目录中:

  • .条目指向目录本身。这种自我指称似乎很奇怪。但这有时很有用,有点像将自己的电话号码存储到智能手机库中。
  • ..条目指向父目录。由于类Unix系统上的文件层次结构严格地组织为一棵树,因此每个目录只有一个父目录。
ls -s: 显示文件分配的大小

这里大小的单位是,在linux中一块可以看作是1024字节,该大小指的不是逻辑大小而是实际大小。如下所示,a、b都是2097152,但是通过块的方式查看b只有1028,因为b目录下由 sparse files

1
2
3
sh:~/ls$ ls -ls a b
2052 -rw-r--r-- 1 sylvain sylvain 2097152 Sep 19 22:18 a
1028 -rw-r--r-- 1 sylvain sylvain 2097152 Sep 19 22:18 b
ls -h: 可读性强的方式显示文件大小

使用-h选项,ls将使用单位后缀显示文件大小,以使其更加用户友好。如图直接使用-h是没用的,结合l和s使用。

ls -d */: 只显示目录(文件夹)
ls -i: 显示文件的索引号

有点我们说的引用地址的意思,该选项在查看文件的硬、软链接时比较有用。比如查看某几个文件是否引用同一基础文件系统对象。

比如下图,切换到根目录,然后ls -ia你会发现... 的索引号都是2,证明指向的是同一个目录,这刚好可以解释根目录的父目录就是根目录自身。

ls ../

查看父目录文件。

ls ~

查看主目录的文件。

排序

ls -t :按修改时间倒序,最近修改的在前。

ls -S: 按文件大小正序,最小在前。

ls -r:反转排序。比如ls -rS,则会变成最大的在前。

ls -R:递归列出子目录,跟find .效果类似。

时间完整显示

ls --full-time:显示完整日期及时间。

OK

结束了,以上列举的80%都是我日常经常用的,希望对大家有帮助。

感谢

背景

我们是做NPMD工具的,但是对自身的产品本身确没有监控,说起来就很惆怅了,当然主要原因还是产品从0到1这个过程,前期放荡不羁的功能造作,导致对基础设施建设这块做的很少。

这是一贯坚持基础建设重要性的我的又一篇分享。关于前端埋点。

所以我当然承认基础建设是体系化的漫长的一个过程,但是因为各种不可抗因素,现没法体系化落地,所以只能先捡最能产生价值的且能引起更多不同之能的同事关注的事开始做,比如前端埋点。

先说痛点:

  1. 整个质量体系监控缺失,前端报错后端报错,全靠经验、人肉日志和用户主动反馈。

    特别是现场问题排查,通常都会先找到前端定位问题,现场又不能远程。

  2. 业务数据的效果无从跟踪。

    如使用某功能的频率无从得知,需要人肉从客户处拿此类数据,且还不准。

  3. 用户的访问行为/设备特征/应用性能信息完全无感知。

    如活跃时间点(避开做升级),软硬件系统和设备比例(做兼容),慢页面优化等无从做起。

前端埋点对我们产品的好处:

  1. 记录访问行为/设备特征/应用性能信息。

    为产品的设计,提供参考数据,对于我们这种从0到1的ToB产品,我个人觉得是特别需要这类用户反馈数据的,避免闭门造车。

    为产品的优化(性能、产品设计…)提供参考。

  2. 记录异常。

    便于问题的排查,我们处理的问题一般是客户现场问题,通常情况下又都是客户的内网,所以是无法远程的,这类异常记录手段刚好能丰富异常记录数据,便于问题的排查。

所以可以看出,前端埋点至少会影响的到产品、UCD、研发、测试,当利益方多的时候,事情才更有推下去的可能性。

埋点

决策源于数据,而数据源于采集,采集源于规则梳理,让这一切发生源于工程师的创造力和执行力。

埋点的方式

  1. 手动埋点

    手动代码埋点比较常见,即需要在采集数据的地方调用埋点的方法。

    优点是流量可控,业务方可以根据需要在任意地点任意场景进行数据采集,采集信息也完全由业务方来控制。这样的有点也带来了一些弊端,需要业务方来写死方法,如果采集方案变了,业务方也需要重新修改代码,重新发布。

    百度统计等第三方数据统计服务商大都采用这种方案;

  2. 可视化埋点

    即通过可视化工具配置采集节点,在前端自动解析配置并上报埋点数据。

    优点业务方工作量少,缺点则是技术上推广和实现起来有点难(业务方前端代码规范是个大前提,比如唯一ID等)。

    代表方案是已经开源的Mixpanel

  3. “无痕埋点”

    无痕埋点则是前端自动采集全部事件,上报埋点数据,由后端来过滤和计算出有用的数据,优点是前端只要加载埋点脚本。缺点是流量和采集的数据过于庞大,服务器性能压力山大,主流的 GrowingIO 就是这种实现方案。

那我们产品现阶段比较适合的方式时什么呢?

手动代码埋点的方案,代码埋点虽然使用起来灵活,但是开发成本较高且对业务代码有强侵入性,并且一旦发布就很难修改,更何况我们这是部署在客户现场的。

可视化埋点也是不太可行的,我们连工程化、规范化都没做到,而且是部署在客户现场的,先天不足,再一个可视化埋点通常基于xpath的方式,只能读取页面上标签元素展示出来的属性,不能够获取上下文(通常内存里)的一些属性。而且页面的结构发生变化时,需重新标记操作。

所以几乎只有考虑无痕埋点,虽然无痕埋点流量消耗和数据计算成本很高,但是因为我们是ToB运维工具,企业内部用的,使用人数也不多,所以也还好,当然这是第一版如果确实在实验局发现压力挺大,我们可以再改进一下,比如采用手动埋点+无痕埋点的方式或者三种结合的方式。

OK先把埋点方式定下来了,我们现在考虑剩下的东西。

埋点需求整理原则

埋点不能乱埋,埋点的原则是基于一系列问题展开,并基于这些问题确定埋点需求,怎么确定埋点需求,可以照下面的问题切入进行梳理

HOW:

  • 怎样证明新功能效果?
  • 需要哪些埋点?我们要采集什么内容,进行哪些采集接口的约定?
  • 我该怎么埋这些点?
  • 部分埋点的计算逻辑是什么?
  • 数据结果怎么看?
  • 通过什么方式来调用我们的采集脚本?
  • 无埋点:考虑到数据量对于服务器的压力,我们需要对无埋点进行开关配置,可以配置进行哪些元素进行无埋点采集

WHO:

  • 我的产品设计面对的用户群里是谁?
  • 用户特征是什么?
  • 这部分特征用户对功能预期的数据结果是什么?
  • 用户习惯是什么?

WHAT:

  • 产品包含哪几个模块?

WHERE:

  • 新功能展示在产品端的哪个位置?
  • 在哪些版本生效?
  • 哪些功能的展示或作用在哪里需要跟服务端等交互?

WHEN:

  • 功能是在用户场景什么时候调用产生?
  • 调用过程中什么时候和服务端交互?
  • 功能调用正常情况下需要大概需要多长时间?
  • 什么情况会影响调用结果?
  • 调用有风险的时候,是否需要加监测?

回答了上面的问题,基本上能知道埋点的意义在哪儿以及需要收集哪些数据等,接下来就得开始指定埋点规范了。

埋点规范

埋点规范就跟编码规范一样,不按照规范就会有很大的隐患比如以下问题:

  1. 埋点混乱

  2. 常常埋错,漏埋

  3. 业务变化后,老埋点数据失去意义

  4. 埋点数据无人使用,浪费资源

  5. 数据团队、研发团队、产品团队协作配合难度大

  6. 很多时候不太重视数据,而是重视业务的快速上线

  7. 埋点语义不明确,同一个按钮存在多种描述,不易检索

  8. 无用/重复埋点太多,干扰了正常埋点数据

  9. 大量存量埋点Owner离职或者转岗,导致大量僵尸埋点信息

    所以为了避免以上问题我们需要建立一个好的规范,比如命名规范和流程规范。

    埋点命名规范

    我们当前的做法是埋点名称只能是由字母、数字、下划线组成,并保证在应用内唯一,比如sw是展示,ck是点击。

    常用规则的举例如下:
    比如行为埋点:{团队|业务|角色}_{组件|页面}_{具体元素}_{动作}
    示例:
    front_alarm_sw : front代表项目,alarm代表功能,sw是展示,ck是点击
    front_alarm1_detail_table_point_ck :front代表项目,alarm1_detail代表功能,table组件,point小圆点组件,ck点击

埋点流程规范

如果你发现每天有大量埋点错误反馈,并导致很多错误的分析结论,一个错误的分析结果还不如没有数据分析结果。造成这个问题的原因包括:

  1. 前端埋点涉及复杂的交互,难以找准埋点位置;
    1. 埋点的验收流程不完善,没有经过测试和产品经理的测试和验收,验证不完备;

好的埋点需求应该和业务功能需求同等重要,也需要经历软件开发的整个生命周期,如果能严格按照一个规范的流程处理埋点,以上问题会得到更好的解决。

规范内容

需求阶段:

确定埋点信息,核心包括五方面信息:事件ID、埋点名称、埋点描述、埋点属性以及截图。

如何落地:

如果不按照规则和流程埋点将不会上报相关数据,制定强制规定。

开发什么功能:

埋点全文检索能力、自动找到重复埋点(语义相近或者数据相近)功能。

开发阶段:

务必和开发沟通,并让开发在完全理解业务语义的情况下,在前端按照埋点代码规范进行埋点。

如何落地:

静态代码检查。

开发什么功能:

开发探测每个埋点对应到的代码文件和代码行,开发根据人均事件量级进行监控报警功能。

测试阶段:

务必和测试沟通,并让测试在完全理解业务语义的情况下,通过CodeReview和埋点测试两种方式对埋点正确性进行验证。

如何落地:

规定如果未经测试的埋点不允许上报埋点数据。

开发什么功能:

提供所见即所得的埋点测试平台。

验收阶段:

确保相关人员在完全理解业务语义的情况下,可以通过与自比较和他比较的方式来进行验证。

举例:

  • 他比较验证:前端某业务点数量和对应服务端数据应该基本相当。

如何落地:

规定如果未经验证的埋点不允许上报埋点数据。

开发什么功能:

提供埋点实时数据查询。

清理阶段:

确认完全理解语义的情况下,可对业务发生巨大变化或者不在维护的埋点进行废弃。

如何落地:

3个月以上未被使用的埋点、1个月以上数据为0的埋点自动废弃。3个月后使用次日会自动开启采集。

开发什么功能:

根据规则提供针对未使用埋点的计算、并自动废弃。

可以看出,规范要落地,需要整个公司的共识,也需要从上而下的压力,还有强势的制度。比如针对全公司个部门做评分,评分规则基于埋点和数据分析抽象出来。


另外我们在前端埋点中也遇到过一些注意事项,整理如下,希望对大家有所帮助。

注意事项:

一些埋点安全性问题:

如果你使用普通的http 协议,在数据传输的过程存在被劫持(包括不限于:JS代码注入等)的可能性,造成H5页面中出现诸如:未知的广告或者原网页被重定向等问题。为了降低此类事件发生的可能性,建议最好使用https 协议(倡导全站https),以确保数据传输过程的完整性,安全性。

版本迭代的时候埋点需要注意什么?

  • 如果是公用模块,可以非常放心安全的全量迁移埋点;
    • 如果是新增模块,那就需要注意是否遵循了最新的埋点规范,有没有重复的埋点命名存在,埋点是否符合业务逻辑;
    • 如果是下线模块,那就需要评估下线后对数据的影响范围,而且要第一时间充分周知相关方,让各方参与评估;
    • 如果是更新模块,需要评估在原有埋点上需要修改的埋点内容,是否需要重新埋点,删除不需要的埋点,而且要修改功能的数据承接。

感谢

背景

虽然我们系统有建设一些监控工具比如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,没有银弹。