เมื่อสองวันก่อนเพิ่งได้ดูรายละเอียด 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
อ้างอิง
Lit https://lit.dev/
JavaScript ES6 (2015) Template Literal https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
lit-html standalone https://lit.dev/docs/libraries/standalone-templates/
Lit-Element — How to render template from an external text? https://stackoverflow.com/a/76290324
eval https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
scoped-eval https://github.com/3cp/scoped-eval
Comments