白一梓

白一梓 查看完整档案

北京编辑齐鲁工业大学  |  计算机科学与技术 编辑一起作业网  |  Node工程师 编辑 blog.whyun.com 编辑
编辑

写css的人中C语言写的最好的;写c语言的人中css写的最好的。

个人动态

白一梓 收藏了文章 · 1月12日

Consul 命令行最全文档

1.启动一个带ACL 控制的Agent

首先,从这个网址下载consul,解压后发现就是个可执行文件,如果不可以执行,chmod +x consul 一下。

为了试验Consul较多的功能,这里我们打算启用一个dev模式,带ACL控制的Consul代理。
配置文件config.json如下

{
    "datacenter":"dc1",
    "primary_datacenter":"dc1",
    "data_dir":"/opt/consul/data/",
    "enable_script_checks":false,
    "bind_addr":"127.0.0.1",
    "node_name":"consul-dev",
    "enable_local_script_checks":true,
    "log_file":"/opt/consul/log/",
    "log_level":"info",
    "log_rotate_bytes":100000000,
    "log_rotate_duration":"24h",
    "encrypt":"krCysDJnrQ8dtA7AbJav8g==",
    "acl":{
        "enabled":true,
        "default_policy":"deny",
        "enable_token_persistence":true,
        "tokens":{
            "master":"cd76a0f7-5535-40cc-8696-073462acc6c7"      
            }
    }
}

下面是参数说明:

  • datacenter 此标志表示代理运行的数据中心。如果未提供,则默认为“dc1”。 Consul拥有对多个数据中心的一流支持,但它依赖于正确的配置。同一数据中心中的节点应在同一个局域网内。
  • primary_datacenter: 这指定了对ACL信息具有权威性的数据中心。必须提供它才能启用ACL。
  • bind_addr: 内部群集通信绑定的地址。这是群集中所有其他节点都应该可以访问的IP地址。默认情况下,这是“0.0.0.0”,这意味着Consul将绑定到本地计算机上的所有地址,并将第一个可用的私有IPv4地址通告给群集的其余部分。如果有多个私有IPv4地址可用,Consul将在启动时退出并显示错误。如果指定“[::]”,Consul将通告第一个可用的公共IPv6地址。如果有多个可用的公共IPv6地址,Consul将在启动时退出并显示错误。 Consul同时使用TCP和UDP,并且两者使用相同的端口。如果您有防火墙,请务必同时允许这两种协议。
  • advertise_addr: 更改我们向群集中其他节点通告的地址。默认情况下,会使用-bind参数指定的地址.
  • server: 是否是server agent节点。
  • connect.enabled: 是否启动Consul Connect,这里是启用的。
  • node_name:节点名称。
  • data_dir: agent存储状态的目录。
  • enable_script_checks: 是否在此代理上启用执行脚本的健康检查。有安全漏洞,默认值就是false,这里单独提示下。
  • enable_local_script_checks: 与enable_script_checks类似,但只有在本地配置文件中定义它们时才启用它们。仍然不允许在HTTP API注册中定义的脚本检查。
  • log-file: 将所有Consul Agent日志消息重定向到文件。这里指定的是/opt/consul/log/目录。
  • log_rotate_bytes:指定在需要轮换之前应写入日志的字节数。除非指定,否则可以写入日志文件的字节数没有限制
  • log_rotate_duration:指定在需要旋转日志之前应写入日志的最长持续时间。除非另有说明,否则日志会每天轮换(24小时。单位可以是"ns", "us" (or "µs"), "ms", "s", "m", "h", 比如设置值为24h
  • encrypt:用于加密Consul Gossip 协议交换的数据。在启动各个server之前,配置成同一个UUID值就行,或者你用命令行consul keygen 命令来生成也可以。
  • acl.enabled: 是否启用acl.
  • acl.default_policy: “allow”或“deny”; 默认为“allow”,但这将在未来的主要版本中更改。当没有匹配规则时,默认策略控制令牌的行为。在“allow”模式下,ACL是黑名单:允许任何未明确禁止的操作。在“deny”模式下,ACL是白名单:阻止任何未明确允许的操作.
  • acl.enable_token_persistence: 可能值为true或者false。值为true时,API使用的令牌集合将被保存到磁盘,并且当代理重新启动时会重新加载。
  • acl.tokens.master: 具有全局管理的权限,也就是最大的权限。它允许操作员使用众所周知的令牌密钥ID来引导ACL系统。需要在所有的server agent上设置同一个值,可以设置为一个随机的UUID。这个值权限最大,注意保管好。

接着我们以dev模式启动agent。

./consul agent -dev -config-file ./config.json 

启动完之后,你会发现出现下面的错误
acl-block
这个错误是没有设置agent-token造成的,agent-token主要用于客户端和服务器执行内部操作.比如catalog api的更新,反熵同步等。

1.先创建agent-token

curl \
    --request PUT \
    --header "X-Consul-Token: cd76a0f7-5535-40cc-8696-073462acc6c7" \
    --data \
'{
  "Name": "Agent Token",
  "Type": "client",
  "Rules": "node \"\" { policy = \"write\" } service \"\" { policy = \"read\" }"
}' http://127.0.0.1:8500/v1/acl/create

注意这里面的X-Consul-Token与上面config.json 里面的acl.tokens.master要是同一个值,此时你会看到生成成功,d118b1fc-77af-d870-8417-667c04b29cdf这一串就是agent-token了。
在这里插入图片描述
2.设置agent-token
由于这里只有一个agent,需要调用接口来设置agent-token。

curl \
    --request PUT \
    --header "X-Consul-Token: cd76a0f7-5535-40cc-8696-073462acc6c7" \
    --data \
'{
  "Token": "d118b1fc-77af-d870-8417-667c04b29cdf"
}' http://127.0.0.1:8500/v1/agent/token/acl_agent_token

然后你会发现之前的错误就消失了。
在这里插入图片描述
如此,一个带有ACL控制的agent就启动好了。如果你想搭建一个带ACL控制的集群,请参见我的另一篇文章

2.命令行

consul 子命令挺多的,如下图,但不要慌,一个个来。
consul-subcommand
最后设置一下环境变量,增加CONSUL_HTTP_TOKEN。
我这里是Mac,改的是~/.bash_profile;其他系统的,请自行搜索。

 sudo vim ~/.bash_profile

在末尾添加

export CONSUL_HTTP_TOKEN=cd76a0f7-5535-40cc-8696-073462acc6c7

然后让新的环境变量生效:

source ~/.bash_profile

acl

关于acl部分,后面我们会在web-ui里面进行控制,我会单独写一篇文章系统介绍Consul ACL如何设置。不过,你可以先看我之前的文章,Consul ACL集群配置说明以及ACL Token的用法, 但这篇文章只告诉了怎么用,并没有讲清楚为什么这么用。

agent

agent就是启动consul代理部分。这里面主要是一些配置信息,如何启动。后面会把完整版的配置翻译出来,供大家参考,不过还是强烈建议大家读读命令行的帮助说明, 即 ./consul agent --help

catalog

与consul的catalog打交道。
列出所有的数据中心: ./consul catalog datacenters
列出所有的节点:./consul catalog nodes
列出所有的服务:./consul catalog services

config

config命令用于与Consul的中央配置系统进行交互。
装备一个配置文件servie-defaults.hcl

Kind = "service-defaults"
Name = "web"
Protocol = "http"

写入一个配置:

./consul config write service-defaults.hcl 

读取刚写入的配置:

./consul config  read -kind service-defaults -name web

列出特定类型的配置:

./consul config list -kind service-defaults

删除一个配置:

./consul config delete -kind service-defaults -name web

connect

和Consul Connect的进行交互。Consul Connect使用相互TLS提供服务到服务连接授权和加密。应用程序可以使用sidecar代理自动为入站和出站连接建立TLS连接,而根本不知道Connect。应用程序还可以与Connect本机集成,以实现最佳性能和安全性。

debug

consul debug命令在指定时间内监视Consul代理,将有关代理,集群和环境的信息记录到写入当前目录的归档中。
下面是执行./consul debug的结果,更多使用说明请执行./consul debug --help
在这里插入图片描述
可以看到输出文件被保存到命令行所在路径/consul-debug-1559985864.tar.gz中。你可以解压该文件,进行debug.

watch

监视(watch)可以监视数据视图(例如,节点列表,KV对,健康检查)的更新。检测到更新时,将调用外部处理程序。处理程序可以是任何可执行文件或HTTP端点。

./consul watch -type=event -name=helloserviceevent /Use/cuixin/ConsulStudy/mac-dev/echo_handler.sh -helloservice

上面我们指定监视的类型是event, 名称是helloserviceevent的事件, 当收到该事件时,将触发执行echo_handler.sh这个脚本,并且参数为helloservice。
下面是echo_handler.sh这个脚本的内容:

#!/bin/bash
 echo “hello  $1” >> test.txt

很简单,会将内容追加到test.txt中。
注意这里需要给echo_handler.sh加下执行权限

chmod +x echo_handler.sh

event

event命令提供了一种将自定义用户事件触发到整个数据中心的机制。这些事件对Consul不透明,但它们可用于构建脚本基础结构,以执行自动部署,重新启动服务或执行任何其他编排操作。可以使用watch(监视)处理事件。使用八卦协议传播事件。
虽然细节对于使用事件并不重要,但理解语义很有用。八卦层将尽最大努力发放活动,但没有保证发放成功。与大多数使用共识复制的Consul数据不同,事件数据纯粹是点对点的八卦。这意味着它没有持久化,也没有总排序。实际上,这意味着您不能依赖消息邮件传递的顺序。然而,优点是即使在没有服务器节点或停机期间仍可以使用事件。
基础八卦也设置了用户事件消息大小的限制。很难给出一个确切的数字,因为它取决于事件的各种参数,但有效载荷应保持非常小(<100字节)。指定太大的事件将返回错误。

有了上面watch部分提供的监视,下面我们就产生对应的事件就行。
./consul event -name=helloserviceevent
这个触发事件的命令可以执行多次,每次触发事件之后,你都会看到test.txt多了一行“hello -helloservice”
在这里插入图片描述

## exec
exec命令提供了一种远程执行机制。 例如,这可用于在提供Web服务的所有计算机上运行uptime命令。这里由于安全性问题,我们在配置里面禁用了远程执行( "enable_script_checks":false)。
否则你可以执行./consul exec uptime 来查看各个节点已经启动多长时间了。
在这里插入图片描述

force-leave

force-leave命令强制Consul集群的成员进入“left”状态。 如果该成员仍然存活,它最终将重新加入群集。 此方法的真正目的是强制删除“failed”状态的节点。

./consul force-leave node-name

info

info命令提供对操作员有用的各种调试信息。 根据代理是客户端还是服务器,将返回有关不同子系统的信息。
目前有顶级键:
agent:提供有关代理的信息
consul:有关Consul(客户或服务器)的信息
raft:提供有关Raft共识库的信息
serf_lan:提供有关LAN八卦池的信息
serf_wan:提供有关WAN八卦池的信息

./consul info

intention

意图通过Connect定义服务的访问控制,用于控制哪些服务可以建立连接。可以通过API,CLI或UI管理意图。 (这部分和Consul Connect联系较为紧密,后面我研究透了,会单独写篇文章)。
创建一个允许“web”与“db”对话的意图:

$ consul intention create web db

测试是否允许“web”连接到“db”:

$ consul intention check web db

找到与“db”服务进行通信的所有意图:

$ consul intention match db

join

通过指定至少一个现有成员,告知正在运行的Consul代理(使用“consul agent”)加入群集

   ./consul join cluster_member1_address

其中 cluster_member1_address 是ip:port的格式,可以通过运行./consul members 发现现有集群的地址。

keygen

keygen命令生成可用于Consul代理流量加密的加密密钥。 keygen命令使用加密强伪随机数生成器来生成密钥。你也可以选择自己生成一个UUID。

./consul keygen

keyring

keyring命令用于检查和修改Consul的Gossip Pools中使用的加密密钥。 它能够向集群分发新的加密密钥,淘汰旧的加密密钥,以及更改集群用来加密消息的密钥。
查看集群目前使用的所有秘钥

./consul keyring -list

在这里插入图片描述
这个值,与我们配置文件中指定的 "encrypt":"krCysDJnrQ8dtA7AbJav8g=="是一致的。

kv

kv命令用于通过命令行与Consul的KV存储进行交互。它公开了用于从KV存储中插入,更新,读取和删除的顶级命令。
使用值“5”创建或更新名为“redis / config / connections”的键:

./consul kv put redis/config/connections 5

读回这个值:

./consul kv get redis/config/connections

或获取详细的关键信息:

./consul kv get -detailed redis/config/connections

最后,删除密钥:

./consul kv delete redis/config/connections

leave

leave命令触发代理的正常离开和关闭进程。 它用于确保其他节点将代理视为“离开了”而不是“失败了”。 离开了的节点在带快照重新启动时不会尝试重新加入群集。

license

这个是企业级consul所带功能,略。

lock

lock命令提供了一种简单分布式锁的机制。在KV存储中的给定前缀处创建锁(或信号量),并且仅在保持时,调用子进程。如果锁丢失或通信中断,子进程将终止。

当-n = 1时,只存在一个提供互斥的锁持有者或领导者。 设置更高的值会切换到允许多个持有者协调的信号量。另外,提供的前缀必须具有写权限

下面举个互斥锁的例子:
获得锁的进程会睡眠10s, 没拿到锁的会在1s内因超时被终止。
1.首先在kv存储放入一个键值对。

./consul kv put redis/config/connections 1

2.然后打开两个终端,切换到consul安装的目录,分别执行以下命令。

./consul lock -n=1 -timeout=1s  redis/config/connections  sleep 10

拿到锁的正常执行,睡眠10s后,执行完毕。
在这里插入图片描述
没拿到锁的因获取锁超时(这里设置是1s)被中止。
在这里插入图片描述

login

login命令将使用请求的auth方法将提供的第三方凭证与新创建的Consul ACL令牌交换。 配对命令consul logout应该用于销毁以这种方式创建的任何令牌,以避免资源泄漏。

  • bearer-token-file = <string> - 包含要与此auth方法一起使用的秘密承载令牌的文件的路径。
  • meta = <value> - 在令牌上设置的元数据,格式为key = value。 可以多次指定该标志以设置多个元字段。
  • method = <string> - 要登录的auth方法的名称。
  • token-sink-file = <string> - 最新令牌的SecretID在此文件中保持最新。
  • $ consul login -method 'minikube' \

       -bearer-token-file '/run/secrets/kubernetes.io/serviceaccount/token' \
       -token-sink-file 'consul.token'
    

    $ cat consul.token
    36103ae4-6731-e719-f53a-d35188cfa41d

    由于这里我还没有学习kubernetes,后面有机会可以补上。

logout

如果是从consul login创建的,则logout命令将销毁提供的令牌。

$ consul logout -token-file 'consul.token'

maint

maint命令提供对服务维护模式的控制。 使用该命令,可以将节点提供的服务或节点上的所有服务标记为“维护中”。 在此操作模式下,该服务不会出现在DNS查询结果或API结果中。 这有效地将服务从服务的可用“健康”节点池中取出。

通过在服务的紧急状态下注册运行状况检查来激活维护模式,并通过取消注册运行状况检查来取消激活维护模式。
下面注册一个helloservice1,在postman中,对应位置输入以下参数。
在这里插入图片描述

 PUT http://localhost:8500/v1/agent/service/register?token=cd76a0f7-5535-40cc-8696-073462acc6c7
{
    "ID": "helloservice1",
    "Name": "helloservice",
    "Tags": [
        "v1",
        "master"
    ],
    "Address": "127.0.0.1",
    "Port": 8000,
    "Meta": {
        "api_version": "1.0"
    },
    "EnableTagOverride": false,
    "Check": {
        "DeregisterCriticalServiceAfter": "90m",
        "HTTP": "http://www.baidu.com/",
        "Interval": "10s"
    }
} 

让服务处于维护状态

./consul maint -enable -service helloservice1 -reason "need to update"

让服务取消维护状态

./consul maint -disable -service helloservice1

members

members命令输出Consul代理知道的当前成员列表及其状态。 节点的状态只能是“alive”,“left”或“failed”。
处于“failed”状态的节点仍然列出,因为在故障实际上只是网络分区的情况下,Consul会尝试在一定时间内重新连接故障节点。
单机版输出:

集群版输出:
在这里插入图片描述

monitor

monitor命令用于连接和跟踪正在运行的Consul代理的日志。 Monitor将显示最近的日志,然后继续关注日志,直到中断或远程代理退出之前不会退出。

monitor命令的强大之处在于它允许您以相对较高的日志级别(例如“warn”)记录代理,但仍然可以访问debug日志并在必要时查看debug日志。

  • log-level - 要显示的消息的日志级别。默认情况下,这是“info”。此日志级别可能比代理配置为运行时更详细。可用的日志级别为“trace”,“debug”,“info”,“warn”和“err”。

举个例子

./consul monitor debug

operator

operator命令为Consul操作员提供集群级工具,例如与Raft子系统交互。具体子命令的使用说明记得使用--help
比如:

./consul operator raft --help

下面列出raft对等集:
单机版
在这里插入图片描述
集群版
在这里插入图片描述
可以看到只有server agent才参加raft 对等集的一部分。

reload

reload命令会触发代理的配置文件重新加载。

SIGHUP信号通常用于触发重新加载配置,但在某些情况下,触发CLI可能更方便。

此命令与信号的操作相同,这意味着它将触发重新加载,但不会等待重新加载完成。 重新加载的任何错误都将出现在代理日志中,而不会出现在此命令的输出中。

注意
并非所有配置选项都可重新加载。 有关支持哪些选项的详细信息,请参阅代理选项页面上的可重新加载配置部分。

举个例子:
我们将一开始的配置文件,config.json 中的 log_level 由 info 改为 debug 。

./consul reload

可以在consul agent运行的命令行中看到在重新加载配置。
在这里插入图片描述

rtt(round trip time)

rtt命令使用Consul的集群网络坐标模型估计两个节点之间的网络往返时间。
由于这里需要多个server,所以这里我用了前面一篇文章在虚拟机上搭建的集群
在这里插入图片描述
这里由于我只有一个数据中心,没法实验多个数据中心的rtt, 不过使用方法类似,如下

 $ consul rtt -wan n1.dc1 n2.dc2
    Estimated n1.dc1 <-> n2.dc2 rtt: 1.275 ms (using WAN coordinates)

services

services命令具有子命令,用于与向本地代理注册的Consul服务进行交互。 它们提供了有用的命令,例如注册和注销,以便在脚本,开发模式等中轻松注册服务。要查看目录中的所有服务,而不是仅查看代理本地服务,请使用./consul catalog services命令。
创建一个简单的服务:

./consul services register -name=web

从一个配置 文件中 创建服务

$ cat web.json
{
  "Service": {
    "Name": "web"
  }
}

$  ./consul services register web.json

注销一个服务(两种方式都可以):

$ ./consul services deregister web.json

$ ./consul services deregister -id web

snapshot

snapshot命令具有子命令,用于保存,恢复和检查Consul服务器的状态以进行灾难恢复。 这些是原子时间点快照,包括键/值条目,服务目录,准备好的查询,会话和ACL。
save:保存Consul服务器状态的快照
restore: 恢复Consul服务器状态的快照
inspect: 显示有关Consul快照文件的信息
在这里插入图片描述

tls

tls命令用于帮助为Consul TLS设置CA和证书。

validate

consul validate命令对Consul配置文件执行彻底的健全性测试。 对于给定的每个文件或目录,该命令将尝试像consul agent命令那样解析内容,并捕获任何错误。

