PWA(Progressive Web App)入门系列:Push

前言

很多时候,原生应用会通过一些消息推送来唤起用户的关注,增加驻留率。网页该怎么做呢?有没有类似原生应用的推送机制?推送功能又能玩出什么花样呢?


Push API

Push API 给与了 Web 应用程序接收从服务器发出的推送消息的能力,无论 Web 应用程序是否在用户设备前台,甚至刚加载完成。这样,开发人员就可以向用户投放异步通知和更新,从而让用户能更及时地获取新内容。

对 Web 应用来说,要想使用推送,必须在应用下的 ServiceWorker 处于激活状态,在 ServiceWorkerRegistration scope 下的 PushManager 来做推送订阅相关工作。

ServiceWorkerGlobalScope scope 下通过 onpush 来监听推送事件。

激活一个 service worker 来提供推送消息会导致资源消耗的增加,尤其是电池。不同的浏览器对此有不同的方案——目前为止还没有标准的机制。Firefox 允许对发送给应用的推送消息做数量限制(配额)。该限制会在站点每一次被访问之后刷新。相比之下,Chrome 选择不做限制,但要求站点在每一次消息到达后都显示通知,这样可以让用户确认他们仍希望接收消息并确保用户可见性。

接口

Push 的相关接口:

  • PushManager
  • PushEvent
  • PushMessageData
  • PushSubscription
  • PushSubscriptionOptions

PushManager

PushManager 接口用于操作推送订阅。

通过 ServiceWorkerRegistration.PushManager获取。

方法:

subscribe()

用于订阅推送服务。

返回一个 Promise 形式的 PushSubscription 对象,该对象包含了推送订阅详情。如果当前 service worker 没有已存在的订阅,则会创建一个新的推送订阅。

语法:

​PushManager.subscribe(options).then(function(pushSubscription) { ... } );

参数:

options:

  • userVisibleOnly:布尔值,表示返回的推送订阅将只能被用于对用户可见的消息。在订阅时必须把此项设置为 true,这样当有消息推送给用户时,浏览器会展示一个消息通知,也就是说不存在静默推送。为了让用户可知。
  • applicationServerKey:推送服务器用来向客户端应用发送消息的公钥。该值是应用程序服务器生成的签名密钥对的一部分,可使用在 P-256 曲线上实现的椭圆曲线数字签名(ECDSA)。这里使用的是 VAPID 协议,VAPID 是 Voluntary Application Server Identification (自主应用服务器标识) 的简称。所以需要将 Base64 的公钥转为 Uint8 的数组。

触发推送时,浏览器的表现:

Base64 转 Uint8

function base64UrlToUint8Array(base64UrlData) {const padding = '='.repeat((4 - base64UrlData.length % 4) % 4);const base64 = (base64UrlData + padding).replace(/\-/g, '+').replace(/_/g, '/');const rawData = window.atob(base64);const buffer = new Uint8Array(rawData.length);for (let i = 0; i < rawData.length; ++i) {buffer[i] = rawData.charCodeAt(i);}return buffer;
}

getSubscription()

用于获取订阅对象 PushSubscription。

它返回一个 Promise 用来处理一个包含已经发布的分支的细节的PushSubscription 对象。如果没有已经发布的分支存在,返回null。

语法:

​PushManager.getSubscription().then(function(pushSubscription) { ... } );

permissionState()

用于获取 PushManager 的权限状态。

语法:

PushManager.permissionState(options).then(function(PushMessagingState) { ... });

参数:

options:

  • userVisibleOnly
  • applicationServerKey

返回 Promise,如下值:

  • granted:WEB 应用已授权 Push 权限。
  • denied:WEB 应用已拒绝 Push 权限。
  • prompt:WEB 应用未授权 Push 权限。

如下使用:

ServiceWorkerRegistration.pushManager.permissionState({userVisibleOnly: true})

PushEvent

Push API 接收消息时的事件。此事件在 ServiceWorkerGlobalScope 下响应。

属性

data:返回对 PushMessageData 类型,包含发送到的数据的对象。

PushMessageData

