top of page
ค้นหา
รูปภาพนักเขียนSathit Jittanupat

"Lit-html" first time: unsafeHTML vs. templateContent vs. eval

อัปเดตเมื่อ 19 ก.พ. 2567

เมื่อสองวันก่อนเพิ่งได้ดูรายละเอียด Lit ซึ่งเป็นโปรเจคท์ที่นำเสนอแนวทางพัฒนา Web Components ซึ่งดูแล้วง่ายกว่า polymer ที่หยุดพัฒนาแล้วและแนะนำให้ย้ายมาใช้ Lit (ทั้งคู่เป็นโปรเจคท์ของ Google) ไม่รู้ว่าผมไปอยู่ไหนจึงไม่ได้ตามข่าวคราวเลย จนตอนนี้ออก version 3 มาแล้ว


ความประทับใจแรก ปรัชญาความเรียบง่าย ละทิ้งพิธีกรรมที่ต้องขึ้นโปรเจคท์ด้วยการติดตั้ง build tools มากมายตามยุคสมัย สามารถเริ่มทดลองเขียนโค้ดด้วยการสร้างไฟล์ HTML ไฟล์เดียว เปิดดูจาก browser ไม่ต้องมีแม้กระทั่ง Live Server


ใช่ครับ! เริ่มต้นง่ายแค่นี้เอง เหมือนครั้งหนึ่งที่เราใช้แค่ notepad ก็ลองเขียนเว็บได้



หลังจากที่สำรวจข้อมูลคร่าว ๆ พบว่า แก่นสำคัญของ lit อยู่ที่การใช้ประโยชน์จากมาตรฐาน JavaScript ES6 (2015) Template Literal เพิ่ม htmlเข้ามาทำให้การเขียนโค้ดที่มี HTML syntax สามารถทำได้ภายในไฟล์ .js โดยไม่ต้อง compile หรือใช้ build tools



const content = html`<h1>Hello ${name || "World"}</h1>`;

ที่สำคัญกว่านั้น html template สามารถนำมาใช้กับโค้ดทั่วไป โดยไม่จำเป็นต้องผูกอยู่ใน LitElement เท่านั้น มี monorepo lit-html แยกออกมาเป็น standalone package


ประเด็นที่ผมสนใจและพยายามหามาตลอดคือ ความสามารถ embedded HTML โดยไม่ต้อง compile ใหม่ ซึ่งทำให้สามารถแยกงานระหว่าง developer ที่ดูแลโค้ดโปรแกรม กับ implementer ที่ customize งานให้กับไซต์ต่าง ๆ ที่ดูแล


Single HTML with CDN

เริ่มต้นจากทดสอบเขียน HTML ไฟล์เดียว แล้วเปิดใน browser โดยไม่ต้องมี server สามารถ import package lit-html จาก CDN มีให้เลือกใช้ unpkg กับ jsdelivr (minified)


  • ใช้ text editor สร้างไฟล์ใหม่ชื่อ lit-html.html 

  • copy&paste โค้ดด้านล่าง 

  • save แล้วเอา path ของไฟล์ไปเปิดด้วย browser



<script type="module">
  import {html, render} from 'https://unpkg.com/lit-html@3.1.2/lit-html.js';

  render(html`<h1>Hello World</h1>`, document.body)
</script>

คำสั่ง html ใช้กับ template literal ดังที่กล่าวมาแล้ว

 

ส่วนคำสั่ง render ใช้สำหรับเอาผลลัพธ์จาก html template นั้นไปใส่ใน HTML element <body> เพื่อแสดงผลใน browser



ต่อไปผมทดสอบ render โดยการส่งข้อความที่เป็น text กับข้อความที่ผ่าน html template


ลองสังเกตผลลัพธ์ที่ได้ จะเห็นว่าข้อความ text ไม่ถูกแปลงเป็น element



<!doctype html>
<html>
<!---->
<body>
  <div id="text"></div> 
  <div id="html"></div> 
</body>

<!----> 
<script type="module">
  import {html, render} from 'https://unpkg.com/lit-html@3.1.2/lit-html.js';

  render(`<h1>Hello World</h1>`, document.querySelector('div#text'))
  render(html`<h1>Hello World</h1>`, document.querySelector('div#html'))

