บริการ HTML: HTML แบบเทมเพลต

คุณผสมโค้ด Apps Script และ HTML เพื่อสร้างหน้าแบบไดนามิกโดยใช้ความพยายามน้อยที่สุดได้ หากคุณเคยใช้ภาษาเทมเพลตที่ผสมโค้ดและ HTML เช่น PHP, ASP หรือ JSP คุณน่าจะคุ้นเคยกับไวยากรณ์นี้

Scriptlet

เทมเพลตของ Apps Script จะมีแท็กพิเศษที่เรียกว่า Scriptlet ได้ 3 แท็ก คุณสามารถเขียนโค้ดใดก็ได้ที่ใช้งานได้ในไฟล์ Apps Script ปกติภายในสคริปต์ย่อย โดยสคริปต์ย่อยสามารถเรียกใช้ฟังก์ชันที่กําหนดไว้ในไฟล์โค้ดอื่นๆ อ้างอิงตัวแปรส่วนกลาง หรือใช้ Apps Script API ใดก็ได้ คุณยังกําหนดฟังก์ชันและตัวแปรภายในสคริปต์เล็ตได้ด้วย แต่ข้อควรระวังคือฟังก์ชันที่กําหนดไว้ในไฟล์โค้ดหรือเทมเพลตอื่นๆ จะเรียกใช้ไม่ได้

หากคุณวางตัวอย่างด้านล่างลงในเครื่องมือแก้ไขสคริปต์ เนื้อหาของแท็ก <?= ... ?> (สคริปต์การพิมพ์) จะปรากฏเป็นเอียง โค้ดตัวเอียงนั้นจะทำงานบนเซิร์ฟเวอร์ก่อนที่หน้าเว็บจะแสดงให้ผู้ใช้เห็น เนื่องจากโค้ดสคริปต์เลตจะทำงานก่อนที่หน้าเว็บจะแสดง โค้ดดังกล่าวจึงทำงานได้เพียงครั้งเดียวต่อหน้าเว็บ ต่างจากฟังก์ชัน JavaScript ฝั่งไคลเอ็นต์หรือ Apps Script ที่คุณเรียกผ่าน google.script.run ซึ่งสคริปต์เลตจะทำงานไม่ได้อีกหลังจากหน้าเว็บโหลด

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>

โปรดทราบว่าฟังก์ชัน doGet() สำหรับ HTML ที่ใช้เทมเพลตแตกต่างจากตัวอย่างสำหรับการสร้างและการแสดง HTML พื้นฐาน ฟังก์ชันที่แสดงที่นี่จะสร้างออบเจ็กต์ HtmlTemplate จากไฟล์ HTML จากนั้นเรียกใช้เมธอด evaluate() เพื่อเรียกใช้สคริปต์เล็ตและแปลงเทมเพลตให้เป็นออบเจ็กต์ HtmlOutput ที่สคริปต์จะแสดงต่อผู้ใช้ได้

Scriptlet มาตรฐาน

สคริปต์เล็ตมาตรฐานซึ่งใช้ไวยากรณ์ <? ... ?> จะเรียกใช้โค้ดโดยไม่แสดงเนื้อหาไปยังหน้าเว็บอย่างชัดเจน อย่างไรก็ตาม ดังที่ตัวอย่างนี้แสดง ผลลัพธ์ของโค้ดภายในสคริปต์เล็ตจะยังคงส่งผลต่อเนื้อหา 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)

ในตัวอย่างนี้ Scriptlet การพิมพ์ตัวแรกจะแสดงผลสตริงโดยตรง ตามด้วย Scriptlet มาตรฐานที่ตั้งค่าอาร์เรย์และลูป ตามด้วย Scriptlet การพิมพ์อีกตัวหนึ่งเพื่อแสดงเนื้อหาของอาร์เรย์

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!"

การบังคับพิมพ์ Scriptlet

การบังคับให้พิมพ์สคริปต์เล็ตซึ่งใช้ไวยากรณ์ <?!= ... ?> จะเหมือนกับการพิมพ์สคริปต์เล็ต ยกเว้นว่าจะไม่ใช้การหลีกหนีตามบริบท

การกำหนดอักขระหลีกตามบริบทมีความสำคัญหากสคริปต์ของคุณอนุญาตการป้อนข้อมูลของผู้ใช้ที่ไม่น่าเชื่อถือ ในทางตรงกันข้าม คุณจะต้องบังคับให้พิมพ์หากเอาต์พุตของสคริปต์เล็ตมี HTML หรือสคริปต์ที่คุณต้องการแทรกตามที่ระบุไว้

โดยทั่วไปแล้ว ให้ใช้สคริปต์การพิมพ์แทนสคริปต์การพิมพ์แบบบังคับ เว้นแต่คุณจะทราบว่าต้องพิมพ์ HTML หรือ JavaScript โดยไม่เปลี่ยนแปลง

โค้ด Apps Script ใน Scriptlet

Scriptlet ไม่ได้จำกัดอยู่ที่การเรียกใช้ JavaScript ปกติ แต่คุณยังสามารถใช้เทคนิค 3 อย่างต่อไปนี้เพื่อให้เทมเพลตเข้าถึงข้อมูล Apps Script ได้

อย่างไรก็ตาม โปรดทราบว่าเทคนิคเหล่านี้จะแสดงเนื้อหาเริ่มต้นในหน้าเว็บได้เท่านั้น เนื่องจากโค้ดเทมเพลตจะทํางานก่อนที่หน้าเว็บจะแสดงต่อผู้ใช้ หากต้องการเข้าถึงข้อมูล Apps Script จากหน้าเว็บแบบอินเทอร์แอกทีฟ ให้ใช้ google.script.run API แทน

การเรียกใช้ฟังก์ชัน Apps Script จากเทมเพลต

สคริปต์เล็ตสามารถเรียกใช้ฟังก์ชันใดก็ได้ที่กําหนดไว้ในไฟล์หรือไลบรารีโค้ด Apps Script ตัวอย่างนี้แสดงวิธีดึงข้อมูลจากสเปรดชีตไปยังเทมเพลต จากนั้นสร้างตาราง HTML จากข้อมูล

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>

เทมเพลตการแก้ไขข้อบกพร่อง

การแก้ไขข้อบกพร่องของเทมเพลตอาจเป็นเรื่องยาก เนื่องจากระบบจะไม่เรียกใช้โค้ดที่คุณเขียนโดยตรง แต่เซิร์ฟเวอร์จะเปลี่ยนรูปแบบเทมเพลตเป็นโค้ด แล้วเรียกใช้โค้ดที่ได้

หากไม่แน่ใจว่าเทมเพลตตีความสคริปต์เล็ตของคุณอย่างไร วิธีการแก้ไขข้อบกพร่อง 2 วิธีในคลาส HtmlTemplate จะช่วยให้คุณเข้าใจสิ่งที่เกิดขึ้นได้ดียิ่งขึ้น

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>

LOG (EVALUATED)

(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() แต่แสดงผลโค้ดที่ประเมินแล้วเป็นความคิดเห็นที่ปรากฏควบคู่ไปกับเทมเพลตต้นฉบับ

เดินชมโค้ดที่ประเมินแล้ว

สิ่งแรกที่คุณจะสังเกตเห็นในตัวอย่างโค้ดที่ประเมินแล้วคือออบเจ็กต์ output ที่ไม่ระบุซึ่งสร้างโดยเมธอด HtmlService.initTemplate() วิธีการนี้ไม่มีเอกสารประกอบเนื่องจากมีเพียงเทมเพลตเท่านั้นที่ต้องใช้ 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. */ ?>