1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[GAS] スプレッドシートとHtmlServiceでHTMLパーツを文字列データとして量産する

Last updated at Posted at 2021-04-07

はじめに

商品情報をスプレッドシートで管理されている方からご相談を受けたときのお話。スプレッドシートから商品情報を参照し、商品ごとのHTMLパーツを自動生成したい、というもの。生成したHTMLはブラウザ表示させるのではなく、商品ごとの文字列データとして、スプレッドシートで管理したい、という追加の要望もありました。

今回はそんな要望に適した、GASをテンプレートエンジン的に使用する小ネタです。

基本的なロジックとしては、テンプレートとなるHTMLパーツを取得し、目印として埋め込んだマーカー(以降、handler)を差し替えるという流れになります。この際GASのHtmlServiceを利用しますが、そのままレンダリングするのではなく文字列として生成する点で、通常の利用用途とは異なります。

HtmlServiceについて

利用用途の多くはGoogleサイトやGoogleフォームでは使えない機能を実装したい場合やSPA(Single Page Application)など、より自由度の高いUI構築が必要な場合かと思います。そのため、通常はスタンドアロンスクリプトとして構築し、デプロイ、httpsレスポンス表示、もしくはコンテナバインドスクリプトにおけるカスタムプロンプト表示用という使い方がメインです。

目的

プロジェクト内にHTMLパーツを登録するため、クライアント様によっては管理しづらいと敬遠されることもありますが、本記事では、HtmlServiceを使ってHTMLデータを生成します。

なお、同様の相談を受けた際、データ内容によってテンプレートを変更したいという要望も多いため、ケースに応じてテンプレートも変更するようにします。

ロジック

100件のダミー商品データ1 に対し、差し替える項目は6項目、テンプレートは2種類としました。どちらのテンプレートを使用するかはデータ内で指定しているものとします。以下、ダミーデータのキャプチャです。
スクリーンショット 2021-04-06 210749.png

プロジェクトへテンプレート登録

スクリプトエディタより、プロジェクト内にHTMLを追加し、それぞれ「typeA」、「typeB」としておきます。

typeA.html
<section>
  <div>
    <h1><?=NAME ?></h1>
    <div>
      <img src="<?=IMG ?>" alt="<?=NAME ?>">
    </div>
    <div><span><?=PRICE ?></span></div>  
    <div>
      <span>カラー:<?=COLOR ?></span>
    </div>
    <div>
      <span>サイズ:<?=SIZE ?></span>
    </div>
    <hr>
    <div>
      <p><?=DESCRIPTION ?></p>
    </div>
  </div>
</section>
typeB.html
<section>
  <div>
    <h1><?=NAME ?></h1>
    <div>
      <img src="<?=IMG ?>" alt="<?=NAME ?>">
    </div>
    <div><span><?=PRICE ?></span></div>
    <div>
      <p><?=DESCRIPTION ?></p>
    </div>
  </div>
  <div>
    <div>
      <label>カラー:</label>
      <select>
        <?!=COLOR ?>
      </select>
    </div>
    <div>
      <label>サイズ:</label>
      <select>
        <?!=SIZE ?>
      </select>
    </div>
  </div>
</section>

サンプルのため小規模かつそれほど差異はないですが、それぞれhandlerの名称は同一とし、テンプレート固有のhandlerは設置していません。相違点としては、テンプレートtypeB.htmlについてはサイズとカラーにバリエーションがあり、<option>を生成する必要があるということです。なお、シート上の当該データはカンマ区切りとなっています。

補足ですが、typeB.html上で使用しているhandlerの内、<?!=XXXXX ?>はタグ(<、>)も含めて出力されるため、2 セキュリティの観点からWEB公開するもの、特にユーザー入力結果を出力するものについては使用しないほうが良いです。3

GASの記述

function generateHTML() {
  const SS       = SpreadsheetApp.getActive()
  const ShData   = SS.getSheetByName('XXXXX') // データシート
  const ShDeploy = SS.getSheetByName('XXXXX') // HTML展開用シート
  const HTMLs    = [] // 生成HTMLセット用配列

  // テンプレート読み込み
  const HtmlTmps = [
    HtmlService.createTemplateFromFile('typeA'),
    HtmlService.createTemplateFromFile('typeB')
  ]

  ShData.getDataRange() // データ取得
  .getValues()
  .slice(1) // ヘッダー行削除
  .forEach(row => { // 順次テンプレートへ展開
    let flag = row[3] === 'A'? true: false
    let html = flag? HtmlTmps[0]: HtmlTmps[1] // テンプレートの選択
    html.NAME        = row[2]
    html.IMG         = row[4]
    html.PRICE       = row[5]
    html.COLOR       = flag? row[6]: genOption(row[6])
    html.SIZE        = flag? row[7]: genOption(row[7])
    html.DESCRIPTION = row[8]

    HTMLs.push([html.evaluate().getBlob().getDataAsString()]) // 生成されたHTMLパーツを配列へセット
  })

  // 指定列へ展開
  ShDeploy.getRange(2, 3, HTMLs.length, 1).setValues(HTMLs)
}

function genOption(val){
  let opt = ''
  val.split(',')
  .forEach(val => {
    opt += `<option value="${val}">${val}</option>\n`
  })
  return opt
}

1-テンプレートの読み込み

事前にテンプレートを読み込み、配列HtmlTmpsへセットしておきます。

2-データを取得し順次テンプレートへ展開

ヘッダー行を削除したShData.getDataRange().getValues().slice(1)forEachで順次テンプレートへ展開します。この際、やりがちなミスは、大きく分けて以下の2点かと思います。当然エラーになります。

  • テンプレートに設定していないhandlerへの値セット
    概ねタイプミスかデバッグ時の消去・追加漏れが原因です。

  • 設定したhandlerへ値をセットできていない(もしくはセットしていない)
    ループ時や条件分岐のロジック不備の他、単純ミスも多いと思います。

テンプレート選択は2択なので、今回はフラグをセットし、3項演算子で分岐しています。テンプレートが3つ以上の場合、switch文を使うなど適宜修正の必要があります。

3-テンプレートを文字列データとして配列へセット

本記事の肝の部分となります。html.evaluate()HtmlOutputとして通常展開した後、文字列として取得するには、getBlob()Blob変換を経由4 してgetDataAsString()とする必要があります。

最後に

今回は単純な構造で短い記述のHTMLパーツということもあり、100件生成で約13秒ほどでした。当然、テンプレートの規模が大きい場合や何十種類もあるようなケースでは、同じ件数でも処理時間が変わってきます。特にテンプレートの規模によってはリソースの無駄遣いが気になりますので、最大公約数部分を抽出するなどの配慮も必要かと思います。

余談ですが、テンプレート内にJavascriptも記述できますが、執筆時点ではコード末尾の「;」は省略不可です。

[参考]

  1. MockarooでダミーのCSVデータを生成。

  2. <?=XXXXX ?>(Printing scriptlets)とは異なり、contextual escaping(出力先のコンテキストを解釈し、適切なエスケープ処理を施す)がされない。

  3. Force-printing scriptlets…注意事項として公式ガイドにも明記されています。

  4. getAs('text/html')で直接取得できればよいのですが、こちらもBlobが返って来ます。

1
2
0

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
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?