在 Ghost 中使用 MapKit JS 嵌入 Apple Maps
忙完暑研之后有些想补上之前的游记,毕竟今年因为疫情的关系也去不了什么地方,就回忆一下之前的旅程吧。
游记里肯定要插入地图,但似乎几种选择都不太理想:
- Apple Maps 的截图:好看,但不可交互。
- Google Maps:能看,但国内无法访问。
- 国内的一些地图:丑,且国外地图数据不完全。
苦恼之时,发现 Apple 在 WWDC 18 时已经推出了用于在网页中嵌入 Apple Maps 的框架:MapKit JS。
更新:MapKit JS 暂不支持中国大陆访问(尽管简介页上的 demo 是可以的)。
需求与注意事项
首先,不像嵌入 Google Maps 那么简单,使用 MapKit JS 是需要与开发者账户进行认证的,也就是说你需要加入每年 99 USD 的 Apple Developer Program。
其次,虽然使用 MapKit JS 不需要支付额外费用,但是是有 quota 限制的:
- 250,000 map views per day
- 25,000 service calls per day
其中「service call」应该指的是规划路线之类的 request(待考证)。这个量对于个人博客而言显然绰绰有余了。
相应地,Apple 提供了 MapKit JS Dashboard 用于监控服务用量。
准备工作
在部署 MapKit JS 前,首先要做好认证的准备工作。
认证材料
认证材料的介绍在 WWDC 18 的这个 session 中已经讲得很详细了,以下简要介绍一下其中的部分内容。
认证所需要的材料有三种:
- Maps Identifier (Maps ID):顾名思义,是嵌入的地图的标识符。Apple 给出的建议是为每个域名创建一个,当然不嫌麻烦的话创建多个也可以。同时也 MapKit JS Dashboard 里的使用情况也是按照这个 ID 来分类的。
- MapKit JS Key:一个 PKCS #8 私钥文件,与一个 Maps ID 相对应,同时文件名中有一个十位的 Key ID 用于标识这个私钥。这个私钥用于签名下面即将提到的 token。
- MapKit JS Token:一个直接用于调用 Apple Maps API 的 token,采用 JSON Web Token (JWT) 标准,有有效期限制。
前面提到 MapKit JS 的用量是有限制的,而这一用量的计算便是通过 token 识别开发者身份完成的。如果他人窃取了你的 token 来用,对应的用量也会算在你的账户里。为了解决这一安全问题,token 按照有效期分为两种:
- Short lived tokens:建议的有效期为30分钟,需要在服务器上部署接口以更新 token。很安全,但对于个人博客来说有些麻烦了。
- Long lived tokens:有效期可以设为很长,适合在静态网站上部署。就是它了。
生成 token
一个 JWT 由 header,payload,signature 三个部分组成,详细的介绍可以参考这里。
能够对 JWT 进行签名的库在这里列出了很多,这里使用 Python 库jwcrypto
进行签名,使用pip3 install jwcrypto
就可以安装了。
首先是 header 部分:
header = {
"alg": "ES256",
"typ": "JWT",
"kid": "K1K1K1K1K1" # Key ID
}
其中alg
必须设为"ES256"
,也就是 ES256 椭圆曲线数字签名算法。
typ
必须为"JWT"
。
kid
是 Key ID,也就是私钥文件名中那一串十位的编号。Apple 的服务器应该是用这一项来获取对应的公钥以验证签名。
然后是 payload:
current_time = time.time() # Seconds since epoch
duration = 31536000 # Seconds in one calendar year
payload = {
"iss": "XTID1XTID1", # Xcode Team ID
"iat": current_time,
"exp": current_time + duration,
"origin": "https://blog.weiheng.me" # Domain name
}
这里iss
是发行者,需要填上你的 Xcode Team ID(在这里查看)。
iat
和exp
分别是发行时间和过期时间,均为 Unix epoch 算起的秒数,这里将exp
设为iat
的一年后,也就是说这个 token 生成后有一年的有效期。
origin
是对地图所嵌入的网站域名的限制,有了这一项就可以防止其他人盗取你的 token 在自己的网站上使用了。
有了 header 和 payload,最后只需要构建 JWT 并用私钥签名就可以了。完整代码如下:
#!/bin/python3
from jwcrypto import jwt, jwk
import time
# Construct header
header = {
"alg": "ES256",
"typ": "JWT",
"kid": "K1K1K1K1K1" # Key ID
}
# Construct payload
current_time = time.time() # Seconds since epoch
duration = 31536000 # Seconds in one calendar year
payload = {
"iss": "XTID1XTID1", # Xcode Team ID
"iat": current_time,
"exp": current_time + duration,
"origin": "https://blog.weiheng.me" # Domain name
}
# Load key file
key_file = open("AuthKey_K1K1K1K1K1.p8", "rb") # Path to private key file
key = jwk.JWK.from_pem(key_file.read())
# Create and sign token
token = jwt.JWT(header=header, claims=payload)
token.make_signed_token(key)
# Print signed token
print(token.serialize())
在 Ghost 中部署 MapKit JS
MapKit JS 的简介中提供了几个有代表性的示例和其源码,拿来参考非常合适。 Developer Documentation 里有更详细的文档。
然而,这几个示例全部采用的是 short lived token,而之前生成的是 long lived token。
适配很简单,只需要将认证过程中的
mapkit.init({
authorizationCallback: function(done) {
var xhr = new XMLHttpRequest();
xhr.open("GET", "/services/jwt");
xhr.addEventListener("load", function() {
done(this.responseText);
});
xhr.send();
}
});
改为
mapkit.init({
authorizationCallback: function(done) {
done("your-token-here");
}
});
以直接通过done
提交 token 就可以了。
接下来就可以在 Ghost 的文章中使用 MapKit JS 插入 Apple Maps 了。这里以简介中的「Embed」这个例子作为示例。
- 在 Code injection 的 header 中添加如下代码。其中
<script>
用于加载mapkit.js
,准确来讲是 MapKit JS 5的最新版本。
<script src="https://cdn.apple-mapkit.com/mk/5.x.x/mapkit.js"></script>
<style>
#map {
width: 100%;
height: 600px;
margin: 2em 0;
}
</style>
- 接下来在正文中添加一个 HTML block,输入
<div id="map"></div>
,也就是一个用于显示地图的<div>
。这个 block 会在 Ghost 编辑器中显示为空白。 - 再添加一个 HTML block,加入以下的 JavaScript 片段以加载地图。这个 block 会在 Ghost 编辑器中显示为
Embedded JavaScript
。
再添加一个 HTML block,加入以下的 JavaScript 片段以加载地图:
<script>
mapkit.init({
authorizationCallback: function(done) {
done("your-token-here");
}
});
var Cupertino = new mapkit.CoordinateRegion(
new mapkit.Coordinate(37.3316850890998, -122.030067374026),
new mapkit.CoordinateSpan(0.167647972, 0.354985255)
);
var map = new mapkit.Map("map");
map.region = Cupertino;
</script>
预览文章,检查地图是否加载成功。
如果只有地图网格,则 token 配置有误。如果一切顺利的话,效果如下: