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 テーブルを作成する方法を示しています。

コード.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 は、append()appendUntrusted() を呼び出すための短縮形である __$ という 2 つの特殊な名前のプロパティを持つ特別な HtmlOutput オブジェクトです。

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

この構文を理解していれば、残りのコードは比較的簡単に理解できるはずです。スクリプトレット外の 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. */ ?>