此接口为 PushEvent.data 中的类型。

与 Fetch 中 Body 的方法相似,不同处再于可以重复调用。

方法

  • arrayBuffer()
  • blob()
  • json()
  • text()

PushSubscription

PushSubscription 为 PushManager.subscribe() 的订阅信息类型。

属性

  • endpoint:包含订阅相关的推送服务器的信息。以 URL 形式展示。最好对于这个 URL 安全,防止被其他人劫持它并滥用推送功能。
  • expirationTime:返回与推送订阅关联的订阅到期时间(如果有),否则返回null。
  • options:PushSubscriptionOptions 类型,订阅时的 options 信息,包含:
    • applicationServerKey
    • userVisibleOnly

方法

getKey()

用于获取 PushSubscription 中订阅的公钥信息,返回 ArrayBuffer。

语法:

​var key = subscription.getKey(name);

参数:

name:

  • p256dh:P-256曲线上的椭圆曲线Diffie-Hellman公钥(即NIST secp256r1椭圆曲线)。 生成的密钥是ANSI X9.62格式的未压缩点。
  • auth:身份验证密钥,Web推送的加密描述。

toJSON()

序列化 PushSubscription 对象,用于存储和发送给应用服务器。

subscription.toJSON()

返回如下结构:

{endpoint: "https://fcm.googleapis.com/fcm/send/xxx:zzzzzzzzz"expirationTime: nullkeys: {auth: "xxxx-zzzz"p256dh: "BasdfasdfasdfasdffsdafasdfFMRs"}
}

unsubscribe()

用于取消订阅推送服务。

语法:

​PushSubscription.unsubscribe().then(function(Boolean) { ... });

返回 Promise 的 Boolean。如果 true,则退订成功。

接口间的关系

相关属性、方法:

Push 相关事件

Push API 通过下面的 serviceWorker 事件来监控并响应推送和订阅更改事件。

onpush

当 ServiceWorker 收到 Push-Server 推送的消息时,就会触发 ServiceWorkerGlobalScope 接口的 onpush 事件。

语法:

ServiceWorkerGlobalScope.onpush = function(PushEvent) { ... }
self.addEventListener('push', function(PushEvent) { ... })

通过 PushEvent.data 来获取 PushMessageData 类型的推送消息中的数据。

onpushsubscriptionchange

当订阅信息发生改变时,会触发 ServiceWorkerGlobalScope 接口的 onpushsubscriptionchange 事件,例如:如果推送服务器设置了订阅到期时间,则可能会触发此事件。(正常订阅/退订时不会触发此事件)

发生此事件时,通常需要重新订阅推送服务器,并把新的订阅体发送给应用服务器。

语法:

ServiceWorkerGlobalScope.onpushsubscriptionchange = function() { ... }
self.addEventListener('pushsubscriptionchange', function() { ... })

订阅原理

浏览器端订阅:

浏览器端在订阅 Push Server 时,必须 Notification 是授权的,否则会出现授权窗口,这里的授权交互和 Notification 的授权是一样的。

注意:Notificatino 的授权状态手动调整改变后,订阅体将失效,需要重新订阅。

注意:目前大部分国内网络环境无法访问 Chrome 的 FCM 推送服务器,所以在不出海的网络环境下浏览器无法完成订阅。FireFox 的推送服务器不存在此问题,所以可以在 FireFox 下测试此功能。