</script>
</html>

unsafeHTML( text )

ตัว lit-html เองมี built-in directive unsafeHTML สามารถแทรก external HTML เข้าไปได้ 

สมมติว่ามี userdata ซึ่งประกอบด้วยฟิลด์ name, fullname และ profileHTML ที่เป็น embedded HTML เพื่อใช้ทดสอบ


  • เพิ่ม import package unsafe-html 

  • เพิ่ม <div id="profile"></div> 

  • เพิ่ม render profileHTML 



ผลลัพธ์จากโค้ดตัวอย่างด้านล่าง จะเห็นว่า unsafeHTML รองรับเฉพาะแปลง HTML text เป็น element ไม่สามารถใช้งาน nested template ได้ ไม่สามารถแทนที่ค่าที่อยู่ใน ${…}



<!doctype html>
<html>
<!---->
<body>

  <div id="greet"></div>
  <hr/>
  <div id="profile" style="border:solid thin; padding:8px;"></div>
  
</body>

<!----> 
<script type="module">
  import {html, render} from 'https://unpkg.com/lit-html@3.1.2/lit-html.js';
  import {unsafeHTML} from 'https://unpkg.com/lit-html@3.1.2/directives/unsafe-html.js';

  const userdata = {
    name: 'John',
    fullname: 'John Doe',
    profileHtml: [
    '<p>',
    '  <b>Name:</b> ${userdata.name}<br/>',
    '  <b>Full Name:</b>  ${userdata.fullname}<br/>',
    '</p>',
    ].join('')
  }

  const greetHTML = _data => (html`
    <h1>Hello ${_data?.name || 'World'}</h1>
  `)

  render(greetHTML(userdata), document.querySelector('div#greet'))

  // profile
  const profileHTML = _data => (html`
    <h3>Profile</h3>
    <div>${unsafeHTML(_data?.profileHtml)}</div>
  `)

  render(profileHTML(userdata), document.querySelector('div#profile'))

</script>
</html>

templateContent(elem)

ใช้ built-in directive templateContent แทนที่จะ embedded ไว้ใน text เปลี่ยนเป็นเอาไว้ใน HTML element <template>..</template>คล้ายกับกรณีของ unsafeHTML ก่อนหน้านี้


  <template id="profile">
    <p>
      <b>Name:</b> ${userdata.name}<br/>
      <b>Full Name:</b>  ${userdata.fullname}<br/>
    </p>
  </template>

พอเขียน template ด้วยโครงสร้าง HTML แบบนี้ ผมพอจะนึกถึงทางเลือกที่อาจใช้ในการพัฒนาได้มากขึ้น แต่น่าเสียดายผลลัพธ์ที่ได้ก็ไม่ต่างกัน สามารถเอา element ที่สร้างไว้ใน <template> มาใช้ได้ แต่ไม่สามารถประมวลผลแทนค่า ${..}




<!doctype html>
<html>
<!---->
<body>
  <div id="greet"></div>
  <hr/>
  <div id="profile" style="border:solid thin; padding:8px;"></div>
  
  <template id="profile">
    <p>
      <b>Name:</b> ${userdata.name}<br/>
      <b>Full Name:</b>  ${userdata.fullname}<br/>
    </p>
  </template>
</body>

<!----> 
<script type="module">
  import {html, render} from 'https://unpkg.com/lit-html@3.1.2/lit-html.js';
  import {templateContent} from 'https://unpkg.com/lit-html@3.1.2/directives/template-content.js';

  const userdata = {
    name: 'John',
    fullname: 'John Doe',
  }

  const greetHTML = _data => (html`
    <h1>Hello ${_data?.name || 'World'}</h1>
  `)

  render(greetHTML(userdata), document.querySelector('div#greet'))

  // profile
  const profileHTML = _data => (html`
    <h3>Profile</h3>
    <div>${templateContent(document.querySelector('template#profile'))}</div>
  `)

  render(profileHTML(userdata), document.querySelector('div#profile'))

</script>
</html>

