この記事は、Box UI ElementsのContent OpenWithでBox Editをつかってみたの続編です。
ファイルの更新を検知して、画面をリフレッシュします。
前回の内容と課題
前回、Box UI ElementsのContent OpenWithでBox Editをつかってみたという記事の中で、Box UI ElementsのOpenWithを使って、カスタム画面から変更するというのを試したのですが、課題として変更した内容が標準のBOXのように検知できていませんでした。
@daichiiiiiiiさんから、コメントをいただいて、BoxのLong Polling APIを利用すれば良いとおしえていただきました。ありがとうございます!
更新ボタン表示させるには、BoxのLong Polling APIを使用して更新情報を監視する必要があります。
https://developer.box.com/guides/events/polling/イベントを検知したら、User Event APIからいま表示しているファイルの更新イベントが見つかったらPopupを出す。みたいなロジックを組み込む必要があります。
残念ながらUI Elementsには上記ロジック内包されていません。。。残念
今回は、このロングポーリングを利用して、変更を検知する操作を試したので、内容を残します。
試したこと概要
以下のような実装を追加しました。
Herokuのアプリケーションから、Boxのロングポーリングにつなぎ、AppUserのイベントを購読するようにする。
Herokuアプリとブラウザを、Websocketでつなぎ、Boxのイベントに更新を見つけたらブラウザに通知する。
ブラウザは更新の通知を受けたら、Confirmのダイアログを開き、更新するか確認しYesの場合画面をリロードし、Noの場合は二度と更新を確認しない。
なお、app.js
はシンプル版と、Heorku対応版を作りました。
利用しているHerokuの特性として、レスポンスが55秒かえってこないと自動的に接続を殺し、エラーにするという機能に対処する必要があるためです。
変更の通知が要件として大切な場合、Herokuのようなインフラを利用しない方がいいと思います。
改良の余地は多分に残っていますが、基本的な動きは確認できたのでコードを共有します。
コードは前回の記事のものをベースに使っています。
具体的な変更
サーバー側の改造
wsモジュールを追加します。
yarn add ws
シンプル版
Boxのロングポーリングイベントを利用し、クライアント側ともWebsocketでつなぎます。
シンプル版です。Herokuだと55秒で動かなくなります。
constexpress=require("express");consthttp=require("http");constboxSDK=require("box-node-sdk");constconfig=require("./config.json");constWebSocket=require("ws");constapp=express();constserver=http.createServer(app);constwss=newWebSocket.Server({server});app.set("views",".");app.set("view engine","ejs");constUSER_ID="12771965844";constFILE_ID="665319803554";constsdk=boxSDK.getPreconfiguredInstance(config);constauClient=sdk.getAppAuthClient("user",USER_ID);app.get("/",async(req,res)=>{// トークンをダウンスコープする。// ここでは、OpenWithで必要なものと、Previewで必要なものを両方スコープにいれてトークンをダウンスコープするconstdownToken=awaitauClient.exchangeToken(["item_execute_integration","item_readwrite","item_preview","root_readwrite",],`https://api.box.com/2.0/folders/0`);// テンプレートにパラメータを渡して、HTMLを返すres.render("index",{fileId:FILE_ID,token:downToken.accessToken,});});// Boxで、ファイルが変更されたことを、ロングポーリングを使って検知し、フロントエンドに通知する。// 必ずしもそうする必要は無いが、ここではブラウザとHeroku間をWebsocketでつないでいる。// Websocketの中で、Heroku ⇔ Box APIを、Long Pollingでつなぐ。// ブラウザ ⇔ (Websocket)⇔ Heroku App ⇔ (Long Polling) ⇔ BOX APIwss.on("connection",async(ws)=>{// ブラウザとHerokuの間のWebsocketのハンドリング// ロングポーリングはAppUserのトークンで行う必要があるconststream=awaitauClient.events.getEventStream();// ロングポーリングからデータを受け取ったときの処理stream.on("data",(event)=>{// 更新されたことを、event_typeで判定(プレビューの場合などもイベントが来る)if(event.event_type&&event.event_type==="ITEM_UPLOAD"){// クライアントに更新を通知。ここでは簡易的にupdatedという文字列を返している。wss.clients.forEach((client)=>{client.send("updated");});}});});server.listen(process.env.PORT||3000,()=>{console.log(`express started on port ${server.address().port}`);});
Heroku対応版
こちらは、Herokuで利用する場合のため、45秒でつなぎ直しとブラウザへのPing打ちをしています。
constexpress=require("express");consthttp=require("http");constboxSDK=require("box-node-sdk");constconfig=require("./config.json");constWebSocket=require("ws");constapp=express();constserver=http.createServer(app);constwss=newWebSocket.Server({server});app.set("views",".");app.set("view engine","ejs");constUSER_ID="12771965844";constFILE_ID="665319803554";constsdk=boxSDK.getPreconfiguredInstance(config);constauClient=sdk.getAppAuthClient("user",USER_ID);app.get("/",async(req,res)=>{// トークンをダウンスコープする。// ここでは、OpenWithで必要なものと、Previewで必要なものを両方スコープにいれてトークンをダウンスコープするconstdownToken=awaitauClient.exchangeToken(["item_execute_integration","item_readwrite","item_preview","root_readwrite",],`https://api.box.com/2.0/folders/0`);// テンプレートにパラメータを渡して、HTMLを返すres.render("index",{fileId:FILE_ID,token:downToken.accessToken,});});// Boxで、ファイルが変更されたことを、ロングポーリングを使って検知し、フロントエンドに通知する。// 必ずしもそうする必要は無いが、ブラウザとHeroku間をWebsocketでつなぐ。// Websocketの中で、Heroku ⇔ Box APIを、Long Pollingでつなぐ。// 関係は、以下のようなイメージ// ブラウザ <= (Websocket) => Heroku App <=(Long Polling)=> BOX APIwss.on("connection",async(ws)=>{// ブラウザとHerokuの間のWebsocketのハンドリング// Herokuではロングポーリングは55秒で強制的に止められるので、setTimeoutをつかって45秒毎につなぎ直している。// 単純なポーティングよりマシだが、本当に更新検知が必要な場合は、インフラにHeorkuを使わないほうがいいと思う。letpollingTimer;asyncfunctionlongPolling(){// Boxのロングポーリングイベント監視// ロングポーリングはAppUserのトークンで行う必要があるconststream=awaitauClient.events.getEventStream();// ロングポーリングからデータを受け取ったときの処理stream.on("data",(event)=>{// 更新されたことを、event_typeで判定(プレビューの場合などもイベントが来る)if(event.event_type&&event.event_type==="ITEM_UPLOAD"){// ここで本当ならfileId等もチェックしたほうがいいかも// クライアントに更新を通知。ここでは簡易的にupdatedという文字列を返している。wss.clients.forEach((client)=>{client.send("updated");});}});// 45秒たったら、もう一度ロングポーリングをつなぎ直すpollingTimer=setTimeout(longPolling,45000);}// setTimeoutは初回は即座に実行されないので、初回分だけ実行しておく。longPolling();// ブラウザとHerokuの間のWSも、Herokuは55秒でシャットダウンしてしまうので、pingだけ飛ばしておく。// Heorkuのようなインフラを使わないのであれば不要constpingTimer=setInterval(()=>{wss.clients.forEach((client)=>{client.send("ping");});},45000);// ブラウザが閉じられたとき、無駄な再接続を止めるws.on("close",()=>{// ブラウザへのpingを止めるclearInterval(pingTimer);// Boxへのロングポーリングを止めるclearTimeout(pollingTimer);});});server.listen(process.env.PORT||3000,()=>{console.log(`express started on port ${server.address().port}`);});
クライアント側の改造
Heroku側とWebsocketでつなぎ、更新があったとき反応するようにします。
変更があった場合、簡易的にConfirmでリフレッシュの要否確認し、previewオブジェクトを作り直してpreview部分のみ再描画しています。
<!DOCTYPE html><htmllang="en-US"><head><metacharset="utf-8"/><title>Sample</title><linkhref="https://cdn01.boxcdn.net/platform/elements/11.0.2/ja-JP/openwith.css"rel="stylesheet"type="text/css"></link><linkhref="https://cdn01.boxcdn.net/platform/preview/2.34.0/ja-JP/preview.css"rel="stylesheet"type="text/css"></link><script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Intl"></script><script src="https://cdn01.boxcdn.net/polyfills/core-js/2.5.3/core.min.js"></script><script src="https://cdn01.boxcdn.net/platform/elements/11.0.2/ja-JP/openwith.js"></script><script src="https://cdn01.boxcdn.net/platform/preview/2.34.0/ja-JP/preview.js"></script><style>.openwith-container{margin-left:250px;}.preview-container{height:800px;width:100%;}</style></head><body><h3>File Id: <%=fileId%></h3><divid="container"><divclass="openwith-container"></div><divclass="preview-container"></div></div><script>// app.jsから渡されたパラメータconstfileId="<%= fileId %>"consttoken="<%= token %>"constopenWith=newBox.ContentOpenWith();openWith.show(fileId,token,{container:".openwith-container"})letpreview=newBox.Preview();preview.show(fileId,token,{container:".preview-container",autoFocus:false});// ファイルの更新に反応するため、Websocketを利用するconsthost=location.origin.replace(/^http/,"ws");// -> wss://xxx.herokuapp.com/constws=newWebSocket(host);letconfirmed=nullws.onmessage=event=>{// Herokuからメッセージが来たとき。// updatedであれば、簡易的にconfirmウィンドウを出す。// OKを押したら、簡易的にリロードして再読み込みし、変更を反映。// OK, Cancelの確認は一度だけ聞く。if(event.data==="updated"&&confirmed!==false){if(confirmed===null){confirmed=confirm("refresh?")}if(confirmed){// previewだけを描画し直す。// preview.hide() // hide()はしてもしなくてもすぐには見た目変わらず。// preview.show(..) // 単純にshowを再度呼んだだけでは画面が更新されない。preview=newBox.Preview();// preivewオブジェクトは再利用できないっぽい。再度newする必要があるみたい。// 毎回プレビューの位置までスクロールされたくないのでautoFocus:falsepreview.show(fileId,token,{container:".preview-container",autoFocus:false});}}}</script></body></html>
試してみる
「開く」ボタンを押して、ローカルでWordを立ち上げ、保存をして数秒でConfirmが表示されるのが確認できました。Yesを押すとリロードしてPreviewに最新のデータが反映されました。
まとめ
@daichiiiiiiiさんから教えていただいた方法でうまく行きました。
Herokuの制限のため再接続している点が微妙です。
本気で更新の通知を実装する場合、この点で制限が無いインフラを選定すべきだと思います。