All posts by dotte

一份非常完整的MySQL规范

一、数据库命令规范

  • 所有数据库对象名称必须使用小写字母并用下划线分割
  • 所有数据库对象名称禁止使用mysql保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来)
  • 数据库对象的命名要能做到见名识意,并且最后不要超过32个字符
  • 临时库表必须以tmp_为前缀并以日期为后缀,备份表必须以bak_为前缀并以日期(时间戳)为后缀
  • 所有存储相同数据的列名和列类型必须一致(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)

二、数据库基本设计规范

1、所有表必须使用Innodb存储引擎

没有特殊要求(即Innodb无法满足的功能如:列存储,存储空间数据等)的情况下,所有表必须使用Innodb存储引擎(mysql5.5之前默认使用Myisam,5.6以后默认的为Innodb)Innodb 支持事务,支持行级锁,更好的恢复性,高并发下性能更好

2、数据库和表的字符集统一使用UTF8

兼容性更好,统一字符集可以避免由于字符集转换产生的乱码,不同的字符集进行比较前需要进行转换会造成索引失效

3、所有表和字段都需要添加注释

使用comment从句添加表和列的备注 从一开始就进行数据字典的维护

4、尽量控制单表数据量的大小,建议控制在500万以内

500万并不是MySQL数据库的限制,过大会造成修改表结构,备份,恢复都会有很大的问题

可以用历史数据归档(应用于日志数据),分库分表(应用于业务数据)等手段来控制数据量大小

5、谨慎使用MySQL分区表

分区表在物理上表现为多个文件,在逻辑上表现为一个表 谨慎选择分区键,跨分区查询效率可能更低 建议采用物理分表的方式管理大数据

6、尽量做到冷热数据分离,减小表的宽度

MySQL限制每个表最多存储4096列,并且每一行数据的大小不能超过65535字节 减少磁盘IO,保证热数据的内存缓存命中率(表越宽,把表装载进内存缓冲池时所占用的内存也就越大,也会消耗更多的IO) 更有效的利用缓存,避免读入无用的冷数据 经常一起使用的列放到一个表中(避免更多的关联操作)

7、禁止在表中建立预留字段

预留字段的命名很难做到见名识义 预留字段无法确认存储的数据类型,所以无法选择合适的类型 对预留字段类型的修改,会对表进行锁定

8、禁止在数据库中存储图片,文件等大的二进制数据

通常文件很大,会短时间内造成数据量快速增长,数据库进行数据库读取时,通常会进行大量的随机IO操作,文件很大时,IO操作很耗时 通常存储于文件服务器,数据库只存储文件地址信息

9、禁止在线上做数据库压力测试

10、禁止从开发环境,测试环境直接连接生成环境数据库

三、数据库字段设计规范

1、优先选择符合存储需要的最小的数据类型

  • 原因

列的字段越大,建立索引时所需要的空间也就越大,这样一页中所能存储的索引节点的数量也就越少也越少,在遍历时所需要的IO次数也就越多, 索引的性能也就越差

  • 方法

1)将字符串转换成数字类型存储,如:将IP地址转换成整形数据。

mysql提供了两个方法来处理ip地址:

inet_aton 把ip转为无符号整型(4-8位)

inet_ntoa 把整型的ip转为地址

插入数据前,先用inet_aton把ip地址转为整型,可以节省空间。显示数据时,使用inet_ntoa把整型的ip地址转为地址显示即可。

2)对于非负型的数据(如自增ID、整型IP)来说,要优先使用无符号整型来存储

因为:无符号相对于有符号可以多出一倍的存储空间

SIGNED INT -2147483648~2147483647

UNSIGNED INT 0~4294967295

VARCHAR(N)中的N代表的是字符数,而不是字节数

使用UTF8存储255个汉字 Varchar(255)=765个字节。过大的长度会消耗更多的内存

2、避免使用TEXT、BLOB数据类型,最常见的TEXT类型可以存储64k的数据

  • 建议把BLOB或是TEXT列分离到单独的扩展表中

Mysql内存临时表不支持TEXT、BLOB这样的大数据类型,如果查询中包含这样的数据,在排序等操作时,就不能使用内存临时表,必须使用磁盘临时表进行。

而且对于这种数据,Mysql还是要进行二次查询,会使sql性能变得很差,但是不是说一定不能使用这样的数据类型。

如果一定要使用,建议把BLOB或是TEXT列分离到单独的扩展表中,查询时一定不要使用select * 而只需要取出必要的列,不需要TEXT列的数据时不要对该列进行查询。

  • TEXT或BLOB类型只能使用前缀索引

因为MySQL对索引字段长度是有限制的,所以TEXT类型只能使用前缀索引,并且TEXT列上是不能有默认值的。

3、避免使用ENUM类型

  • 修改ENUM值需要使用ALTER语句
  • ENUM类型的ORDER BY操作效率低,需要额外操作
  • 禁止使用数值作为ENUM的枚举值

4、尽可能把所有列定义为NOT

原因:

  • 索引列需要额外的空间来保存,所以要占用更多的空间;
  • 进行比较和计算时要对值做特别的处理

5、使用TIMESTAMP(4个字节)或DATETIME类型(8个字节)存储时间

TIMESTAMP 占用4字节和INT相同,但比INT可读性高

超出TIMESTAMP取值范围的使用DATETIME类型存储。

经常会有人用字符串存储日期型的数据(不正确的做法):

  • 缺点1:无法用日期函数进行计算和比较
  • 缺点2:用字符串存储日期要占用更多的空间

6、同财务相关的金额类数据必须使用decimal类型

  • 非精准浮点:float,double
  • 精准浮点:decimal

Decimal类型为精准浮点数,在计算时不会丢失精度。占用空间由定义的宽度决定,每4个字节可以存储9位数字,并且小数点要占用一个字节。可用于存储比bigint更大的整型数据。

四、索引设计规范

1、限制每张表上的索引数量,建议单张表索引不超过5个

索引并不是越多越好!索引可以提高效率同样可以降低效率。

索引可以增加查询效率,但同样也会降低插入和更新的效率,甚至有些情况下会降低查询效率。

因为mysql优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划,如果同时有很多个索引都可以用于查询,就会增加mysql优化器生成执行计划的时间,同样会降低查询性能。

2、禁止给表中的每一列都建立单独的索引

5.6版本之前,一个sql只能使用到一个表中的一个索引,5.6以后,虽然有了合并索引的优化方式,但是还是远远没有使用一个联合索引的查询方式好

3、每个Innodb表必须有个主键

Innodb是一种索引组织表:数据的存储的逻辑顺序和索引的顺序是相同的。

每个表都可以有多个索引,但是表的存储顺序只能有一种 Innodb是按照主键索引的顺序来组织表的。

不要使用更新频繁的列作为主键,不适用多列主键(相当于联合索引) 不要使用UUID、MD5、HASH、字符串列作为主键(无法保证数据的顺序增长)。

主键建议使用自增ID值。

五、常见索引列建议

  • 出现在SELECT、UPDATE、DELETE语句的WHERE从句中的列
  • 包含在ORDER BY、GROUP BY、DISTINCT中的字段

    并不要将符合1和2中的字段的列都建立一个索引,通常将1、2中的字段建立联合索引效果更好

  • 多表join的关联列

六、如何选择索引列的顺序

建立索引的目的是:希望通过索引进行数据查找,减少随机IO,增加查询性能 ,索引能过滤出越少的数据,则从磁盘中读入的数据也就越少。

  • 区分度最高的放在联合索引的最左侧(区分度=列中不同值的数量/列的总行数);
  • 尽量把字段长度小的列放在联合索引的最左侧(因为字段长度越小,一页能存储的数据量越大,IO性能也就越好);
  • 使用最频繁的列放到联合索引的左侧(这样可以比较少的建立一些索引)。

七、避免建立冗余索引和重复索引

因为这样会增加查询优化器生成执行计划的时间。

  • 重复索引示例:primary key(id)、index(id)、unique index(id)
  • 冗余索引示例:index(a,b,c)、index(a,b)、index(a)

八、优先考虑覆盖索引

对于频繁的查询优先考虑使用覆盖索引。

覆盖索引:就是包含了所有查询字段(where,select,ordery by,group by包含的字段)的索引

覆盖索引的好处:

  • 避免Innodb表进行索引的二次查询

Innodb是以聚集索引的顺序来存储的,对于Innodb来说,二级索引在叶子节点中所保存的是行的主键信息,

如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次查询 ,减少了IO操作,提升了查询效率。

  • 可以把随机IO变成顺序IO加快查询效率

由于覆盖索引是按键值的顺序存储的,对于IO密集型的范围查找来说,对比随机从磁盘读取每一行的数据IO要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的IO转变成索引查找的顺序IO。

九、索引SET规范

尽量避免使用外键约束

  • 不建议使用外键约束(foreign key),但一定要在表与表之间的关联键上建立索引;
  • 外键可用于保证数据的参照完整性,但建议在业务端实现;
  • 外键会影响父表和子表的写操作从而降低性能。

十、数据库SQL开发规范

1、建议使用预编译语句进行数据库操作

预编译语句可以重复使用这些计划,减少SQL编译所需要的时间,还可以解决动态SQL所带来的SQL注入的问题 只传参数,比传递SQL语句更高效 相同语句可以一次解析,多次使用,提高处理效率。

2、避免数据类型的隐式转换

隐式转换会导致索引失效。如:select name,phone from customer where id = 111;

3、充分利用表上已经存在的索引

  • 避免使用双%号的查询条件。

如a like %123%,(如果无前置%,只有后置%,是可以用到列上的索引的)

  • 一个SQL只能利用到复合索引中的一列进行范围查询

如:有 a,b,c列的联合索引,在查询条件中有a列的范围查询,则在b,c列上的索引将不会被用到,在定义联合索引时,如果a列要用到范围查找的话,就要把a列放到联合索引的右侧。

  • 使用left join或 not exists来优化not in操作

因为not in 也通常会使用索引失效。

4、数据库设计时,应该要对以后扩展进行考虑

5、程序连接不同的数据库使用不同的账号,进制跨库查询

  • 为数据库迁移和分库分表留出余地
  • 降低业务耦合度
  • 避免权限过大而产生的安全风险

6、禁止使用SELECT * 必须使用SELECT <字段列表> 查询

原因:

  • 消耗更多的CPU和IO以网络带宽资源
  • 无法使用覆盖索引
  • 可减少表结构变更带来的影响

7、禁止使用不含字段列表的INSERT语句

如:insert into values (a,b,c);

应使用insert into t(c1,c2,c3) values (a,b,c);

8、避免使用子查询,可以把子查询优化为join操作

通常子查询在in子句中,且子查询中为简单SQL(不包含union、group by、order by、limit从句)时,才可以把子查询转化为关联查询进行优化。

子查询性能差的原因:

  • 子查询的结果集无法使用索引,通常子查询的结果集会被存储到临时表中,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能会受到一定的影响;
  • 特别是对于返回结果集比较大的子查询,其对查询性能的影响也就越大;
  • 由于子查询会产生大量的临时表也没有索引,所以会消耗过多的CPU和IO资源,产生大量的慢查询。

9、避免使用JOIN关联太多的表

对于Mysql来说,是存在关联缓存的,缓存的大小可以由join_buffer_size参数进行设置。

在Mysql中,对于同一个SQL多关联(join)一个表,就会多分配一个关联缓存,如果在一个SQL中关联的表越多,所占用的内存也就越大。

如果程序中大量的使用了多表关联的操作,同时join_buffer_size设置的也不合理的情况下,就容易造成服务器内存溢出的情况,就会影响到服务器数据库性能的稳定性。

同时对于关联操作来说,会产生临时表操作,影响查询效率Mysql最多允许关联61个表,建议不超过5个。

10、减少同数据库的交互次数

数据库更适合处理批量操作 合并多个相同的操作到一起,可以提高处理效率

11、对应同一列进行or判断时,使用in代替or

in的值不要超过500个in操作可以更有效的利用索引,or大多数情况下很少能利用到索引。

12、禁止使用order by rand 进行随机排序

会把表中所有符合条件的数据装载到内存中,然后在内存中对所有数据根据随机生成的值进行排序,并且可能会对每一行都生成一个随机值,如果满足条件的数据集非常大,就会消耗大量的CPU和IO及内存资源。

推荐在程序中获取一个随机值,然后从数据库中获取数据的方式

13、WHERE从句中禁止对列进行函数转换和计算

对列进行函数转换或计算时会导致无法使用索引。

  • 不推荐:

where date(create_time)=20190101

  • 推荐:

where create_time >= 20190101 and create_time < 20190102

14、在明显不会有重复值时使用UNION ALL而不是UNION

  • UNION会把两个结果集的所有数据放到临时表中后再进行去重操作
  • UNION ALL不会再对结果集进行去重操作

15、拆分复杂的大SQL为多个小SQL

  • 大SQL:逻辑上比较复杂,需要占用大量CPU进行计算的SQL
  • MySQL:一个SQL只能使用一个CPU进行计算
  • SQL拆分后可以通过并行执行来提高处理效率

十一、数据库操作行为规范

1、超100万行的批量写(UPDATE、DELETE、INSERT)操作,要分批多次进行操作

  • 大批量操作可能会造成严重的主从延迟

主从环境中,大批量操作可能会造成严重的主从延迟,大批量的写操作一般都需要执行一定长的时间,而只有当主库上执行完成后,才会在其他从库上执行,所以会造成主库与从库长时间的延迟情况

  • binlog日志为row格式时会产生大量的日志

大批量写操作会产生大量日志,特别是对于row格式二进制数据而言,由于在row格式中会记录每一行数据的修改,我们一次修改的数据越多,产生的日志量也就会越多,日志的传输和恢复所需要的时间也就越长,这也是造成主从延迟的一个原因。

  • 避免产生大事务操作

大批量修改数据,一定是在一个事务中进行的,这就会造成表中大批量数据进行锁定,从而导致大量的阻塞,阻塞会对MySQL的性能产生非常大的影响。

特别是长时间的阻塞会占满所有数据库的可用连接,这会使生产环境中的其他应用无法连接到数据库,因此一定要注意大批量写操作要进行分批。

2、对于大表使用pt-online-schema-change修改表结构

  • 避免大表修改产生的主从延迟
  • 避免在对表字段进行修改时进行锁表