// 浏览器订阅
navigator.serviceWorker.ready.then(swReg => {swReg.pushManager.subscribe({userVisibleOnly: true,applicationServerKey: urlB64ToUint8Array("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")}).then(pushSubscription => {// 将订阅信息发送到你的应用服务器fetch("https://你的应用服务器", {method: "post",body: JSON.stringify(pushSubscription.toJSON())});}).catch(e => {console.log('订阅失败', e)console.log('授权状态:' + await self.registration.pushManager.permissionState({userVisibleOnly:true}))});
});

关于推送请求问题,需要使用 VAPID 协议。

订阅时applicationServerKey 使用 VAPID 公钥作为识别标示,规范中要求公钥需要 UInt8 类型,所以订阅前要进行类型转换。

应用服务器端发送:

应用服务器从数据库里取出你的订阅信息,然后根据 Web Push 协议要求,对要发送的消息进行拼装和加密,然后发送给相应的 Push 服务器,然后 Push 服务器再根据订阅信息中的标志发送给相应的终端。

设备端接收:

浏览器端收到推送消息后,会激活相应的 ServiceWorker 线程,并触发 Push 事件。

例如收到消息后,展示一个 Notification,或者做任何其他的事:

// serviceWorker 环境下
self.addEventListener("push", function(event) {// 此处可以做任何事console.log("push", event);var data = event.data.json();if (!(self.Notification && self.Notification.permission === "granted")) {return;}self.registration.showNotification(data.title, {body: data.body});
});

详细执行过程

加密认证

浏览器订阅

subscribe() 方法中的 applicationServerKey 选项用于推送服务器鉴别订阅用户的应用服务,并用确保推送消息发送给哪个订阅用户。

applicationServerKey 是一对公私钥。私钥应用服务器保存,公钥交给浏览器,浏览器订阅时将这个公钥传给推送服务器,这样推送服务器可以将你的公钥和用户的 PushSubscription 绑定。

你的服务器发送

当你的服务器要发送推送消息时,需要创建一个 Authorization 的 header 头,Authorization 由规范要求的加密算法进行私钥加密。推送消息收到消息时,首先取消息请求中 endpoint 对应的公钥,解码消息请求中签名过的 Authorization header 头,验证签名是否合法,防止它人伪造身份。通过后,推送服务器把消息发送到相应的设备浏览器。

注:这里说的 applicationServerKey 就是 VAPID key。

Authorization 的签名采用 JWT(JSON web token),JWT 是一种向第三方发送消息的方式,三方收到后,获取发送者的公钥进行验证 JWT 的签名。

JWT 结构:

JWT 信息和 JWT 数据需要使用 base64 编码,所以内容是公开的。

JWT 信息部分必须包含:

{  "typ": "JWT",  "alg": "ES256"  
}

说明此签名用的哪种算法。

JWT 数据部分,提供有关 JWT 的发送者、目标用户及有效时间等信息。

{  "aud": "https://xxx.push-server.com","exp": "1469632224","sub": "mailto:xxx@contact.com"  
}
  • aud:推送服务器的地址。
  • exp:签名过期时间,单位秒,必须不大于 24 小时。
  • sub:必须是 URL 或者 邮箱地址。用于推送服务器联系发送人。

JWT 签名部分,是取 JWT 信息部分和 JWT 数据部分的字符串拼接结果,中间用.连接,生成未签名的令牌,然后进行签名生成的。

签名是基于应用服务器生成的 VAPID 私钥进行加密的,nodejs 可以使用 jws 库来签名:

const jws = require('jws');
const asn1 = require('asn1.js');const header = {typ: 'JWT',alg: 'ES256'
};const jwtPayload = {aud: audience,exp: expiration,sub: subject
};const jwt = jws.sign({header: header,payload: jwtPayload,privateKey: toPEM(privateKey)
});function toPEM(key) {return asn1.define("ECPrivateKey", function() {this.seq().obj(this.key("version").int(),this.key("privateKey").octstr(),this.key("parameters").explicit(0).objid().optional(),this.key("publicKey").explicit(1).bitstr().optional());}).encode({version: 1,privateKey: key,parameters: [1, 2, 840, 10045, 3, 1, 7] // prime256v1},"pem",{label: "EC PRIVATE KEY"});
}

Authorization 对 JWT 签名的格式要求:

Authorization: 'WebPush <JWT Info>.<JWT Data>.<Signature>'

在签名的前面加上 WebPush 作为 Authorization 头的值发送给推送服务器。

推送协议同时要求Crypto-Key header 头,用来发送公钥,并需要p256ecdsa=前缀,格式:

Crypto-Key: p256ecdsa=<URL Safe Base64 Public Application Server Key>

关于消息部分的加密

发送的消息部分,也就是 payload,为了保证安全性,协议里同样要求需要加密,且推送服务器无法解密,只有浏览器才能解密消息数据。

在浏览器向推送服务器进行订阅后产生的订阅体,在这里就用的上了,再看下结构:

{endpoint: "https://fcm.googleapis.com/fcm/send/xxx:zzzzzzzzz"expirationTime: nullkeys: {auth: "xxxx-zzzz"p256dh: "BasdfasdfasdfasdffsdafasdfFMRs"}
}

结构中的 keys 字段就是浏览器端的密钥信息,由浏览器生成。

加密需要 authp256dhpayload 三个值做为输入进行加密,加密过程比较复杂。

可以看一下,生成的具体要发送给推送服务器的字段,下面是 FCM 的请求:

{'hostname': "fcm.googleapis.com",'port': null,'path':"/fcm/send/xxx-xx:APA91bFzxDp-j-xoN_kxqzie3uJS1aSNI5wI4SXL34dLWPFFa3QSZVBOE6eG7b4tb2RIvqUy3d3ww57In2lFsZW5MVsjQRtPFfbKoq9XqqrsTwRZiPDbPcbwZ4vkmv_1lnIHRo5yOxQF",'headers': {'TTL': 3600,"Content-Length": 224,"Content-Type": "application/octet-stream","Content-Encoding": "aesgcm",'Encryption': "salt=lIiVReih7lcahHxS2UhENA","Crypto-Key":"dh=BG9SmS2AixNf9UgRlOr1aEiVQMH5h47cAz0FW-_m9MRiwLqrUUP9DhrbFGXqaHAYh12IyKtvySbnDYNmF3Mh0d0;p256ecdsa=BDTgN25YAAabqE6ANPP49d2EkoLAMxT4xDZxE5BdrCHPyq1zk36LofZ2M3DYosxZzSG7i_26S1ViOGC_rBifW_U",'Authorization':"WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL2ZjbS5nb29nbGVhcGlzLmNvbSIsImV4cCI6MTU1OTA3ODEwOSwic3ViIjoiaHR0cHM6Ly9kZXZlbG9wZXJzLmdvb2dsZS5jb20vd2ViL2Z1bmRhbWVudGFscy8ifQ.Fa3nW6Lt7cp2dGML71aZItdyIcEabZ4GRVtkQBc3dWavAGH3_xSh0jnT-Cy8vGHJrwwRSRKaOcbt-uniIYt6fA"},'method': "POST"
};

VAPID key 生成

密钥使用 ECDSA(椭圆曲线迪菲-赫尔曼金钥交换)的 ES256 算法(ECDSA使用 P-256 曲线和 SHA-256 哈希算法的缩写)。

基于 node 实现:

$ npm install -g web-push
$ web-push generate-vapid-keys

基于浏览器 JS 实现:

function generateNewKeys() {return crypto.subtle.generateKey({name: 'ECDH', namedCurve: 'P-256'},true, ['deriveBits']).then((keys) => {return cryptoKeyToUrlBase64(keys.publicKey, keys.privateKey);});
}function cryptoKeyToUrlBase64(publicKey, privateKey) {const promises = [];promises.push(crypto.subtle.exportKey('jwk', publicKey).then((jwk) => {const x = base64UrlToUint8Array(jwk.x);const y = base64UrlToUint8Array(jwk.y);const publicKey = new Uint8Array(65);publicKey.set([0x04], 0);publicKey.set(x, 1);publicKey.set(y, 33);return publicKey;}));promises.push(crypto.subtle.exportKey('jwk', privateKey).then((jwk) => {return base64UrlToUint8Array(jwk.d);}));return Promise.all(promises).then((exportedKeys) => {return {public: uint8ArrayToBase64Url(exportedKeys[0]),private: uint8ArrayToBase64Url(exportedKeys[1]),};});
}function base64UrlToUint8Array(base64UrlData) {const padding = '='.repeat((4 - base64UrlData.length % 4) % 4);const base64 = (base64UrlData + padding).replace(/\-/g, '+').replace(/_/g, '/');const rawData = window.atob(base64);const buffer = new Uint8Array(rawData.length);for (let i = 0; i < rawData.length; ++i) {buffer[i] = rawData.charCodeAt(i);}return buffer;
}function uint8ArrayToBase64Url(uint8Array, start, end) {start = start || 0;end = end || uint8Array.byteLength;const base64 = window.btoa(String.fromCharCode.apply(null, uint8Array.subarray(start, end)));return base64.replace(/\=/g, '') // eslint-disable-line no-useless-escape.replace(/\+/g, '-').replace(/\//g, '_');
}

应用服务器端实现

这里用 node 来实现一下应用服务器向推送服务器发送消息。(其他语言环境可以参考 web-push-libs)

const webpush = require("web-push");const options = {vapidDetails: {subject: "mail@you.com", // 你的联系邮箱publicKey: "公钥",privateKey: "私钥"},TTL: 60 * 60 // 有效时间,单位秒
};const subscription = db.getUser("xxx"); // 从数据库取用户的订阅对象
const payload = {// 要发送的消息msg: "hellow"
};// 发送消息到推送服务器
webpush.sendNotification(subscription, payload, options).then(() => {}).catch(err => {// err.statusCode});

基于 web-push-libs 这种封装好的库工具用起来很方便,几行代码就可以实现应用服务器到推送服务器之间的数据请求。

推送服务器的相应状态码

状态码描述
201创建,收到并接受发送推送消息的请求
429请求过多,意味着应用程序服务器已经达到了推送服务的速率限制。推送服务会包括 Retry-After 标头,来指示在下一个请求发出之前等多长时间
400无效的请求,这通常意味着存在无效的 header 或格式不正确
404未找到,这表示订阅已过期且无法使用。在这种情况下,你应该删除 PushSubscription 并等待客户端重新订阅用户
410被移除,订阅不再有效,应从应用程序服务器中删除。可以通过在 PushSubscription 上调用 unsubscribe() 来重现
413有效负载过大,一个推送服务支持的最小的有效负载大小是 4096 bytes (或者 4kb)

常见问题

1. 浏览器关闭可否收到推送?

Android 系统:

Android 系统的消息机制是系统级的,系统有单独的进程去监听推送消息,收到消息就会唤醒对应的应用程序来处理这个推送消息,无论应用是否关闭。所有应用都采用这种处理方式。所以当收到浏览器的推送消息时,会唤醒浏览器,然后浏览器再去激活相应 的 ServiceWorker 线程,然后触发推送事件。

MAC 系统:

MAC 系统下当打开应用后,默认关闭应用实际上还在后台运行,可以通过 dock 来查看:

可以看到未完全关闭的应用下面会有一个黑点来标志,在这种情况下,浏览器是可以收到推送消息的。

如果浏览器完全关闭,则当在浏览器打开后,浏览器同样会收到通知消息(TTL 有效时间内)。

Windows 系统:

Windows 系统和 MAC 相似,但判断浏览器是否在后台运行比较复杂。

2. 对于消息推送如何在浏览器上调试查看?

Chrome 环境下,地址栏输入chrome://gcm-internals/,并点击Start Recording按钮进行录制。

通常来说,主要有两方面的问题:

  • 发送消息时的问题:
    • 授权问题
    • HTTP 状态码错误问题
  • 接收消息时的问题:
    • payload 加密问题
    • 连接问题

如果通过上述工具解决不了问题,可以提交问题到官方:

  • Chrome bugs: https://bugs.chromium.org/p/chromium/issues/list
  • Firefox bugs: https://bugzilla.mozilla.org/home

3. 为什么 Push 比 Web Sockets 好?

Push 是工作在 serviceWorker 线程下的,所以不关系浏览器窗口是否打开。而 Web Sockets 必须保证浏览器和网页处于打开状态才能正常工作。

4. 国内服务器无法与 FCM/GCM 推送服务器通讯,怎么办?

关于这一点,可以在国内服务器对消息通讯的请求上部署代理服务器,如在 node 环境下用 web-push 库可以这么写:

webpush.sendNotification(subscription,data,{... options,proxy: '代理地址'}
)

或者可以基于三方的推送工具来实现,如:onesignal。


工具

  • 推送加密验证工具:地址
  • Mozilla 推送数据加密测试页:地址
  • Google Codelab 推送工具:地址
  • JWT 验证:地址

兼容性


博客名称:王乐平博客

CSDN博客地址:http://blog.csdn.net/lecepin

知识共享许可协议
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/401630.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Dom学习笔记

DOM document object model 文档 对象 模型 文档&#xff1a;html页面 文档对象&#xff1a;页面中的元素 文档对象模型: 文档对象模型是w3c 为了能够让js去操作页面中的元素&#xff0c;定义的一套标准 DOM会把当前文档看作一棵树 树种的每一个元素就是文档树 的一个节点 同…

PWA(Progressive Web App)入门系列:消息通讯

前言 serviceWorker 的能力决定它要处理的事情&#xff0c;网站页面的部分逻辑处理会转移到 serviceWorker 层进行处理&#xff0c;这里就要页面层和 serviceWorker 层进行交互来实现消息通讯。 下面就说一下两个环境下的消息通讯。 窗口向 serviceWorker 通讯 这里列举出窗…

查看Linux上程序或进程用到的库

为什么80%的码农都做不了架构师&#xff1f;>>> ldd /path/to/program 要找出某个特定可执行依赖的库&#xff0c;可以使用ldd命令。这个命令调用动态链接器去找到程序的库文件依赖关系。 objdump -p /path/to/program | grep NEEDED 注意&#xff01;并不推荐为任…

超方便的 IndexDB 库

前言 做为 Web 浏览器层的本地存储&#xff0c;IndexDB 做为一个很好的选择&#xff0c;几乎可以存储任意类型的数据&#xff0c;且是异步的。但是正常使用方式下需要在监听各种事件来处理结果&#xff0c;不是很方便&#xff0c;下面就对这一层进行了包装&#xff0c;使用方便…

BP网络识别26个英文字母matlab

wx供重浩&#xff1a;创享日记 对话框发送&#xff1a;字母识别 获取完整源码源工程文件 一、 设计思想 字符识别在现代日常生活的应用越来越广泛&#xff0c;比如车辆牌照自动识别系统&#xff0c;手写识别系统&#xff0c;办公自动化等等。本文采用BP网络对26个英文字母进行…

PWA(Progressive Web App)入门系列:Sync 后台同步

前言 当我们在一些地下停车场&#xff0c;或者在火车上、电梯等无法避免的信号不稳定的场所&#xff0c;使用网站应用处理一些表单操作或者上传数据的操作时&#xff0c;面临的将是网络连接错误的响应&#xff0c;使用户的操作白费。 而此刻 PWA 的 Sync API 就很好的解决了这…

PWA(Progressive Web App)入门系列:安装 Web 应用

前言 在传统的 Web 应用中&#xff0c;通常只能通过在浏览器的地址栏里输入相应的网址才能进行访问&#xff0c;或者把网页地址创建到桌面上通过点击&#xff0c;然后在浏览器里打开。 传统模式下&#xff0c;图标、启动画面、主题色、视图模式、屏幕方向等等都无法去自定义和…

「工具」IndexDB 版备忘录

前言 工作日常需要做一些备忘录&#xff0c;记录一些要做的事。在 Mac 上有使用系统的备忘录&#xff0c;但功能偏弱且文本格式调整不方便。再就是使用浏览器找专门的备忘录网站&#xff0c;功能是满足了&#xff0c;但是链路长&#xff0c;没有桌面软件直接。 所以最后干脆自…

「工具」PWA Manifest图标及 favicon.ico 生成工具

PWA 其中有一个能力就是把网站安装到系统桌面&#xff0c;以原生应用的体验来运行网站&#xff0c;使用户无需再找开浏览器输入网址进入网站&#xff0c;而是可以直接点击安装好的应用直接运行&#xff0c;给使网站访问缩短路径及增加网站的曝光度。 其中有一个问题就是需要生…

各种浏览器缓存浅析

前言 目前浏览器的缓存类型众多&#xff0c;HTTP Cache、Disk Cache、Memory Cache、ServiceWorker Cache、Push Cache 等等&#xff0c;这些缓存是如何产生的&#xff1f;命中优先级是怎么样&#xff1f;又该如何去使用它们&#xff1f; Disk Cache、Memory Cache Disk Cac…

[工具]TS 视频合并工具

简介 当下载 m3u8 资源时&#xff0c;通常产生的是多个 ts 视频文件&#xff0c;所以需要借助某些工具来将这些 ts 视频片段整合为一个视频文件。 本软件主要解决的就是这个问题&#xff0c;底层基于ffmpeg&#xff0c;可正常运行在 Windows 和 Mac 平台。不仅可以合并 ts 为单…

[会议分享]2020全球软件大会分享-PWA在项目中的最佳实践

大会地址&#xff1a;https://www.bagevent.com/event/1233659# PPT下载&#xff1a;https://download.csdn.net/download/lecepin/12871373

「浏览器插件」非常好用的JSON-View

Chrome 商店&#xff1a;地址 下载地址&#xff1a;地址 Github&#xff1a;https://github.com/lecepin/lp-json-view 查看/格式化 二合一。 功能 自动识别 JSON 内容&#xff0c;并在页面右下角创建切换按钮。支持展开/折叠节点。支持全部展开、全部折叠、展开一二三层节点…

Github Action 快速构建 Electron 应用

前言 在开发 Electron 应用时&#xff0c;比较耗时的部分应该是构建打包的过程&#xff0c;像用 electron-builder 这种打包工具来说&#xff0c;它会根据你要打包的系统来下载应用的系统镜像打包工具&#xff0c;由于这些镜像的源文件托管在 Github 上&#xff0c;且 nodejs …

「浏览器插件」网址小尾巴终结者

前言 在我们日常的开发调试中&#xff0c;会在 URL 上添加一些特殊的小尾巴 用来显示调试界面或者开启一些特殊功能&#xff0c;当你接触了越来越多的系统后&#xff0c;你需要使用的小尾巴就变得越来越多&#xff0c;记忆和使用成本非常大&#xff0c;以及含有小尾巴的网址 在…

什么是低代码?

低代码 是一种软件开发方法&#xff0c;可以减少手工编码的过程&#xff0c;尽可能快的交付应用程序。 低代码平台 是工具的合集&#xff0c;这些工具可以通过建模和图形界面来进行应用程序的可视化开发。低代码使开发人员可以跳过手工编码&#xff0c;从而加快了应用程序的开…

什么是 LOW-CODE ?

低代码平台的特征 可视化建模工具 使用可视化方法和模型创建应用程序比使用代码进行开发要快。具有可视化建模功能的低代码平台&#xff0c;使用内置的组件&#xff0c;以任何人都可读的形式表示任何信息&#xff0c;从没有技术技能的常规企业用户到专业开发人员。 开箱即用…

简简单单 上传下载

背景 我们经常会有手机向电脑传文件 或者 电脑像手机传文件的需求。 而通常的解决方案是&#xff1a;手机上安装一个聊天软件&#xff08;如微信&#xff09;&#xff0c;电脑上安装一个聊天软件&#xff0c;然后自己给自己传输&#xff0c;然后进行下载。 这种方式是很麻烦的…

「VSCode插件」提效工具 - 快捷面板

前言 做为一个开发人员&#xff0c;在日常的开发过程中&#xff0c;经常会在 Terminal 中输入各种命令&#xff0c;如&#xff1a;npm i、npm start、git init、rm -rf、node_modules 等命令&#xff0c;看似方便&#xff0c;实则高频率的输入很烦人。那有什么方法可以把这些高…

VSCode摸鱼插件 — FreeWindow

背景 在一些不是很忙的时候&#xff0c;想高效利用下时间&#xff0c;看看书丰富一下自己&#xff0c;但是大庭广众下长时间看一本实体书&#xff0c;或者看手机的电子书&#xff0c;或者在电脑上看网页书都不太合适&#xff0c;显得自己很闲的样子&#xff0c;那该如何看起来…