HTML サービス: テンプレート化された HTML

Apps Script コードと HTML を組み合わせて、最小限の労力で動的ページを作成できます。PHP、ASP、JSP など、コードと HTML を組み合わせたテンプレート言語を使用したことがある場合は、構文に親しみがあるはずです。

スクリプトレット

Apps Script テンプレートには、スクリプレットと呼ばれる 3 つの特殊タグを含めることができます。スクリプトレット内には、通常の Apps Script ファイルで機能する任意のコードを記述できます。スクリプトレットでは、他のコードファイルで定義された関数を呼び出したり、グローバル変数を参照したり、任意の Apps Script API を使用したりできます。スクリプトレット内で関数と変数を定義することもできますが、コードファイルや他のテンプレートで定義された関数から呼び出すことはできません。

次の例をスクリプト エディタに貼り付けると、<?= ... ?> タグの内容(印刷スクリプトレット)が斜体で表示されます。太字のコードは、ページがユーザーに提供される前にサーバーで実行されます。スクリプトレット コードはページが提供される前に実行されるため、ページごとに 1 回しか実行できません。google.script.run を介して呼び出すクライアントサイドの JavaScript 関数や Apps Script 関数とは異なり、スクリプトレットはページの読み込み後に再度実行できません。

コード.gs

function doGet() {
  return HtmlService
      .createTemplateFromFile('Index')
      .evaluate();
}

Index.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    Hello, World! The time is <?= new Date() ?>.
  </body>
</html>

テンプレート HTML の doGet() 関数は、基本的な HTML の作成と提供の例とは異なります。ここに示す関数は、HTML ファイルから HtmlTemplate オブジェクトを生成し、その evaluate() メソッドを呼び出してスクリプレットを実行し、テンプレートをスクリプトがユーザーに提供できる HtmlOutput オブジェクトに変換します。

標準スクリプトレット

<? ... ?> 構文を使用する標準スクリプトレットでは、コンテンツをページに明示的に出力せずにコードを実行します。ただし、この例に示すように、スクリプトレット内のコードの結果は、スクリプトレット外の HTML コンテンツに影響する可能性があります。

コード.gs

function doGet() {
  return HtmlService
      .createTemplateFromFile('Index')
      .evaluate();
}

Index.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <? if (true) { ?>
      <p>This will always be served!</p>
    <? } else  { ?>
      <p>This will never be served.</p>
    <? } ?>
  </body>
</html>

スクリプトレット印刷

<?= ... ?> 構文を使用する印刷スクリプトレットでは、コンテキスト エスケープを使用してコードの結果をページに出力します。

コンテキスト エスケープとは、Apps Script がページ上(HTML 属性内、クライアントサイドの script タグ内、またはその他の場所)の出力のコンテキストを追跡し、エスケープ文字を自動的に追加してクロスサイト スクリプティング(XSS)攻撃から保護することを意味します。

この例では、最初の出力スクリプレットは文字列を直接出力します。次に、配列とループを設定する標準スクリプレットが続き、さらに配列の内容を出力する別の出力スクリプレットが続きます。

コード.gs

function doGet() {
  return HtmlService
      .createTemplateFromFile('Index')
      .evaluate();
}

Index.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <?= 'My favorite Google products:' ?>
    <? var data = ['Gmail', 'Docs', 'Android'];
      for (var i = 0; i < data.length; i++) { ?>
        <b><?= data[i] ?></b>
    <? } ?>
  </body>
</html>

出力スクリプトレットでは、最初のステートメントの値のみが出力されます。残りのステートメントは、標準スクリプトレットに含まれているかのように動作します。たとえば、スクリプトレット <?= 'Hello, world!'; 'abc' ?> は「Hello, world!」のみを出力します。

スクリプトレット強制出力

<?!= ... ?> 構文を使用する強制出力スクリプトレットは、コンテキスト エスケープを回避するという点を除き、出力スクリプトレットと同様です。

スクリプトで信頼できないユーザー入力を許可している場合は、コンテキスト エスケープが重要です。一方、指定どおりに挿入する HTML またはスクリプトをスクリプレットの出力に意図的に含める場合は、強制的に出力する必要があります。

一般的なルールとして、HTML または JavaScript を変更せずに印刷する必要がある場合を除き、強制印刷スクリプトレットではなく、印刷スクリプトレットを使用します。

スクリプトレット内の Apps Script コード

スクリプトレットは、通常の JavaScript の実行に限定されません。次の 3 つの方法のいずれかを使用して、テンプレートに Apps Script データへのアクセス権を付与することもできます。

ただし、テンプレート コードはページがユーザーに提供される前に実行されるため、これらの手法ではページに初期コンテンツのみをフィードできます。ページから Apps Script データにインタラクティブにアクセスするには、代わりに google.script.run API を使用します。

テンプレートから Apps Script 関数を呼び出す

スクリプトレットでは、Apps Script のコードファイルまたはライブラリで定義された任意の関数を呼び出すことができます。この例は、スプレッドシートからテンプレートにデータを取得し、そのデータから HTML テーブルを作成する 1 つの方法を示しています。

コード.gs

function doGet() {
  return HtmlService
      .createTemplateFromFile('Index')
      .evaluate();
}

function getData() {
  return SpreadsheetApp
      .openById('1234567890abcdefghijklmnopqrstuvwxyz')
      .getActiveSheet()
      .getDataRange()
      .getValues();
}

Index.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <? var data = getData(); ?>
    <table>
      <? for (var i = 0; i < data.length; i++) { ?>
        <tr>
          <? for (var j = 0; j < data[i].length; j++) { ?>
            <td><?= data[i][j] ?></td>
          <? } ?>
        </tr>
      <? } ?>
    </table>
  </body>