对大表数据结构的修改一定要谨慎,会造成严重的锁表操作,尤其是生产环境,是不能容忍的。

pt-online-schema-change它会首先建立一个与原表结构相同的新表,并且在新表上进行表结构的修改,然后再把原表中的数据复制到新表中,并在原表中增加一些触发器。

把原表中新增的数据也复制到新表中,在行所有数据复制完成之后,把新表命名成原表,并把原来的表删除掉。

把原来一个DDL操作,分解成多个小的批次进行。

3、禁止为程序使用的账号赋予super权限

当达到最大连接数限制时,还运行1个有super权限的用户连接super权限只能留给DBA处理问题的账号使用。

4、对于程序连接数据库账号,遵循权限最小原则

程序使用数据库账号只能在一个DB下使用,不准跨库 程序使用的账号原则上不准有drop权限。

一、数据库命令规范

  • 所有数据库对象名称必须使用小写字母并用下划线分割
  • 所有数据库对象名称禁止使用mysql保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来)
  • 数据库对象的命名要能做到见名识意,并且最后不要超过32个字符
  • 临时库表必须以tmp_为前缀并以日期为后缀,备份表必须以bak_为前缀并以日期(时间戳)为后缀
  • 所有存储相同数据的列名和列类型必须一致(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)

二、数据库基本设计规范

1、所有表必须使用Innodb存储引擎

没有特殊要求(即Innodb无法满足的功能如:列存储,存储空间数据等)的情况下,所有表必须使用Innodb存储引擎(mysql5.5之前默认使用Myisam,5.6以后默认的为Innodb)Innodb 支持事务,支持行级锁,更好的恢复性,高并发下性能更好

2、数据库和表的字符集统一使用UTF8

兼容性更好,统一字符集可以避免由于字符集转换产生的乱码,不同的字符集进行比较前需要进行转换会造成索引失效

3、所有表和字段都需要添加注释

使用comment从句添加表和列的备注 从一开始就进行数据字典的维护

4、尽量控制单表数据量的大小,建议控制在500万以内

500万并不是MySQL数据库的限制,过大会造成修改表结构,备份,恢复都会有很大的问题

可以用历史数据归档(应用于日志数据),分库分表(应用于业务数据)等手段来控制数据量大小

5、谨慎使用MySQL分区表

分区表在物理上表现为多个文件,在逻辑上表现为一个表 谨慎选择分区键,跨分区查询效率可能更低 建议采用物理分表的方式管理大数据

6、尽量做到冷热数据分离,减小表的宽度

MySQL限制每个表最多存储4096列,并且每一行数据的大小不能超过65535字节 减少磁盘IO,保证热数据的内存缓存命中率(表越宽,把表装载进内存缓冲池时所占用的内存也就越大,也会消耗更多的IO) 更有效的利用缓存,避免读入无用的冷数据 经常一起使用的列放到一个表中(避免更多的关联操作)

7、禁止在表中建立预留字段

预留字段的命名很难做到见名识义 预留字段无法确认存储的数据类型,所以无法选择合适的类型 对预留字段类型的修改,会对表进行锁定

8、禁止在数据库中存储图片,文件等大的二进制数据

通常文件很大,会短时间内造成数据量快速增长,数据库进行数据库读取时,通常会进行大量的随机IO操作,文件很大时,IO操作很耗时 通常存储于文件服务器,数据库只存储文件地址信息

9、禁止在线上做数据库压力测试

10、禁止从开发环境,测试环境直接连接生成环境数据库

三、数据库字段设计规范

1、优先选择符合存储需要的最小的数据类型

  • 原因

列的字段越大,建立索引时所需要的空间也就越大,这样一页中所能存储的索引节点的数量也就越少也越少,在遍历时所需要的IO次数也就越多, 索引的性能也就越差

  • 方法

1)将字符串转换成数字类型存储,如:将IP地址转换成整形数据。

mysql提供了两个方法来处理ip地址:

inet_aton 把ip转为无符号整型(4-8位)

inet_ntoa 把整型的ip转为地址

插入数据前,先用inet_aton把ip地址转为整型,可以节省空间。显示数据时,使用inet_ntoa把整型的ip地址转为地址显示即可。

2)对于非负型的数据(如自增ID、整型IP)来说,要优先使用无符号整型来存储

因为:无符号相对于有符号可以多出一倍的存储空间

SIGNED INT -2147483648~2147483647

UNSIGNED INT 0~4294967295

VARCHAR(N)中的N代表的是字符数,而不是字节数

使用UTF8存储255个汉字 Varchar(255)=765个字节。过大的长度会消耗更多的内存

2、避免使用TEXT、BLOB数据类型,最常见的TEXT类型可以存储64k的数据

  • 建议把BLOB或是TEXT列分离到单独的扩展表中

Mysql内存临时表不支持TEXT、BLOB这样的大数据类型,如果查询中包含这样的数据,在排序等操作时,就不能使用内存临时表,必须使用磁盘临时表进行。

而且对于这种数据,Mysql还是要进行二次查询,会使sql性能变得很差,但是不是说一定不能使用这样的数据类型。

如果一定要使用,建议把BLOB或是TEXT列分离到单独的扩展表中,查询时一定不要使用select * 而只需要取出必要的列,不需要TEXT列的数据时不要对该列进行查询。

  • TEXT或BLOB类型只能使用前缀索引

因为MySQL对索引字段长度是有限制的,所以TEXT类型只能使用前缀索引,并且TEXT列上是不能有默认值的。

3、避免使用ENUM类型

  • 修改ENUM值需要使用ALTER语句
  • ENUM类型的ORDER BY操作效率低,需要额外操作
  • 禁止使用数值作为ENUM的枚举值

4、尽可能把所有列定义为NOT

原因:

  • 索引列需要额外的空间来保存,所以要占用更多的空间;
  • 进行比较和计算时要对值做特别的处理

5、使用TIMESTAMP(4个字节)或DATETIME类型(8个字节)存储时间

TIMESTAMP 占用4字节和INT相同,但比INT可读性高

超出TIMESTAMP取值范围的使用DATETIME类型存储。

经常会有人用字符串存储日期型的数据(不正确的做法):

  • 缺点1:无法用日期函数进行计算和比较
  • 缺点2:用字符串存储日期要占用更多的空间

6、同财务相关的金额类数据必须使用decimal类型

  • 非精准浮点:float,double
  • 精准浮点:decimal

Decimal类型为精准浮点数,在计算时不会丢失精度。占用空间由定义的宽度决定,每4个字节可以存储9位数字,并且小数点要占用一个字节。可用于存储比bigint更大的整型数据。

四、索引设计规范

1、限制每张表上的索引数量,建议单张表索引不超过5个

索引并不是越多越好!索引可以提高效率同样可以降低效率。

索引可以增加查询效率,但同样也会降低插入和更新的效率,甚至有些情况下会降低查询效率。

因为mysql优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划,如果同时有很多个索引都可以用于查询,就会增加mysql优化器生成执行计划的时间,同样会降低查询性能。

2、禁止给表中的每一列都建立单独的索引

5.6版本之前,一个sql只能使用到一个表中的一个索引,5.6以后,虽然有了合并索引的优化方式,但是还是远远没有使用一个联合索引的查询方式好

3、每个Innodb表必须有个主键

Innodb是一种索引组织表:数据的存储的逻辑顺序和索引的顺序是相同的。

每个表都可以有多个索引,但是表的存储顺序只能有一种 Innodb是按照主键索引的顺序来组织表的。

不要使用更新频繁的列作为主键,不适用多列主键(相当于联合索引) 不要使用UUID、MD5、HASH、字符串列作为主键(无法保证数据的顺序增长)。

主键建议使用自增ID值。

五、常见索引列建议

  • 出现在SELECT、UPDATE、DELETE语句的WHERE从句中的列
  • 包含在ORDER BY、GROUP BY、DISTINCT中的字段

    并不要将符合1和2中的字段的列都建立一个索引,通常将1、2中的字段建立联合索引效果更好

  • 多表join的关联列

六、如何选择索引列的顺序

建立索引的目的是:希望通过索引进行数据查找,减少随机IO,增加查询性能 ,索引能过滤出越少的数据,则从磁盘中读入的数据也就越少。

  • 区分度最高的放在联合索引的最左侧(区分度=列中不同值的数量/列的总行数);
  • 尽量把字段长度小的列放在联合索引的最左侧(因为字段长度越小,一页能存储的数据量越大,IO性能也就越好);
  • 使用最频繁的列放到联合索引的左侧(这样可以比较少的建立一些索引)。

七、避免建立冗余索引和重复索引

因为这样会增加查询优化器生成执行计划的时间。

  • 重复索引示例:primary key(id)、index(id)、unique index(id)
  • 冗余索引示例:index(a,b,c)、index(a,b)、index(a)

八、优先考虑覆盖索引

对于频繁的查询优先考虑使用覆盖索引。

覆盖索引:就是包含了所有查询字段(where,select,ordery by,group by包含的字段)的索引

覆盖索引的好处:

  • 避免Innodb表进行索引的二次查询

Innodb是以聚集索引的顺序来存储的,对于Innodb来说,二级索引在叶子节点中所保存的是行的主键信息,

如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次查询 ,减少了IO操作,提升了查询效率。

  • 可以把随机IO变成顺序IO加快查询效率

由于覆盖索引是按键值的顺序存储的,对于IO密集型的范围查找来说,对比随机从磁盘读取每一行的数据IO要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的IO转变成索引查找的顺序IO。

九、索引SET规范

尽量避免使用外键约束

  • 不建议使用外键约束(foreign key),但一定要在表与表之间的关联键上建立索引;
  • 外键可用于保证数据的参照完整性,但建议在业务端实现;
  • 外键会影响父表和子表的写操作从而降低性能。

十、数据库SQL开发规范

1、建议使用预编译语句进行数据库操作

预编译语句可以重复使用这些计划,减少SQL编译所需要的时间,还可以解决动态SQL所带来的SQL注入的问题 只传参数,比传递SQL语句更高效 相同语句可以一次解析,多次使用,提高处理效率。

2、避免数据类型的隐式转换

隐式转换会导致索引失效。如:select name,phone from customer where id = 111;

3、充分利用表上已经存在的索引

  • 避免使用双%号的查询条件。

如a like %123%,(如果无前置%,只有后置%,是可以用到列上的索引的)

  • 一个SQL只能利用到复合索引中的一列进行范围查询

如:有 a,b,c列的联合索引,在查询条件中有a列的范围查询,则在b,c列上的索引将不会被用到,在定义联合索引时,如果a列要用到范围查找的话,就要把a列放到联合索引的右侧。

  • 使用left join或 not exists来优化not in操作

因为not in 也通常会使用索引失效。

4、数据库设计时,应该要对以后扩展进行考虑

5、程序连接不同的数据库使用不同的账号,进制跨库查询

  • 为数据库迁移和分库分表留出余地
  • 降低业务耦合度
  • 避免权限过大而产生的安全风险

6、禁止使用SELECT * 必须使用SELECT <字段列表> 查询

原因:

  • 消耗更多的CPU和IO以网络带宽资源
  • 无法使用覆盖索引
  • 可减少表结构变更带来的影响

7、禁止使用不含字段列表的INSERT语句

如:insert into values (a,b,c);

应使用insert into t(c1,c2,c3) values (a,b,c);

8、避免使用子查询,可以把子查询优化为join操作

通常子查询在in子句中,且子查询中为简单SQL(不包含union、group by、order by、limit从句)时,才可以把子查询转化为关联查询进行优化。

子查询性能差的原因:

  • 子查询的结果集无法使用索引,通常子查询的结果集会被存储到临时表中,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能会受到一定的影响;
  • 特别是对于返回结果集比较大的子查询,其对查询性能的影响也就越大;
  • 由于子查询会产生大量的临时表也没有索引,所以会消耗过多的CPU和IO资源,产生大量的慢查询。

9、避免使用JOIN关联太多的表

对于Mysql来说,是存在关联缓存的,缓存的大小可以由join_buffer_size参数进行设置。

在Mysql中,对于同一个SQL多关联(join)一个表,就会多分配一个关联缓存,如果在一个SQL中关联的表越多,所占用的内存也就越大。

如果程序中大量的使用了多表关联的操作,同时join_buffer_size设置的也不合理的情况下,就容易造成服务器内存溢出的情况,就会影响到服务器数据库性能的稳定性。

同时对于关联操作来说,会产生临时表操作,影响查询效率Mysql最多允许关联61个表,建议不超过5个。

10、减少同数据库的交互次数

数据库更适合处理批量操作 合并多个相同的操作到一起,可以提高处理效率

11、对应同一列进行or判断时,使用in代替or

in的值不要超过500个in操作可以更有效的利用索引,or大多数情况下很少能利用到索引。

12、禁止使用order by rand 进行随机排序

会把表中所有符合条件的数据装载到内存中,然后在内存中对所有数据根据随机生成的值进行排序,并且可能会对每一行都生成一个随机值,如果满足条件的数据集非常大,就会消耗大量的CPU和IO及内存资源。

推荐在程序中获取一个随机值,然后从数据库中获取数据的方式

13、WHERE从句中禁止对列进行函数转换和计算

对列进行函数转换或计算时会导致无法使用索引。

  • 不推荐:

where date(create_time)=20190101

  • 推荐:

where create_time >= 20190101 and create_time < 20190102

14、在明显不会有重复值时使用UNION ALL而不是UNION

  • UNION会把两个结果集的所有数据放到临时表中后再进行去重操作
  • UNION ALL不会再对结果集进行去重操作

15、拆分复杂的大SQL为多个小SQL

  • 大SQL:逻辑上比较复杂,需要占用大量CPU进行计算的SQL
  • MySQL:一个SQL只能使用一个CPU进行计算
  • SQL拆分后可以通过并行执行来提高处理效率

十一、数据库操作行为规范

1、超100万行的批量写(UPDATE、DELETE、INSERT)操作,要分批多次进行操作

  • 大批量操作可能会造成严重的主从延迟

主从环境中,大批量操作可能会造成严重的主从延迟,大批量的写操作一般都需要执行一定长的时间,而只有当主库上执行完成后,才会在其他从库上执行,所以会造成主库与从库长时间的延迟情况

  • binlog日志为row格式时会产生大量的日志

