Servicio HTML: HTML basado en plantillas

Puedes combinar código de Apps Script y HTML para producir páginas dinámicas con un mínimo esfuerzo. Si usaste un lenguaje de plantillas que mezcla código y HTML, como PHP, ASP o JSP, la sintaxis te resultará familiar.

Scriptlets

Las plantillas de Apps Script pueden contener tres etiquetas especiales, llamadas scriptlets. Dentro de un scriptlet, puedes escribir cualquier código que funcione en un archivo normal de Apps Script: los scriptlets pueden llamar a funciones definidas en otros archivos de código, hacer referencia a variables globales o usar cualquiera de las APIs de Apps Script. Incluso puedes definir funciones y variables dentro de los scriptlets, con la advertencia de que no pueden ser llamadas por funciones definidas en archivos de código o en otras plantillas.

Si pegas el siguiente ejemplo en el editor de secuencias de comandos, el contenido de la etiqueta <?= ... ?> (un scriptlet de impresión) aparecerá en cursiva. Ese código en cursiva se ejecuta en el servidor antes de que se muestre la página al usuario. Dado que el código de los scriptlets se ejecuta antes de que se publique la página, solo se puede ejecutar una vez por página. A diferencia de las funciones de JavaScript del cliente o de Apps Script que llamas a través de google.script.run, los scriptlets no se pueden volver a ejecutar después de que se carga la página.

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>

Ten en cuenta que la función doGet() para el código HTML basado en plantillas difiere de los ejemplos para crear y publicar código HTML básico. La función que se muestra aquí genera un objeto HtmlTemplate a partir del archivo HTML y, luego, llama a su método evaluate() para ejecutar los scriptlets y convertir la plantilla en un objeto HtmlOutput que la secuencia de comandos puede entregar al usuario.

Scriptlets estándar

Los scriptlets estándar, que usan la sintaxis <? ... ?>, ejecutan código sin generar contenido de forma explícita en la página. Sin embargo, como se muestra en este ejemplo, el resultado del código dentro de un scriptlet aún puede afectar el contenido HTML fuera del scriptlet:

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>

Secuencias de comandos de impresión

Los scriptlets de impresión, que usan la sintaxis <?= ... ?>, generan los resultados de su código en la página con escape contextual.

El escape contextual significa que Apps Script realiza un seguimiento del contexto de salida en la página (dentro de un atributo HTML, dentro de una etiqueta script del cliente o en cualquier otro lugar) y agrega automáticamente caracteres de escape para proteger contra ataques de secuencias de comandos entre sitios (XSS).

En este ejemplo, el primer scriptlet de impresión genera una cadena directamente. Le sigue un scriptlet estándar que configura un array y un bucle, y otro scriptlet de impresión para generar el contenido del array.

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>

Ten en cuenta que un scriptlet de impresión solo genera el valor de su primera instrucción; las instrucciones restantes se comportan como si estuvieran contenidas en un scriptlet estándar. Por ejemplo, el scriptlet <?= 'Hello, world!'; 'abc' ?> solo imprime "Hello, world!".

Scriptlets de impresión forzada

Los scriptlets de impresión forzada, que usan la sintaxis <?!= ... ?>, son similares a los scriptlets de impresión, pero evitan el escape contextual.

El escape contextual es importante si tu secuencia de comandos permite la entrada del usuario no confiable. Por el contrario, deberás forzar la impresión si la salida de tu scriptlet contiene intencionalmente HTML o secuencias de comandos que deseas insertar exactamente como se especificó.

Como regla general, usa scriptlets de impresión en lugar de scriptlets de impresión forzada, a menos que sepas que necesitas imprimir HTML o JavaScript sin cambios.

Código de Apps Script en scriptlets

Los scriptlets no se limitan a ejecutar JavaScript normal, sino que también puedes usar cualquiera de las siguientes tres técnicas para que tus plantillas accedan a los datos de Apps Script.

Sin embargo, recuerda que, como el código de la plantilla se ejecuta antes de que se muestre la página al usuario, estas técnicas solo pueden proporcionar contenido inicial a una página. Para acceder a los datos de Apps Script desde una página de forma interactiva, usa la API de google.script.run.

Cómo llamar a funciones de Apps Script desde una plantilla

Los scriptlets pueden llamar a cualquier función definida en un archivo de código o biblioteca de Apps Script. En este ejemplo, se muestra una forma de extraer datos de una hoja de cálculo a una plantilla y, luego, construir una tabla HTML a partir de los datos.

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>

Llamar directamente a las APIs de Apps Script

También puedes usar código de Apps Script directamente en los scriptlets. Este ejemplo logra el mismo resultado que el anterior, pero carga los datos en la plantilla en lugar de hacerlo a través de una función separada.

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>

Envío de variables a plantillas

Por último, puedes insertar variables en una plantilla asignándolas como propiedades del objeto HtmlTemplate. Una vez más, este ejemplo logra el mismo resultado que los ejemplos anteriores.

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>

Cómo depurar plantillas

Las plantillas pueden ser difíciles de depurar porque el código que escribes no se ejecuta directamente. En cambio, el servidor transforma tu plantilla en código y, luego, ejecuta ese código resultante.

Si no es obvio cómo la plantilla interpreta tus fragmentos de secuencia de comandos, dos métodos de depuración en la clase HtmlTemplate pueden ayudarte a comprender mejor lo que sucede.

getCode()

getCode() devuelve una cadena que contiene el código que el servidor crea a partir de la plantilla. Si registras el código y, luego, lo pegas en el editor de secuencias de comandos, puedes ejecutarlo y depurarlo como si fuera código normal de Apps Script.

Aquí tienes la plantilla simple que vuelve a mostrar una lista de productos de Google, seguida del resultado de 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>

REGISTRO (EVALUADO)

(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() es similar a getCode(), pero devuelve el código evaluado como comentarios que aparecen uno al lado del otro con la plantilla original.

Cómo recorrer el código evaluado

Lo primero que notarás en cualquiera de los ejemplos de código evaluado es el objeto output implícito creado por el método HtmlService.initTemplate(). Este método no está documentado porque solo las plantillas lo necesitan. output es un objeto HtmlOutput especial con dos propiedades con nombres inusuales, _ y _$, que son abreviaturas para llamar a append() y appendUntrusted().

output tiene una propiedad especial más, $out, que hace referencia a un objeto HtmlOutput normal que no posee estas propiedades especiales. La plantilla devuelve ese objeto normal al final del código.

Ahora que comprendes esta sintaxis, el resto del código debería ser bastante fácil de seguir. El contenido HTML fuera de los scriptlets (como la etiqueta b) se agrega con output._ = (sin escape contextual), y los scriptlets se agregan como JavaScript (con o sin escape contextual, según el tipo de scriptlet).

Ten en cuenta que el código evaluado conserva los números de línea de la plantilla. Si recibes un error mientras ejecutas código evaluado, la línea corresponderá al contenido equivalente en la plantilla.

Jerarquía de comentarios

Dado que el código evaluado conserva los números de línea, es posible que los comentarios dentro de los scriptlets comenten otros scriptlets e incluso código HTML. En estos ejemplos, se muestran algunos efectos sorprendentes de los comentarios:

<? 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. */ ?>