这对于仅对配置进行测试很有用,而无需实际启动代理。 这将执行代理程序将执行的所有验证,因此应该为此提供将由代理程序加载的完整配置文件集。 此命令无法对部分配置片段进行操作,因为这些片段不会通过完整的代理验证。
比如说验证下我们一开始的配置文件

./consul validate config.json

在这里插入图片描述

version

version命令打印Consul的版本以及它与其他代理进行通信时理解的协议版本。

./consul version

最后的说明

1.由于篇幅有限,本文的大体上是对命令行的基本介绍,想要用好命令行还需要读读官方文档和linux的说明文档。

2.另外文中出现的集群版:请参考我的另一篇文章,Consul1.5.0 带ACL控制集群搭建

参考:

https://www.consul.io/docs/co...
https://www.consul.io/docs/ac...
https://www.consul.io/docs/ag...

查看原文

白一梓 发布了文章 · 2019-10-10

vipkid 的 rtc 使用介绍

跨机房应用之间互联,使用专线进行,在专线不可用的情况下可以回退使用公网。
vipkid 自研的 VDR rtc 比公版的腾讯云要更好的适应网络抖动,在抖动在 20-30% 的时候,对比明显。
慢速网络下,冗余增加一倍,降码率。

查看原文

赞 0 收藏 0 评论 0

白一梓 收藏了文章 · 2019-04-20

实现electron-bridge

electron-bridge

github链接 求star

Motivition

  • 如果想一套代码同时能跑在web环境和electron环境中,就需要在代码中先判断环境,再分别写对应的逻辑。每次写到electron环境下的逻辑,又要区分渲染进程和主进程,因为有些事只能渲染进程做,有些事只能主进程做。所以,我希望能将这些抽象出来,某个方法,只能在electron环境下被调用,并且不需要关心在什么进程下,web只要判断环境,调不同的方法就行,不需要关心和electron的交互。

  • 如果,我需要快速的开启另一个electron的项目,我希望我web里的代码能轻易的获取到electron的能力,而不是重新开始编写,这个时候,我希望有一层对electron能力的封装。

  • 团队内有些成员对web很熟悉,但是对electron不是很了解,如果加入项目,就需要去学习electron的知识,这个时候,如果能有一个库列出了所有electron能做的事,你只需要调用,无需关心它是怎么实现的,能很大程度提高开发效率。

Goals

  1. 给web注入适当的环境变量,让web知道自己的环境

  2. 给web注入一个对象,包含所有electron能做的事(包括主进程、渲染进程)

How to do

在load web页面的时候,有个webPreferences配置,我们在这里预加载一个js文件,就是electron-bridge.js

这个文件拥有node的能力,并且它是属于渲染进程的,所以它能做渲染进程里的事, 也能跟主进程通讯。

st=>start: start
op0=>operation: index.js去调用bridge.js暴露出来的方法, ElectronBridge.setFullScreen()
op1=>operation: bridge.js通过ipcRender告诉ipacMain做什么,并把回调暂存起来
op2=>operation:  主进程做完告诉bridge.js做完了,发送数据
op4=>operation:  bridge.js带上收到的数据,执行暂存的回调
op3=>operation:  bridge.js直接做完,触发回调
cond=>condition: bridge.js判断是不是主进程做的事?
e=>end: end

st->op0->cond
cond(yes)->op1->op2->op4->e
cond(no)->op3->e

Let's do it

给web注入适当的环境变量

加载bridge.js

win = new BrowserWindow({
  width: 800,
  height: 600,
  show: false,
  webPreferences: {
    preload: path.join(__dirname, '../bridge/bridge.js'),
    plugins: true
  }
});

当我们启动electron的时候,主进程开始通知这个渲染进程,给渲染进程注入主进程的环境变量,再有渲染进程挂载到window对象上,这样web就能获取自己的环境信息

//bridge.js

const {ipcRenderer} = require('electron');

//监听主进程,设置环境变量
ipcRenderer.on('set-env', (event, msg) => {
  for (const key in msg) {
    window[key] = msg[key];
  }
});
//main.js
const {BrowserWindow, ipcMain} = require('electron');

const win = new BrowserWindow({...});

//获取创建好的window对象发送消息
win.webContents.on('did-finish-load', function() {
  win.webContents.send('set-env', { //设置web环境变量
    __ELECTRON__: true,
    __DEV__: true,
    __PRO__: false,
    __SERVER__: false,
    windowLoaded: true
  });
});

通过bridge.js 来调用主进程的方法

我们通过ipcRender给主进程发送一系列消息,包括做什么事情(eventName), 根据哪些参数(params),对外根据不同的事件暴露不同的方法,接受参数,和回调函数。

  • 先将回调函数放在 eventsMap上暂存起来,因为ipcRender不能发送函数,所有的信息会被序列化后再发送给主进程,所以,我们先生成一个时间戳,让 eventsMap[时间戳] = cb 并把时间戳一同发送过去,等一会儿,主进程通知渲染进程调用哪个时间戳函数

  • 通过'resist-event'频道, 发送参数,包括 eventName、params、timeStamp

//bridge.js
const {ipcRenderer} = require('electron');

const eventsMap = {};

//调用原生事件
function registEvent(eventName, params, cb) {
  //允许只传两个数据
  if (!cb) {
    cb = params;
    params = {};
  }

  //如果win还未ready
  if (!windowLoaded) {
    cb(new Error('window not ready'));
    return;
  }

  const stamp = String(new Date().getTime());
  const opts = Object.assign({eventName}, params, {stamp});
  eventsMap[stamp] = cb; //注册唯一函数
  ipcRenderer.send('regist-event', opts); //发送事件
}

//进入全屏
function setFullScreen(cb) {
  registEvent(SET_FULL_SCREEN, cb);
}

window.ElectronBridge = {
  setFullScreen
};

主进程监听‘resist-event’频道,做对应的事。我们会将所有主进程能做的事,放在eventsList对象下,当接受到渲染进程的通知,去eventsList找有没有对应的事能做,有,做完通过promise,或者通过回调函数,去在‘fire-event’频道通知,渲染进程,事情已经做完,并把数据传回去,包括 stamp(之前渲染进程传过来的,现在传回去,告诉渲染进程执行哪个回调函数) 、 payload(返回数据) 、err (错误信息)

//main.js
const {ipcMain} = require('electron');

//监听对原生的调用
ipcMain.on('regist-event', (event, arg) => {
  const nativeEvent = eventsList[arg.eventName];
  if (nativeEvent) {
    const result =  nativeEvent(app, win, arg.params);
    if (isPromise(result)) {
      result.then(res => {
        event.sender.send('fire-event', {
          stamp: arg.stamp,
          payload: res
        });
      }).catch(err => {
        event.sender.send('fire-event', {
          stamp: arg.stamp,
          err
        });
      });
    } else {
      event.sender.send('fire-event', {
        stamp: arg.stamp,
        payload: result
      });
    }
  } else {
    event.sender.send('fire-event', {
      stamp: arg.stamp,
      err: new Error('event not support')
    });
  }
});

渲染进程监听‘fire-event’执行对应时间戳回调函数,并把主进程传过来的数据传给回调函数。触发完成后,删掉该回调函数。

//bridge.js

//触发事件回调
ipcRenderer.on('fire-event', (event, arg) => {
  const cb = eventsMap[arg.stamp];
  if (cb) {
    if (arg.err) {
      cb(arg.err, arg.payload);
    } else {
      cb(false, arg.payload);
    }
    delete eventsMap[arg.stamp];
  }
});

如果是渲染进程能做的事,就不需要再和主进程通讯,可以直接完成触发回调

//bridge.js
const {webFrame} = require('electron');
//设置缩放比,只能在渲染进程中实现
function setZoomFactor(params, cb) {
  webFrame.setZoomFactor(params);
  cb && cb();
}

window.ElectronBridge = {
  setZoomFactor
};

最终web中的js代码去调用bridge.js暴露出来的方法

// ../web/index.js

$btn1.addEventListener('click', function() {
  if (__ELECTRON__ && ElectronBridge) { //electron 环境
    ElectronBridge.setFullScreen((err) => {
      if (err) return;
      console.log('done');
    });
  } else { //web 环境
    alert('不能设置全屏')
    //do something else
  }
});
查看原文

白一梓 赞了文章 · 2019-04-20

实现electron-bridge

electron-bridge

github链接 求star

Motivition

  • 如果想一套代码同时能跑在web环境和electron环境中,就需要在代码中先判断环境,再分别写对应的逻辑。每次写到electron环境下的逻辑,又要区分渲染进程和主进程,因为有些事只能渲染进程做,有些事只能主进程做。所以,我希望能将这些抽象出来,某个方法,只能在electron环境下被调用,并且不需要关心在什么进程下,web只要判断环境,调不同的方法就行,不需要关心和electron的交互。

  • 如果,我需要快速的开启另一个electron的项目,我希望我web里的代码能轻易的获取到electron的能力,而不是重新开始编写,这个时候,我希望有一层对electron能力的封装。

  • 团队内有些成员对web很熟悉,但是对electron不是很了解,如果加入项目,就需要去学习electron的知识,这个时候,如果能有一个库列出了所有electron能做的事,你只需要调用,无需关心它是怎么实现的,能很大程度提高开发效率。

Goals

  1. 给web注入适当的环境变量,让web知道自己的环境

  2. 给web注入一个对象,包含所有electron能做的事(包括主进程、渲染进程)

How to do

在load web页面的时候,有个webPreferences配置,我们在这里预加载一个js文件,就是electron-bridge.js

这个文件拥有node的能力,并且它是属于渲染进程的,所以它能做渲染进程里的事, 也能跟主进程通讯。

st=>start: start
op0=>operation: index.js去调用bridge.js暴露出来的方法, ElectronBridge.setFullScreen()
op1=>operation: bridge.js通过ipcRender告诉ipacMain做什么,并把回调暂存起来
op2=>operation:  主进程做完告诉bridge.js做完了,发送数据
op4=>operation:  bridge.js带上收到的数据,执行暂存的回调
op3=>operation:  bridge.js直接做完,触发回调
cond=>condition: bridge.js判断是不是主进程做的事?
e=>end: end