大批量写操作会产生大量日志,特别是对于row格式二进制数据而言,由于在row格式中会记录每一行数据的修改,我们一次修改的数据越多,产生的日志量也就会越多,日志的传输和恢复所需要的时间也就越长,这也是造成主从延迟的一个原因。

  • 避免产生大事务操作

大批量修改数据,一定是在一个事务中进行的,这就会造成表中大批量数据进行锁定,从而导致大量的阻塞,阻塞会对MySQL的性能产生非常大的影响。

特别是长时间的阻塞会占满所有数据库的可用连接,这会使生产环境中的其他应用无法连接到数据库,因此一定要注意大批量写操作要进行分批。

2、对于大表使用pt-online-schema-change修改表结构

  • 避免大表修改产生的主从延迟
  • 避免在对表字段进行修改时进行锁表

对大表数据结构的修改一定要谨慎,会造成严重的锁表操作,尤其是生产环境,是不能容忍的。

pt-online-schema-change它会首先建立一个与原表结构相同的新表,并且在新表上进行表结构的修改,然后再把原表中的数据复制到新表中,并在原表中增加一些触发器。

把原表中新增的数据也复制到新表中,在行所有数据复制完成之后,把新表命名成原表,并把原来的表删除掉。

把原来一个DDL操作,分解成多个小的批次进行。

3、禁止为程序使用的账号赋予super权限

当达到最大连接数限制时,还运行1个有super权限的用户连接super权限只能留给DBA处理问题的账号使用。

4、对于程序连接数据库账号,遵循权限最小原则

程序使用数据库账号只能在一个DB下使用,不准跨库 程序使用的账号原则上不准有drop权限。

from:https://www.cnblogs.com/duaimili/p/10277515.html

AI领域必看的45篇论文

而AI领域的发展会是IT中最快的。我们所看到的那些黑客技,其后面无不堆积了大量的论文。而且都是最新、最前沿的论文。

从某种调度来讲,他们所用的技术跟书籍里的内容确实不是一个时代。要想与时俱进,就必须改变思路——从论文入手。

今天给大家介绍45篇让你跟上AI时代的论文。

一、神经网络基础部分

No1  wide_deep模型论文:

关于神经元、全连接网络之类的基础结构,想必每个AI学者都有了解。那么你是否真的了解全连接网络中深层与浅层的关系呢?来看看wide_deep模型吧。这篇论文会使你对全连接有个更深刻的理解。

关于该模型的更多介绍可以参考论文:

https://arxiv.org/pdf/1606.07792.pdf

在wide_deep模型中,wide模型和deep模型具有各自不同的分工。

  • —wide模型:一种浅层模型。它通过大量的单层网络节点,实现对训练样本的高度拟合性。它的缺点是泛化能力很差。
  • —deep模型:一种深层模型。它通过多层的非线性变化,使模型具有很好的泛化性。它的缺点是拟合度欠缺。

将二者结合起来——用联合训练方法共享反向传播的损失值来进行训练—可以使两个模型综合优点,得到最好的结果。

No2  wide_deep模型论文:

为什么Adam被广泛使用?光会用可不行,还得把原理看懂。这样出去喷一喷,才会显得更有面子。

Adam的细节请参阅论文《Adam: A Method for Stochastic Optimization》,该论文的链接网址是:

https://arxiv.org/pdf/1412.6980v8.pdf

No3  Targeted Dropout模型论文:

你还再用普通的Dropout吗?我已经开始用Targeted Dropout了。比你的又快,又好。你不知道吧,赶紧学习一下。

Targeted Dropout不再像原有的Dropout那样按照设定的比例随机丢弃部分节点,而是对现有的神经元进行排序,按照神经元权重重要性来丢弃节点。这种方式比随机丢弃的方式更智能,效果更好。更多理论见以下论文:

https://openreview.net/pdf?id=HkghWScuoQ

二、图像分类部分

No4  Xception模型论文:

在那个图像分类的时代,谷歌的Xception系列,像x战警一样,一个一个的打破记录。其中的技术也逐渐成为AI发展的知识体系。有必要看一下。或许会对自己的工作有所启发。

详细情况请查看原论文《Xception: Deep Learning with Depthwise Separable Convolutions》,该论文网址是:

https://arxiv.org/abs/1610.02357

No5  残差结构论文:

运气好到没朋友,现有模型,后完善理论指的就是残差结构这哥们。他的传奇导致即使到今天的AI技术,也无法将它割舍,就来常微分方程都得拿它比肩。快来学学吧。用处大着呢。好多模型都拿他当先锋。

利用残差结构,可以使得网络达到上百层的深度。详情请参阅原始论文《Deep ResidualLearning for Image Recognition》,该论文网址是:

https://arxiv.org/abs/1512.03385

No6  空洞卷积论文:

NasNet的招牌动作,虽然不是出于NASNet,但是却被人家用得如火纯青。有时不得不惊叹,机器设计出来的模型还真实跟人设计的不一样!

想知道空洞卷积感受野为什么与层数呈指数级关系吗?

细节请查看原论文《Multi-scale context aggregation by dilated convolutions》,该论文网址是:

https://arxiv.org/abs/1511.07122v3

No7  DenseNet论文:

这个模型使我想到了“一根筋”,再次证明了只有轴的人才能成大事!令类的模型,神奇的效果,快来体验一下吧。这可是比华佗还牛的神医哦!

有关DenseNet模型的细节,请参考原始论文《Densely Connected Convolutional Networks》,该论文的连接是:

https://arxiv.org/abs/1608.06993

No8  EfficientNet模型论文:

知道目前位置图像分类界谁是老大吗? 来,看看这个!

EfficientNet模型的论文地址如下:

https://arxiv.org/pdf/1905.11946.pdf

No9  Grad-CAM模型论文:

如果你能把神经元搞得透彻,你也会想到这个点子。不想聊太多!一个字“绝”!这TMD才叫卷积网络的可视化!

详细情况请参阅论文《Grad-CAM:Visual Explanations from Deep Networks via Gradient-based Localization》,该论文的链接网址是:

https://arxiv.org/pdf/1610.02391.pdf

No10  分类模型泛化能力论文:

知道为啥都喜欢使用ResNet模型当先锋吗?运气好就是运气好!好到大家都喜欢用它,还说不出为啥它那么好!反正就是好,不信的话看看这篇论文的实验结果。

论文中,在选取模型的建议中,多次提到了ResNet模型。原因是,ResNet模型在Imgnet数据集上输出的特征向量所表现的泛化能力是最强的。具体可以参考以下论文:

https://arxiv.org/pdf/1805.08974.pdf

三、批量正则化部分

No11  批量正则化论文:

这个没的说,必修课,不懂的化,会被鄙视成渣渣!

论文《Batch Normalization Accelerating Deep Network Training by Reducing Internal Covariate Shift》,该论文网址是:

https://arxiv.org/abs/1502.03167

No12  实例归一化论文:

时代不同了,批量归一化也升级了,赶紧学学新的归一化吧。

在对抗神经网络模型、风格转换这类生成式任务中,常用实例归一化取代批量归一化。

因为,生成式任务的本质是——将生成样本的特征分布与目标样本的特征分布进行匹配。生成式任务中的每个样本都有独立的风格,不应该与批次中其他的样本产生太多联系。所以,实例归一化适用于解决这种基于个体的样本分布问题。详细说明见以下链接:

https://arxiv.org/abs/1607.08022

No13  ReNorm算法论文:

ReNorm算法与BatchNorm算法一样,注重对全局数据的归一化,即对输入数据的形状中的N维度、H维度、W维度做归一化处理。不同的是,ReNorm算法在BatchNorm算法上做了一些改进,使得模型在小批次场景中也有良好的效果。具体论文见以下链接:

https://arxiv.org/pdf/1702.03275.pdf

No14  GroupNorm算法论文:

GroupNorm算法是介于LayerNorm算法和InstanceNorm算法之间的算法。它首先将通道分为许多组(group),再对每一组做归一化处理。

GroupNorm算法与ReNorm算法的作用类似,都是为了解决BatchNorm算法对批次大小的依赖。具体论文见下方链接:

https://arxiv.org/abs/1803.08494

No15  SwitchableNorm算法论文:

我们国人做产品都喜欢这么干!all in one  ,好吧。既然那么多批量归一化的方法。来,来,来,我们来个all in one吧。不服来辩,我这啥都有!

SwitchableNorm算法是将BN算法、LN算法、IN算法结合起来使用,并为每个算法都赋予权重,让网络自己去学习归一化层应该使用什么方法。具体论文见下方链接:

https://arxiv.org/abs/1806.1077

四、注意力部分

No16  大道至简的注意力论文:

把AI搞成玄学也就算了!居然还扯到道家了!谷歌的工程师真实中外通吃啊!搞出来了一个只用注意力就能做事的模型,连卷积都不要了!你所好玩不好玩!至简不至简!刺激不刺激!

大名鼎鼎的Attention is All You Need 注意力机制论文,注意力机制因2017年谷歌的一篇论文Attention is All You Need而名声大噪。下面就来介绍该技术的具体内容。如果想了解更多,还可以参考原论文,具体地址如下:

https://arxiv.org/abs/1706.03762

No17-18  孪生注意力论文:

好比LSTM与GRU一样,注意力他们家也除了一对双胞胎,长得略微有点不同。但是功能一样,都能吃能喝,还能注意。老虎老鼠傻傻的不清楚!

—BahdanauAttention:https://arxiv.org/abs/1409.0473。

—LuongAttention:https://arxiv.org/abs/1508.04025。

No19  各自升级的孪生注意力论文:

话说这对双胞胎,出生后就分开了。各自学的不同的语言,一个学习汉语,一个学习中文。若干年后,见面,发现二者的能力还是一样!

BahdanauAttention注意力升级成了normed_BahdanauAttention,而LuongAttention注意力升级成了scaled_LuongAttention。都一样的效果,你爱用哪个用哪个吧!

例如:

在BahdanauAttention类中有一个权重归一化的版本(normed_BahdanauAttention),它可以加快随机梯度下降收敛速度。在使用时,将初始化函数中的参数normalize设为True即可。

具体可以参考以下论文:

https://arxiv.org/pdf/1602.07868.pdf

No20  单调注意力机制论文:

老公主动表忠心,我以后不看别的美女。老婆觉得不够,再加个限制:你以后不准看别的女人!于是单调注意力就出来了。

单调注意力机制(monotonic attention),是在原有注意力机制上添加了一个单调约束。该单调约束的内容为:

  • 假设在生成输出序列过程中,模型是以从左到右的方式处理输入序列的。
  • 当某个输入序列所对应的输出受到关注时,在该输入序列之前出现的其他输入将不能在后面的输出中被关注。

即已经被关注过的输入序列,其前面的序列中不再被关注。

更多描述可以参考以下论文:

https://arxiv.org/pdf/1704.00784.pdf

No21  混合注意力机制论文:

这个注意力很强大,比一般的注意力专注的地方更多,信息更丰富。我已经注意你很久了!呵呵呵~~~

因为混合注意力中含有位置信息,所以它可以在输入序列中选择下一个编码的位置。这样的机制更适用于输出序列大于输入序列的Seq2Seq任务,例如语音合成任务。

具体可以参考以下论文:

https://arxiv.org/pdf/1506.07503.pdf

五、高级的卷积网络知识

No22  胶囊网络与动态路由的论文:

这是一股为图像分类降温的寒风,深刻而又尖锐的点出了卷积网络的硬伤! 从事最大池化再无翻身之日。

虽然胶囊网络再实际应用中,不像它的理论那么牛,但是对AI的帮助,卷积的理解是革命性的。非常值得一读。另外,这也是一篇绝对让你对数学彻底绝望的论文。花几根白头发把里面的算法啃下来吧。这样你与大神就能更近一步。

胶囊网络分为主胶囊与数字胶囊,主胶囊与数字胶囊之间的耦合系数是通过训练得来的。在训练过程中,耦合系数的更新不是通过反向梯度传播实现的,而是采用动态路由选择算法完成的。该算法来自以下论文链接:

https://arxiv.org/pdf/1710.09829.pdf

目前胶囊网络的研究还处于初级阶段,随着人们研究的深入,相信这些问题会得到解决。

No23  矩阵胶囊网络与EM路由算法:

如果你觉得不过瘾,那么还可以再看一篇。继续自虐一下。

带有EM(期望最大化)路由的矩阵胶囊网络是动态路由胶囊网络的一个改进版本。论文链接如下:

https://openreview.net/pdf?id=HJWLfGWRb

No24  胶囊网络的其它用处:

胶囊网络混身是宝,但就是自己不争气。这也说明还有上升的空间。就拿其中一个动态路由算法来讲,居然比普通的注意力还好。

看完之后,相信你一定会手痒!要不要也试试?把你的注意力换一下。值得你尝试,会有彩蛋的!

该论文的实践也证明,与原有的注意力机制相比,动态路由算法确实在精度上有所提升。具体介绍可见以下论文:

https://arxiv.org/pdf/1806.01501.pdf

No25  卷积网络新玩法TextCNN模型:

早先小编在一个项目中,自己用卷积网络处理字符数据。自己感觉很Happy。没想到,无意间居然发现了一篇同样这么干的论文。居然还有个名字,叫TextCNN。哎!可惜啊!小编文化少,只会写代码,不会写论文。

TextCNN模型是利用卷积神经网络对文本进行分类的算法,由 Yoon Kim 在 Convolutional Neural Networks for Sentence Classification 一文中提出。论文地址:

https://arxiv.org/pdf/1408.5882.pdf

六、图像内容处理部分

No26  FPN模型论文(包含了ROIAlign的匹配算法):

要是搞计算机视觉,还是要建议看一下。非常的基础。也是图像分割方面的用得最多得模型。

FPN的原理是:将骨干网络最终特征层和中间特征层的多个尺度的特征以类似金字塔的形式融合在一起。最终的特征可以兼顾两个特点——指向收敛目标的特征准确、特征语义信息丰富。更多信息可以参考论文:

ROIAlign层中的匹配算法也来自于这篇FPN论文,链接如下:

https://arxiv.org/abs/1612.03144

No27 Mask R-CNN模型论文:

效果好,代码多!硬货!来啃吧!

Mask R-CNN模型是一个简单、灵活、通用的对象实例分割框架。它能够有效地检测图像中的对象,并为每个实例生成高质量的分割掩码,还可以通过增加不同的分支完成不同的任务。它可以完成目标分类、目标检测语义分割、实例分割、人体姿势识别等多种任务。具体细节可以参考以下论文:

https://arxiv.org/abs/1703.06870

No28  YOLO V3模型论文:

这个模型的提点就是快!

目标识别强烈推荐,YOLO V3模型的更多信息可以参考以下链接中的论文:

https://pjreddie.com/media/files/papers/YOLOv3.pdf

No29  Anchor-Fress模型–FCOS模型论文:

随着AI技术的进步Anchor-Fress模型死灰复燃(早先是YOLO V1那一批模型),这次不一样的是彻底干掉带Anchor的模型。训练起来那就一个爽!妈妈再也不用为我准备单独的Anchor标签了。

YOLO V1相比, FCOS模型的思想与YOLO V1模型非常相似,唯一不同的是FCOS模型没有像YOLOv1那样只考虑中心附近的点,而是利用了ground truth边框中所有的点来进行预测边框。并且通过 center-ness 分支来抑制那些效果不行的检测边框。这样FCOS 就可以改善YOLO V1模型总会漏掉部分检测边框的缺点。

相关论文地址:

https://arxiv.org/abs/1904.01355

No30  Anchor-Fress模型–CornerNet-Lite模型论文:

一样也是Anchor-Fress模型,与FCOS效果差不多少。具体看一下论文吧CornerNet-Lite模型。相关论文地址:

https://arxiv.org/pdf/1904.08900.pdf

No31  栈式沙漏网络模型–Hourglass论文:

最初用户人的姿态估计,在符合模型中也是常被使用的模型。论文地址:

https://arxiv.org/abs/1603.06937

No32  OCR必修课——STN模型论文:

可以让模型自动仿射变化,你说牛不牛!要学OCR,就得从这个开始。

有关STN模型的论文链接如下:

https://arxiv.org/abs/1506.02025

七、循环神经网络部分

No33  QRNN模型论文:

在RNN模型的cell里,如果还只知道LSTM和GRU。那就太low了。快了补补吧:

如果想更多了解QRNN,可以参考以下论文:

https://arxiv.org/abs/1611.01576

No34  SRU模型论文:

接着来,各种RNN的Cell。又漂亮,又好吃!

SRU单元在本质上与QRNN单元很像。从网络构建上看,SRU单元有点像QRNN单元中的一个特例,但是又比QRNN单元多了一个直连的设计。

若需要研究SRU单元更深层面的理论,可以参考如下论文:

https://arxiv.org/abs/1709.02755

No35  IndRNN模型论文:

再补一个,这可都是好cell啊!

将IndRNN单元配合ReLu等非饱和激活函数一起使用,会使模型表现出更好的鲁棒性。

有关IndRNN单元的更多理论,可以参考论文:

https://arxiv.org/abs/1803.04831

No36  IndRNN模型论文:

最后,再来一个cell,如想要了解更多关于JANET单元的内容,可以参考以下论文:

https://arxiv.org/abs/1804.04849

八、AI合成部分

No37-38  Tacotron与Tacotron-2模型论文:

AI合成部分的经典模型,以上结构来自Tacotron与Tacotron-2两个结构,更多内容可以参考以下两篇论文:

https://arxiv.org/pdf/1703.10135.pdf

https://arxiv.org/pdf/1712.05884.pdf

No39  DeblurGAN模型论文:

图片合成的论文太多了。这里简单列几个,大体原理和思路了解,即可。

DeblurGAN模型是一个对抗神经网络模型,由生成器模型和判别器模型组成。

  • —生成器模型,根据输入的模糊图片模拟生成清晰的图片。
  • —判别器模型,用在训练过程中,帮助生成器模型达到更好的效果。

具体可以参考论文:

https://arxiv.org/pdf/1711.07064.pdf

No40  AttGAN模型论文:

同样,这也是个图片合成的。不同的是多属性合成,相对比较有意思。

AttGAN模型由两个子模型组成:

  • 利用编码器模型将图片特征提取出来。
  • 将提取的特征与指定的属性值参数一起输入编码器模型中,合成出最终的人脸图片。

更多细节可以参考论文:

https://arxiv.org/pdf/1711.10678.pdf

No41  RNN.WGAN模型论文:

可以合成文本的GAN。离散数据也能干!

RNN.WGAN模型使用了WGAN模型的方法进行训练。详细做法可以参考如下论文:

https://arxiv.org/abs/1704.00028

九、多任务学习

No42  MKR模型论文:

多任务学习模型有必要了解一下。这里推荐一个论文给你看看。

MKR是一个多任务学习的端到端框架。该框架能够将两个不同任务的低层特征抽取出来,并融合在一起实现联合训练,从而达到最优的结果。有关MKR的更多介绍可以参考以下链接:

https://arxiv.org/pdf/1901.08907.pdf

十、NLP部分

No43  BERT模型论文:

如果你搞NLP,那么这个就不用我来介绍了。如果你准备搞NLP,那么赶紧来看看这个,跟上时代。

BERT相关论文链接

https://arxiv.org/abs/1810.04805

在BERT之后,又出了好多优秀的模型。但是,还是先把这个啃下来,再看别的才不费劲。

十一、模型攻防

No44  FGSM模型论文:

攻击模型的经典方法。值得掌握。

FGSM(Fast Gradient Sign Method)是一种生成对抗样本的方法。该方法的描述如下:

  • 将输入图片当作训练的参数,使其在训练过程中可以被调整。
  • 在训练时,通过损失函数诱导模型对图片生成错误的分类。
  • 当多次迭代导致模型收敛后,训练出来的图片就是所要得到的对抗样本

具体可以参考论文:

https://arxiv.org/pdf/1607.02533.pdf

No45  黑箱攻击论文:

基于雅可比(Jacobian)矩阵的数据增强方法,是一种常用的黑箱攻击方法。该方法可以快速构建出近似于被攻击模型的决策边界,从而使用最少量的输入样本。即:构建出代替模型,并进行后续的攻击操作。

详细请见如下链接:

https://arxiv.org/abs/1602.02697

这里只是列了一些基础的论文。如果这45篇论文看完。可以保证你再看到大厂的产品时,不会感觉有代沟。

from:https://www.jiqizhixin.com/articles/2019-07-25-3

Web通信协议

