見出し画像

【GAS】HTMLサービスを使ってクリックできるURLを表示したり、非同期処理問題をなんとかする



やりたいこと

①:スプレッドシートの「入力用」シートに入力した情報を、「出力用」シートに転記・整形して、出力用シートをPDFとして書き出すという処理をボタン一つでやりたい。
②:①で書き出してドライブに保存したPDFのURLを画面上に表示し、クリックで表示できるようにしたい

問題点①

転記・整形する処理とPDF書き出し処理を別のfunctionとして記述すると(おそらく)処理スレッドが分かれてしまう関係で、転記した情報が反映されていないままの出力用シートが出力されてしまう。

つまり、

function 処理実行(){
  転記処理()
  PDF発行処理()
}
function 転記処理(){
  // 入力用シートの内容を取得して出力用シートに転記する処理
}
function PDF発行処理(){
  // 出力用シートの内容をPDFとして書き出す処理
}

こう書いてしまうと、転記処理とPDF発行処理が同時に別のスレッドで走ってしまって、転記処理される前のまっさらな出力用シートがむなしく出力されてきます。
ちなみに、PDF発行処理の前にSleepを入れても無駄でした。

すべての処理をfunction分けないでべた書きしたら期待通りに動きはしたんですが、それはあまりにも保守で死ぬ。

問題点②

クリックできるリンクを表示するには、 Browser.msgBoxではなく、SpreadsheetApp.getUi().showModalDialog()で表示する方のUiオブジェクト(HTMLを表示できる)を使う必要があります。そこまではいい。

ところが、これがまたスレッド分かれてしまって、このウインドウを表示していても(たとえ「Modal」ウインドウを表示していも)、GASの処理はウインドウの操作を待たずにどんどん進みます。
(Browser.msgBox()の方は操作を待って次の処理に進んでくれるけどリンクをクリックできるようにできない!)

なので、「リンクを表示→PDFの中身を確認→問題なければ出力用シートを削除」とかやりたくて

function 出力(){
  let PDFのアドレス = 現在のシートをPDF出力()
  let リンク付きのウインドウを表示(PDFのアドレス)
  出力用シートを削除()
}
// それぞれの具体的な処理は割愛

とかすると、リンク用のウインドウを表示した瞬間から裏では出力用シートの削除が走ってしまいます。いやだーー。

というわけでそれを何とかしていきます。

SpreadsheetApp.getUi().showModalDialog()でリンク付きのウインドウを表示する

class メッセージボックス {
  constructor(タイトル, 幅 = 300, 高さ = 300){
    this.タイトル = タイトル
    this.幅 = 幅
    this.高さ = 高さ
    this.メッセージ = []
  }

  メッセージ追記(文字列){
    this.メッセージ.push("<p>" + 文字列 + "</p>")
  }

  表示(){
    let htmlOutput = HtmlService
      .createHtmlOutput(this.メッセージ.join(""))
      .setSandboxMode(HtmlService.SandboxMode.IFRAME)
      .setWidth(this.幅)
      .setHeight(this.高さ);
    return SpreadsheetApp.getUi().showModalDialog(htmlOutput, this.タイトル);
  }
}

とりあえずこれで、簡単なHTMLで装飾した文章を表示できるようになります。

let ウインドウ = new メッセージボックス("TEST")
ウインドウ.追記("<span style="color:red">注意!</span>)
ウインドウ.追記("裏では処理が進行中です")
ウインドウ.表示()

のように使えるようになっています。ちなみにこのウインドウ、タイトルはすぐに出てきますが中身が出てくるまでにちょっと時間がかかるので要注意。

スクリプトを仕込める、複雑なHTMLを使ってウインドウを作る

// コード.gs
function ウインドウを表示する(){
  let htmlOutput = HtmlService.createHtmlOutputFromFile('Index');
  SpreadsheetApp.getUi().showModalDialog(htmlOutput, "HOGE")
}
function HTMLから呼ばれる(){
  Browser.msgBox("呼ばれたよ")
}
<!-- Index.html側 -->
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <script>
      function GASを呼ぶ(){
        google.script.run.HTMLから呼ばれる()
      }
    </script>
  </head>
  <body>
    <button id="testButton" onClick="GASを呼ぶ()">GASを呼ぶ</button>
  </body>
</html>

GASのエディタでHTMLって追加できるじゃないですか。あれずっと何だろうと思ってたんですが、こういうことだそうです。
こうやって作成したHTMLファイル名を、let htmlOutput = HtmlService.createHtmlOutputFromFile('Index');の引数として与えてあげることで、簡易的なウェブページをウインドウとして表示させることができます。

で、google.script.runを経由して、GAS側で定義しておいた関数を呼ぶことができます。こいつもまた裏で勝手に動きます。

これを利用して、出力処理を行う→HTMLを表示する(出力処理のスレッドは終了する)→HTMLからPDF発行処理を呼ぶ→PDF発行処理のスレッドが動き始める、という中継を行うことで、出力が終わった状態のシートをPDFとして発行できるようになります。

GAS側からHTMLに表示する内容を渡す

// コード.gs側
function メッセージを渡す(){
  return "何らかのメッセージ"
}
<!-- Index.html側 -->
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <script>
      function GASを呼ぶ(){
        google.script.run.HTMLから呼ばれる()
      }

    // GASから渡したメッセージを表示する。出力先として、HTML側にIDタグを付与した要素を用意しておく。
      function 渡されたメッセージを取得(メッセージ) {
        var div = document.getElementById('output');
        div.innerHTML = メッセージ;
      }
      // withSuccessHandler経由で実行することで同期的に処理される
      google.script.run.withSuccessHandler(渡されたメッセージを取得).メッセージを渡す();
    // GASから渡したメッセージを表示するの、ここまで
    </script>
  </head>
  <body>
    // 出力用のDOM要素を用意しておく
    <p id="output"> </p>
    <button id="testButton" onClick="GASを呼ぶ()">GASを呼ぶ</button>
  </body>
</html>

通常HTML側から呼んだJsは非同期で処理されてしまい、メッセージを渡す()関数からの戻り値が返ってきていようがいまいが処理が進んでしまうので、outputに入力されるのが空文字になってしまったりしますが、withSuccessHandlerを経由して呼び出すことでGAS側の処理を待ってからHTML側の処理を続けさせることができます。やったね。

あとはこれらを組み合わせて、最初の処理の最後にHTMLウインドウを表示して、HTMLウインドウ側から後続の処理を呼び出すようにすればOKです。
HTMLウインドウがわに仕込んだJsで、GAS側の反応があれば自動で先に進むようにしてもよし、ユーザーに何か確認してもらって、ボタン押したら次に進むようにしてもよし。

この記事が気に入ったらサポートをしてみませんか?