</html>

Apps Script API を直接呼び出す

また、スクリプトレット内で Apps Script コードを直接使用することもできます。この例では、別個の関数ではなくテンプレート自体にデータを読み込むことで、前述の例と同じ結果を実現しています。

コード.gs

function doGet() {
  return HtmlService
      .createTemplateFromFile('Index')
      .evaluate();
}

Index.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <? var data = SpreadsheetApp
        .openById('1234567890abcdefghijklmnopqrstuvwxyz')
        .getActiveSheet()
        .getDataRange()
        .getValues(); ?>
    <table>
      <? for (var i = 0; i < data.length; i++) { ?>
        <tr>
          <? for (var j = 0; j < data[i].length; j++) { ?>
            <td><?= data[i][j] ?></td>
          <? } ?>
        </tr>
      <? } ?>
    </table>
  </body>
</html>

テンプレートに変数をプッシュする

最後に、変数を HtmlTemplate オブジェクトのプロパティとして割り当てることで、変数をテンプレートにプッシュできます。この例でも、前の例と同じ結果が得られます。

コード.gs

function doGet() {
  var t = HtmlService.createTemplateFromFile('Index');
  t.data = SpreadsheetApp
      .openById('1234567890abcdefghijklmnopqrstuvwxyz')
      .getActiveSheet()
      .getDataRange()
      .getValues();
  return t.evaluate();
}

Index.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <table>
      <? for (var i = 0; i < data.length; i++) { ?>
        <tr>
          <? for (var j = 0; j < data[i].length; j++) { ?>
            <td><?= data[i][j] ?></td>
          <? } ?>
        </tr>
      <? } ?>
    </table>
  </body>
</html>

テンプレートのデバッグ

テンプレートは、作成したコードが直接実行されず、サーバーがテンプレートをコードに変換してからそのコードを実行するため、デバッグが難しい場合があります。

テンプレートがスクリプトレットをどのように解釈しているかが不明な場合は、HtmlTemplate クラスの 2 つのデバッグ方法を使用して、状況を把握できます。

getCode()

getCode() は、サーバーがテンプレートから作成するコードを含む文字列を返します。コードをログに記録してスクリプト エディタに貼り付けると、通常の Apps Script コードと同様に実行してデバッグできます。

以下は、Google プロダクトのリストを再度表示し、その後に getCode() の結果を表示するシンプルなテンプレートです。

コード.gs

function myFunction() {
  Logger.log(HtmlService
      .createTemplateFromFile('Index')
      .getCode());
}

Index.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <?= 'My favorite Google products:' ?>
    <? var data = ['Gmail', 'Docs', 'Android'];
      for (var i = 0; i < data.length; i++) { ?>
        <b><?= data[i] ?></b>
    <? } ?>
  </body>
</html>

LOG(評価済み)

(function() { var output = HtmlService.initTemplate(); output._ =  '<!DOCTYPE html>\n';
  output._ =  '<html>\n' +
    '  <head>\n' +
    '    <base target=\"_top\">\n' +
    '  </head>\n' +
    '  <body>\n' +
    '    '; output._$ =  'My favorite Google products:' ;
  output._ =  '    ';  var data = ['Gmail', 'Docs', 'Android'];
        for (var i = 0; i < data.length; i++) { ;
  output._ =  '        <b>'; output._$ =  data[i] ; output._ =  '</b>\n';
  output._ =  '    ';  } ;
  output._ =  '  </body>\n';
  output._ =  '</html>';
  /* End of user code */
  return output.$out.append('');
})();

getCodeWithComments()

getCodeWithComments()getCode() に似ていますが、評価されたコードをコメントとして返します。このコメントは元のテンプレートの横に表示されます。

評価されたコードを確認する

評価対象のコードのサンプルのいずれでも、まず目に留まるのは、メソッド HtmlService.initTemplate() によって作成される暗黙的な output オブジェクトです。このメソッドは、テンプレート自体でのみ使用する必要があることにより、ドキュメント化されていません。output は、__$ という 2 つの通常とは異なる名前のプロパティを持つ特別な HtmlOutput オブジェクトです。これは、append()appendUntrusted() の呼び出しの省略形です。

output には、$out という特別なプロパティがもう 1 つあります。これは、これらの特別なプロパティを持たない通常の HtmlOutput オブジェクトを参照します。テンプレートは、コードの最後にその通常のオブジェクトを返します。

この構文を理解できたら、残りのコードは比較的簡単に理解できるはずです。スクリプトレット外の HTML コンテンツ(b タグなど)は output._ = を使用して(コンテキスト エスケープなしで)追加され、スクリプトレットは JavaScript として追加されます(スクリプトレットのタイプに応じて、コンテキスト エスケープありまたはなし)。

評価されたコードは、テンプレートの行番号を保持します。評価されたコードの実行中にエラーが発生した場合、その行はテンプレートの同等のコンテンツに対応します。

コメントの階層

評価されたコードは行番号を保持するため、スクリプトレット内のコメントで他のスクリプトレットや HTML コードをコメントアウトできます。次の例は、コメントがもたらす驚くべき効果を示しています。

<? var x; // a comment ?> This sentence won't print because a comment begins inside a scriptlet on the same line.

<? var y; // ?> <?= "This sentence won't print because a comment begins inside a scriptlet on the same line.";
output.append("This sentence will print because it's on the next line, even though it's in the same scriptlet.”) ?>

<? doSomething(); /* ?>
This entire block is commented out,
even if you add a */ in the HTML
or in a <script> */ </script> tag,
<? until you end the comment inside a scriptlet. */ ?>