st->op0->cond
cond(yes)->op1->op2->op4->e
cond(no)->op3->e

Let's do it

给web注入适当的环境变量

加载bridge.js

win = new BrowserWindow({
  width: 800,
  height: 600,
  show: false,
  webPreferences: {
    preload: path.join(__dirname, '../bridge/bridge.js'),
    plugins: true
  }
});

当我们启动electron的时候,主进程开始通知这个渲染进程,给渲染进程注入主进程的环境变量,再有渲染进程挂载到window对象上,这样web就能获取自己的环境信息

//bridge.js

const {ipcRenderer} = require('electron');

//监听主进程,设置环境变量
ipcRenderer.on('set-env', (event, msg) => {
  for (const key in msg) {
    window[key] = msg[key];
  }
});
//main.js
const {BrowserWindow, ipcMain} = require('electron');

const win = new BrowserWindow({...});

//获取创建好的window对象发送消息
win.webContents.on('did-finish-load', function() {
  win.webContents.send('set-env', { //设置web环境变量
    __ELECTRON__: true,
    __DEV__: true,
    __PRO__: false,
    __SERVER__: false,
    windowLoaded: true
  });
});

通过bridge.js 来调用主进程的方法

我们通过ipcRender给主进程发送一系列消息,包括做什么事情(eventName), 根据哪些参数(params),对外根据不同的事件暴露不同的方法,接受参数,和回调函数。

  • 先将回调函数放在 eventsMap上暂存起来,因为ipcRender不能发送函数,所有的信息会被序列化后再发送给主进程,所以,我们先生成一个时间戳,让 eventsMap[时间戳] = cb 并把时间戳一同发送过去,等一会儿,主进程通知渲染进程调用哪个时间戳函数

  • 通过'resist-event'频道, 发送参数,包括 eventName、params、timeStamp

//bridge.js
const {ipcRenderer} = require('electron');

const eventsMap = {};

//调用原生事件
function registEvent(eventName, params, cb) {
  //允许只传两个数据
  if (!cb) {
    cb = params;
    params = {};
  }

  //如果win还未ready
  if (!windowLoaded) {
    cb(new Error('window not ready'));
    return;
  }

  const stamp = String(new Date().getTime());
  const opts = Object.assign({eventName}, params, {stamp});
  eventsMap[stamp] = cb; //注册唯一函数
  ipcRenderer.send('regist-event', opts); //发送事件
}

//进入全屏
function setFullScreen(cb) {
  registEvent(SET_FULL_SCREEN, cb);
}

window.ElectronBridge = {
  setFullScreen
};

主进程监听‘resist-event’频道,做对应的事。我们会将所有主进程能做的事,放在eventsList对象下,当接受到渲染进程的通知,去eventsList找有没有对应的事能做,有,做完通过promise,或者通过回调函数,去在‘fire-event’频道通知,渲染进程,事情已经做完,并把数据传回去,包括 stamp(之前渲染进程传过来的,现在传回去,告诉渲染进程执行哪个回调函数) 、 payload(返回数据) 、err (错误信息)

//main.js
const {ipcMain} = require('electron');

//监听对原生的调用
ipcMain.on('regist-event', (event, arg) => {
  const nativeEvent = eventsList[arg.eventName];
  if (nativeEvent) {
    const result =  nativeEvent(app, win, arg.params);
    if (isPromise(result)) {
      result.then(res => {
        event.sender.send('fire-event', {
          stamp: arg.stamp,
          payload: res
        });
      }).catch(err => {
        event.sender.send('fire-event', {
          stamp: arg.stamp,
          err
        });
      });
    } else {
      event.sender.send('fire-event', {
        stamp: arg.stamp,
        payload: result
      });
    }
  } else {
    event.sender.send('fire-event', {
      stamp: arg.stamp,
      err: new Error('event not support')
    });
  }
});

渲染进程监听‘fire-event’执行对应时间戳回调函数,并把主进程传过来的数据传给回调函数。触发完成后,删掉该回调函数。

//bridge.js

//触发事件回调
ipcRenderer.on('fire-event', (event, arg) => {
  const cb = eventsMap[arg.stamp];
  if (cb) {
    if (arg.err) {
      cb(arg.err, arg.payload);
    } else {
      cb(false, arg.payload);
    }
    delete eventsMap[arg.stamp];
  }
});

如果是渲染进程能做的事,就不需要再和主进程通讯,可以直接完成触发回调

//bridge.js
const {webFrame} = require('electron');
//设置缩放比,只能在渲染进程中实现
function setZoomFactor(params, cb) {
  webFrame.setZoomFactor(params);
  cb && cb();
}

window.ElectronBridge = {
  setZoomFactor
};

最终web中的js代码去调用bridge.js暴露出来的方法

// ../web/index.js

$btn1.addEventListener('click', function() {
  if (__ELECTRON__ && ElectronBridge) { //electron 环境
    ElectronBridge.setFullScreen((err) => {
      if (err) return;
      console.log('done');
    });
  } else { //web 环境
    alert('不能设置全屏')
    //do something else
  }
});
查看原文

赞 3 收藏 7 评论 0

白一梓 收藏了文章 · 2019-03-13

nodejs和树莓派开发以及点亮RGB的LED灯代码

前段时间集团举行前端IOT比赛,借此机会熟悉了树莓派相关的东西,特此记录一些相关的文档和开发指南。

先介绍一些树莓派的入门教程

阮一峰的树莓派入门

微雪电子-树莓派硬件中文官网

ssh链接树莓派

ssh pi@dd.dd.dd.dd(ip)
密码:raspberry

设置显示设备

推荐选购3.5吋或者5吋的HDMI显示设备,我第一次买的3.2吋的串口显示器,占用了我20个串口的针脚。

设备链接见这里

使用3.5吋显示器

cd /boot/LCD-show/
./LCD35-show
使用HDMI输出

cd /boot/LCD-show/
./LCD-hdmi
设置旋转屏幕

设置显示方向
安装完触摸驱动后,可以通过运行以下命令修改屏幕旋转方向。

旋转0度:

cd /boot/LCD-show/
./LCD35-show 0
旋转90度:

cd /boot/LCD-show/
./LCD35-show 90
旋转180度:

cd /boot/LCD-show/
./LCD35-show 180
旋转270度:

cd /boot/LCD-show/
./LCD35-show 270
声音设置为非HDMI输出

Bash
sudo amixer cset numid=3 1
需要注意的是如果你是浏览器播放声音。。拔掉显示器后貌似浏览器就进入后台模式不播放声音了。

介绍一些相关的nodejs的库

https://github.com/rwaldron/j...

一个适配各种板子的串口的基础库,当你需要点亮LED小灯泡的时候需要用到它

Raspi-io

Raspi-io is a Firmata API compatible library for Raspbian running on the Raspberry Pi that can be used as an I/O plugin with Johnny-Five.

和上面一个库搭配使用。

rpio

https://github.com/jperkin/no...

This is a high performance node.js addon which provides access to the Raspberry Pi GPIO interface, supporting regular GPIO as well as i²c, PWM, and SPI.

一个控制打开某个串口针脚的基础库。

serialport

https://github.com/EmergingTe...

一个链接控制硬件的基础库,比如控制USB串口,和链接USB串口的设备进行通信等,他有很多版本,树莓派的版本见这里

https://www.npmjs.com/package...

安装有点,麻烦。我折腾了3小时、、、、

点亮一个LED灯

LED灯分为简单的两个针脚的二极管灯,点亮见前面阮一峰博客,下面重点介绍一下RGB的LED灯

TB1KFkTSXXXXXcxXXXXXXXXXXXX-772-570.jpg
如上所示。这样的灯点亮的教程比较少。

第一步选择对应的串口针脚,首先不要把插针脚2,即:+5V口那个。

我插了两个分别是RGB为:[29,31,33],[36,38,40]

代码如下

var five = require("johnny-five");
var Raspi = require('raspi-io')
var rpio = require('rpio');
var isLED1On=false;
var isLED2On=false;
var LED = {
    LED1:null,
    LED2:null,
    init(LED1=[29,31,33],LED2=[36,38,40]){
        var board = new five.Board({
            io:new Raspi({enableSoftPwm:true})
        });
        this.LED1=LED1;
        this.LED2=LED2
        board.on('ready',function(){
            return new Promise(function(resolve,reject){
                var led1 =  new five.Led.RGB({
                    pins: {
                        red: `P1-${LED1[0]}`,
                        green: `P1-${LED1[1]}`,
                        blue:`P1-${LED1[2]}`,
                    }
                })
                var led2 =  new five.Led.RGB({
                    pins: {
                        red: `P1-${LED2[0]}`,
                        green: `P1-${LED2[1]}`,
                        blue:`P1-${LED2[2]}`,
                    }
                })
                // 打开 11 号针脚(GPIO17) 作为输出
                rpio.open(LED1[0], rpio.OUTPUT);
                rpio.open(LED1[1], rpio.OUTPUT);
                rpio.open(LED1[2], rpio.OUTPUT);
                rpio.open(LED2[0], rpio.OUTPUT);
                rpio.open(LED2[1], rpio.OUTPUT);
                rpio.open(LED2[2], rpio.OUTPUT);
                rpio.open(LED1[0], rpio.HIGH);
                rpio.open(LED1[1], rpio.HIGH);
                rpio.open(LED1[2], rpio.HIGH);
                resolve(board);
            })
        })
    },
    openLED1(){
        console.log('led1'+JSON.stringify(this))
        rpio.write(this.LED1[0], rpio.HIGH);
        rpio.write(this.LED1[1], rpio.HIGH);
        rpio.write(this.LED1[2], rpio.HIGH);
        isLED1On=true;
    },
    openLED2(){
        rpio.write(this.LED2[0], rpio.HIGH);
        rpio.write(this.LED2[1], rpio.HIGH);
        rpio.write(this.LED2[2], rpio.HIGH);
        isLED2On=true;
    },
    closeLED1(){
        console.log('led1'+JSON.stringify(this))
        rpio.write(this.LED1[0], rpio.LOW);
        rpio.write(this.LED1[1], rpio.LOW);
        rpio.write(this.LED1[2], rpio.LOW);
        isLED1On=false;
    },
    closeLED2(){
        rpio.write(this.LED2[0], rpio.LOW);
        rpio.write(this.LED2[1], rpio.LOW);
        rpio.write(this.LED2[2], rpio.LOW);
        isLED2On=false;
    },
    flashLED1(){
        if(isLED1On){
            return;
        }
        var self = this;
        self.openLED1();
        setTimeout(function () {
            self.closeLED1();
        },3000);
    },
    flashLED2(){
        if(isLED2On){
            return;
        }
        var self = this;
        self.openLED2();
        setTimeout(function () {
            self.closeLED2()
        },3000);
    },

}
module.exports={
    led:LED
}

