LoginSignup
3
1

More than 3 years have passed since last update.

Box UI ElementsのContent OpenWithでファイルの更新に反応してみた

Last updated at Posted at 2020-05-16

この記事のシリーズ:

Box UI ElementsのContent OpenWithでBox Editをつかってみた
Box UI ElementsのContent OpenWithでG Suiteを開いてみた
Box UI ElementsのContent OpenWithでファイルの更新に反応してみた ← この記事
Box UI ElementsのContent OpenWithでファイルの更新に反応してみた クライアントサイド編

コードは、Githubでも確認いただけます。

前回の内容と課題

前回、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秒で動かなくなります。

app_ws.js
const express = require("express");
const http = require("http");
const boxSDK = require("box-node-sdk");
const config = require("./config.json");
const WebSocket = require("ws");

const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

app.set("views", ".");
app.set("view engine", "ejs");

const USER_ID = "12771965844";
const FILE_ID = "665319803554";

const sdk = boxSDK.getPreconfiguredInstance(config);
const auClient = sdk.getAppAuthClient("user", USER_ID);

app.get("/", async (req, res) => {
  // トークンをダウンスコープする。
  // ここでは、OpenWithで必要なものと、Previewで必要なものを両方スコープにいれてトークンをダウンスコープする
  const downToken = await auClient.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 API
wss.on("connection", async (ws) => {
  // ブラウザとHerokuの間のWebsocketのハンドリング

  // ロングポーリングはAppUserのトークンで行う必要がある
  const stream = await auClient.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打ちをしています。

app_ws_heroku.js
const express = require("express");
const http = require("http");
const boxSDK = require("box-node-sdk");
const config = require("./config.json");
const WebSocket = require("ws");

const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

app.set("views", ".");
app.set("view engine", "ejs");

const USER_ID = "12771965844";
const FILE_ID = "665319803554";

const sdk = boxSDK.getPreconfiguredInstance(config);
const auClient = sdk.getAppAuthClient("user", USER_ID);

app.get("/", async (req, res) => {
  // トークンをダウンスコープする。
  // ここでは、OpenWithで必要なものと、Previewで必要なものを両方スコープにいれてトークンをダウンスコープする
  const downToken = await auClient.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 API
wss.on("connection", async (ws) => {
  // ブラウザとHerokuの間のWebsocketのハンドリング
  // Herokuではロングポーリングは55秒で強制的に止められるので、setTimeoutをつかって45秒毎につなぎ直している。
  // 単純なポーティングよりマシだが、本当に更新検知が必要な場合は、インフラにHeorkuを使わないほうがいいと思う。
  let pollingTimer;
  async function longPolling() {
    // Boxのロングポーリングイベント監視
    // ロングポーリングはAppUserのトークンで行う必要がある
    const stream = await auClient.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のようなインフラを使わないのであれば不要
  const pingTimer = 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部分のみ再描画しています。

index_ws.ejs

<!DOCTYPE html>
<html lang="en-US">
<head>
    <meta charset="utf-8" />
    <title>Sample</title>

    <link href="https://cdn01.boxcdn.net/platform/elements/11.0.2/ja-JP/openwith.css" rel="stylesheet" type="text/css"></link>
    <link href="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>

    <div id="container">
        <div class="openwith-container"></div>
        <div class="preview-container"></div>
    </div>

    <script>
        // app.jsから渡されたパラメータ
        const fileId = "<%= fileId %>"
        const token = "<%= token %>"

        const openWith = new Box.ContentOpenWith();
        openWith.show(fileId, token,  { container: ".openwith-container"})

        let preview = new Box.Preview();
        preview.show(fileId, token, { container: ".preview-container", autoFocus: false });

        // ファイルの更新に反応するため、Websocketを利用する
        const host = location.origin.replace(/^http/, "ws"); // -> wss://xxx.herokuapp.com/
        const ws = new WebSocket(host);

        let confirmed = null
        ws.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 = new Box.Preview(); // preivewオブジェクトは再利用できないっぽい。再度newする必要があるみたい。
                    // 毎回プレビューの位置までスクロールされたくないのでautoFocus:false
                    preview.show(fileId, token, { container: ".preview-container", autoFocus: false });
                }
            }
        }
    </script>
</body>
</html>


試してみる

「開く」ボタンを押して、ローカルでWordを立ち上げ、保存をして数秒でConfirmが表示されるのが確認できました。Yesを押すとリロードしてPreviewに最新のデータが反映されました。

スクリーンショット 2020-05-16 13.33.51.png

まとめ

@daichiiiiiii さんから教えていただいた方法でうまく行きました。

Herokuの制限のため再接続している点が微妙です。
本気で更新の通知を実装する場合、この点で制限が無いインフラを選定すべきだと思います。

3
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1