
CDN(Cloudflare Workers)の中で地理空間データの計算処理を行い、結果をGeoJSONとしてクライアントに返す。
※この記事は、現在のworkersの仕様にくらべてだいぶ古い内容となりつかえなくなっています。最新のworkersについては公式のドキュメントを参照してください。
お手軽エッジコンピューティング!
発端
Service Workerの勉強に、JSONデータをペイロードしたPOSTリクエストをService Workerでフックし、そのまんま受け取ったJSONをレスポンスとして返すというエコーサーバ的なコードを書いていたのですが、その最中「Service Workerの中でGeoJSONを生成して返したりすると面白いんじゃないか」と思いついて脇道に逸れたのがきっかけです。
結果的にservice Workerの中でGeoJSONを生成することにはあまり意味がなかったのですが、CDN上でJavaScriptを実行できるCloudflare WorkersがService WorkerのAPIを用いて実装されているので、Cloudflare Workers上に移して実行することで負荷が高くなりがちな地理空間データの分析処理をCDN上で行える可能性が見えてきました。
Service Worker
まずは、シンプルにService Workerの中でGeoJSONを生成するサンプルを試してみました。
POSTリクエストにペイロードされたbbox(範囲を指定する2点の座標)を受け取り、Turf.jsを使って指定範囲内のポイントグリッドをGeoJSONとして生成し返却します。
Turf.jsの読み込みをimportScriptsでなくrequireで行なっているのは後ほどCloudflare Workersに登録する兼ね合いでwebpackを使って一枚のjsファイルに結合しているためです。
(一つのjsファイルに結合できればなんでもいいのですが、webpackが一番なれていたので)
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | <!DOCTYPE html> <html lang='jp'> <head> </head> <body> <button id="btn">Send bbox</button> <script> //サービスワーカーの登録     navigator.serviceWorker.register('main.js').then(function(reg) {     if(!reg.active) location.reload(); }).catch(function(error) {     console.log('Service worker:' + error); }); //ボタンがクリックされたら架空のjsonファイル(pointGrid.json)にたいしてPOSTリクエストを送信する。 const bbox = [ -180, -90, 180, 90 ]; document.querySelector("#btn").addEventListener("click", function(){     sendPostRequest("pointGrid.json",  bbox)         .then(response => {             //サービスワーカーの中で生成されたGeoJSONを受け取る             console.log(response);         }); }); //POSTリクエストを送信する function sendPostRequest(url, data) {     return fetch(url, {         method: "POST",          mode: "cors",          headers: {             "Content-Type": "application/json; charset=utf-8",         },         body: JSON.stringify(data),      })     .then(response => response.json())     .catch(error => console.error(error)); } </script>     </body> </html> | 
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | //Service Worker登録時に発火するイベント self.addEventListener('install', function(event) {     console.log(event); }); //リクエストフック時に発火するイベント self.addEventListener('fetch', function(event) {     //pointGrid.jsonへのリクエストに対して処理を行う     if (event.request.url.match(/pointGrid.json/i)) {         event.respondWith(             event.request.json().then(function(bbox) {                 //送信されたbboxの値を確認                 console.log(bbox);                 //turf.jsを使って指定範囲内のポイントグリッドを生成する                 var res = turf.pointGrid(bbox、0.5);                 return new Response(JSON.stringify(res), {                     status: 200,                     statusText: 'OK',                     headers: {                         'Content-Type': 'application/json'                     }                 });             })         );     } }); | 
Service Workerが正しく登録されているかは、Chrome開発者ツールのApplicationのタブで確認することができます。
また、登録されているスクリプトのアップデートや削除もこのタブ上で行えます。

Service Workerが正しく登録されていれば、「Send bbox」を押して生成されたGeoJSONがちゃんと返ってくるか確認しましょう。

NetworkタブでpointGrid.jsonの通信内容をみると、データがService Workerから返却されていることが確認できます。

Cloudflare Workers
Cloudflare Workers はCDNプロバイダのCloudflareが提供する、CDNのエッジにJavaScriptコードを配置し実行することができるサービスです。
Cloudflare Workersを使用することで、開発者はエンドユーザーの近くにあるCloudflareのエッジでJavaScriptコードを展開できます。Service Workers APIに基づいて、今や開発者はユーザーのデバイス上のブラウザーを経由せずに、コードを安全に実行することができます。
参考:VMよりコンテナよりもさらに軽量な分離技術、V8のIsolateを用いてサーバレスコンピューティングを提供するCloudflare Workers
Service Workerとして正しく動作していることが確認できていれば、サーバーにindex.htmlをアップロードしビルドしたjsをCloudflare Workersのエディタにコピペすればほぼ問題なく動作するはずです。
(index.htmlをアップロードする際は、service workerを登録するコードは削除しておきます)
そのままコピペしてもつまらないので、index.htmlではleafletを読み込み地図上でbboxを指定できるように変更しました。

