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 の関数と異なり、ページが読み込まれた後にスクリプトレットを再実行することはできません。

Code.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 コンテンツにも影響します。

Code.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)攻撃から保護することを意味します。

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

Code.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 つの方法を示します。

Code.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 のコードをスクリプトレットで直接使用することもできます。この例では、別の関数を使用するのではなく、テンプレート自体にデータを読み込むことで、前の例と同じ結果が得られます。

Code.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 オブジェクトのプロパティとして代入することで、変数をテンプレートにプッシュできます。やはり、この例は前の例と同じ結果になります。

Code.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() の結果を表示します。

Code.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 は、通常は名前が付いていない 2 つのプロパティ(__$)を持つ特別な HtmlOutput オブジェクトです。これらは 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. */ ?>