最近在研究pwa(progressive web application),所以翻看了一些资料,发现有一篇文章写的是推送通知的,写得真心不错。我希望这篇文章能有更多人看到,我就把这篇文章翻译了一下。然后加上一些自己的个人见解(我的基于这个改的demo地址https://github.com/Chasen-Zhang/push-demo),如果你英文还行,就不要看我的了,直接看原文。原文链接是(http://thihara.github.io/Web-Push/);
注(很重要):本示例代码可以在火狐中运行,但是在谷歌中仍然无反应,后来查找资料是因为国内访问被墙,需要。之后,客户端可以订阅。但是服务器不能推送,原因应该也是网络原因,在google Chrome中当推送服务时,后台node服务会报这样的错误:
{ Error: connect ETIMEDOUT 216.58.200.234:443
at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1117:14)
errno: 'ETIMEDOUT',
code: 'ETIMEDOUT',
syscall: 'connect',
address: '216.58.200.234',
port: 443 } 'error'
详情请参考:https://github.com/web-push-libs/web-push/issues/280以及https://www.cnblogs.com/hellohello/p/8441188.html。谷歌走的是FCM模式。而火狐走的是不一样的。我们从浏览器生成的endpoint便可以知道。
谷歌的endpoint像这样:
https://fcm.googleapis.com/fcm/send/dgETprOAWaA:AP…X3DYH8MkmKn2pXWIbVJi5ABjlVNkiKARhih89-xFaGp4O6v76;
而火狐的类似这样:https://updates.push.services.mozilla.com/wpush/v2/gAAAAABcH1HEGZP1wcK8fzFx956YoVUpGtwMmN879K-RfLnLlwg6gWve9zl0mRWs-kBrzgbWZ9mC2yzqiu-0Uu5b1NH07jU8LUU1iOPD2hK6vqUmK_fjym0umm
可想而知实现方式不一样。至于看到很多通知消息在谷歌上也是可以的,为什么呢?也许国内的网站并没有通过FCM。个人之见!
国内有些APP使用小米的Push服务,有些使用百度的,还有些使用腾讯的信鸽等等,这些Push都需要在后台运行线程,并且不能休眠,这就导致了手机在休眠状态时仍然有很多线程在运行着,使得手机耗电速度很快。最后还直接导致今年工信部出台要成立安卓统一推送联盟。而苹果有一套统一的推送机制,大家把Push发给苹果的服务器,然后再由苹果下发给相应的苹果设备。Safari现在不支持Service Worker,但是可以用Apple Push,缺点是这种推送苹果说不能用来发送重要的数据,并且目测只能弹框显示,没办法在后台处理消息而不弹框。(https://juejin.im/post/59d9b38ef265da064a0f72cc)
我画的图(请放大观看,或者另存为),便于自己理解消息推送:
以下是原文翻译。
#Web 推送通知
推送通知已经很容易地为移动平台提供了相当长的一段时间。但是推送通知对于web技术来说任然是一个新技术,并且还没有广泛使用。在我写这个博客的时候,只有Chrome,火狐和欧朋浏览器支持推送通知。Safari通过适当的API可以实现推送通知但是超出了本文的写作范畴。
因此,让我们来看看在支持的浏览器中是如何实现推送通知的。
#Servce Worker
在研究推送通知之前,了解service Workers的概念是很重要的(以下简称SW)。SW是运行在浏览器后台的的脚本,不管它们所属的站点当前是打开的还是关闭的。这些脚本可以做很多事情,但是我们的重点是SW如何帮助我们显示推送通知的。
#高级概述
在研究代码之前,我们来看看推送通知是怎么样在非常高的级别上工作的。严格地讲,从实现的角度来看就,有三个相关的组件。
1.浏览器
浏览器提供了检测和显示推送通知的机制,它还将提供我们可以用来加密消息的加密密钥,以及我们可以向其提交推送消息的端点。
2.后台服务
在这里,我们将用来存储推送订阅数据(解密秘钥和端点endPoint),并将消息提交到给定的推送通知端点(客户端)。
3.浏览器提供端点(endpoint)信息
这是有浏览器拥有的发送到服务器(通过ajax)由服务器维护的(主要是存储和删除)端点。我们将把推送消息提交到这个端点。
当我们开始实现推送通知时,我们将详细介绍
#加密技术概述
如果您已经熟悉非对称或公钥加密,可以跳过本节,如果不熟悉,请继续阅读。
可逆密码学(即加密消息可以解密)分为两类。
1、对称密钥密码学,又称私钥密码学。
2、非对称密钥密码学,又称公钥密码学。
做一个比喻。我们将加密一个文件看做锁上门。对称密钥密码学中,只有一个密钥可用来锁门和开锁。用来锁门(加密文件)的密钥必须是用来打开门(解密文件)的密钥。这个密钥称为私钥。如果你丢了那把钥匙,你就不能开门。如果有人复制了你的钥匙,或者偷了你的钥匙,他们也可以用它来开门。问题是这把钥匙必须由锁门的人和开锁的人同时保管,这样就增加了有人偷钥匙的风险。
然而,在非对称密钥加密中,有两个密钥。一个锁门,另一个开门。锁门的钥匙叫做公钥。谁有那把钥匙的复印件并不重要,因为它只能锁上门,永远打不开。你可以把公钥给你的邻居和他们的狗,而不受任何后果。能打开门的钥匙叫做(你猜对了!)私钥。它必须保持安全。这里的优点是我们可以在没有任何风险的情况下分发公钥,我们只需要在一个位置保护私钥。
当我们将推送消息提交到服务器时,我们使用非对称密钥加密来加密推送消息。
#实现
让我们看看如何为web实现推送通知。您首先需要拥有安装npm和Node。安装说明非常清楚和直接。我们将使用node.js和web-push开发一个demo。
继续,创建一个名为push-notifications的目录,或者您想给这个项目起的任何其他名称,并使用npm初始化它。
npm init
现在继续安装web-push库,稍后我们将需要它来生成一对加密密钥
npm install web-push --save
#客户端
对于客户端,我们需要三个核心文件。一个javascript文件,检查是否支持推送通知和SW、注册SW以及发送订阅信息到后台服务。当有推送信息时,SW会被触发以及显示一条推送通知。一个简单的html文件用来加载main.js,其他的文件只是锦上添花,可有可无。
我们的客户端页面只是一个简单的页面有一个简单的按钮,这个按钮是给你订阅和取消订阅信息用的,如果推送通知由于某种原因不支持,我们将会显示一条不支持的信息并且disable这个按钮。
现在看看这个文件长什么样。
HTML文件
Push Notification Demo
Push Notification Demo
我们的重点将是main.js文件。稍后我们详细看看这个文件如何写。
#VAPID
我们先讲讲VAPID。VAPID 是Voluntary Application Server Identification(服务器唯一标识)的简写。这是一种机制,它允许推送通知端点的服务器通过签名JWT(JSON web token)标识您。
web推送通知规范没有解决任何类型的强制身份验证机制的需求。VAPID完全是自愿的,但是在现实中,目前只有Firefox提供没有任何身份验证的推送服务。Chrome需要VAPID方案或者他们专有的FCM(Firebase云消息)认证。
考虑到web推送通知体系结构中固有的拒绝服务攻击风险的增加,认为类似于VAPID的身份验证机制可能成为强制性的也不是没有道理的。在这个演示中,我们将使用VAPID作为身份验证机制,以防止任何供应商锁定谷歌的FCM。
我们不必纠结VAPID的具体实现,因为这些细节我们通过一个库处理(web-push)。但是需要注意的是,我们需要一对非对称加密密钥——公钥和对应的私钥。我们可以使用之前安装的库web-push生成它们。现在,要通过cli调用这个库,您有两个选项,一个是使用-g选项全局安装它,并通过从cli调用web-push命令来使用它,或者我喜欢的方法,使用以下命令:
node_modules/web-push/src/cli.js generate-vapid-keys
这假定您当前的工作目录是项目的根目录。node_modules是你的项目依赖文件。
如果没错的话,你应该可以看到控制台打印出:
Public Key:
BJMCdnqsQsHqHgzf8JTEuQe854IbRBc-9HjXOrf8qCSvKcX4MvCoANRLpgm4Mtl73Nn7si4mp10Lpq2ftfK9jBw
Private Key:
oOwRPpMcyC4Q2yw2ew3sOefMKlsBdT4R1Sjimo-nX58
(每个人生成的不一样),这是我们将在项目中使用的用于实现VAPID方案的密钥对,所以记录下来。
如果你想获得更多关于VAPID,您可以在这里vapid详细阅读说明书。
#Promises
下面的代码块充分利用了promises,如何使用promises不是本次介绍的重点。但是我们可以快速的了解一下他的语法,防止你措手不及。
promise会解析成两种状态,成功和失败,也就是说,封装在promise中的操作要么成功完成,要么以失败告终。分别用then和catch方法表示。如果操作成功,那么将使用指定的参数调用传递给then方法的函数。同样,在操作失败时,将使用错误参数调用catch方法。
somePromise.then(function(successParam){
//Hurray success!!
console.log('Operation completed successfully!');
}).catch(function(error) {
//Failed
console.log('Operation failed');
});
#service worker
让我们看看SW,这个文件我们命名为sw.js,唯一的责任是在浏览器中接收到推送消息后显示它。
self.addEventListener('push', function(event) {
if (!(self.Notification && self.Notification.permission === 'granted')) {
return;
}
var data = {};
if (event.data) {
data = event.data.json();
}
var title = data.title;
var message = data.message;
var icon = "img/FM_logo_2013.png";
self.clickTarget = data.clickTarget;
event.waitUntil(self.registration.showNotification(title, {
body: message,
tag: 'push-demo',
icon: icon,
badge: icon
}));
});
self隐式地引用SW。我们需要向push事件添加一个事件侦听器,当浏览器接收到消息时将触发该事件。我们正在检查SW中是否有Notification
(通知)api,以及是否已经授予了必要的权限。
然后我们使用showNotification
方法显示通知,waitUntil
方法防止浏览器在操作完成之前终止SW。
接下来我们想当用户点击弹窗时打开页面。
self.addEventListener('notificationclick', function(event) {
console.log('[Service Worker] Notification click Received.');
event.notification.close();
if(clients.openWindow){
event.waitUntil(clients.openWindow(self.clickTarget));
}
});
我们需要向SW的notificationclick事件注册一个事件侦听器,在事件监听中,我们关闭通知并使用推送消息中提供的url打开一个新选项卡。正如您所看到的,SW的代码并不十分复杂。
#main.js
它将在前端完成大部分工作。为了简洁起见,以下部分省略了一些常见的和UI相关的方法。
获取用户授权
为了显示信息我们需要获取用户授权,如果用户拒绝授权,那就game over了。否则,我们可以着手注册我们的SW。
Notification.requestPermission().then(function (status) {
if (status === 'denied') {
console.log('[Notification.requestPermission] The user has blocked notifications.');
disableAndSetBtnMessage('Notification permission denied');
} else if (status === 'granted') {
console.log('[Notification.requestPermission] Initializing service worker.');
initialiseServiceWorker();
}
});
注册SW和检查浏览器支持情况。
让我们看看如何检查浏览器是否支持SW,以及如何注册SW。
var initialiseServiceWorker = function () {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register(serviceWorkerName).then(handleSWRegistration);
} else {
console.log('Service workers aren\'t supported in this browser.');
disableAndSetBtnMessage('Service workers unsupported');
}
};
你可以看到SW的状态,当它安装时,在下面的handleSWRegistration方法中。
function handleSWRegistration(reg) {
if (reg.installing) {
console.log('Service worker installing');
} else if (reg.waiting) {
console.log('Service worker installed');
} else if (reg.active) {
console.log('Service worker active');
}
swRegistration = reg;
initialiseState(reg);
}
接下来有两件事需要检查,一是检查浏览器是否支持通知。另一个是它是否支持通过PushManager接口进行web推送。
// Are Notifications supported in the service worker?
if (!(reg.showNotification)) {
console.log('Notifications aren\'t supported on service workers.');
disableAndSetBtnMessage('Notifications unsupported');
}
// Check if push messaging is supported
if (!('PushManager' in window)) {
console.log('Push messaging isn\'t supported.');
disableAndSetBtnMessage('Push messaging unsupported');
return;
}
检查订阅状态
一旦SW准备就绪,我们将从PushManager获取订阅详细信息,并根据用户是否订阅了推送消息通知来设置按钮状态。我们可以通过使用pushManager.getSubscription方法来实现这一点。
// We need the service worker registration to check for a subscription
navigator.serviceWorker.ready.then(function (reg) {
// Do we already have a push message subscription?
reg.pushManager.getSubscription()
.then(function (subscription) {
if (!subscription) {
console.log('Not yet subscribed to Push')
isSubscribed = false;
makeButtonSubscribable();
} else {
// initialize status, which includes setting UI elements for subscribed status
// and updating Subscribers list via push
isSubscribed = true;
makeButtonUnsubscribable();
}
})
.catch(function (err) {
console.log('Error during getSubscription()', err);
});
});
#订阅
让我们看看如何订阅推送通知。我们将从浏览器的API获取订阅细节,并将其发送到我们的后端服务器。
navigator.serviceWorker.ready.then(function (reg) {
var subscribeParams = {userVisibleOnly: true};
//Setting the public key of our VAPID key pair.
var applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
subscribeParams.applicationServerKey = applicationServerKey;
reg.pushManager.subscribe(subscribeParams)
.then(function (subscription) {
// Update status to subscribe current user on server, and to let
// other users know this user has subscribed
var endpoint = subscription.endpoint;
var key = subscription.getKey('p256dh');
var auth = subscription.getKey('auth');
sendSubscriptionToServer(endpoint, key, auth);
isSubscribed = true;
makeButtonUnsubscribable();
})
.catch(function (e) {
// A problem occurred with the subscription.
console.log('Unable to subscribe to push.', e);
});
});
还记得我们之前讨论VAPID方案时生成的键对吗?这就是它在前端发挥作用的地方。我们需要在密钥对中提供公钥,作为pushManager.subscribe()方法的参数之一。
然而,我们拥有的公钥编码在url base64中,我们需要将它解码成Uint8Array对象,作为参数传递给pushManager.subscribe。
var applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
这是用于解码公钥的urlB64ToUint8Array方法。
function urlB64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (var i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
下一个有趣的片段是我们如何提取订阅细节并将其传递到后端服务器。
var endpoint = subscription.endpoint;
我们正在检索要向其提交推送消息的端点。就像这样
https://fcm.googleapis.com/fcm/send/ezxEyDe-SQs:APA91bE_Js6DAo_lN56dDG8FAbhcpSd0IrcE198R-C6ZO4IOx4vX6Gpe0bNrleng6T3x
vuuekl-AeneMhVSz9H7Bv7VTwPmE1LUMDP-BMvheSOwdrqBhb83C915WL9e7oxZCYKHnZRre
var key = subscription.getKey('p256dh');
var auth = subscription.getKey('auth');
这里我们检索用于订阅的公钥和共享密钥,这些密钥将用于在提交到端点之前加密推送消息。稍后我们将在服务器端开发一节中介绍加密算法。
将这些数据发送到后端服务器是一种非常简单的操作。
function sendSubscriptionToServer(endpoint, key, auth) {
var encodedKey = btoa(String.fromCharCode.apply(null, new Uint8Array(key)));
var encodedAuth = btoa(String.fromCharCode.apply(null, new Uint8Array(auth)));
$.ajax({
type: 'POST',
url: url,
data: {publicKey: encodedKey, auth: encodedAuth, notificationEndPoint: endpoint},
success: function (response) {
console.log('Subscribed successfully! ' + JSON.stringify(response));
},
dataType: 'json'
});
}
请注意,我们如何将公钥和共享秘密(以字节表示)编码为base64格式,以便通过http传输。
#取消订阅
从推送通知程序上取消订阅与订阅类似,但不那么复杂。
swRegistration.pushManager.getSubscription()
.then(function(subscription) {
if (subscription) {
endpoint = subscription.endpoint;
return subscription.unsubscribe();
}
})
.catch(function(error) {
console.log('Error unsubscribing', error);
})
.then(function() {
removeSubscriptionFromServer(endpoint);
console.log('User is unsubscribed.');
isSubscribed = false;
makeButtonSubscribable(endpoint);
});
我们需要对swRegistration变量调用unsubscribe方法,我们在注册时将SW注册对象存储在该变量上。我们基本上已经讨论完了main.js文件。您可以从这里看到完整的文件main.js
#服务端
由于使用了web-push库,我们可以非常直接地为演示开发服务器端应用程序。
推送消息加密
尽管我们使用的库很好地封装了所有加密细节,但在高级别上理解后端发生了什么仍然很重要。如果你对数学不感兴趣,就很难掌握加密方案的细节。但这完全没有问题,您可以完全跳过加密部分,仍然可以发送推送通知,而不知道支持它的加密算法。还记得我们以前讨论过加密吗?还记得我们如何从浏览器中的推送订阅中检索公钥和共享秘密吗?用于加密我们的信息的方法叫做 Elliptic Curve Diffie-Hellman (ECDH) on a P-256 curve。哇! 草他妈的(原文确实就是这么粗鲁)!?当你第一次读的时候,听起来确实很复杂。我们试着把它分解一下看看是否有用。
Elliptic curve (EC) & P-256 curve
椭圆曲线密码学是一种基于有限域椭圆曲线代数结构的公钥加密算法。如果你对代数上的胡言乱语有点摸不着头脑,别担心,它只是指一组满足数学方程的点。
方程和结果图像是这样的。y=x^3+ax+b
该算法中使用的椭圆曲线的名称为P-256。这是(NIST)发布的标准曲线之一。你可以在这里(here)找到他们发布的曲线的完整列表。如果你不信任美国政府,这是可以理解的,有些人就是这么认为的(美国国家安全局),那么就会有其他的弯路,而你总能找到自己的弯路。
#Diffie-Hellman (DH)
Diffie-Hellman或简称DH,是一种协议(方法),用于在公共通道上安全地交换加密密钥。它以Whitfield Diffie和Martin Hellman的名字命名。
如果你想了解更多,这里有一篇关于eliptic curve cryptography的不错的文章(here)。
我们正在使用ECDH算法对要发送的消息进行加密,然后将其发布到订阅端点。处理该端点的服务器可以访问我们订阅的私钥,并将使用它解密消息并将其推送到浏览器。
另一个需要记住的重要方面是,我们需要提供与我们在前端提供的公钥相匹配的VAPID(还记得我们在前端订阅推送通知时吗?)如果你提供了一个错误的键,Chrome会拒绝你的推送通知。
您可以从其中获得关于web push加密方案的更多信息(here)
#实现
唷,总算站稳脚跟了。让我们进入演示的服务器实现。
我们使用node和express框架来构建服务器端应用程序。所有订阅服务器数据存储在内存数组中(这毕竟是一个小演示),在服务器重启时将丢失。除了package.json文件对于服务器端应用程序,我们只有一个文件index.js。现在让我们看一下里面的代码。
#初始化
let express = require("express");
let webPush = require("web-push");
let atob = require('atob');
let bodyParser = require('body-parser');
let util = require('util');
let app = express();
let subscribers = [];
let VAPID_SUBJECT = process.env.VAPID_SUBJECT;
let VAPID_PUBLIC_KEY = process.env.VAPID_PUBLIC_KEY;
let VAPID_PRIVATE_KEY = process.env.VAPID_PRIVATE_KEY;
//Auth secret used to authentication notification requests.
let AUTH_SECRET = process.env.AUTH_SECRET;
if (!VAPID_SUBJECT) {
return console.error('VAPID_SUBJECT environment variable not found.')
} else if (!VAPID_PUBLIC_KEY) {
return console.error('VAPID_PUBLIC_KEY environment variable not found.')
} else if (!VAPID_PRIVATE_KEY) {
return console.error('VAPID_PRIVATE_KEY environment variable not found.')
} else if (!AUTH_SECRET) {
return console.error('AUTH_SECRET environment variable not found.')
}
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
webPush.setVapidDetails(
VAPID_SUBJECT,
VAPID_PUBLIC_KEY,
VAPID_PRIVATE_KEY
);
app.use(express.static('static'));
我们从环境变量中检索VAPID键和主题,验证它们以确保它们确实可用,并将它们提供给web-push库。这里唯一不熟悉的是VAPID的subject。它应该是应用服务器的联系人URI,可以是mailto: (email),也可以是https: URI。例如,我将VAPID_SUBJECT变量设置为mailto:[email protected]。
还要注意,我们正在检索一个名为AUTH_SECRET的环境变量。你可以在这里输入任何值,你会看到它是如何使用的。
其余代码只是初始化express框架,并公开提供静态文件夹(其中包含所有前端文件)的所有内容。
#订阅
订阅路由只是接受前端发送的订阅数据并将其存储在数组中。它以web-push库期望的格式组织订阅数据。
app.post('/subscribe', function (req, res) {
let endpoint = req.body['notificationEndPoint'];
let publicKey = req.body['publicKey'];
let auth = req.body['auth'];
let pushSubscription = {
endpoint: endpoint,
keys: {
p256dh: publicKey,
auth: auth
}
};
subscribers.push(pushSubscription);
res.send('Subscription accepted!');
});
#取消订阅
取消订阅路由在订阅数组上迭代,并删除与传递的端点参数匹配的元素。
app.post('/unsubscribe', function (req, res) {
let endpoint = req.body['notificationEndPoint'];
subscribers = subscribers.filter(subscriber => { endpoint == subscriber.endpoint });
res.send('Subscription removed!');
});
#通知
首先,即使这是一个简单的演示应用程序,我们真的不能让任何人来发送你的订阅者推送消息。你真的想在你的屏幕上看到“Hello Twat Monkey”(或者你老板的?)嗯....-)当恶作剧者听到公众演示API的风声时?因此,我们通过名为auth-secret的头添加了基本验证。它应该包含与放置在AUTH_SECRET环境变量中的值相同的值。否则,请求将被HTTP 401响应代码拒绝。
我们在notify/all路由中接受三个参数,如果其中任何一个不存在,我们将提供一些默认值。
message——这是将显示在通知对话框中的消息。
clickTarget——当用户单击通知对话框时,我们将指向这个URL。
title——这是将显示在通知对话框中的标题。
app.get('/notify/all', function (req, res) {
if(req.get('auth-secret') != AUTH_SECRET) {
console.log("Missing or incorrect auth-secret header. Rejecting request.");
return res.sendStatus(401);
}
let message = req.query.message || `Willy Wonka's chocolate is the best!`;
let clickTarget = req.query.clickTarget || `http://www.favoritemedium.com`;
let title = req.query.title || `Push notification received!`;
subscribers.forEach(pushSubscription => {
//Can be anything you want. No specific structure necessary.
let payload = JSON.stringify({message : message, clickTarget: clickTarget, title: title});
webPush.sendNotification(pushSubscription, payload, {}).then((response) =>{
console.log("Status : "+util.inspect(response.statusCode));
console.log("Headers : "+JSON.stringify(response.headers));
console.log("Body : "+JSON.stringify(response.body));
}).catch((error) =>{
console.log("Status : "+util.inspect(error.statusCode));
console.log("Headers : "+JSON.stringify(error.headers));
console.log("Body : "+JSON.stringify(error.body));
});
});
res.send('Notification sent!');
});
正如您所看到的,我们迭代订阅者数组并使用webPush.sendNotification()方法将通知发送给所有订阅者。
最后我们大功告成!
您可以使用以下查看完整源代码(https://github.com/thihara/web_push_notifications)。
可以在存储库的READERME找到快速启动说明。
#测试
您可以使用如下所示的curl命令来测试代码
curl -G --header "auth-secret: qwertyuiop" "http://localhost:8080/notify/all" \
--data-urlencode "title=Willy Wonka" \
--data-urlencode "message=Willy Wonka's new chocklate is awesome" \
--data-urlencode "clickTarget=http://www.favoritemedium.com"
或者通过POSTMAN。(或者自己写一个ajax请求模拟一个请求,以上命令是在linux中实行)。
#结论
现在您知道了如何在支持它们的浏览器中利用推送通知。虽然只使用最近出现的众多web推送服务中的一种可能很诱人,但是实现您自己的服务并不难,正如您从本文中看到的那样。
不过需要注意的是,这些接口和特性仍然处于试验阶段,可能在没有警告的情况下发生变化。如果出现这种情况,就需要在生产通知停机时重新挖掘新文档。
写于2017年1月11日(作者git仓库https://github.com/thihara,尊重原创@thihara)
森泉于2018年12月17号21:45翻译。