一見すると昨日あげた記事のサンプルとなにも変わっていないように見えますが、ここでのTurf.jsを使った地理情報データの処理は全てCDN上で実行されています。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | <!DOCTYPE html> <html lang='jp'> <head> integrity="sha512-puBpdR0798OZvTTbP4A8Ix/l+A4dHDD0DGqYW6RQ+9jxkRFclaxxQb/SJAWZfWAkuyeQUytO7+7N4QKrDh+drA==" crossorigin=""/> integrity="sha512-QVftwZFqvtRNi0ZyCtsznlKSWOStnDORoefr1enyq5mVL4tmKB3S/EnC3rRJcxCPavG10IcrVGSmPh6Qw5lwrg==" crossorigin=""></script> <link rel="stylesheet" href="https://unpkg.com/leaflet.pm@latest/dist/leaflet.pm.css" /> <script src="https://unpkg.com/leaflet.pm@latest/dist/leaflet.pm.min.js"></script> <style> html, body {     width: 100%;     height: 100%;     padding: 0px;     margin: 0px; } #mapid {     width:100%;     height:100%; } </style> </head> <body> <div id="mapid"></div> <script> var mymap = L.map('mapid').setView([36.3426631, 138.6092733], 13); L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}', {     attribution: 'Map data © <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',     maxZoom: 18,     id: 'mapbox.streets',     accessToken: 'Mapboxアクセストークを記述する' }).addTo(mymap); //draw Toolの設定(drawReactang以外を非表示にしている) var controler = mymap.pm.addControls({   position: 'topleft',   drawMarker:false,   drawPolyline:false,   drawRectangle:true,   drawPolygon:false,   drawCircle: false,   editMode:false,   dragMode:false,   cutPolygon:false,   removalMode:false, }); //地図上にRectangleが作成されたら発火する mymap.on("pm:create", function(e){     const bbox = e.layer.getBounds().toBBoxString().split(",")         .map(function(d){ return +d });     sendPostRequest("cloudflareWorkers/pointGrid.json", bbox)         .then(function(response){             if(response.error){                 alert(response.error)                   mymap.removeLayer(e.layer);             }else{                 L.geoJSON(response).addTo(mymap);             }                  }); }); //POSTリクエストを送信 function sendPostRequest(url, data) {     return fetch(url, {         method: "POST",         headers: {             "Content-Type": "application/json; charset=utf-8",         },         body: JSON.stringify(data)     })     .then(function(response){ return response.json() })     .catch(error => console.error(error)); } </script>     </body> </html> | 
Turf.jsは丸ごと読み込むとビルド後のファイルサイズが大きくなるので、必要なモジュールのみ読み込むように変更しています。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | const bboxPolygon = require('turf-bbox-polygon'); const pointGrid = require('turf-point-grid'); const areaMeasure = require('turf-area'); module.exports = (event) => {     event.respondWith(         event.request.json().then(function(bbox) {             //選択範囲の面積を計算する             var area = areaMeasure(bboxPolygon(bbox));             //レスポンスヘッダ             var responseheader = {                 status: 200,                 statusText: 'OK',                 headers: {                     'Content-Type': 'application/json'                 }             };             //選択範囲が広すぎる場合はエラーを返す             if (area / 1000 > 800000) {                 return new Response(JSON.stringify({ error: '選択範囲が大きすぎます' }), responseheader);             } else {                 //ポイントグリッドを生成する                 var res = pointGrid(bbox, 0.5);                 return new Response(JSON.stringify(res), responseheader);             }         })     ); }; | 
上記スクリプトを、CloudflareダッシュボードのWorkers Editorで「Script」のエリアに貼り付けます。

登録されたスクリプトは、即座にCDNに配信され動作するようになります。
Cloudflare Workers 感想
思いの外、簡単にサクッと使えてとても楽しいです。
Lambda@Edgeはちょっとハードル高いなって感じている人は、試しにCloudflare Workersを使ってみてはいかがでしょうか。
一つ残念なこととしては、Enterpriseプラン以外では一つのドメインで一つのスクリプトしか動かせないことですかね。
なので複数の処理を行おうと思ったら、受け取ったリクエストをルーティングするコードが必要になってきます。
How many Workers scripts can I have per domain? – Developers / Workers – Cloudflare Community
そこだけ気をつければ、お手軽価格(月5$)で遊べるのでおすすめです。