更多内容详见我的博客

查看原文

白一梓 赞了文章 · 2019-03-13

nodejs和树莓派开发以及点亮RGB的LED灯代码

前段时间集团举行前端IOT比赛,借此机会熟悉了树莓派相关的东西,特此记录一些相关的文档和开发指南。

先介绍一些树莓派的入门教程

阮一峰的树莓派入门

微雪电子-树莓派硬件中文官网

ssh链接树莓派

ssh pi@dd.dd.dd.dd(ip)
密码:raspberry

设置显示设备

推荐选购3.5吋或者5吋的HDMI显示设备,我第一次买的3.2吋的串口显示器,占用了我20个串口的针脚。

设备链接见这里

使用3.5吋显示器

cd /boot/LCD-show/
./LCD35-show
使用HDMI输出

cd /boot/LCD-show/
./LCD-hdmi
设置旋转屏幕

设置显示方向
安装完触摸驱动后,可以通过运行以下命令修改屏幕旋转方向。

旋转0度:

cd /boot/LCD-show/
./LCD35-show 0
旋转90度:

cd /boot/LCD-show/
./LCD35-show 90
旋转180度:

cd /boot/LCD-show/
./LCD35-show 180
旋转270度:

cd /boot/LCD-show/
./LCD35-show 270
声音设置为非HDMI输出

Bash
sudo amixer cset numid=3 1
需要注意的是如果你是浏览器播放声音。。拔掉显示器后貌似浏览器就进入后台模式不播放声音了。

介绍一些相关的nodejs的库

https://github.com/rwaldron/j...

一个适配各种板子的串口的基础库,当你需要点亮LED小灯泡的时候需要用到它

Raspi-io

Raspi-io is a Firmata API compatible library for Raspbian running on the Raspberry Pi that can be used as an I/O plugin with Johnny-Five.

和上面一个库搭配使用。

rpio

https://github.com/jperkin/no...

This is a high performance node.js addon which provides access to the Raspberry Pi GPIO interface, supporting regular GPIO as well as i²c, PWM, and SPI.

一个控制打开某个串口针脚的基础库。

serialport

https://github.com/EmergingTe...

一个链接控制硬件的基础库,比如控制USB串口,和链接USB串口的设备进行通信等,他有很多版本,树莓派的版本见这里

https://www.npmjs.com/package...

安装有点,麻烦。我折腾了3小时、、、、

点亮一个LED灯

LED灯分为简单的两个针脚的二极管灯,点亮见前面阮一峰博客,下面重点介绍一下RGB的LED灯

TB1KFkTSXXXXXcxXXXXXXXXXXXX-772-570.jpg
如上所示。这样的灯点亮的教程比较少。

第一步选择对应的串口针脚,首先不要把插针脚2,即:+5V口那个。

我插了两个分别是RGB为:[29,31,33],[36,38,40]

代码如下

var five = require("johnny-five");
var Raspi = require('raspi-io')
var rpio = require('rpio');
var isLED1On=false;
var isLED2On=false;
var LED = {
    LED1:null,
    LED2:null,
    init(LED1=[29,31,33],LED2=[36,38,40]){
        var board = new five.Board({
            io:new Raspi({enableSoftPwm:true})
        });
        this.LED1=LED1;
        this.LED2=LED2
        board.on('ready',function(){
            return new Promise(function(resolve,reject){
                var led1 =  new five.Led.RGB({
                    pins: {
                        red: `P1-${LED1[0]}`,
                        green: `P1-${LED1[1]}`,
                        blue:`P1-${LED1[2]}`,
                    }
                })
                var led2 =  new five.Led.RGB({
                    pins: {
                        red: `P1-${LED2[0]}`,
                        green: `P1-${LED2[1]}`,
                        blue:`P1-${LED2[2]}`,
                    }
                })
                // 打开 11 号针脚(GPIO17) 作为输出
                rpio.open(LED1[0], rpio.OUTPUT);
                rpio.open(LED1[1], rpio.OUTPUT);
                rpio.open(LED1[2], rpio.OUTPUT);
                rpio.open(LED2[0], rpio.OUTPUT);
                rpio.open(LED2[1], rpio.OUTPUT);
                rpio.open(LED2[2], rpio.OUTPUT);
                rpio.open(LED1[0], rpio.HIGH);
                rpio.open(LED1[1], rpio.HIGH);
                rpio.open(LED1[2], rpio.HIGH);
                resolve(board);
            })
        })
    },
    openLED1(){
        console.log('led1'+JSON.stringify(this))
        rpio.write(this.LED1[0], rpio.HIGH);
        rpio.write(this.LED1[1], rpio.HIGH);
        rpio.write(this.LED1[2], rpio.HIGH);
        isLED1On=true;
    },
    openLED2(){
        rpio.write(this.LED2[0], rpio.HIGH);
        rpio.write(this.LED2[1], rpio.HIGH);
        rpio.write(this.LED2[2], rpio.HIGH);
        isLED2On=true;
    },
    closeLED1(){
        console.log('led1'+JSON.stringify(this))
        rpio.write(this.LED1[0], rpio.LOW);
        rpio.write(this.LED1[1], rpio.LOW);
        rpio.write(this.LED1[2], rpio.LOW);
        isLED1On=false;
    },
    closeLED2(){
        rpio.write(this.LED2[0], rpio.LOW);
        rpio.write(this.LED2[1], rpio.LOW);
        rpio.write(this.LED2[2], rpio.LOW);
        isLED2On=false;
    },
    flashLED1(){
        if(isLED1On){
            return;
        }
        var self = this;
        self.openLED1();
        setTimeout(function () {
            self.closeLED1();
        },3000);
    },
    flashLED2(){
        if(isLED2On){
            return;
        }
        var self = this;
        self.openLED2();
        setTimeout(function () {
            self.closeLED2()
        },3000);
    },

}
module.exports={
    led:LED
}

更多内容详见我的博客

查看原文

赞 1 收藏 2 评论 0

白一梓 收藏了文章 · 2019-03-06

NodeJS中被忽略的内存

原文链接:BlueSun | NodeJS中被忽略的内存

如朴灵说过,Node对内存泄露十分敏感,一旦线上应用有成千上万的流量,那怕是一个字节的内存泄漏也会造成堆积,垃圾回收过程中将会耗费更多时间进行对象扫描,应用响应缓慢,直到进程内存溢出,应用奔溃。

虽然从很久以前就知道内存问题是不容忽视的,但是日常开发的时候并没有碰到性能上的瓶颈,直到最近做了一个百万PV级的营销项目,由于访问量,并发量都达到了一个量级。一些细小的、平时没注意到的问题被放大,这才映入眼帘,开始注意到了内存问题。殊不知Node对内存的泄露是如此的敏感。
为此,赶紧去补习了一下V8中的内存处理机制。
那么,V8中的内存机制是怎么样的?

V8的内存机制

内存的限制

Node中并不像其他后端语言中,对内存的使用没有多少限制。在Node中使用内存,只能使用到系统的一部分内存,64位系统下约为1.4GB,32位系统下约为0.7GB。这归咎于Node使用了本来运行在浏览器的V8引擎。

V8引擎的设计之初只是运行在浏览器中,而在浏览器的一般应用场景下使用起来绰绰有余,足以胜任前端页面中的所有需求。

虽然服务端操作大内存也不是常见的需求,但是万一有这样的需求,还是可以解除限制的。
在启动node程序的时候,可以传递两个参数来调整内存限制的大小。

node --max-nex-space-size=1024 app.js // 单位为KB
node --max-old-space-size=2000 app.js // 单位为MB

这两条命令分别对应Node内存堆中的「新生代」和「老生代」

不受内存限制的特例

在Node中,使用Buffer可以读取超过V8内存限制的大文件。原因是Buffer对象不同于其他对象,它不经过V8的内存分配机制。这在于Node并不同于浏览器的应用场景。在浏览器中,JavaScript直接处理字符串即可满足绝大多数的业务需求,而Node则需要处理网络流和文件I/O流,操作字符串远远不能满足传输的性能需求。

内存的分配

一切JavaScript对象都用堆来存储

当我们在代码中声明变量并赋值时,所使用对象的内存就分配在堆中。如果已申请的对空闲内存不够分配新的对象,讲继续申请堆内存,直到堆的大小超过V8的限制为止。

V8的堆示意图

V8的垃圾回收机制

分代式垃圾回收

V8的垃圾回收策略主要基于「分代式垃圾回收机制」,基于这个机制,V8把内存分为「新生代(New Space)」和 「老生代 (Old Space)」。
新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。
前面提及到的--max-old-space-size命令就是设置老生代内存空间的最大值,而--max-new-space-size命令则可以设置新生代内存空间的大小。

V8的分代示意图

为什么要分成新老两代?

垃圾回收算法有很多种,但是并没有一种是胜任所有的场景,在实际的应用中,需要根据对象的生存周期长短不一,而使用不同的算法,已达到最好的效果。在V8中,按对象的存活时间将内存的垃圾回收进行不同的分代,然后分别对不同的内存施以更高效的算法。

新生代中的垃圾回收