eval( “html`" + … + “`" )

มีคำถามใน StackOverflow เกี่ยวกับการใช้ external text กับ Lit และคำตอบหนึ่งบอกว่า สามารถใช้ eval() ได้


  // profile
  const profileHTML = _data => (html`
    <h3>Profile</h3>
    <div>${eval("html`" + (_data?.profileHtml) + "`")}</div>
  `)

งานที่ผ่านมาของผม มีการใช้ประโยชน์จาก eval หลายอย่าง เช่น การออกแบบจอเครื่องคิดเลข เมื่อผู้ใช้คีย์ตัวเลข 10+2+3 ก็สามารถเรียกใช้ eval("10+2+3") คำนวณคำตอบโดยไม่ต้องทำอะไรซับซ้อน 


กรณีของ lit-html ถ้าใช้ eval ได้น่าจะเปิดโอกาสของความเป็นไปได้อีกมากมาย




<!doctype html>
<html>
<!---->
<body>

  <div id="greet"></div>
  <hr/>
  <div id="profile" style="border:solid thin; padding:8px;"></div>
  
</body>

<!----> 
<script type="module">
  import {html, render} from 'https://unpkg.com/lit-html@3.1.2/lit-html.js';

  const userdata = {
    name: 'John',
    fullname: 'John Doe',
    profileHtml: [
    '<p>',
    '  <b>Name:</b> ${userdata.name}<br/>',
    '  <b>Full Name:</b>  ${userdata.fullname}<br/>',
    '</p>',
    ].join('')
  }

  const greetHTML = _data => (html`
    <h1>Hello ${_data?.name || 'World'}</h1>
  `)

  render(greetHTML(userdata), document.querySelector('div#greet'))

  // profile
  const profileHTML = _data => (html`
    <h3>Profile</h3>
    <div>${eval("html`" + (_data?.profileHtml) + "`")}</div>
  `)

  render(profileHTML(userdata), document.querySelector('div#profile'))

</script>
</html>

innerHTML

เราสามารถใช้ eval โดยเอา innerHTML ของ elem ที่ทำไว้ใน <template> มาใช้ได้เช่นกัน


  // profile
  const profileHTML = _data => (html`
    <h3>Profile</h3>
    <div>${eval("html`" + document.querySelector('template#profile').innerHTML + "`")}</div>
  `)



<!doctype html>
<html>
<!---->
<body>
  <div id="greet"></div>
  <hr/>
  <div id="profile" style="border:solid thin; padding:8px;"></div>
  
  <template id="profile">
    <p>
      <b>Name:</b> ${userdata.name}<br/>
      <b>Full Name:</b>  ${userdata.fullname}<br/>
    </p>
  </template>
</body>

<!----> 
<script type="module">
  import {html, render} from 'https://unpkg.com/lit-html@3.1.2/lit-html.js';

  const userdata = {
    name: 'John',
    fullname: 'John Doe',
  }

  const greetHTML = _data => (html`
    <h1>Hello ${_data?.name || 'World'}</h1>
  `)

  render(greetHTML(userdata), document.querySelector('div#greet'))

  // profile
  const profileHTML = _data => (html`
    <h3>Profile</h3>
    <div>${eval("html`" + document.querySelector('template#profile').innerHTML + "`")}</div>
  `)

  render(profileHTML(userdata), document.querySelector('div#profile'))

</script>
</html>

Attention!

ถึงแม้ว่า eval จะใช้ประโยชน์ได้ในกรณีนี้ แต่ก็ควรระวังความปลอดภัยหากไม่สามารถควบคุมผู้เข้าถึงส่วน embedded code เพราะสามารถเขียนโค้ดให้ทะลุผ่าน script ใน eval() มาได้ ตามตัวอย่างข้างต้น สามารถเข้าถึงตัวแปร userdata ที่อยู่ใน global และ module scope มาใช้ตรง ๆ หมายความว่าเราอาจเข้าไปเปลี่ยนค่าในนั้นได้เช่นกัน หากจำเป็นต้องใช้งานใน production จริง อาจพิจารณาออกแบบให้รัดกุมกว่านี้ เช่น ใช้ scopedEval


อ้างอิง


ดู 9 ครั้ง0 ความคิดเห็น

โพสต์ล่าสุด

ดูทั้งหมด

Comments


Post: Blog2_Post
bottom of page