前端的最重要的基础知识点是什么?

  • 原生javaScriptHTML,CSS.
  • Dom操作
  • EventLoop和渲染机制
  • 各类工程化的工具原理以及使用,根据需求定制编写插件和包。(webpack的plugin和babel的预设包)
  • 数据结构和算法(特别是IM以及超大型高并发网站应用等,例如B站
  • 最后便是通信协议

在使用某个技术的时候,一定要去追寻原理和底层的实现,长此以往坚持,只要自身底层的基础扎实,无论技术怎么变化,学习起来都不会太累,总的来说就是拒绝5分钟技术

从输入一个url地址,到显示页面发生了什么出发:

  • 1.浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;
  • 2.建立TCP连接(三次握手);
  • 3.浏览器发出读取文件(URL 中域名后面部分对应的文件)的HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;
  • 4.服务器对浏览器请求作出响应,并把对应的 html 文本发送给浏览器;
  • 5.浏览器将该 html 文本并显示内容;
  • 6.释放 TCP连接(四次挥手);

目前常见的通信协议都是建立在TCP链接之上

那么什么是TCP

TCP是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出SYN连接请求后,等待对方回答

TCP三次握手的过程如下:

  • 客户端发送SYN报文给服务器端,进入SYN_SEND状态。
  • 服务器端收到SYN报文,回应一个SYN(SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。
  • 客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态。
  • 三次握手完成,TCP客户端和服务器端成功地建立连接,可以开始传输数据了。

如图所示:

clipboard.png

TCP的四次挥手:

  • 建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的。具体过程如下图所示。
  • 某个应用进程首先调用close,称该端执行“主动关闭”(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
  • 接收到这个FIN的对端执行 “被动关闭”(passive close),这个FIN由TCP确认。

注意:FIN的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。

  • 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
  • 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。 [3]

既然每个方向都需要一个FIN和一个ACK,因此通常需要4个分节。

特别提示: SYN报文用来通知,FIN报文是用来同步的

clipboard.png

以上就是面试官常问的三次握手,四次挥手,但是这不仅仅面试题,上面仅仅答到了一点皮毛,学习这些是为了让我们后续方便了解他的优缺点。

TCP连接建立后,我们可以有多种协议的方式通信交换数据:

最古老的方式一:http 1.0

  • 早先1.0的HTTP版本,是一种无状态、无连接的应用层协议。
  • HTTP1.0规定浏览器和服务器保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器处理完成后立即断开TCP连接(无连接),服务器不跟踪每个客户端也不记录过去的请求(无状态)。
  • 这种无状态性可以借助cookie/session机制来做身份认证和状态记录。而下面两个问题就比较麻烦了。
  • 首先,无连接的特性导致最大的性能缺陷就是无法复用连接。每次发送请求的时候,都需要进行一次TCP的连接,而TCP的连接释放过程又是比较费事的。这种无连接的特性会使得网络的利用率非常低。
  • 其次就是队头阻塞(headoflineblocking)。由于HTTP1.0规定下一个请求必须在前一个请求响应到达之前才能发送。假设前一个请求响应一直不到达,那么下一个请求就不发送,同样的后面的请求也给阻塞了。

Http 1.0的致命缺点,就是无法复用TCP连接和并行发送请求,这样每次一个请求都需要三次握手,而且其实建立连接和释放连接的这个过程是最耗时的,传输数据相反却不那么耗时。还有本地时间被修改导致响应头expires的缓存机制失效的问题~(后面会详细讲)

  • 常见的请求报文~

clipboard.png

于是出现了Http 1.1,这也是技术的发展必然结果~

  • Http 1.1出现,继承了Http1.0的优点,也克服了它的缺点,出现了keep-alive这个头部字段,它表示会在建立TCP连接后,完成首次的请求,并不会立刻断开TCP连接,而是保持这个连接状态~进而可以复用这个通道
  • Http 1.1并且支持请求管道化,“并行”发送请求,但是这个并行,也不是真正意义上的并行,而是可以让我们把先进先出队列从客户端(请求队列)迁移到服务端(响应队列)

例如:客户端同时发了两个请求分别来获取html和css,假如说服务器的css资源先准备就绪,服务器也会先发送html再发送css。

  • B站首页,就有keep-alive,因为他们也有IM的成分在里面。需要大量复用TCP连接~

clipboard.png

  • HTTP1.1好像还是无法解决队头阻塞的问题

实际上,现阶段的浏览器厂商采取了另外一种做法,它允许我们打开多个TCP的会话。也就是说,上图我们看到的并行,其实是不同的TCP连接上的HTTP请求和响应。这也就是我们所熟悉的浏览器对同域下并行加载6~8个资源的限制。而这,才是真正的并行!

Http 1.1的致命缺点:

  • 1.明文传输
  • 2.其实还是没有解决无状态连接的
  • 3.当有多个请求同时被挂起的时候 就会拥塞请求通道,导致后面请求无法发送
  • 4.臃肿的消息首部:HTTP/1.1能压缩请求内容,但是消息首部不能压缩;在现今请求中,消息首部占请求绝大部分(甚至是全部)也较为常见.

我们也可以用dns-prefetch和 preconnect tcp来优化~

<link rel="preconnect" href="//example.com" crossorigin>
<link rel="dns=prefetch" href="//example.com">
  • Tipwebpack可以做任何事情,这些都可以用插件实现

基于这些缺点,出现了Http 2.0

相较于HTTP1.1,HTTP2.0的主要优点有采用二进制帧封装,传输变成多路复用,流量控制算法优化,服务器端推送,首部压缩,优先级等特点。

HTTP1.x的解析是基于文本的,基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多。而HTTP/2会将所有传输的信息分割为更小的消息和帧,然后采用二进制的格式进行编码,HTTP1.x的头部信息会被封装到HEADER frame,而相应的RequestBody则封装到DATAframe里面。不改动HTTP的语义,使用二进制编码,实现方便且健壮。

多路复用

  • 所有的请求都是通过一个 TCP 连接并发完成。HTTP/1.x 虽然通过 pipeline 也能并发请求,但是多个请求之间的响应会被阻塞的,所以 pipeline 至今也没有被普及应用,而 HTTP/2 做到了真正的并发请求。同时,流还支持优先级和流量控制。当流并发时,就会涉及到流的优先级和依赖。即:HTTP2.0对于同一域名下所有请求都是基于流的,不管对于同一域名访问多少文件,也只建立一路连接。优先级高的流会被优先发送。图片请求的优先级要低于 CSS 和 SCRIPT,这个设计可以确保重要的东西可以被优先加载完

流量控制

  • TCP协议通过sliding window的算法来做流量控制。发送方有个sending window,接收方有receive window。http2.0的flow control是类似receive window的做法,数据的接收方通过告知对方自己的flow window大小表明自己还能接收多少数据。只有Data类型的frame才有flow control的功能。对于flow control,如果接收方在flow window为零的情况下依然更多的frame,则会返回block类型的frame,这张场景一般表明http2.0的部署出了问题。

服务器端推送

  • 服务器端的推送,就是服务器可以对一个客户端请求发送多个响应。除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。当浏览器请求一个html,服务器其实大概知道你是接下来要请求资源了,而不需要等待浏览器得到html后解析页面再发送资源请求。

首部压缩

  • HTTP 2.0 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;通信期间几乎不会改变的通用键-值对(用户代理、可接受的媒体类型,等等)只 需发送一次。事实上,如果请求中不包含首部(例如对同一资源的轮询请求),那么 首部开销就是零字节。此时所有首部都自动使用之前请求发送的首部。
  • 如果首部发生变化了,那么只需要发送变化了数据在Headers帧里面,新增或修改的首部帧会被追加到“首部表”。首部表在 HTTP 2.0 的连接存续期内始终存在,由客户端和服务器共同渐进地更新 。
  • 本质上,当然是为了减少请求啦,通过多个js或css合并成一个文件,多张小图片拼合成Sprite图,可以让多个HTTP请求减少为一个,减少额外的协议开销,而提升性能。当然,一个HTTP的请求的body太大也是不合理的,有个度。文件的合并也会牺牲模块化和缓存粒度,可以把“稳定”的代码or 小图 合并为一个文件or一张Sprite,让其充分地缓存起来,从而区分开迭代快的文件。

Demo的性能对比:

clipboard.png

Http的那些致命缺陷,并没有完全解决,于是有了https,也是目前应用最广的协议之一

HTTP+ 加密 + 认证 + 完整性保护 =HTTPS ?

可以这样认为~HTTP 加上加密处理和认证以及完整性保护后即是 HTTPS

  • 如果在 HTTP 协议通信过程中使用未经加密的明文,比如在 Web 页面中输入信用卡号,如果这条通信线路遭到窃听,那么信用卡号就暴露了。
  • 另外,对于 HTTP 来说,服务器也好,客户端也好,都是没有办法确认通信方的。

因为很有可能并不是和原本预想的通信方在实际通信。并且还需要考虑到接收到的报文在通信途中已经遭到篡改这一可能性。

  • 为了统一解决上述这些问题,需要在 HTTP 上再加入加密处理和认证等机制。我们把添加了加密及认证机制的 HTTP 称为 HTTPS

不加密的重要内容被wireshark这类工具抓到包,后果很严重~

HTTPS 是身披 SSL 外壳的 HTTP

  • HTTPS 并非是应用层的一种新协议。只是 HTTP 通信接口部分用 SSL(SecureSocket Layer)和 TLS(Transport Layer Security)协议代替而已。

通常,HTTP 直接和 TCP 通信。

  • 当使用 SSL 时,则演变成先和 SSL 通信,再由 SSL和 TCP 通信了。简言之,所谓 HTTPS,其实就是身披 SSL 协议这层外壳的HTTP。
  • 在采用 SSL 后,HTTP 就拥有了 HTTPS 的加密、证书和完整性保护这些功能。SSL 是独立于 HTTP 的协议,所以不光是 HTTP 协议,其他运行在应用层的 SMTP和 Telnet 等协议均可配合 SSL 协议使用。可以说 SSL 是当今世界上应用最为广泛的网络安全术。

clipboard.png

相互交换密钥的公开密钥加密技术 —–对称加密

clipboard.png

  • 在对 SSL 进行讲解之前,我们先来了解一下加密方法。SSL 采用一种叫做公开密钥加密(Public-key cryptography)的加密处理方式。
  • 近代的加密方法中加密算法是公开的,而密钥却是保密的。通过这种方式得以保持加密方法的安全性。

加密和解密都会用到密钥。没有密钥就无法对密码解密,反过来说,任何人只要持有密钥就能解密了。如果密钥被攻击者获得,那加密也就失去了意义。

HTTPS 采用混合加密机制

  • HTTPS 采用共享密钥加密和公开密钥加密两者并用的混合加密机制。
  • 但是公开密钥加密与共享密钥加密相比,其处理速度要慢。所以应充分利用两者各自的优势,将多种方法组合起来用于通信。在交换密钥环节使用公开密钥加密方式,之后的建立通信交换报文阶段则使用共享密钥加密方式。

HTTPS虽好,非对称加密虽好,但是不要滥用

HTTPS 也存在一些问题,那就是当使用 SSL 时,它的处理速度会变慢。

SSL 的慢分两种。一种是指通信慢。另一种是指由于大量消耗 CPU 及内存等资源,导致处理速度变慢。

  • 和使用 HTTP 相比,网络负载可能会变慢 2 到 100 倍。除去和 TCP 连接、发送 HTTP 请求 ? 响应以外,还必须进行 SSL 通信,因此整体上处理通信量不可避免会增加。
  • 另一点是 SSL 必须进行加密处理。在服务器和客户端都需要进行加密和解密的运算处理。因此从结果上讲,比起 HTTP 会更多地消耗服务器和客户端的硬件资源,导致负载增强。

针对速度变慢这一问题,并没有根本性的解决方案,我们会使用 SSL 加速器这种(专用服务器)硬件来改善该问题。该硬件为 SSL 通信专用硬件,相对软件来讲,能够提高数倍 SSL 的计算速度。仅在 SSL 处理时发挥 SSL加速器的功效,以分担负载。

为什么不一直使用 HTTPS

  • 既然 HTTPS 那么安全可靠,那为何所有的 Web 网站不一直使用 HTTPS?

其中一个原因是,因为与纯文本通信相比,加密通信会消耗更多的 CPU 及内存资源。如果每次通信都加密,会消耗相当多的资源,平摊到一台计算机上时,能够处理的请求数量必定也会随之减少。

  • 因此,如果是非敏感信息则使用 HTTP 通信,只有在包含个人信息等敏感数据时,才利用 HTTPS 加密通信。

特别是每当那些访问量较多的 Web 网站在进行加密处理时,它们所承担着的负载不容小觑。在进行加密处理时,并非对所有内容都进行加密处理,而是仅在那些需要信息隐藏时才会加密,以节约资源。

  • 除此之外,想要节约购买证书的开销也是原因之一。

要进行 HTTPS 通信,证书是必不可少的。而使用的证书必须向认证机构(CA)购买。证书价格可能会根据不同的认证机构略有不同。通常,一年的授权需要数万日元(现在一万日元大约折合 600 人民币)。那些购买证书并不合算的服务以及一些个人网站,可能只会选择采用HTTP 的通信方式。

clipboard.png

复习完了基本的协议,介绍下报文格式:

  • 请求报文格式

clipboard.png

  • 响应报文格式

clipboard.png

所谓响应头,请求头,其实都可以自己添加字段,只要前后端给对应的处理机制即可

Node.js代码实现响应头的设置


  if (config.cache.expires) {
                        res.setHeader("expries", new Date(Date.now() + (config.cache.maxAge * 1000)))
                    }
                    if (config.cache.lastModified) {
                        res.setHeader("last-modified", stat.mtime.toUTCString())
                    }
                    if (config.cache.etag) {
                        res.setHeader('Etag', etagFn(stat))
                    }
}

响应头的详解:

clipboard.png

本人的开源项目,手写的Node.js静态资源服务器,https://github.com/JinJieTan/…,欢迎 star~

浏览器的缓存策略:

  • 首次请求:

clipboard.png

  • 非首次请求:

clipboard.png

  • 用户行为与缓存:

clipboard.png

不能缓存的请求:

无法被浏览器缓存的请求如下:

  • HTTP信息头中包含Cache-Control:no-cache,pragma:no-cache(HTTP1.0),或Cache-Control:max-age=0等告诉浏览器不用缓存的请求
  • 需要根据Cookie,认证信息等决定输入内容的动态请求是不能被缓存的
  • 经过HTTPS安全加密的请求(有人也经过测试发现,ie其实在头部加入Cache-Control:max-age信息,firefox在头部加入Cache-Control:Public之后,能够对HTTPS的资源进行缓寸)
  • 经过HTTPS安全加密的请求(有人也经过测试发现,ie其实在头部加入Cache-Control:max-age信息,firefox在头部加入Cache-Control:Public之后,能够对HTTPS的资源进行缓存,参考《HTTPS的七个误解》)
  • POST请求无法被缓存
  • HTTP响应头中不包含Last-Modified/Etag,也不包含Cache-Control/Expires的请求无法被缓存

即时通讯协议

从最初的没有websocket协议开始:

传统的协议无法服务端主动push数据,于是有了这些骚操作:

  • 轮询,在一个定时器中不停向服务端发送请求。
  • 长轮询,发送请求给服务端,直到服务端觉得可以返回数据了再返回响应,否则这个请求一直挂起~
  • 以上两种都有瑕疵,而且比较明显,这里不再描述。

为了解决实时通讯,数据同步的问题,出现了webSocket.

  • webSockets的目标是在一个单独的持久连接上提供全双工、双向通信。在Javascript创建了Web Socket之后,会有一个HTTP请求发送到浏览器以发起连接。在取得服务器响应后,建立的连接会将HTTP升级从HTTP协议交换为WebSocket协议。
  • webSocket原理: 在TCP连接第一次握手的时候,升级为ws协议。后面的数据交互都复用这个TCP通道。
  • 客户端代码实现:
  const ws = new WebSocket('ws://localhost:8080');
        ws.onopen = function () {
            ws.send('123')
            console.log('open')
        }
        ws.onmessage = function () {
            console.log('onmessage')
        }
        ws.onerror = function () {
            console.log('onerror')
        }
        ws.onclose = function () {
            console.log('onclose')
        }
  • 服务端使用 Node.js语言实现
const express = require('express')
const { Server } = require("ws");
const app = express()
const wsServer = new Server({ port: 8080 })
wsServer.on('connection', (ws) => {
    ws.onopen = function () {
        console.log('open')
    }
    ws.onmessage = function (data) {
        console.log(data)
        ws.send('234')
        console.log('onmessage' + data)
    }
    ws.onerror = function () {
        console.log('onerror')
    }
    ws.onclose = function () {
        console.log('onclose')
    }
});

app.listen(8000, (err) => {
    if (!err) { console.log('监听OK') } else {
        console.log('监听失败')
    }
})

webSocket的报文格式有一些不一样:

![图片上传中…]

  • 客户端和服务端进行Websocket消息传递是这样的:
    • 客户端:将消息切割成多个帧,并发送给服务端。
    • 服务端:接收消息帧,并将关联的帧重新组装成完整的消息。

即时通讯的心跳检测:

pingandpong

  • 服务端Go实现:
package main

import (
    "net/http"
    "time"

    "github.com/gorilla/websocket"
)

var (
    //完成握手操作
    upgrade = websocket.Upgrader{
       //允许跨域(一般来讲,websocket都是独立部署的)
       CheckOrigin:func(r *http.Request) bool {
            return true
       },
    }
)

func wsHandler(w http.ResponseWriter, r *http.Request) {
   var (
         conn *websocket.Conn
         err error
         data []byte
   )
   //服务端对客户端的http请求(升级为websocket协议)进行应答,应答之后,协议升级为websocket,http建立连接时的tcp三次握手将保持。
   if conn, err = upgrade.Upgrade(w, r, nil); err != nil {
        return
   }

    //启动一个协程,每隔5s向客户端发送一次心跳消息
    go func() {
        var (
            err error
        )
        for {
            if err = conn.WriteMessage(websocket.TextMessage, []byte("heartbeat")); err != nil {
                return
            }
            time.Sleep(5 * time.Second)
        }
    }()

   //得到websocket的长链接之后,就可以对客户端传递的数据进行操作了
   for {
         //通过websocket长链接读到的数据可以是text文本数据,也可以是二进制Binary
        if _, data, err = conn.ReadMessage(); err != nil {
            goto ERR
     }
     if err = conn.WriteMessage(websocket.TextMessage, data); err != nil {
         goto ERR
     }
   }
ERR:
    //出错之后,关闭socket连接
    conn.Close()
}

func main() {
    http.HandleFunc("/ws", wsHandler)
    http.ListenAndServe("0.0.0.0:7777", nil)
}

客户端的心跳检测(Node.js实现):

this.heartTimer = setInterval(() => {
      if (this.heartbeatLoss < MAXLOSSTIMES) {
        events.emit('network', 'sendHeart');
        this.heartbeatLoss += 1;
        this.phoneLoss += 1;
      } else {
        events.emit('network', 'offline');
        this.stop();
      }
      if (this.phoneLoss > MAXLOSSTIMES) {
        this.PhoneLive = false;
        events.emit('network', 'phoneDisconnect');
      }
    }, 5000);

自定义即时通信协议:

new Socket开始:

  • 目前即时通讯大都使用现有大公司成熟的SDK接入,但是逼格高些还是自己重写比较好。
  • 打个小广告,我们公司就是自己定义的即时通讯协议~招聘一位高级前端,地点深圳-深南大道,做跨平台IM桌面应用开发的~
  • 客户端代码实现(Node.js):

const {Socket} = require('net') 
const tcp = new Socket()
tcp.setKeepAlive(true);
tcp.setNoDelay(true);
//保持底层tcp链接不断,长连接
指定对应域名端口号链接
tcp.connect(80,166.166.0.0)
建立连接后
根据后端传送的数据类型 使用对应不同的解析
readUInt8 readUInt16LE readUInt32LE readIntLE等处理后得到myBuf 
const myBuf = buffer.slice(start);//从对应的指针开始的位置截取buffer
const header = myBuf.slice(headstart,headend)//截取对应的头部buffer
const body = JSON.parse(myBuf.slice(headend-headstart,bodylength).tostring())
//精确截取数据体的buffer,并且转化成js对象

即时通讯强烈推荐使用Golang,GRPC,Prob传输数据。

上面的一些代码,都在我的开源项目中:

from:https://segmentfault.com/a/1190000019891825

用JS开发跨平台桌面应用,从原理到实践

导读

使用Electron开发客户端程序已经有一段时间了,整体感觉还是非常不错的,其中也遇到了一些坑点,本文是从【运行原理】到【实际应用】对Electron进行一次系统性的总结。【多图,长文预警~】

本文所有实例代码均在我的github electron-react上,结合代码阅读文章效果更佳。另外electron-react还可作为使用Electron + React + Mobx + Webpack技术栈的脚手架工程。

一、桌面应用程序

桌面应用程序,又称为 GUI 程序(Graphical User Interface),但是和 GUI 程序也有一些区别。桌面应用程序 将 GUI 程序从GUI 具体为“桌面”,使冷冰冰的像块木头一样的电脑概念更具有 人性化,更生动和富有活力。

我们电脑上使用的各种客户端程序都属于桌面应用程序,近年来WEB和移动端的兴起让桌面程序渐渐暗淡,但是在某些日常功能或者行业应用中桌面应用程序仍然是必不可少的。

传统的桌面应用开发方式,一般是下面两种:

1.1 原生开发

直接将语言编译成可执行文件,直接调用系统API,完成UI绘制等。这类开发技术,有着较高的运行效率,但一般来说,开发速度较慢,技术要求较高,例如:

  • 使用C++ / MFC开发Windows应用
  • 使用Objective-C开发MAC应用

1.2 托管平台

一开始就有本地开发和UI开发。一次编译后,得到中间文件,通过平台或虚机完成二次加载编译或解释运行。运行效率低于原生编译,但平台优化后,其效率也是比较可观的。就开发速度方面,比原生编译技术要快一些。例如:

  • 使用C# / .NET Framework(只能开发Windows应用)
  • Java / Swing

不过,上面两种对前端开发人员太不友好了,基本是前端人员不会涉及的领域,但是在这个【大前端?】的时代,前端开发者正在想方设法涉足各个领域,使用WEB技术开发客户端的方式横空出世。

1.3 WEB开发

使用WEB技术进行开发,利用浏览器引擎完成UI渲染,利用Node.js实现服务器端JS编程并可以调用系统API,可以把它想像成一个套了一个客户端外壳的WEB应用。

在界面上,WEB的强大生态为UI带来了无限可能,并且开发、维护成本相对较低,有WEB开发经验的前端开发者很容易上手进行开发。

本文就来着重介绍使用WEB技术开发客户端程序的技术之一【electron

二、Electron

Electron是由Github开发,用HTML,CSSJavaScript来构建跨平台桌面应用程序的一个开源库。 Electron通过将ChromiumNode.js合并到同一个运行时环境中,并将其打包为Mac,WindowsLinux系统下的应用来实现这一目的。

2.1 使用Electron开发的理由:

  • 1.使用具有强大生态的Web技术进行开发,开发成本低,可扩展性强,更炫酷的UI
  • 2.跨平台,一套代码可打包为Windows、Linux、Mac三套软件,且编译快速
  • 3.可直接在现有Web应用上进行扩展,提供浏览器不具备的能力
  • 4.你是一个前端?‍?~

当然,我们也要认清它的缺点:性能比原生桌面应用要低,最终打包后的应用比原生应用大很多。

2.2 开发体验

兼容性

虽然你还在用WEB技术进行开发,但是你不用再考虑兼容性问题了,你只需要关心你当前使用Electron的版本对应Chrome的版本,一般情况下它已经足够新来让你使用最新的API和语法了,你还可以手动升级Chrome版本。同样的,你也不用考虑不同浏览器带的样式和代码兼容问题。

Node环境

这可能是很多前端开发者曾经梦想过的功能,在WEB界面中使用Node.js提供的强大API,这意味着你在WEB页面直接可以操作文件,调用系统API,甚至操作数据库。当然,除了完整的Node API,你还可以使用额外的几十万个npm模块。

跨域

你可以直接使用Node提供的request模块进行网络请求,这意味着你无需再被跨域所困扰。

强大的扩展性

借助node-ffi,为应用程序提供强大的扩展性(后面的章节会详细介绍)。

2.3 谁在用Electron

现在市面上已经有非常多的应用在使用Electron进行开发了,包括我们熟悉的VS Code客户端、GitHub客户端、Atom客户端等等。印象很深的,去年迅雷在发布迅雷X10.1时的文案:

从迅雷X 10.1版本开始,我们采用Electron软件框架完全重写了迅雷主界面。使用新框架的迅雷X可以完美支持2K、4K等高清显示屏,界面中的文字渲染也更加清晰锐利。从技术层面来说,新框架的界面绘制、事件处理等方面比老框架更加灵活高效,因此界面的流畅度也显著优于老框架的迅雷。至于具体提升有多大?您一试便知。

你可以打开VS Code,点击【帮助】【切换开发人员工具】来调试VS Code客户端的界面。

三、Electron运行原理

Electron 结合了 ChromiumNode.js 和用于调用操作系统本地功能的API

3.1 Chromium

ChromiumGoogle为发展Chrome浏览器而启动的开源项目,Chromium相当于Chrome的工程版或称实验版,新功能会率先在Chromium上实现,待验证后才会应用在Chrome上,故Chrome的功能会相对落后但较稳定。

ChromiumElectron提供强大的UI能力,可以在不考虑兼容性的情况下开发界面。

3.2 Node.js

Node.js是一个让JavaScript运行在服务端的开发平台,Node使用事件驱动,非阻塞I/O模型而得以轻量和高效。

单单靠Chromium是不能具备直接操作原生GUI能力的,Electron内集成了Nodejs,这让其在开发界面的同时也有了操作系统底层API的能力,Nodejs 中常用的 Path、fs、Crypto 等模块在 Electron 可以直接使用。

3.3 系统API

为了提供原生系统的GUI支持,Electron内置了原生应用程序接口,对调用一些系统功能,如调用系统通知、打开系统文件夹提供支持。

在开发模式上,Electron在调用系统API和绘制界面上是分离开发的,下面我们来看看Electron关于进程如何划分。

3.4 主进程

Electron区分了两种进程:主进程和渲染进程,两者各自负责自己的职能。

Electron 运行package.jsonmain 脚本的进程被称为主进程。一个 Electron 应用总是有且只有一个主进程。

职责:

  • 创建渲染进程(可多个)
  • 控制了应用生命周期(启动、退出APP以及对APP做一些事件监听)
  • 调用系统底层功能、调用原生资源

可调用的API:

  • Node.js API
  • Electron提供的主进程API(包括一些系统功能和Electron附加功能)

3.5 渲染进程

由于 Electron 使用了 Chromium 来展示 web 页面,所以 Chromium 的多进程架构也被使用到。 每个Electron 中的 web页面运行在它自己的渲染进程中。

主进程使用 BrowserWindow 实例创建页面。 每个 BrowserWindow 实例都在自己的渲染进程里运行页面。 当一个 BrowserWindow 实例被销毁后,相应的渲染进程也会被终止。

你可以把渲染进程想像成一个浏览器窗口,它能存在多个并且相互独立,不过和浏览器不同的是,它能调用Node API

职责:

  • HTMLCSS渲染界面
  • JavaScript做一些界面交互

可调用的API:

  • DOM API
  • Node.js API
  • Electron提供的渲染进程API

四、Electron基础

4.1 Electron API

在上面的章节我们提到,渲染进和主进程分别可调用的Electron API。所有ElectronAPI都被指派给一种进程类型。 许多API只能被用于主进程中,有些API又只能被用于渲染进程,又有一些主进程和渲染进程中都可以使用。

你可以通过如下方式获取Electron API

const { BrowserWindow, ... } = require('electron')

下面是一些常用的Electron API

在后面的章节我们会选择其中常用的模块进行详细介绍。

4.2 使用 Node.js 的 API

你可以同时在Electron的主进程和渲染进程使用Node.js API,)所有在Node.js可以使用的API,在Electron中同样可以使用。

import {shell} from 'electron';
import os from 'os';

document.getElementById('btn').addEventListener('click', () => { 
  shell.showItemInFolder(os.homedir());
})

有一个非常重要的提示: 原生Node.js模块 (即指,需要编译源码过后才能被使用的模块) 需要在编译后才能和Electron一起使用。

4.3 进程通信

主进程和渲染进程虽然拥有不同的职责,然是他们也需要相互协作,互相通讯。

例如:在web页面管理原生GUI资源是很危险的,会很容易泄露资源。所以在web页面,不允许直接调用原生GUI相关的API。渲染进程如果想要进行原生的GUI操作,就必须和主进程通讯,请求主进程来完成这些操作。

4.4 渲染进程向主进程通信

ipcRenderer 是一个 EventEmitter 的实例。 你可以使用它提供的一些方法,从渲染进程发送同步或异步的消息到主进程。 也可以接收主进程回复的消息。

在渲染进程引入ipcRenderer

import { ipcRenderer } from 'electron';

异步发送:

通过 channel 发送同步消息到主进程,可以携带任意参数。

在内部,参数会被序列化为 JSON,因此参数对象上的函数和原型链不会被发送。

ipcRenderer.send('sync-render', '我是来自渲染进程的异步消息');

同步发送:

 const msg = ipcRenderer.sendSync('async-render', '我是来自渲染进程的同步消息');

注意: 发送同步消息将会阻塞整个渲染进程,直到收到主进程的响应。

主进程监听消息:

ipcMain模块是EventEmitter类的一个实例。 当在主进程中使用时,它处理从渲染器进程(网页)发送出来的异步和同步信息。 从渲染器进程发送的消息将被发送到该模块。

ipcMain.on:监听 channel,当接收到新的消息时 listener 会以 listener(event, args...) 的形式被调用。

  ipcMain.on('sync-render', (event, data) => {
    console.log(data);
  });

4.5 主进程向渲染进程通信

在主进程中可以通过BrowserWindowwebContents向渲染进程发送消息,所以,在发送消息前你必须先找到对应渲染进程的BrowserWindow对象。:

const mainWindow = BrowserWindow.fromId(global.mainId);
 mainWindow.webContents.send('main-msg', `ConardLi]`)

根据消息来源发送:

ipcMain接受消息的回调函数中,通过第一个参数event的属性sender可以拿到消息来源渲染进程的webContents对象,我们可以直接用此对象回应消息。

  ipcMain.on('sync-render', (event, data) => {
    console.log(data);
    event.sender.send('main-msg', '主进程收到了渲染进程的【异步】消息!')
  });

渲染进程监听:

ipcRenderer.on:监听 channel, 当新消息到达,将通过listener(event, args...)调用 listener

ipcRenderer.on('main-msg', (event, msg) => {
    console.log(msg);
})

4.6 通信原理

ipcMainipcRenderer 都是 EventEmitter 类的一个实例。EventEmitter 类是 NodeJS 事件的基础,它由 NodeJS 中的 events 模块导出。

EventEmitter 的核心就是事件触发与事件监听器功能的封装。它实现了事件模型需要的接口, 包括 addListener,removeListener, emit 及其它工具方法. 同原生 JavaScript 事件类似, 采用了发布/订阅(观察者)的方式, 使用内部 _events 列表来记录注册的事件处理器。

我们通过 ipcMainipcRendereron、send 进行监听和发送消息都是 EventEmitter 定义的相关接口。

4.7 remote

remote 模块为渲染进程(web页面)和主进程通信(IPC)提供了一种简单方法。 使用 remote 模块, 你可以调用 main 进程对象的方法, 而不必显式发送进程间消息, 类似于 JavaRMI

import { remote } from 'electron';

remote.dialog.showErrorBox('主进程才有的dialog模块', '我是使用remote调用的')

但实际上,我们在调用远程对象的方法、函数或者通过远程构造函数创建一个新的对象,实际上都是在发送一个同步的进程间消息。

在上面通过 remote 模块调用 dialog 的例子里。我们在渲染进程中创建的 dialog 对象其实并不在我们的渲染进程中,它只是让主进程创建了一个 dialog 对象,并返回了这个相对应的远程对象给了渲染进程。

4.8 渲染进程间通信

Electron并没有提供渲染进程之间相互通信的方式,我们可以在主进程中建立一个消息中转站。

渲染进程之间通信首先发送消息到主进程,主进程的中转站接收到消息后根据条件进行分发。

4.9 渲染进程数据共享

在两个渲染进程间共享数据最简单的方法是使用浏览器中已经实现的HTML5 API。 其中比较好的方案是用Storage APIlocalStorage,sessionStorage 或者 IndexedDB。

就像在浏览器中使用一样,这种存储相当于在应用程序中永久存储了一部分数据。有时你并不需要这样的存储,只需要在当前应用程序的生命周期内进行一些数据的共享。这时你可以用 Electron 内的 IPC 机制实现。

将数据存在主进程的某个全局变量中,然后在多个渲染进程中使用 remote 模块来访问它。

在主进程中初始化全局变量:

global.mainId = ...;
global.device = {...};
global.__dirname = __dirname;
global.myField = { name: 'ConardLi' };

在渲染进程中读取:

import { ipcRenderer, remote } from 'electron';

const { getGlobal } = remote;

const mainId = getGlobal('mainId')
const dirname = getGlobal('__dirname')
const deviecMac = getGlobal('device').mac;

在渲染进程中改变:

getGlobal('myField').name = 'code秘密花园';

多个渲染进程共享同一个主进程的全局变量,这样即可达到渲染进程数据共享和传递的效果。

五、窗口

5.1 BrowserWindow

主进程模块BrowserWindow用于创建和控制浏览器窗口。

  mainWindow = new BrowserWindow({
    width: 1000,
    height: 800,
    // ...
  });
  mainWindow.loadURL('http://www.conardli.top/');

你可以在这里查看它所有的构造参数。

5.2 无框窗口

无框窗口是没有镶边的窗口,窗口的部分(如工具栏)不属于网页的一部分。

BrowserWindow的构造参数中,将frame设置为false可以指定窗口为无边框窗口,将工具栏隐藏后,就会产生两个问题:

  • 1.窗口控制按钮(最小化、全屏、关闭按钮)会被隐藏
  • 2.无法拖拽移动窗口

可以通过指定titleBarStyle选项来再将工具栏按钮显示出来,将其设置为hidden表示返回一个隐藏标题栏的全尺寸内容窗口,在左上角仍然有标准的窗口控制按钮。

new BrowserWindow({
    width: 200,
    height: 200,
    titleBarStyle: 'hidden',
    frame: false
  });

5.3 窗口拖拽

默认情况下, 无边框窗口是不可拖拽的。我们可以在界面中通过CSS属性-webkit-app-region: drag手动制定拖拽区域。

在无框窗口中, 拖动行为可能与选择文本冲突,可以通过设定-webkit-user-select: none;禁用文本选择:

.header {
  -webkit-user-select: none;
  -webkit-app-region: drag;
}

相反的,在可拖拽区域内部设置 -webkit-app-region: no-drag则可以指定特定不可拖拽区域。

5.4 透明窗口

通过将transparent选项设置为true, 还可以使无框窗口透明:

new BrowserWindow({
    transparent: true,
    frame: false
  });

5.5 Webview

使用 webview 标签在Electron 应用中嵌入 “外来” 内容。外来内容包含在 webview 容器中。 应用中的嵌入页面可以控制外来内容的布局和重绘。

iframe 不同, webview 在与应用程序不同的进程中运行。它与您的网页没有相同的权限, 应用程序和嵌入内容之间的所有交互都将是异步的。

六、对话框

dialog 模块提供了api来展示原生的系统对话框,例如打开文件框,alert框,所以web应用可以给用户带来跟系统应用相同的体验。

注意:dialog是主进程模块,想要在渲染进程调用可以使用remote

6.1 错误提示

dialog.showErrorBox用于显示一个显示错误消息的模态对话框。

 remote.dialog.showErrorBox('错误', '这是一个错误弹框!')

6.2 对话框

dialog.showErrorBox用于调用系统对话框,可以为指定几种不同的类型: “none“, “info“, “error“, “question” 或者 “warning“。

在 Windows 上, “question” 与”info”显示相同的图标, 除非你使用了 “icon” 选项设置图标。 在 macOS 上, “warning” 和 “error” 显示相同的警告图标

remote.dialog.showMessageBox({
  type: 'info',
  title: '提示信息',
  message: '这是一个对话弹框!',
  buttons: ['确定', '取消']
}, (index) => {
  this.setState({ dialogMessage: `【你点击了${index ? '取消' : '确定'}!!】` })
})

6.3 文件框

dialog.showOpenDialog用于打开或选择系统目录。

remote.dialog.showOpenDialog({
  properties: ['openDirectory', 'openFile']
}, (data) => {
  this.setState({ filePath: `【选择路径:${data[0]}】 ` })
})

6.4 信息框

这里推荐直接使用HTML5 API,它只能在渲染器进程中使用。

let options = {
  title: '信息框标题',
  body: '我是一条信息~~~',
}
let myNotification = new window.Notification(options.title, options)
myNotification.onclick = () => {
  this.setState({ message: '【你点击了信息框!!】' })
}

七、系统

7.1 获取系统信息

通过remote获取到主进程的process对象,可以获取到当前应用的各个版本信息:

  • process.versions.electronelectron版本信息
  • process.versions.chromechrome版本信息
  • process.versions.nodenode版本信息
  • process.versions.v8v8版本信息

获取当前应用根目录:

remote.app.getAppPath()

使用nodeos模块获取当前系统根目录:

os.homedir();

7.2 复制粘贴

Electron提供的clipboard在渲染进程和主进程都可使用,用于在系统剪贴板上执行复制和粘贴操作。

以纯文本的形式写入剪贴板:

clipboard.writeText(text[, type])

以纯文本的形式获取剪贴板的内容:

clipboard.readText([type])

7.3 截图

desktopCapturer用于从桌面捕获音频和视频的媒体源的信息。它只能在渲染进程中被调用。

下面的代码是一个获取屏幕截图并保存的实例:

  getImg = () => {
    this.setState({ imgMsg: '正在截取屏幕...' })
    const thumbSize = this.determineScreenShotSize()
    let options = { types: ['screen'], thumbnailSize: thumbSize }
    desktopCapturer.getSources(options, (error, sources) => {
      if (error) return console.log(error)
      sources.forEach((source) => {
        if (source.name === 'Entire screen' || source.name === 'Screen 1') {
          const screenshotPath = path.join(os.tmpdir(), 'screenshot.png')
          fs.writeFile(screenshotPath, source.thumbnail.toPNG(), (error) => {
            if (error) return console.log(error)
            shell.openExternal(`file://${screenshotPath}`)
            this.setState({ imgMsg: `截图保存到: ${screenshotPath}` })
          })
        }
      })
    })
  }

  determineScreenShotSize = () => {
    const screenSize = screen.getPrimaryDisplay().workAreaSize
    const maxDimension = Math.max(screenSize.width, screenSize.height)
    return {
      width: maxDimension * window.devicePixelRatio,
      height: maxDimension * window.devicePixelRatio
    }
  }

八、菜单

应用程序的菜单可以帮助我们快捷的到达某一功能,而不借助客户端的界面资源,一般菜单分为两种:

  • 应用程序菜单:位于应用程序顶部,在全局范围内都能使用
  • 上下文菜单:可自定义任意页面显示,自定义调用,如右键菜单

Electron为我们提供了Menu模块用于创建本机应用程序菜单和上下文菜单,它是一个主进程模块。

你可以通过Menu的静态方法buildFromTemplate(template),使用自定义菜单模版来构造一个菜单对象。

template是一个MenuItem的数组,我们来看看MenuItem的几个重要参数:

  • label:菜单显示的文字
  • click:点击菜单后的事件处理函数
  • role:系统预定义的菜单,例如copy(复制)、paste(粘贴)、minimize(最小化)…
  • enabled:指示是否启用该项目,此属性可以动态更改
  • submenu:子菜单,也是一个MenuItem的数组

推荐:最好指定role与标准角色相匹配的任何菜单项,而不是尝试手动实现click函数中的行为。内置role行为将提供最佳的本地体验。

下面的实例是一个简单的额菜单template

const template = [
  {
    label: '文件',
    submenu: [
      {
        label: '新建文件',
        click: function () {
          dialog.showMessageBox({
            type: 'info',
            message: '嘿!',
            detail: '你点击了新建文件!',
          })
        }
      }
    ]
  },
  {
    label: '编辑',
    submenu: [{
      label: '剪切',
      role: 'cut'
    }, {
      label: '复制',
      role: 'copy'
    }, {
      label: '粘贴',
      role: 'paste'
    }]
  },
  {
    label: '最小化',
    role: 'minimize'
  }
]

8.1 应用程序菜单

使用Menu的静态方法setApplicationMenu,可创建一个应用程序菜单,在 WindowsLinux 上,menu将被设置为每个窗口的顶层菜单。

注意:必须在模块ready事件后调用此 API app。

我们可以根据应用程序不同的的生命周期,不同的系统对菜单做不同的处理。

app.on('ready', function () {
  const menu = Menu.buildFromTemplate(template)
  Menu.setApplicationMenu(menu)
})

app.on('browser-window-created', function () {
  let reopenMenuItem = findReopenMenuItem()
  if (reopenMenuItem) reopenMenuItem.enabled = false
})

app.on('window-all-closed', function () {
  let reopenMenuItem = findReopenMenuItem()
  if (reopenMenuItem) reopenMenuItem.enabled = true
})

if (process.platform === 'win32') {
  const helpMenu = template[template.length - 1].submenu
  addUpdateMenuItems(helpMenu, 0)
}

8.2 上下文菜单

使用Menu的实例方法menu.popup可自定义弹出上下文菜单。

    let m = Menu.buildFromTemplate(template)
    document.getElementById('menuDemoContainer').addEventListener('contextmenu', (e) => {
      e.preventDefault()
      m.popup({ window: remote.getCurrentWindow() })
    })

8.3 快捷键

在菜单选项中,我们可以指定一个accelerator属性来指定操作的快捷键:

  {
    label: '最小化',
    accelerator: 'CmdOrCtrl+M',
    role: 'minimize'
  }

另外,我们还可以使用globalShortcut来注册全局快捷键。

    globalShortcut.register('CommandOrControl+N', () => {
      dialog.showMessageBox({
        type: 'info',
        message: '嘿!',
        detail: '你触发了手动注册的快捷键.',
      })
    })

CommandOrControl代表在macOS上为Command键,以及在Linux和Windows上为Control键。

九、打印

很多情况下程序中使用的打印都是用户无感知的。并且想要灵活的控制打印内容,往往需要借助打印机给我们提供的api再进行开发,这种开发方式非常繁琐,并且开发难度较大。第一次在业务中用到Electron其实就是用到它的打印功能,这里就多介绍一些。

Electron提供的打印api可以非常灵活的控制打印设置的显示,并且可以通过html来书写打印内容。Electron提供了两种方式进行打印,一种是直接调用打印机打印,一种是打印到pdf

并且有两种对象可以调用打印:

  • 通过windowwebcontent对象,使用此种方式需要单独开出一个打印的窗口,可以将该窗口隐藏,但是通信调用相对复杂。
  • 使用页面的webview元素调用打印,可以将webview隐藏在调用的页面中,通信方式比较简单。

上面两种方式同时拥有printprintToPdf方法。

9.1 调用系统打印

contents.print([options], [callback])

打印配置(options)中只有简单的三个配置:

  • silent:打印时是否不展示打印配置(是否静默打印)
  • printBackground:是否打印背景
  • deviceName:打印机设备名称

首先要将我们使用的打印机名称配置好,并且要在调用打印前首先要判断打印机是否可用。

使用webContentsgetPrinters方法可获取当前设备已经配置的打印机列表,注意配置过不是可用,只是在此设备上安装过驱动。

通过getPrinters获取到的打印机对象:https://electronjs.org/docs/api/structures/printer-info

我们这里只管关心两个,namestatusstatus0时表示打印机可用。

print的第二个参数callback是用于判断打印任务是否发出的回调,而不是打印任务完成后的回调。所以一般打印任务发出,回调函数即会调用并返回参数true。这个回调并不能判断打印是否真的成功了。

    if (this.state.curretnPrinter) {
      mainWindow.webContents.print({
        silent: silent, printBackground: true, deviceName: this.state.curretnPrinter
      }, () => { })
    } else {
      remote.dialog.showErrorBox('错误', '请先选择一个打印机!')
    }

9.2 打印到PDF

printToPdf的用法基本和print相同,但是print的配置项非常少,而printToPdf则扩展了很多属性。这里翻了一下源码发现还有很多没有被贴进api的,大概有三十几个包括可以对打印的margin,打印页眉页脚等进行配置。

contents.printToPDF(options, callback)

callback函数在打印失败或打印成功后调用,可获取打印失败信息或包含PDF数据的缓冲区。

    const pdfPath = path.join(os.tmpdir(), 'webviewPrint.pdf');
    const webview = document.getElementById('printWebview');
    const renderHtml = '我是被临时插入webview的内容...';
    webview.executeJavaScript('document.documentElement.innerHTML =`' + renderHtml + '`;');
    webview.printToPDF({}, (err, data) => {
      console.log(err, data);
      fs.writeFile(pdfPath, data, (error) => {
        if (error) throw error
        shell.openExternal(`file://${pdfPath}`)
        this.setState({ webviewPdfPath: pdfPath })
      });
    });

这个例子中的打印是使用webview完成的,通过调用executeJavaScript方法可动态向webview插入打印内容。

9.3 两种打印方案的选择

上面提到,使用webviewwebcontent都可以调用打印功能,使用webcontent打印,首先要有一个打印窗口,这个窗口不能随时打印随时创建,比较耗费性能。可以将它在程序运行时启动好,并做好事件监听。

此过程需和调用打印的进行做好通信,大致过程如下:

可见通信非常繁琐,使用webview进行打印可实现同样的效果但是通信方式会变得简单,因为渲染进程和webview通信不需要经过主进程,通过如下方式即可:

  const webview = document.querySelector('webview')
  webview.addEventListener('ipc-message', (event) => {
    console.log(event.channel)
  })
  webview.send('ping')const {ipcRenderer} = require('electron')
  ipcRenderer.on('ping', () => {
    ipcRenderer.sendToHost('pong')
  })

之前专门为ELectron打印写过一个DEMO:electron-print-demo有兴趣可以clone下来看一下。

9.4 打印功能封装

下面是几个针对常用打印功能的工具函数封装。

/**
 * 获取系统打印机列表
 */
export function getPrinters() {
  let printers = [];
  try {
    const contents = remote.getCurrentWindow().webContents;
    printers = contents.getPrinters();
  } catch (e) {
    console.error('getPrintersError', e);
  }
  return printers;
}
/**
 * 获取系统默认打印机
 */
export function getDefaultPrinter() {
  return getPrinters().find(element => element.isDefault);
}
/**
 * 检测是否安装了某个打印驱动
 */
export function checkDriver(driverMame) {
  return getPrinters().find(element => (element.options["printer-make-and-model"] || '').includes(driverMame));
}
/**
 * 根据打印机名称获取打印机对象
 */
export function getPrinterByName(name) {
  return getPrinters().find(element => element.name === name);
}

十、程序保护

10.1 崩溃

崩溃监控是每个客户端程序必备的保护功能,当程序崩溃时我们一般期望做到两件事:

  • 1.上传崩溃日志,及时报警
  • 2.监控程序崩溃,提示用户重启程序

electron为我们提供给了crashReporter来帮助我们记录崩溃日志,我们可以通过crashReporter.start来创建一个崩溃报告器:

const { crashReporter } = require('electron')
crashReporter.start({
  productName: 'YourName',
  companyName: 'YourCompany',
  submitURL: 'https://your-domain.com/url-to-submit',
  uploadToServer: true
})

当程序发生崩溃时,崩溃报日志将被储存在临时文件夹中名为YourName Crashes的文件文件夹中。submitURL用于指定你的崩溃日志上传服务器。 在启动崩溃报告器之前,您可以通过调用app.setPath('temp', 'my/custom/temp')API来自定义这些临时文件的保存路径。你还可以通过crashReporter.getLastCrashReport()来获取上次崩溃报告的日期和ID

我们可以通过webContentscrashed来监听渲染进程的崩溃,另外经测试有些主进程的崩溃也会触发该事件。所以我们可以根据主window是否被销毁来判断进行不同的重启逻辑,下面使整个崩溃监控的逻辑:

import { BrowserWindow, crashReporter, dialog } from 'electron';
// 开启进程崩溃记录
crashReporter.start({
  productName: 'electron-react',
  companyName: 'ConardLi',
  submitURL: 'http://xxx.com',  // 上传崩溃日志的接口
  uploadToServer: false
});
function reloadWindow(mainWin) {
  if (mainWin.isDestroyed()) {
    app.relaunch();
    app.exit(0);
  } else {
    // 销毁其他窗口
    BrowserWindow.getAllWindows().forEach((w) => {
      if (w.id !== mainWin.id) w.destroy();
    });
    const options = {
      type: 'info',
      title: '渲染器进程崩溃',
      message: '这个进程已经崩溃.',
      buttons: ['重载', '关闭']
    }
    dialog.showMessageBox(options, (index) => {
      if (index === 0) mainWin.reload();
      else mainWin.close();
    })
  }
}
export default function () {
  const mainWindow = BrowserWindow.fromId(global.mainId);
  mainWindow.webContents.on('crashed', () => {
    const errorMessage = crashReporter.getLastCrashReport();
    console.error('程序崩溃了!', errorMessage); // 可单独上传日志
    reloadWindow(mainWindow);
  });
}

10.2 最小化到托盘

有的时候我们并不想让用户通过点关闭按钮的时候就关闭程序,而是把程序最小化到托盘,在托盘上做真正的退出操作。

首先要监听窗口的关闭事件,阻止用户关闭操作的默认行为,将窗口隐藏。

function checkQuit(mainWindow, event) {
  const options = {
    type: 'info',
    title: '关闭确认',
    message: '确认要最小化程序到托盘吗?',
    buttons: ['确认', '关闭程序']
  };
  dialog.showMessageBox(options, index => {
    if (index === 0) {
      event.preventDefault();
      mainWindow.hide();
    } else {
      mainWindow = null;
      app.exit(0);
    }
  });
}
function handleQuit() {
  const mainWindow = BrowserWindow.fromId(global.mainId);
  mainWindow.on('close', event => {
    event.preventDefault();
    checkQuit(mainWindow, event);
  });
}

这时程序就再也找不到了,任务托盘中也没有我们的程序,所以我们要先创建好任务托盘,并做好事件监听。

windows平台使用ico文件可以达到更好的效果

export default function createTray() {
  const mainWindow = BrowserWindow.fromId(global.mainId);
  const iconName = process.platform === 'win32' ? 'icon.ico' : 'icon.png'
  tray = new Tray(path.join(global.__dirname, iconName));
  const contextMenu = Menu.buildFromTemplate([
    {
      label: '显示主界面', click: () => {
        mainWindow.show();
        mainWindow.setSkipTaskbar(false);
      }
    },
    {
      label: '退出', click: () => {
        mainWindow.destroy();
        app.quit();
      }
    },
  ])
  tray.setToolTip('electron-react');
  tray.setContextMenu(contextMenu);
}

十一、扩展能力

在很多情况下,你的应用程序要和外部设备进行交互,一般情况下厂商会为你提供硬件设备的开发包,这些开发包基本上都是通过C++ 编写,在使用electron开发的情况下,我们并不具备直接调用C++代码的能力,我们可以利用node-ffi来实现这一功能。

node-ffi提供了一组强大的工具,用于在Node.js环境中使用纯JavaScript调用动态链接库接口。它可以用来为库构建接口绑定,而不需要使用任何C++代码。

注意node-ffi并不能直接调用C++代码,你需要将C++代码编译为动态链接库:在 Windows下是 Dll ,在 Mac OS下是 dylib ,Linuxsonode-ffi 加载 Library是有限制的,只能处理 C风格的 Library

下面是一个简单的实例:

const ffi = require('ffi');
const ref = require('ref');
const SHORT_CODE = ref.refType('short');


const DLL = new ffi.Library('test.dll', {
    Test_CPP_Method: ['int', ['string',SHORT_CODE]], 
  })

testCppMethod(str: String, num: number): void {
  try {
    const result: any = DLL.Test_CPP_Method(str, num);
    return result;
  } catch (error) {
    console.log('调用失败~',error);
  }
}

this.testCppMethod('ConardLi',123);

上面的代码中,我们用ffi包装C++接口生成的动态链接库test.dll,并使用ref进行一些类型映射。

使用JavaScript调用这些映射方法时,推荐使用TypeScript来约定参数类型,因为弱类型的JavaScript在调用强类型语言的接口时可能会带来意想不到的风险。

借助这一能力,前端开发工程师也可以在IOT领域一展身手了?~

十二、环境选择

一般情况下,我们的应用程序可能运行在多套环境下(productionbetauatmokedevelopment…),不同的开发环境可能对应不同的后端接口或者其他配置,我们可以在客户端程序中内置一个简单的环境选择功能来帮助我们更高效的开发。

具体策略如下:

  • 在开发环境中,我们直接进入环境选择页面,读取到选择的环境后进行响应的重定向操作
  • 在菜单保留环境选择入口,以便在开发过程中切换
const envList = ["moke", "beta", "development", "production"];
exports.envList = envList;
const urlBeta = 'https://wwww.xxx-beta.com';
const urlDev = 'https://wwww.xxx-dev.com';
const urlProp = 'https://wwww.xxx-prop.com';
const urlMoke = 'https://wwww.xxx-moke.com';
const path = require('path');
const pkg = require(path.resolve(global.__dirname, 'package.json'));
const build = pkg['build-config'];
exports.handleEnv = {
  build,
  currentEnv: 'moke',
  setEnv: function (env) {
    this.currentEnv = env
  },
  getUrl: function () {
    console.log('env:', build.env);
    if (build.env === 'production' || this.currentEnv === 'production') {
      return urlProp;
    } else if (this.currentEnv === 'moke') {
      return urlMoke;
    } else if (this.currentEnv === 'development') {
      return urlDev;
    } else if (this.currentEnv === "beta") {
      return urlBeta;
    }
  },
  isDebugger: function () {
    return build.env === 'development'
  }
}

十三、打包

最后也是最重要的一步,将写好的代码打包成可运行的.app.exe可执行文件。

这里我把打包氛围两部分来做,渲染进程打包和主进程打包。

13.1 渲染进程打包和升级

一般情况下,我们的大部分业务逻辑代码是在渲染进程完成的,在大部分情况下我们仅仅需要对渲染进程进行更新和升级而不需要改动主进程代码,我们渲染进程的打包实际上和一般的web项目打包没有太大差别,使用webpack打包即可。

这里我说说渲染进程单独打包的好处:

打包完成的htmljs文件,我们一般要上传到我们的前端静态资源服务器下,然后告知服务端我们的渲染进程有代码更新,这里可以说成渲染进程单独的升级。

注意,和壳的升级不同,渲染进程的升级仅仅是静态资源服务器上htmljs文件的更新,而不需要重新下载更新客户端,这样我们每次启动程序的时候检测到离线包有更新,即可直接刷新读取最新版本的静态资源文件,即使在程序运行过程中要强制更新,我们的程序只需要强制刷新页面读取最新的静态资源即可,这样的升级对用户是非常友好的。

这里注意,一旦我们这样配置,就意味着渲染进程和主进程打包升级的完全分离,我们在启动主窗口时读取的文件就不应该再是本地文件,而是打包完成后放在静态资源服务器的文件。

为了方便开发,这里我们可以区分本地和线上加载不同的文件:

function getVersion (mac,current){
  // 根据设备mac和当前版本获取最新版本
}
export default function () {
  if (build.env === 'production') {
    const version = getVersion (mac,current);
    return 'https://www.xxxserver.html/electron-react/index_'+version+'.html';
  }
  return url.format({
    protocol: 'file:',
    pathname: path.join(__dirname, 'env/environment.html'),
    slashes: true,
    query: { debugger: build.env === "development" }
  });
}

具体的webpack配置这里就不再贴出,可以到我的github electron-react/scripts目录下查看。

这里需要注意,在开发环境下我们可以结合webpackdevServerelectron命令来启动app

  devServer: {
    contentBase: './assets/',
    historyApiFallback: true,
    hot: true,
    port: PORT,
    noInfo: false,
    stats: {
      colors: true,
    },
    setup() {
      spawn(
        'electron',
        ['.'],
        {
          shell: true,
          stdio: 'inherit',
        }
      )
        .on('close', () => process.exit(0))
        .on('error', e => console.error(e));
    },
  },//...

13.2 主进程打包

主进程,即将整个程序打包成可运行的客户端程序,常用的打包方案一般有两种,electron-packagerelectron-builder

electron-packager在打包配置上我觉得有些繁琐,而且它只能将应用直接打包为可执行程序。

这里我推荐使用electron-builder,它不仅拥有方便的配置 protocol 的功能、内置的 Auto Update、简单的配置 package.json 便能完成整个打包工作,用户体验非常不错。而且electron-builder不仅能直接将应用打包成exe app等可执行程序,还能打包成msi dmg等安装包格式。

你可以在package.json方便的进行各种配置:

  "build": {
    "productName": "electron-react", // app中文名称
    "appId": "electron-react",// app标识
    "directories": { // 打包后输出的文件夹
      "buildResources": "resources",
      "output": "dist/"
    }
    "files": [ // 打包后依然保留的源文件
      "main_process/",
      "render_process/",
    ],
    "mac": { // mac打包配置
      "target": "dmg",
      "icon": "icon.ico"
    },
    "win": { // windows打包配置
      "target": "nsis",
      "icon": "icon.ico"
    },
    "dmg": { // dmg文件打包配置
      "artifactName": "electron_react.dmg",
      "contents": [
        {
          "type": "link",
          "path": "/Applications",
          "x": 410,
          "y": 150
        },
        {
          "type": "file",
          "x": 130,
          "y": 150
        }
      ]
    },
    "nsis": { // nsis文件打包配置
      "oneClick": false,
      "allowToChangeInstallationDirectory": true,
      "shortcutName": "electron-react"
    },
  }

执行electron-builder打包命令时,可指定参数进行打包。

  --mac, -m, -o, --macos   macOS打包
  --linux, -l              Linux打包
  --win, -w, --windows     Windows打包
  --mwl                    同时为macOS,Windows和Linux打包
  --x64                    x64 (64位安装包)
  --ia32                   ia32(32位安装包)

关于主进程的更新你可以使用electron-builder自带的Auto Update模块,在electron-react也实现了手动更新的模块,由于篇幅原因这里就不再赘述,如果有兴趣可以到我的github查看main下的update模块。

13.3 打包优化

electron-builder打包出来的App要比相同功能的原生客户端应用体积大很多,即使是空的应用,体积也要在100mb以上。原因有很多:

第一点;为了达到跨平台的效果,每个Electron应用都包含了整个V8引擎和Chromium内核。

第二点:打包时会将整个node_modules打包进去,大家都知道一个应用的node_module体积是非常庞大的,这也是使得Electron应用打包后的体积较大的原因。

第一点我们无法改变,我们可以从第二点对应用体积进行优化:Electron在打包时只会将denpendencies的依赖打包进去,而不会将 devDependencies 中的依赖进行打包。所以我们应尽可能的减少denpendencies中的依赖。在上面的进程中,我们使用webpack对渲染进程进行打包,所以渲染进程的依赖全部都可以移入devDependencies

另外,我们还可以使用双packajson.json的方式来进行优化,把只在开发环境中使用到的依赖放在整个项目的根目录的package.json下,将与平台相关的或者运行时需要的依赖装在app目录下。具体详见two-package-structure。

参考

  • https://electronjs.org/docs
  • http://jlord.us/essential-electron/
  • https://imweb.io/topic/5b9f500cc2ec8e6772f34d79
  • https://www.jianshu.com/p/1ece6fd7a80c
  • https://zhuanlan.zhihu.com/p/52991793

本项目源码地址:https://github.com/ConardLi/electron-react

from: https://cloud.tencent.com/developer/article/1446636

CPU 高负载排查实践

前言

前几日早上打开邮箱收到一封监控报警邮件:某某 ip 服务器 CPU 负载较高,请研发尽快排查解决,发送时间正好是凌晨。

其实早在去年我也处理过类似的问题,并记录下来:《一次生产 CPU 100% 排查优化实践》

不过本次问题产生的原因却和上次不太一样,大家可以接着往下看。

问题分析

收到邮件后我马上登陆那台服务器,看了下案发现场还在(负载依然很高)。

于是我便利用这类问题的排查套路定位一遍。


首先利用 top -c 将系统资源使用情况实时显示出来 (-c 参数可以完整显示命令)。

接着输入大写 P 将应用按照 CPU 使用率排序,第一个就是使用率最高的程序。

果不其然就是我们的一个 Java 应用。

这个应用简单来说就是定时跑一些报表使的,每天凌晨会触发任务调度,正常情况下几个小时就会运行完毕。


常规操作第二步自然是得知道这个应用中最耗 CPU 的线程到底再干嘛。

利用 top -Hp pid 然后输入 P 依然可以按照 CPU 使用率将线程排序。

这时我们只需要记住线程的 ID 将其转换为 16 进制存储起来,通过 jstack pid >pid.log生成日志文件,利用刚才保存的 16 进制进程 ID 去这个线程快照中搜索即可知道消耗 CPU 的线程在干啥了。

如果你嫌麻烦,我也强烈推荐阿里开源的问题定位神器 arthas 来定位问题。

比如上述操作便可精简为一个命令 thread -n 3 即可将最忙碌的三个线程快照打印出来,非常高效。

更多关于 arthas 使用教程请参考官方文档

由于之前忘记截图了,这里我直接得出结论吧:

最忙绿的线程是一个 GC 线程,也就意味着它在忙着做垃圾回收。

GC 查看

排查到这里,有经验的老司机一定会想到:多半是应用内存使用有问题导致的。

于是我通过 jstat -gcutil pid 200 50 将内存使用、gc 回收状况打印出来(每隔 200ms 打印 50次)。

从图中可以得到以下几个信息:

  • Eden 区和 old 区都快占满了,可见内存回收是有问题的。
  • fgc 回收频次很高,10s 之内发生了 8 次回收((866493-866485)/ (200 *5))。
  • 持续的时间较长,fgc 已经发生了 8W 多次。

内存分析

既然是初步定位是内存问题,所以还是得拿一份内存快照分析才能最终定位到问题。

通过命令 jmap -dump:live,format=b,file=dump.hprof pid 可以导出一份快照文件。

这时就得借助 MAT 这类的分析工具出马了。

问题定位

通过这张图其实很明显可以看出,在内存中存在一个非常大的字符串,而这个字符串正好是被这个定时任务的线程引用着。

大概算了一下这个字符串所占的内存为 258m 左右,就一个字符串来说已经是非常大的对象了。

那这个字符串是咋产生的呢?

其实看上图中的引用关系及字符串的内容不难看出这是一个 insert 的 SQL 语句。

这时不得不赞叹 MAT 这个工具,他还能帮你预测出这个内存快照可能出现问题地方同时给出线程快照。

最终通过这个线程快照找到了具体的业务代码:

他调用一个写入数据库的方法,而这个方法会拼接一个 insert 语句,其中的 values 是循环拼接生成,大概如下:

1
2
3
4
5
6
7
<insert id=“insert” parameterType=“java.util.List”>
insert into xx (files)
values
<foreach collection=“list” item=“item” separator=“,”>
xxx
</foreach>
</insert>

所以一旦这个 list 非常大时,这个拼接的 SQL 语句也会很长。

通过刚才的内存分析其实可以看出这个 List 也是非常大的,也就导致了最终的这个 insert 语句占用的内存巨大。

优化策略

既然找到问题原因那就好解决了,有两个方向:

  • 控制源头 List 的大小,这个 List 也是从某张表中获取的数据,可以分页获取;这样后续的 insert 语句就会减小。
  • 控制批量写入数据的大小,其实本质还是要把这个拼接的 SQL 长度降下来。
  • 整个的写入效率需要重新评估。

总结

本次问题从分析到解决花的时间并不长,也还比较典型,其中的过程再总结一下:

  • 首先定位消耗 CPU 进程。
  • 再定位消耗 CPU 的具体线程。
  • 内存问题 dump 出快照进行分析。
  • 得出结论,调整代码,测试结果。

最后愿大家都别接到生产告警。

from:https://crossoverjie.top/2019/06/18/troubleshoot/cpu-percent-100-02/