在新生代中,主要通过Scavenge算法进行垃圾回收。

Scavenge

在Scavenge算法中,它将堆内存一分为二,每一部分空间称为semispace。在这两个semispace空间中,只有一个处于使用中,另外一个处于闲置状态。处于使用状态的semispace称为From空间,处于闲置状态的semispace称为To空间。当我们分配对象时,先是从From空间中分配。当开始进行垃圾回收时,会检查From空间中存活的对象,这些存活的对象会被复制到To空间中,而非存活的对象占用的空间会被释放。完成复制后,From空间和To空间角色互换。简而言之,在垃圾回收的过程中,就是通过将存活对象在两个semispace空间之间进行复制。

V8的堆内存示意图

在新生代中的对象怎样才能到老生代中?

在新生代存活周期长的对象会被移动到老生代中,主要符合两个条件中的一个:

1. 对象是否经历过Scavenge回收。
对象从From空间中复制到To空间时,会检查它的内存地址来判断这个对象是否已经经历过一次Scavenge回收,如果已经经历过了,则将该对象从From空间中复制到老生代空间中。

2. To空间的内存占比超过25%限制。
当对象从From空间复制到To空间时,如果To空间已经使用超过25%,则这个对象直接复制到老生代中。这么做的原因在于这次Scavenge回收完成后,这个To空间会变成From空间,接下来的内存分配将在这个空间中进行。如果占比过高,会影响后续的内存分配。

老生代中的垃圾回收

对于老生代的对象,由于存活对象占比较大比重,使用Scavenge算法显然不科学。一来复制的对象太多会导致效率问题,二来需要浪费多一倍的空间。所以,V8在老生代中主要采用「Mark-Sweep」算法与「Mark-Compact」算法相结合的方式进行垃圾回收。

Mark-Sweep

Mark-Sweep是标记清除的意思,分为标记和清除两个阶段。在标记阶段遍历堆中的所有对象,并标记存活的对象,在随后的清除阶段中,只清除标记之外的对象。

Mark-Sweep在老生代空间中标记后的示意图

但是Mark-Sweep有一个很严重的问题,就是进行一次标记清除回收之后,内存会变得碎片化。如果需要分配一个大对象,这时候就无法完成分配了。这时候就该Mark-Compact出场了。

Mark-Compact

Mark-Compact是标记整理的意思,是在Mark-Sweep基础上演变而来。Mark-Compact在标记存活对象之后,在整理过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。

Mark-Compact完成标记并移动存活对象后的示意图

Incremental Marking

鉴于Node单线程的特性,V8每次垃圾回收的时候,都需要将应用逻辑暂停下来,待执行完垃圾回收后再恢复应用逻辑,被称为「全停顿」。在分代垃圾回收中,一次小垃圾回收只收集新生代,且存活对象也相对较少,即使全停顿也没有多大的影响。但是在老生代中,存活对象较多,垃圾回收的标记、清理、整理都需要长时间的停顿,这样会严重影响到系统的性能。
所以「增量标记 (Incrememtal Marking)」被提出来。它从标记阶段入手,将原本要一口气停顿完成的动作改为增量标记,拆分为许多小「步进」,每做完一「步进」就让JavaScript应用逻辑执行一小会,垃圾回收与应用逻辑这样交替执行直到标记阶段完成。

内存泄露排查的工具

node-heapdump

它允许对V8堆内存抓取快照,用于事后分析。
在程序中引入

var heapdump = require("node-heapdump");

之后可以通过向服务器发送SIGUSR2信号,让node-heapdump抓拍一份堆内存的快照:

$ kill -USR2 <pid>

这份抓拍的快照会默认存放在文件目录下,这是一份大JSON文件,可以通过Chrome的开发者工具打开查看。

Chrome Profile

node-memwatch

需要注意,node-memwatch只是支持到node v0.12.x为止,当使用更高的版本的时候,就会安装不上,这时候可以使用node-watch-next 替代,一摸一样的API。

不同于node-heapdump,它提供了两个事件监听器,用来提供内存泄露的以及垃圾回收的信息:

  1. stats事件:每次进行全堆回收时,会触发改时间,传递内存的统计信息
  2. leak事件:经过五次垃圾回收之后,内存仍没有被释放的对象,会触发leak事件,传递相关的信息。

node-profiler

node-profiler 是 alinode团队出品的一个与node-heapdump类似的抓取内存堆快照的工具,不同的是,node-profiler的实现不一样,使用起来更便捷。附上他们的教程:如何使用Node Profiler

alinode

alinode官方如似说:

alinode 是阿里云出品的 Node.js 应用服务解决方案,是一套基于社区 Node 改进的运行时环境和服务平台。在社区的基础上我们内建了强大的支持功能,帮助开发者迅速洞见性能细节,快速定位疑难杂症,直探问题根源。

以上内容参考自

A tour of V8: Garbage Collection
V8 之旅: 垃圾回收器
《深入浅出Node.js》

查看原文

白一梓 收藏了文章 · 2019-01-25

谈谈前端异常捕获与上报

关于

前言

Hello,大家好,又与大家见面了,这次给大家分享下前端异常监控中需要了解的异常捕获与上报机制的一些要点,同时包含了实战性质的参考代码和流程。

首先,我们为什么要进行异常捕获和上报呢?

正所谓百密一疏,一个经过了大量测试及联调的项目在有些时候还是会有十分隐蔽的bug存在,这种复杂而又不可预见性的问题唯有通过完善的监控机制才能有效的减少其带来的损失,因此对于直面用户的前端而言,异常捕获与上报是至关重要的。

虽然目前市面上已经有一些非常完善的前端监控系统存在,如sentrybugsnag等,但是知己知彼,才能百战不殆,唯有了解原理,摸清逻辑,使用起来才能得心应手。

异常捕获方法

1. try catch

通常,为了判断一段代码中是否存在异常,我们会这一写:

try {
    var a = 1;
    var b = a + c;
} catch (e) {
    // 捕获处理
    console.log(e); // ReferenceError: c is not defined
}

使用try catch能够很好的捕获异常并对应进行相应处理,不至于让页面挂掉,但是其存在一些弊端,比如需要在捕获异常的代码上进行包裹,会导致页面臃肿不堪,不适用于整个项目的异常捕获。

2. window.onerror

相比try catch来说window.onerror提供了全局监听异常的功能:

window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) {
    console.log('errorMessage: ' + errorMessage); // 异常信息
    console.log('scriptURI: ' + scriptURI); // 异常文件路径
    console.log('lineNo: ' + lineNo); // 异常行号
    console.log('columnNo: ' + columnNo); // 异常列号
    console.log('error: ' + error); // 异常堆栈信息
};

console.log(a);

如图:

window.onerror即提供了我们错误的信息,还提供了错误行列号,可以精准的进行定位,如此似乎正是我们想要的,但是接下来便是填坑过程。

异常捕获问题

1. Script error.

我们合乎情理地在本地页面进行尝试捕获异常,如:

<!-- http://localhost:3031/ -->
<script>
window.onerror = function() {
    console.log(arguments);
};
</script>
<script data-original="http://cdn.xxx.com/index.js"></script>

这里我们把静态资源放到异域上进行优化加载,但是捕获的异常信息却是:

经过分析发现,跨域之后window.onerror是无法捕获异常信息的,所以统一返回Script error.,解决方案便是script属性配置 crossorigin="anonymous" 并且服务器添加Access-Control-Allow-Origin。

<script data-original="http://cdn.xxx.com/index.js" crossorigin="anonymous"></script>

一般的CDN网站都会将Access-Control-Allow-Origin配置为*,意思是所有域都可以访问。

2. sourceMap

解决跨域或者将脚本存放在同域之后,你可能会将代码压缩一下再发布,这时候便出现了压缩后的代码无法找到原始报错位置的问题。如图,我们用webpack将代码打包压缩成bundle.js:

// webpack.config.js
var path = require('path');

// webpack 4.1.1
module.exports = {
    mode: 'development',
    entry: './client/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'client')
    }
}

最后我们页面引入的脚本文件是这样的:

!function(e){var o={};function n(r){if(o[r])return o[r].exports;var t=o[r]={i:r,l:!1,exports:{}}...;

所以我们看到的异常信息是这样的:

lineNo可能是一个非常小的数字,一般是1,而columnNo会是一个很大的数字,这里是730,因为所有代码都压缩到了一行。

那么该如何解决呢?聪明的童鞋可能已经猜到启用source-map了,没错,我们利用webpack打包压缩后生成一份对应脚本的map文件就能进行追踪了,在webpack中开启source-map功能:

module.exports = {
    ...
    devtool: '#source-map',
    ...
}

打包压缩的文件末尾会带上这样的注释:

!function(e){var o={};function n(r){if(o[r])return o[r].exports;var t=o[r]={i:r,l:!1,exports:{}}...;
//# sourceMappingURL=bundle.js.map

意思是该文件对应的map文件为bundle.js.map。下面便是一个source-map文件的内容,是一个JSON对象:

version: 3, // Source map的版本
sources: ["webpack:///webpack/bootstrap", ...], // 转换前的文件
names: ["installedModules", "__webpack_require__", ...], // 转换前的所有变量名和属性名
mappings: "aACA,IAAAA,KAGA,SAAAC...", // 记录位置信息的字符串
file: "bundle.js", // 转换后的文件名
sourcesContent: ["// The module cache var installedModules = {};..."], // 源代码
sourceRoot: "" // 转换前的文件所在的目录

如果你想详细了解关于sourceMap的知识,可以前往:JavaScript Source Map 详解

如此,既然我们拿到了对应脚本的map文件,那么我们该如何进行解析获取压缩前文件的异常信息呢?这个我会在下面异常上报的时候进行介绍。

3. MVVM框架

现在越来越多的项目开始使用前端框架,在MVVM框架中如果你一如既往的想使用window.onerror来捕获异常,那么很可能会竹篮打水一场空,或许根本捕获不到,因为你的异常信息被框架自身的异常机制捕获了。比如Vue 2.x中我们应该这样捕获全局异常

Vue.config.errorHandler = function (err, vm, info) {
    let { 
        message, // 异常信息
        name, // 异常名称
        script,  // 异常脚本url
        line,  // 异常行号
        column,  // 异常列号
        stack  // 异常堆栈信息
    } = err;
    
    // vm为抛出异常的 Vue 实例
    // info为 Vue 特定的错误信息,比如错误所在的生命周期钩子
}

目前script、line、column这3个信息打印出来是undefined,不过这些信息在stack中都可以找到,可以通过正则匹配去进行获取,然后进行上报。

同样的在react也提供了异常处理的方式,在 React 16.x 版本中引入了 Error Boundary:

class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props);
        this.state = { hasError: false };
    }

    componentDidCatch(error, info) {
        this.setState({ hasError: true });
        
        // 将异常信息上报给服务器
        logErrorToMyService(error, info); 
    }

    render() {
        if (this.state.hasError) {
            return '出错了';
        }
    
        return this.props.children;
    }
}

然后我们就可以这样使用该组件:

<ErrorBoundary>
    <MyWidget />
</ErrorBoundary>

详见官方文档:Error Handling in React 16

异常上报

以上介绍了前端异常捕获的相关知识点,那么接下来我们既然成功捕获了异常,那么该如何上报呢?

在脚本代码没有被压缩的情况下可以直接捕获后上传对应的异常信息,这里就不做介绍了,下面主要讲解常见的处理压缩文件上报的方法。

1. 提交异常

当捕获到异常时,我们可以将异常信息传递给接口,以window.onerror为例:

window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) {

    // 构建错误对象
    var errorObj = {
        errorMessage: errorMessage || null,
        scriptURI: scriptURI || null,
        lineNo: lineNo || null,
        columnNo: columnNo || null,
        stack: error && error.stack ? error.stack : null
    };

    if (XMLHttpRequest) {
        var xhr = new XMLHttpRequest();
    
        xhr.open('post', '/middleware/errorMsg', true); // 上报给node中间层处理
        xhr.setRequestHeader('Content-Type', 'application/json'); // 设置请求头
        xhr.send(JSON.stringify(errorObj)); // 发送参数
    }
}

2. sourceMap解析

其实source-map格式的文件是一种数据类型,既然是数据类型那么肯定有解析它的办法,目前市面上也有专门解析它的相应工具包,在浏览器环境或者node环境下比较流行的是一款叫做'source-map'的插件。

通过require该插件,前端浏览器可以对map文件进行解析,但因为前端解析速度较慢,所以这里不做推荐,我们还是使用服务器解析。如果你的应用有node中间层,那么你完全可以将异常信息提交到中间层,然后解析map文件后将数据传递给后台服务器,中间层代码如下:

const express = require('express');
const fs = require('fs');
const router = express.Router();
const fetch = require('node-fetch');
const sourceMap = require('source-map');
const path = require('path');
const resolve = file => path.resolve(__dirname, file);

// 定义post接口
router.post('/errorMsg/', function(req, res) {
    let error = req.body; // 获取前端传过来的报错对象
    let url = error.scriptURI; // 压缩文件路径

    if (url) {
        let fileUrl = url.slice(url.indexOf('client/')) + '.map'; // map文件路径

        // 解析sourceMap
        let smc = new sourceMap.SourceMapConsumer(fs.readFileSync(resolve('../' + fileUrl), 'utf8')); // 返回一个promise对象
        
        smc.then(function(result) {
        
            // 解析原始报错数据
            let ret = result.originalPositionFor({
                line: error.lineNo, // 压缩后的行号
                column: error.columnNo // 压缩后的列号
            });
            
            let url = ''; // 上报地址
        
            // 将异常上报至后台
            fetch(url, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    errorMessage: error.errorMessage, // 报错信息
                    source: ret.source, // 报错文件路径
                    line: ret.line, // 报错文件行号
                    column: ret.column, // 报错文件列号
                    stack: error.stack // 报错堆栈
                })
            }).then(function(response) {
                return response.json();
            }).then(function(json) {
                res.json(json);         
            });
        })
    }
});

module.exports = router;

这里我们通过前端传过来的异常文件路径获取服务器端map文件地址,然后将压缩后的行列号传递给sourceMap返回的promise对象进行解析,通过originalPositionFor方法我们能获取到原始的报错行列号和文件地址,最后通过ajax将需要的异常信息统一传递给后台存储,完成异常上报。下图可以看到控制台打印出了经过解析后的真是报错位置和文件:

附:source-map API

3. 注意点

以上是异常捕获和上报的主要知识点和流程,还有一些需要注意的地方,比如你的应用访问量很大,那么一个小异常都可能会把你的服务器搞挂,所以上报的时候可以进行信息过滤和采样等,设置一个调控开关,服务器也可以对相似的异常进行过滤,在一个时间段内不进行多次存储。另外window.onerror这样的异常捕获不能捕获promise的异常错误信息,这点需要注意。

最终大致的流程图如下:

结语

前端异常捕获与上报是前端异常监控的前提,了解并做好了异常数据的收集和分析才能实现一个完善的错误响应和处理机制,最终达成数据可视化。本文详细实例代码地址:https://github.com/luozhihao/error-catch-report

查看原文

白一梓 赞了文章 · 2019-01-25

谈谈前端异常捕获与上报

关于

前言

Hello,大家好,又与大家见面了,这次给大家分享下前端异常监控中需要了解的异常捕获与上报机制的一些要点,同时包含了实战性质的参考代码和流程。

首先,我们为什么要进行异常捕获和上报呢?

正所谓百密一疏,一个经过了大量测试及联调的项目在有些时候还是会有十分隐蔽的bug存在,这种复杂而又不可预见性的问题唯有通过完善的监控机制才能有效的减少其带来的损失,因此对于直面用户的前端而言,异常捕获与上报是至关重要的。

虽然目前市面上已经有一些非常完善的前端监控系统存在,如sentrybugsnag等,但是知己知彼,才能百战不殆,唯有了解原理,摸清逻辑,使用起来才能得心应手。

异常捕获方法

1. try catch

通常,为了判断一段代码中是否存在异常,我们会这一写:

try {
    var a = 1;
    var b = a + c;
} catch (e) {
    // 捕获处理
    console.log(e); // ReferenceError: c is not defined
}

使用try catch能够很好的捕获异常并对应进行相应处理,不至于让页面挂掉,但是其存在一些弊端,比如需要在捕获异常的代码上进行包裹,会导致页面臃肿不堪,不适用于整个项目的异常捕获。

2. window.onerror

相比try catch来说window.onerror提供了全局监听异常的功能:

window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) {
    console.log('errorMessage: ' + errorMessage); // 异常信息
    console.log('scriptURI: ' + scriptURI); // 异常文件路径
    console.log('lineNo: ' + lineNo); // 异常行号
    console.log('columnNo: ' + columnNo); // 异常列号
    console.log('error: ' + error); // 异常堆栈信息
};

console.log(a);

如图:

window.onerror即提供了我们错误的信息,还提供了错误行列号,可以精准的进行定位,如此似乎正是我们想要的,但是接下来便是填坑过程。

异常捕获问题

1. Script error.

我们合乎情理地在本地页面进行尝试捕获异常,如:

<!-- http://localhost:3031/ -->
<script>
window.onerror = function() {
    console.log(arguments);
};
</script>
<script data-original="http://cdn.xxx.com/index.js"></script>

这里我们把静态资源放到异域上进行优化加载,但是捕获的异常信息却是:

经过分析发现,跨域之后window.onerror是无法捕获异常信息的,所以统一返回Script error.,解决方案便是script属性配置 crossorigin="anonymous" 并且服务器添加Access-Control-Allow-Origin。

<script data-original="http://cdn.xxx.com/index.js" crossorigin="anonymous"></script>

一般的CDN网站都会将Access-Control-Allow-Origin配置为*,意思是所有域都可以访问。

2. sourceMap

解决跨域或者将脚本存放在同域之后,你可能会将代码压缩一下再发布,这时候便出现了压缩后的代码无法找到原始报错位置的问题。如图,我们用webpack将代码打包压缩成bundle.js:

// webpack.config.js
var path = require('path');

// webpack 4.1.1
module.exports = {
    mode: 'development',
    entry: './client/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'client')
    }
}

最后我们页面引入的脚本文件是这样的:

