使用Node.js创建瓦片服务器(从mbtiles中传输pbf格式的瓦片)
首先
虽然在矢量瓦片的生产、样式、主机、优化和消费等领域都积累了一些经验,但这里我们想要谈论的是矢量瓦片的主机。
UNVTツールには、数年前に@hfuさんが開発したun-vector-tile-toolkit/onyxというパッケージが含まれており、nodejsを使用してベクトルタイルの配信を実現しています。タイルのホスティングにはtileserver-glやGitHubページなど、いくつかの選択肢がありますが、nodejsサーバーを使用する場合の利点は以下の通りです。
-
- シンプルで反応が早い。スケーラブル。
性能のよいサーバーにも適用できるし、ラズベリーパイのような小型PCでも実装可能。
mapboxのnpmモジュールを使うことで、mbtilesからpbfを配信できる。
大容量サイズのデータ配信が容易(このツールは大容量のデータを配信したい場合に向いている。少しのデータならウェブサーバーなどで十分。)。
nodejsのスクリプトと、pm2を組み合わせることで、デーモンプロセスとして実行するのも簡単。
本次任务的目标是什么?
-
- onyxのスクリプトをもとにして、nodejs/expressでつくったサーバーを立てる。(その過程を記録する)
- わかりやすさのために、ベクトルタイルを配信するモジュールは./route/VT.jsとして切り分ける。
(Onyx也已经开发了大约3年时间。在我使用的服务器上,需要确认每个npm模块的新版本是否能够正常运行,以及确认服务器是否能够在nodejs 16上运行。这次,我创建了一个简单的服务器来进行验证,并顺便做了一些备注。)
环境
我們使用 GitHub,在 Windows 電腦的文本編輯器進行開發。實驗用的 Linux 版本有以下兩種。
環境1:レンタルサーバー(個人の実験用)
OS: CentOS Linux release 7.8.2003
メモリ: 1 GB
nodejs version: 16.13.1 ※nvmで管理
npm version 8.1.2 ※nvmで管理
pm2 version 4.4.0
ドメインは取得済み。SSL/TLS認証はLet’s Encryptで取得。
環境2:ラズベリーパイ(3b)
OS: Debian 11.1
メモリ: 1 GB
nodejs version 16.13.1
ローカルでの利用なので、SSL/TLS認証なし
步骤
步骤1:搭建Node.js服务器(仅静态托管)
首先,我创建了一个名为app0.js的简单文件,内容如下。在进行日志设置和跨域资源共享(CORS)配置之后,我使用express.static(htdocsPath)函数来托管静态内容。(htdocsPath是在配置文件中进行设置的。很抱歉,其中还有一些暂时未使用的变量。)
const config = require('config')
const fs = require('fs')
const express = require('express')
const spdy = require('spdy') //for https
const cors = require('cors')
const morgan = require('morgan')
//const TimeFormat = require('hh-mm-ss')
const winston = require('winston')
const DailyRotateFile = require('winston-daily-rotate-file')
// config constants
const morganFormat = config.get('morganFormat')
const htdocsPath = config.get('htdocsPath')
const privkeyPath = config.get('privkeyPath')
const fullchainPath = config.get('fullchainPath')
const port = config.get('port')
const mbtilesDir = config.get('mbtilesDir')
const logDirPath = config.get('logDirPath')
// logger configuration
const logger = winston.createLogger({
transports: [
new winston.transports.Console(),
new DailyRotateFile({
filename: `${logDirPath}/server-%DATE%.log`,
datePattern: 'YYYY-MM-DD'
})
]
})
logger.stream = {
write: (message) => { logger.info(message.trim()) }
}
// app
const app = express()
app.use(cors())
app.use(morgan(morganFormat, {
stream: logger.stream
}))
app.use(express.static(htdocsPath))
//for http
app.listen(port, () => {
console.log(`Running at Port ${port} ...`)
})
/* for https
spdy.createServer({
key: fs.readFileSync(privkeyPath),
cert: fs.readFileSync(fullchainPath)
}, app).listen(port)
*/
除了这个之外,我们还准备了以下类似的config文件(config/default.hjson)。privkeyPath和fullchainPath是用于HTTPS的,所以在第一步暂时不使用。mbtilesDir在第一步也不使用。(可以保持原样)
{
morganFormat: tiny
htdocsPath: htdocs
privkeyPath: ./key/privkey.pem
fullchainPath: ./key/cert.pem
logDirPath: log
port: 8836
mbtilesDir: mbtiles
}
以下是我使用的主要npm软件包:
这个app0.js是在https://github.com/ubukawa/server-test-01 上创建的。您可以使用以下命令来执行它。
git clone https://github.com/ubukawa/server-test-01
cd server-test-01
npm install
node app0.js
只需要这样做,将文件放入htdocs,然后我认为文件将从端口8836进行HTTP主机托管。请尝试访问http(s)://(服务器根目录):8836/index.html,看看是否可见。


当数据大小较小时,将其以PBF格式静态托管即可,这已足够(理论上不需要使用Node.js服务器)。例如,在这个仓库里,我们将PBF文件放置在htdocs/pbf_tiles文件夹中作为练习用途,你可以通过访问http(s)://(服务器根目录):8836/pbf_tiles/ne-test/{z}/{x}/{y}.pbf来获取瓦片。
步骤2:添加从mbtiles返回pbf瓦片的功能。
在Step 1中创建的简单Node.js服务器上,添加一个路由来从mbtiles返回pbf瓦片。在const app = express()的下方添加var VTRouter = require(‘./routes/VT’)。然后,在稍后的app.use位置添加app.use(‘/VT’, VTRouter)。这样,以/VT/路径发来的请求将被转发到routes/VT.js。
const app = express()
var VTRouter = require('./routes/VT') //tiling
app.use(cors())
app.use(morgan(morganFormat, {
stream: logger.stream
}))
app.use(express.static(htdocsPath))
app.use('/VT', VTRouter)
然后,准备一个名为routes/VT.js的文件。这个文件通过请求路径确定瓦片的名称、z、x、y,然后使用mapbox/mbtiles(版本0.12.1)的npm模块,从mbtiles中提取pbf瓦片并返回。基本的机制是参考了unvt/onyx。
(详细说明:与onyx不同的是,这里为了简单起见,没有使用空间模块。在满足一个数据源对应一个mbtiles的条件下,我们决定返回pbf。此外,将app.js与模块分开可以使app.js保持简洁,为未来的Azure AD认证添加等准备。)
var express = require('express')
var router = express.Router()
const config = require('config')
const fs = require('fs')
const cors = require('cors')
const MBTiles = require('@mapbox/mbtiles')
const TimeFormat = require('hh-mm-ss')
// config constants
const mbtilesDir = config.get('mbtilesDir')
// global variables
let mbtilesPool = {}
let busy = false
var app = express()
app.use(cors())
//specify the target mbtiles from the path
const getMBTiles = async (t, z, x, y) => {
let mbtilesPath = `${mbtilesDir}/${t}.mbtiles`
return new Promise((resolve, reject) => {
if (mbtilesPool[mbtilesPath]) {
resolve(mbtilesPool[mbtilesPath].mbtiles)
} else {
if (fs.existsSync(mbtilesPath)) {
new MBTiles(`${mbtilesPath}?mode=ro`, (err, mbtiles) => {
if (err) {
reject(new Error(`${mbtilesPath} could not open.`))
} else {
mbtilesPool[mbtilesPath] = {
mbtiles: mbtiles, openTime: new Date()
}
resolve(mbtilesPool[mbtilesPath].mbtiles)
}
})
} else {
reject(new Error(`${mbtilesPath} was not found.`))
}
}
})
}
//Get tile from mbtiles with z,x,y
const getTile = async (mbtiles, z, x, y) => {
return new Promise((resolve, reject) => {
mbtiles.getTile(z, x, y, (err, tile, headers) => {
if (err) {
reject()
} else {
resolve({tile: tile, headers: headers})
}
})
})
}
//GET Tile(router)- t,z,x,y are extracted from the path
router.get(`/zxy/:t/:z/:x/:y.pbf`,
async function(req, res) {
busy = true
const t = req.params.t
const z = parseInt(req.params.z)
const x = parseInt(req.params.x)
const y = parseInt(req.params.y)
getMBTiles(t, z, x, y).then(mbtiles => {
getTile(mbtiles, z, x, y).then(r => {
if (r.tile) {
res.set('content-type', 'application/vnd.mapbox-vector-tile')
res.set('content-encoding', 'gzip')
res.set('last-modified', r.headers['Last-Modified'])
res.set('etag', r.headers['ETag'])
res.send(r.tile)
busy = false
} else {
res.status(404).send(`tile not found: /zxy/${t}/${z}/${x}/${y}.pbf`)
busy = false
}
}).catch(e => {
res.status(404).send(`tile not found (getTile error): /zxy/${t}/${z}/${x}/${y}.pbf`)
busy = false
})
}).catch(e => {
res.status(404).send(`mbtiles not found for /zxy/${t}/${z}/${x}/${y}.pbf`)
})
}
);
module.exports = router;
这个文件已经上传到了同样的位置https://github.com/ubukawa/server-test-01。如果你将其复制到你自己的服务器上,就可以通过运行node app.js来执行它。
如果在 mbtiles 文件夹中有一个名为 ne-test.mbtiles 的矢量瓦片数据,那么您可以通过以下链接访问:http(s)://(服务器的根地址):8836/VT/ZXY/ne-test/{z}/{x}/{y}.pbf
步骤3:根据需要将网站从HTTP转换为HTTPS。
如果服务器已经获得了域名,并且对该域名进行了SSH / TLS认证,那么很容易将其配置为HTTPS。如果您想尝试,请按照以下第三步进行操作。
-
- 事前準備
私は個人のテストサーバーでドメインを取得し、Let’s EncryptでSSL/TLS認証を取得しました。
必要な作業
config/default.hjson で指定したパスに、プライベートキーとCertificateを保存します。(拡張子pem)
app.jsの最後の部分を以下のように修正します。httpの部分をコメントアウトして、httpsの部分のコメントアウトを取ります。
/* for http
app.listen(port, () => {
console.log(`Running at Port ${port} ...`)
})
*/
// for https
spdy.createServer({
key: fs.readFileSync(privkeyPath),
cert: fs.readFileSync(fullchainPath)
}, app).listen(port)
第四步:尝试查看存储库的示例地图。
这个仓库(https://github.com/ubukawa/server-test-01)包含了用于示例的矢量瓦片和Web地图(使用Mapbox GL JS版本1.x的库)。

步骤5:使用pm2(根据需要)
通过使用pm2,您可以在后台运行nodejs服务器。因为它是一个守护进程,所以即使从服务器注销也可以安全运行。另外,通过使用crontab,您还可以定期重新启动服务器。


整理和课题

在这里,我们确认了基本动作,接下来将按照以下方式发展。
对多个mbtiles文件的支持
在这次的例子中,假设我们有一个向量瓦片源对应一个mbtiles文件,但是当文件变得很大时会变得困难。在un-vector-tile-toolkit/onyx中,我们会为特定的缩放级别(例如ZL6)创建一个mbtiles文件,将全世界分割成多个mbtiles文件进行管理。为了实现相应的瓦片分发,onyx的app.js中包含了路由。另外,在un-vector-tile-toolkit/coesite中,由于区块的大小不一,我们会做一些更复杂的处理。
提供 Azure AD 認證
本日暂时不涉及,但是在这个服务器组件的基础上,还附加了一个可以在nodejs中使用的Azure AD模块(un-vector-tile-toolkit/coesite)。通过Azure AD认证登录后,可以查看Web地图,但是由于添加了认证后,由于跨域(?),瓦片无法共享(可能是因为用户ID是基于会话进行管理),所以还需要进一步努力。
对于HTTP和HTTPS的分析
如果假设有互联网连接,那么使用https是可以的,但是根据(@hfu,2021)关于将树莓派用作服务器的利用,离线环境或本地网络与https之间存在一些复杂关系。特别是对于离线环境下应该使用何种服务器,我们将来需要进一步考虑。
感谢辞等
关于Node.js的托管,我完全参考了@hfu的成果物。非常感谢。
此外,我希望能在这个领域(托管)继续进行一些研究,如果有任何建议、问题等,请随时告诉我,我会非常感激。
请参考
-
- 今回のレポジトリ ubukawa/server-test-01
-
- un-vector-tile-toolkit/onyx
-
- un-vector-tile-toolkit/coesite
ベクトルタイルはCORSしよう by @hfu
Raspberry Pi のサーバーとしての活用についてby @hfu
npm