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

Apps Script のコードと HTML を組み合わせることで、最小限の労力で動的ページを生成できます。コードと HTML が混在するテンプレート言語(PHP、ASP、JSP など)を使用している場合、構文は違和感なく使用できます。

スクリプトレット

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

下記の例をスクリプト エディタに貼り付けると、<?= ... ?> タグ(出力スクリプレット)の内容が斜体で表示されます。この斜体のコードは、ページがユーザーに表示される前にサーバー上で実行されます。スクリプレット コードはページが提供される前に実行されるため、1 ページにつき 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 のコードファイルまたはライブラリで定義されている任意の関数を呼び出すことができます。この例では、スプレッドシートからテンプレートにデータを pull し、そのデータから 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>

テンプレートに変数を push する

最後に、変数を HtmlTemplate オブジェクトのプロパティとして代入することで、変数をテンプレートに push できます。やはり、この例の結果は前の例と同じ結果になります。

コード.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>

ログ(評価済み)

(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 は特別な HtmlOutput オブジェクトで、通常は名前が異なり、__$ という 2 つのプロパティがあります。これらは append()appendUntrusted() の呼び出しの省略形です。

output にはもう 1 つの特殊なプロパティ、$out があります。これは、こうした特別なプロパティを持たない通常の 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. */ ?>