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

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

スクリプレット

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>

テンプレートへの変数のプッシュ

最後に、変数を 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. */ ?>