!function(e){var o={};function n(r){if(o[r])return o[r].exports;var t=o[r]={i:r,l:!1,exports:{}}...;

所以我们看到的异常信息是这样的:

lineNo可能是一个非常小的数字,一般是1,而columnNo会是一个很大的数字,这里是730,因为所有代码都压缩到了一行。

那么该如何解决呢?聪明的童鞋可能已经猜到启用source-map了,没错,我们利用webpack打包压缩后生成一份对应脚本的map文件就能进行追踪了,在webpack中开启source-map功能:

module.exports = {
    ...
    devtool: '#source-map',
    ...
}

打包压缩的文件末尾会带上这样的注释:

!function(e){var o={};function n(r){if(o[r])return o[r].exports;var t=o[r]={i:r,l:!1,exports:{}}...;
//# sourceMappingURL=bundle.js.map

意思是该文件对应的map文件为bundle.js.map。下面便是一个source-map文件的内容,是一个JSON对象:

version: 3, // Source map的版本
sources: ["webpack:///webpack/bootstrap", ...], // 转换前的文件
names: ["installedModules", "__webpack_require__", ...], // 转换前的所有变量名和属性名
mappings: "aACA,IAAAA,KAGA,SAAAC...", // 记录位置信息的字符串
file: "bundle.js", // 转换后的文件名
sourcesContent: ["// The module cache var installedModules = {};..."], // 源代码
sourceRoot: "" // 转换前的文件所在的目录

如果你想详细了解关于sourceMap的知识,可以前往:JavaScript Source Map 详解

如此,既然我们拿到了对应脚本的map文件,那么我们该如何进行解析获取压缩前文件的异常信息呢?这个我会在下面异常上报的时候进行介绍。

3. MVVM框架

现在越来越多的项目开始使用前端框架,在MVVM框架中如果你一如既往的想使用window.onerror来捕获异常,那么很可能会竹篮打水一场空,或许根本捕获不到,因为你的异常信息被框架自身的异常机制捕获了。比如Vue 2.x中我们应该这样捕获全局异常

Vue.config.errorHandler = function (err, vm, info) {
    let { 
        message, // 异常信息
        name, // 异常名称
        script,  // 异常脚本url
        line,  // 异常行号
        column,  // 异常列号
        stack  // 异常堆栈信息
    } = err;
    
    // vm为抛出异常的 Vue 实例
    // info为 Vue 特定的错误信息,比如错误所在的生命周期钩子
}

目前script、line、column这3个信息打印出来是undefined,不过这些信息在stack中都可以找到,可以通过正则匹配去进行获取,然后进行上报。

同样的在react也提供了异常处理的方式,在 React 16.x 版本中引入了 Error Boundary:

class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props);
        this.state = { hasError: false };
    }

    componentDidCatch(error, info) {
        this.setState({ hasError: true });
        
        // 将异常信息上报给服务器
        logErrorToMyService(error, info); 
    }

    render() {
        if (this.state.hasError) {
            return '出错了';
        }
    
        return this.props.children;
    }
}

然后我们就可以这样使用该组件:

<ErrorBoundary>
    <MyWidget />
</ErrorBoundary>

详见官方文档:Error Handling in React 16

异常上报

以上介绍了前端异常捕获的相关知识点,那么接下来我们既然成功捕获了异常,那么该如何上报呢?

在脚本代码没有被压缩的情况下可以直接捕获后上传对应的异常信息,这里就不做介绍了,下面主要讲解常见的处理压缩文件上报的方法。

1. 提交异常

当捕获到异常时,我们可以将异常信息传递给接口,以window.onerror为例:

window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) {

    // 构建错误对象
    var errorObj = {
        errorMessage: errorMessage || null,
        scriptURI: scriptURI || null,
        lineNo: lineNo || null,
        columnNo: columnNo || null,
        stack: error && error.stack ? error.stack : null
    };

    if (XMLHttpRequest) {
        var xhr = new XMLHttpRequest();
    
        xhr.open('post', '/middleware/errorMsg', true); // 上报给node中间层处理
        xhr.setRequestHeader('Content-Type', 'application/json'); // 设置请求头
        xhr.send(JSON.stringify(errorObj)); // 发送参数
    }
}

2. sourceMap解析

其实source-map格式的文件是一种数据类型,既然是数据类型那么肯定有解析它的办法,目前市面上也有专门解析它的相应工具包,在浏览器环境或者node环境下比较流行的是一款叫做'source-map'的插件。

通过require该插件,前端浏览器可以对map文件进行解析,但因为前端解析速度较慢,所以这里不做推荐,我们还是使用服务器解析。如果你的应用有node中间层,那么你完全可以将异常信息提交到中间层,然后解析map文件后将数据传递给后台服务器,中间层代码如下:

const express = require('express');
const fs = require('fs');
const router = express.Router();
const fetch = require('node-fetch');
const sourceMap = require('source-map');
const path = require('path');
const resolve = file => path.resolve(__dirname, file);

// 定义post接口
router.post('/errorMsg/', function(req, res) {
    let error = req.body; // 获取前端传过来的报错对象
    let url = error.scriptURI; // 压缩文件路径

    if (url) {
        let fileUrl = url.slice(url.indexOf('client/')) + '.map'; // map文件路径

        // 解析sourceMap
        let smc = new sourceMap.SourceMapConsumer(fs.readFileSync(resolve('../' + fileUrl), 'utf8')); // 返回一个promise对象
        
        smc.then(function(result) {
        
            // 解析原始报错数据
            let ret = result.originalPositionFor({
                line: error.lineNo, // 压缩后的行号
                column: error.columnNo // 压缩后的列号
            });
            
            let url = ''; // 上报地址
        
            // 将异常上报至后台
            fetch(url, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    errorMessage: error.errorMessage, // 报错信息
                    source: ret.source, // 报错文件路径
                    line: ret.line, // 报错文件行号
                    column: ret.column, // 报错文件列号
                    stack: error.stack // 报错堆栈
                })
            }).then(function(response) {
                return response.json();
            }).then(function(json) {
                res.json(json);         
            });
        })
    }
});

module.exports = router;

这里我们通过前端传过来的异常文件路径获取服务器端map文件地址,然后将压缩后的行列号传递给sourceMap返回的promise对象进行解析,通过originalPositionFor方法我们能获取到原始的报错行列号和文件地址,最后通过ajax将需要的异常信息统一传递给后台存储,完成异常上报。下图可以看到控制台打印出了经过解析后的真是报错位置和文件:

附:source-map API

3. 注意点

以上是异常捕获和上报的主要知识点和流程,还有一些需要注意的地方,比如你的应用访问量很大,那么一个小异常都可能会把你的服务器搞挂,所以上报的时候可以进行信息过滤和采样等,设置一个调控开关,服务器也可以对相似的异常进行过滤,在一个时间段内不进行多次存储。另外window.onerror这样的异常捕获不能捕获promise的异常错误信息,这点需要注意。

最终大致的流程图如下:

结语

前端异常捕获与上报是前端异常监控的前提,了解并做好了异常数据的收集和分析才能实现一个完善的错误响应和处理机制,最终达成数据可视化。本文详细实例代码地址:https://github.com/luozhihao/error-catch-report

查看原文

赞 107 收藏 278 评论 11

白一梓 收藏了文章 · 2019-01-17

监听微信返回事件踩坑指南

PC浏览器返回等于重新进入上一个页面,会触发刷新动作,而微信不会。也就是困扰我多时的微信返回不刷新。

大概再2017年初和2016末(大概也是从那个时候我开始做微信公众号),还可以通过在sessionStorage中记录刷新标志,让上一个页面根据标识刷新。也就是说当时微信返回还是会触发渲染事件的(具体是什么事件也不清楚,因为当时没有深究,但是确实是触发了componentDidMount)。

但是某个时刻起,这种方法也不再有效了,说明通过storage记录需要刷新标志是完全失效的了。

另外可以发现,上一个页面会保持上一次操作的状态,并且不会再有静态资源的请求,不会触发load事件。那也可以这么理解,在微信中的页面跳转,其实更类似于浏览器中的打开新标签页。所以上一个页面的内容没有被销掉,而是会保持你跳走前的状态。所以我们很多页面会有点击返回但是loading还是在转的现象。

尝试一:visibilitychange

由此,我想到了第一个检查他是否返回的方法——监听页面的visibilitychange事件。因为PC浏览器中如果标签切换或者是浏览器缩略,其可见性改变的时候,都会触发该事件。

有兴趣的可以打开控制台输入以下代码,看看有什么不同。

window.addEventListener('visibilitychange', function () {
  console.log(document.hidden)
});

总之我先尝试了以下代码:

let isPageBack = false; 

window.addEventListener('visibilitychange', function () {
  if(document.hiden){ 
     isPageBack = true
 } else if ( isPageBack ) {
     fetch('/data') //因为visibilitychange事件中alert可以看到被模拟器禁了,所以就改用改了fetch自己的接口,通过查看日志检查是否触发
 }
});

尝试之后发现该事件并没有被触发。疑惑之余,我尝试了chrome手机浏览器,发现同样,该事件没有被触发。

另外,因为好奇如果app压后台会不会触发该事件,所以尝试这段代码↓,结果发现即使压后台页面也不会被挂起。

setInterval(function () {
  var p = document.createElement('p');
  p.appendChild(document.createTextNode(`${Date.now()}`));
  document.body.appendChild(p);
}, 1000)

尝试二:pageshow & pagehide

与visibilitychange类似的还有pageshowpagehide事件。

pageshow事件触发点是 a session history entry is being traversed to. 同时根据MDN的介绍在back/forward时也会被触发

于是我改了改代码

let isPageBack = false;

window.addEventListener('pageshow', function () {
  if (isPageBack ) fetch('/data')
})

window.addEventListener('pagehide', function () {
  isPageBack = true
})

居然意外的能行,,,

pageshowpagehide事件可以被监听到。返回页可以通过页面是否隐藏过知道是否是返回回来的。

尝试三:history

history可以修改历史记录或url主要是 history.pushStatehistory.replaceState

使用pushState 等于多推一条历史记录,replaceState 等于修改了历史记录,另外我们要知道reload是不计入历史记录的。

理论上来说如果使用pushState修改url,那么页面访问就会像这样 A -> A1 -> B

当B返回A1时就会触发 popstate 事件。在popstate事件里面可以做一些自定义的事情。

这里用了代码

 var state = {
   date: Date.now()
 };
 window.history.pushState(state, 'csb');
 window.addEventListener('popstate', function (event) {
   if(event.state) location.reload()
 })

检查history时,可以看到state里面有一个key是date的时间戳,同时历史记录的长度+1。
但是使用pushState会增加历史记录,会导致同一个页面需要返回好几次才能退出去,不过可以利用他做返回退出公众号

window.history.pushState({}, 'csb');
window.addEventListener('popstate', function (event) {
  if (event.state) { 
    wx.ready(function () {
      wx.closeWindow();
    });
  }
});


但是因为replaceState不会增加历史记录,所以利用它这样返回刷新页面

history.replaceState(null, null, '#');
window.addEventListener('popstate', function (event) {
 self.location.reload();
})

另外如果要如果A->B->C,而C返回时想要直接返回A可以这样

B页面:

history.replaceState(null, null, '/c'); //将url替换成C,这样跳转到C页面等于被转变成了reload行为,但直观上来说,是我们删除了一条历史记录
查看原文

认证与成就

  • 获得 159 次点赞
  • 获得 141 枚徽章 获得 8 枚金徽章, 获得 52 枚银徽章, 获得 81 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2014-05-19
个人主页被 5.2k 人浏览