はじめに
商品情報をスプレッドシートで管理されている方からご相談を受けたときのお話。スプレッドシートから商品情報を参照し、商品ごとのHTMLパーツを自動生成したい、というもの。生成したHTMLはブラウザ表示させるのではなく、商品ごとの文字列データとして、スプレッドシートで管理したい、という追加の要望もありました。
今回はそんな要望に適した、GASをテンプレートエンジン的に使用する小ネタです。
基本的なロジックとしては、テンプレートとなるHTMLパーツを取得し、目印として埋め込んだマーカー(以降、handler
)を差し替えるという流れになります。この際GASのHtmlServiceを利用しますが、そのままレンダリングするのではなく文字列として生成する点で、通常の利用用途とは異なります。
HtmlServiceについて
利用用途の多くはGoogleサイトやGoogleフォームでは使えない機能を実装したい場合やSPA(Single Page Application)など、より自由度の高いUI構築が必要な場合かと思います。そのため、通常はスタンドアロンスクリプトとして構築し、デプロイ、httpsレスポンス表示、もしくはコンテナバインドスクリプトにおけるカスタムプロンプト表示用という使い方がメインです。
目的
プロジェクト内にHTMLパーツを登録するため、クライアント様によっては管理しづらいと敬遠されることもありますが、本記事では、HtmlServiceを使ってHTMLデータを生成します。
なお、同様の相談を受けた際、データ内容によってテンプレートを変更したいという要望も多いため、ケースに応じてテンプレートも変更するようにします。
ロジック
100件のダミー商品データ1 に対し、差し替える項目は6項目、テンプレートは2種類としました。どちらのテンプレートを使用するかはデータ内で指定しているものとします。以下、ダミーデータのキャプチャです。
プロジェクトへテンプレート登録
スクリプトエディタより、プロジェクト内にHTMLを追加し、それぞれ「typeA」、「typeB」としておきます。
<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>
<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も記述できますが、執筆時点ではコード末尾の「;」は省略不可です。
[参考]
- Class HtmlService | Apps Script | Google Developers
- HTML Service: Templated HTML | Apps Script | Google Developers