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

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

Scriptlets

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

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

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() เพื่อเรียกใช้ Scriptlet และแปลงเทมเพลตเป็นออบเจ็กต์ HtmlOutput ที่สคริปต์สามารถแสดงต่อผู้ใช้

สคริปต์เล็ตมาตรฐาน

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

กำลังพิมพ์ Scriptlet

การพิมพ์ Scriptlet ซึ่งใช้ไวยากรณ์ <?= ... ?> จะแสดงผลลัพธ์ของโค้ดในหน้าเว็บโดยใช้การ Escape ตามบริบท

การกำหนดเป็นอักขระหลีกตามบริบทหมายความว่า Apps Script จะติดตามบริบทของเอาต์พุตในหน้าเว็บ ทั้งภายในแอตทริบิวต์ HTML ภายในแท็ก script ฝั่งไคลเอ็นต์ หรือที่อื่นๆ และเพิ่มอักขระหลีกโดยอัตโนมัติเพื่อป้องกันการโจมตีด้วย Cross-site Scripting (XSS)

ในตัวอย่างนี้ 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>

โปรดทราบว่า Scriptlet การพิมพ์จะแสดงผลแค่ค่าของคำสั่งแรกเท่านั้น คำสั่งที่เหลือทั้งหมดจะทำงานเหมือนกับว่าอยู่ใน Scriptlet มาตรฐาน ตัวอย่างเช่น สคริปต์ <?= 'Hello, world!'; 'abc' ?> จะพิมพ์แค่ "สวัสดีโลก!"

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

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

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

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

โค้ด Apps Script ใน Scriptlet

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

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

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

Scriptlets สามารถเรียกใช้ฟังก์ชันใดก็ได้ที่กำหนดไว้ในไฟล์โค้ด 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 ใน Scriptlet ได้โดยตรง ตัวอย่างนี้จะให้ผลลัพธ์เดียวกับตัวอย่างก่อนหน้านี้ด้วยการโหลดข้อมูลในเทมเพลตแทนการใช้ผ่านฟังก์ชันแยกต่างหาก

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 (ประเมินแล้ว)

(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 ที่อยู่นอก Scriptlet (เช่น แท็ก b) จะถูกผนวกเข้ากับ output._ = (โดยไม่มีการ Escape ตามบริบท) และต่อท้าย Scriptlet ด้วย JavaScript (มีหรือไม่มีการ Escape ตามบริบท ทั้งนี้ขึ้นอยู่กับประเภทของ Scriptlet)

โปรดทราบว่าโค้ดที่ประเมินจะเก็บหมายเลขบรรทัดจากเทมเพลตไว้ หากได้รับข้อผิดพลาดขณะเรียกใช้โค้ดที่ประเมิน บรรทัดจะตรงกับเนื้อหาที่เทียบเท่าในเทมเพลต

ลำดับขั้นของความคิดเห็น

เนื่องจากโค้ดที่ประเมินจะเก็บหมายเลขบรรทัดไว้ ความคิดเห็นภายใน Scriptlet จึงอาจแสดงความคิดเห็นจาก Scriptlet อื่นๆ หรือแม้แต่โค้ด 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. */ ?>