Web Components คืออะไร? + สอนวิธีใช้

Suranart Niamcome
SiamHTML
Published in
5 min readJan 5, 2015

--

web components

บทความนี้ผมจะขอพูดถึงเรื่อง Web Components ซึ่งความจริงผมตั้งใจว่าจะเขียนบทความเรื่องนี้มานานมากแล้ว(เกือบปี) แต่ก็ตัดสินใจพักเอาไว้ก่อน เพราะในตอนนั้นมันยังเอาไปใช้งานจริงไม่ค่อยจะได้ แต่มาถึงตอนนี้ เราก็จะเริ่มเห็นการใช้ Web Components บน production กันบ้างแล้ว (เช่น GitHub) เนื่องจากเราสามารถใช้ polyfills เพื่อกำหนด fallback ให้กับ web browser ที่ยังไม่รองรับ Web Components ได้นั่นเอง ดังนั้น ผมว่ามันถึงเวลาแล้วที่เราจะมาเริ่มทำความรู้จักกับมันซะที

ปัญหา

ในการทำเว็บไซต์ต่างๆ เชื่อว่าหลายคนคงจะเคยเขียนโดยแยก UI ที่ใช้บ่อยๆ (ผมขอเรียกมันว่า component) ออกมาเป็นส่วนย่อยๆ เพื่อที่จะได้นำมา reuse ได้ง่ายๆ ตัวอย่างเช่น ส่วนของเมนูที่เราใช้บ่อยๆ เวลาเราจะเริ่มงานใหม่ เราก็คงไม่อยากเขียนเมนูแบบเดิมใหม่หมด แต่เราอยากจะเอาโค้ดเดิมที่เขียนไว้ดีแล้วมาใช้เลยมากกว่า ทั้งประหยัดเวลา แถมยังลดข้อผิดพลาดอีกด้วย !

แต่เนื่องจากรูปแบบการเขียนโค้ดที่จะทำให้ reuse ได้นั้น มันยังไม่มีมาตรฐานใดๆ ครับ ทำให้ component ของนักพัฒนาแต่ละคนนั้นถูกพัฒนาไปคนละทาง ลองนึกดูว่าหากเรานำ component จากหลายๆ คนมาใช้ แล้วแต่ละ component ต่างก็มีวิธีนำไปใช้ที่ไม่เหมือนกันเลย เราจะต้องปวดหัวขนาดไหน ? และปัญหาที่ว่านี้เอง ที่ทำให้เกิดมาตรฐานใหม่ที่มีชื่อว่า “Web Components”

Web Components คืออะไร ?

อย่างที่เล่าไปครับว่า Web Components เกิดขึ้นมาเพื่อทำให้การพัฒนา component ต่างๆ เป็นไปอย่างมีมาตรฐานครับ หรือพูดง่ายๆ ก็คือเพื่อให้ทุกคนเขียน component ไปในทางเดียวกัน จะได้แชร์กันง่ายๆ นั่นเอง โดย Web Components นั้นประกอบไปด้วย 4 ส่วนย่อยๆ ดังนี้ครับ

  • HTML Templates เอาไว้กำหนด markup ที่คิดว่าจะนำมาใช้ซ้ำในส่วนอื่นๆ
  • Shadow DOM คือ DOM อีกประเภทหนึ่ง ที่แสดงผลออกมาเหมือน DOM แบบปกติ แต่จะไม่ได้รับผลใดๆ จาก css และ js ของหน้านั้นๆ (ตอนนี้อาจจะงง แต่พอลองอ่านหัวข้อ workshop ก็จะเข้าใจครับ)
  • Custom Elements เอาไว้สร้าง HTML Element ใหม่ขึ้นมาใช้เอง
  • HTML Imports เอาไว้ import ไฟล์ html เข้ามา (คล้ายๆ กับการ import ไฟล์ css นั่นล่ะครับ)

ลองอ่านแต่ละส่วนดีๆ ก็พอจะเดาได้แล้วใช่มั้ยล่ะครับว่า Web Components มันก็คือการนำทั้ง 4 ส่วนด้านบน มาประยุกต์ใช้ร่วมกันเพื่อสร้าง component ใหม่ๆ ขึ้นมานั่นเอง

Polyfills

น่าเสียดายครับ ที่เราสามารถใช้ Web Components ได้เฉพาะ web browser ใหม่ๆ อย่าง chrome เท่านั้น เพราะการจะใช้ Web Components ได้อย่างสมบูรณ์แบบนั้น web browser จำเป็นจะต้องรองรับ Web Components ครบทั้ง 4 ส่วน เลยครับ ซึ่ง web browser อื่นๆ ยังรองรับไม่ครบ

แต่อย่างที่เกริ่นเอาไว้ตั้งแต่ตอนแรกครับ ตอนนี้มีคนทำ polyfills ของ Web Components ออกมาแล้ว วิธีใช้ก็แค่ใส่ webcomponents.js เอาไว้ก่อนที่จะมีการเรียกใช้ Web Components เท่านั้นเองครับ

<head>
...
<script src="path/to/webcomponents.js"></script>
...
</head>

Workshop

Web Components เป็นเรื่องที่อ่านยังไงก็ไม่เข้าใจครับ ต้องลองลงมือทำเลย แต่ถ้าใครยังไม่สะดวกเขียนโค้ดในตอนนี้ ลองค่อยๆ อ่านแล้วทำความเข้าใจตามไปก็ได้ครับ

เพื่อให้เห็นภาพมากขึ้น ผมจะขอตั้งโจทย์ขึ้นมาแล้วกันนะครับ สมมติว่าเรามี widget อันหนึ่งที่อยากจะนำไปใช้ในหน้าอื่นๆ หรืองานอื่นๆ ด้วย โดย widget นี้จะมีทั้ง html และ css ซึ่งเราไม่อยากจะมาพะวงกับโค้ดอีกแล้ว เราอยากได้แบบเรียกใช้ง่ายๆ แล้วออกมาสวยเหมือนกันในทุกๆ ที่

1. HTML Templates

สำหรับโจทย์ข้างต้น ให้เราเริ่มด้วยการเขียนโค้ดสำหรับ widget นั้น ขึ้นมาตามปกติเลยครับ สมมติโค้ด widget เราเป็นแบบนี้

<div class="followme">
<h3>Follow me</h3>
<a href="https://www.facebook.com/siamhtml">facebook</a>
<a href="https://twitter.com/SuranartN">twitter</a>
</div>

จากโค้ดด้านบน ก็พอจะมองออกว่า widget นี้คือกล่องสำหรับให้คนมา follow เว็บเรานั่นเองครับ แต่เพื่อความสวยงาม ผมขอใส่สไตล์ให้มันนิดนึงด้วย css

<style>
.followme {
width: 100%;
padding: 0px 20px 10px;
border: 1px solid #eee;
border-radius: 4px;
}
.followme > a {
display: block;
text-decoration: none;
}
</style>

เมื่อลองพรีวิวดู เราก็จะได้กล่องสำหรับ follow เว็บแล้วล่ะครับ ทีนี้เราจะเห็นว่ากล่องมันแสดงออกมาเลยถูกมั้ยครับ แต่จริงๆ แล้ว เราอยากแค่สร้าง component มารอไว้ก่อนเฉยๆ แล้วพอต้องการจะใช้ component เมื่อไร ก็ค่อยให้มันแสดงออกมา

ในกรณีนี้ เราจะใช้ HTML Templates เข้ามาช่วยครับ ให้เราครอบโค้ดเดิมของเราด้วย template tag แบบนี้

<template id="follow-me-template">
<style>
.followme {
width: 100%;
padding: 0px 20px 10px;
border: 1px solid #eee;
border-radius: 4px;
}
.followme > a {
display: block;
text-decoration: none;
}
</style>
<div class="followme">
<h3>Follow me</h3>
<a href="https://www.facebook.com/siamhtml">facebook</a>
<a href="https://twitter.com/SuranartN">twitter</a>
</div>
</template>

markup ใดๆ ที่ถูกครอบด้วย template tag จะไม่แสดงผลออกมาครับ งานต่อไปของเราก็คือ การเขียน js เพื่อดึงเอาเนื้อหาใน template ออกมาใช้งานเมื่อเราต้องการ จะสังเกตว่าผมได้ใส่ id ให้กับ template tag ด้วยนะครับ เพราะจะได้ดึงง่ายๆ ลองมาดูโค้ดสำหรับดึง template มาแสดงผลกันเลยครับ

// หา widget
var widget = document.getElementById('follow-me');
// หา template
var template = document.getElementById("follow-me-template");
// เอา template ยัดใส่ widget
widget.innerHTML = template.innerHTML;

ทีนี้ เวลาเราอยากจะให้ widget นี้แสดงตรงไหน ก็ให้ใส่โค้ด html แบบนี้ลงไปได้เลยครับ

<div id="follow-me"></div>

สิ่งที่เกิดขึ้นก็คือ โค้ด js ของเราจะไปเอาเนื้อหาที่อยู่ใน template มายัดใส่กล่อง widget เปล่าๆ จนกลายเป็น widget ที่มีเนื้อหาสมบูรณ์นั่นเองครับ

แต่อย่าลืมนะครับว่า component ของเราจะต้องมีหน้าตาสมบูรณ์เหมือนกันในทุกๆ ที่ ไม่ว่าจะเอาไปใช้กับหน้าไหน เว็บไหนก็ตาม หากในหน้านั้นๆ มี css rule บางส่วนที่ไปส่งผลกับบาง element ใน component ของเราแล้วล่ะก็ หน้าตาของ component ก็มีโอกาสเพี้ยนได้ครับ

2. Shadow DOM

ปัญหานี้เราสามารถแก้ได้ด้วย Shadow DOM ครับ คือแทนที่เราจะใช้วิธีพ่น template ใส่ element ที่เป็น container เราจะเปลี่ยนมาใช้วิธีแปลงร่าง element นั้นๆ ไปเลยแทน แต่ก่อนอื่นผมต้องขอนิยามศัพท์นิดนึงเกี่ยวกับ Shadow DOM ว่ามันประกอบไปด้วย 2 ส่วน ดังนี้ครับ

  • Shadow Host HTML Element ที่เราต้องการจะแปลงร่างมันเป็นอย่างอื่น (ตัวมันจะไม่แสดงผลออกมา)
  • Shadow Root HTML Element ใหม่ ที่เราสร้างขึ้น (ตัวมันจะแสดงผลแทน Shadow Host)

อย่างในกรณีนี้ จะเห็นว่าจริงๆ แล้ว เราไม่ได้อยากได้ div ที่เป็น container เลยใช่มั้ยครับ เราแค่ใช้มันเป็นที่วาง widget เท่านั้นเอง สิ่งที่เราต้องการจริงๆ คือเนื้อหาที่อยู่ใน template ต่างหาก

ดังนั้น เราจะมาแปลงร่างมันด้วย Shadow DOM ครับ โดย div ที่เป็น container ของเราก็จะเป็น Shadow Host แล้วเราก็จะสร้าง Shadow Root ขึ้นมาทับมันเพื่อที่จะเอาไว้แสดงเนื้อหาใน template ลองมาดูโค้ดด้านล่างนี้ครับ

// หา Shadow Host
var host = document.getElementById('follow-me');
// สร้าง Shadow Root บน Shadow Host
var root = host.createShadowRoot();
// หา template
var template = document.getElementById("follow-me-template");
// ยัด template ลงใน Shadow Root
root.appendChild(template.content);

เมื่อลองพรีวิวดู เราก็จะได้กล่อง widget หน้าตาเหมือนเดิมนั่นแหละครับ เพียงแต่ภายใน Shadow Root นั้นจะคล้ายกับการสร้างอีกมิติหนึ่งขึ้นมาครับ คือ css และ js ของหน้านั้นๆ จะไม่สามารถเข้าไปยุ่งอะไรกับสิ่งที่อยู่ใน Shadow Root ได้เลย และ css และ js ที่อยู่ภายใน Shadow Root ก็ไม่สามารถติดต่อกับ HTML Element ภายนอกได้เช่นเดียวกัน เราจึงสามารถมั่นใจได้ว่า component ของเราจะมีหน้าตาสวยงามเหมือนเดิมไม่ว่าจะนำไปใช้ที่ไหนก็ตามครับ (จริงๆ แล้วก็สามารถข้ามมิติได้อยู่ดีแหละครับ เพียงแต่จะต้องใช้ CSS Pseudo Selector เฉพาะสำหรับ Shadow DOM หรือ property .shadowRoot สำหรับ js)

หากสังเกตดีๆ จะเห็นว่า Shadow DOM นี่เป็นพระเอกของ Web Components เลยนะครับ เพราะเราสามารถทำให้การแสดงผลของ HTML Element นั้นเป็นไปตามที่เราต้องการได้หมดเลย หรือพูดง่ายๆ ก็คือ “Shadow DOM ทำให้เราสามารถแยกส่วนของการแสดงผลออกมามาจากเนื้อหาได้” คือเพื่อให้ถูกหลักเราอาจจำเป็นต้องใช้ tag นี้นะ แต่ตอนที่จะแสดงผลออกมา เราไม่จำเป็นต้องยึดติดอยู่กับ tag นั้นอีกต่อไปแล้ว เช่น บางคนอาจใช้ Shadow DOM เพื่อแปลงร่าง input แบบ file ให้กลายเป็น img ในตอนแสดงผล เพื่อจะได้สามารถสร้างปุ่มอัพโหลดไฟล์แบบ native ที่มีรูปเป็นอะไรก็ได้นั่นเอง

บางคนบอกว่า เฮ้ย! เอ็งเล่นยัด url ลงใน template เลยเรอะ แล้วแบบนี้มันจะ reuse ได้ยังไง ? จริงด้วยครับ เพื่อที่จะทำให้ component นี้สามารถนำไปใช้ได้ในทุกๆ เว็บ เราจะต้องเอาส่วนที่เป็น url ออกมาจาก template ลองมาดูโค้ดที่ผมปรับใหม่ดูครับ

<div id="follow-me">
<a href="https://www.facebook.com/siamhtml">facebook</a>
<a href="https://twitter.com/SuranartN">twitter</a>
</div>

จะเห็นว่าผมย้ายเอาส่วนที่เป็นลิ้งค์ออกมานอก template แล้วเอามาใส่ไว้ใน container ที่จะเป็น Shadow Host แทนครับ จากนั้นผมก็ต้องไปแก้ template อีกนิดนึง เพื่อที่จะให้มันสามารถดึงลิ้งค์ที่อยู่ใน Shadow Host มาใช้งานได้ ลองดูหน้าตาของ template ใหม่ครับ

<template id="follow-me-template">
<style>
.followme {
width: 100%;
padding: 0px 20px 10px;
border: 1px solid #eee;
border-radius: 4px;
}
/* ต้องเปลี่ยนมาใช้ pseudo element ::content ถึงจะ match เจอ */
::content a {
display: block;
text-decoration: none;
}
</style>
<div class="followme">
<h3>Follow me</h3>
<!-- เปลี่ยนจากลิ้งค์มาเป็น content tag -->
<content></content>
</div>
</template>

จะเห็นว่าผมได้แทนที่ส่วนที่เป็นลิ้งค์ด้วย content tag ครับ ซึ่งเจ้า content tag นี้เกิดมาเพื่อ Shadow DOM โดยเฉพาะเลยนะครับ หน้าที่ของมันก็คือการดูดเอาเนื้อหาทั้งหมดที่อยู่ใน Shadow Host มาใส่ไว้ ณ ตำแหน่งที่มันอยู่นั่นเองครับ ในส่วนของ css rule ผมก็จำเป็นต้องปรับเล็กน้อยด้วยนะครับ เพราะ rule เดิมอย่าง .followme > a จะ match ไม่เจอ element จาก Shadow Host ที่ถูกดูดมาด้วยฝีมือของ content tag ครับ

เพียงเท่านี้ widget ของเราก็จะสามารถนำไปใช้ได้ในทุกๆ ที่ เพียงแค่เรากำหนด url ของ facebook และ twitter ให้เป็นของเว็บที่จะเอาไปใช้เท่านั้นเองครับ

3. Custom Elements

เพื่อให้ widget ของเราใช้งานได้สะดวกขึ้น เราลองมาทำมันให้กลายเป็น HTML Element ใหม่ขึ้นมาเลยดีกว่าครับ เริ่มด้วยการเปลี่ยน container ที่เป็น div เดิม มาเป็น follow-me tag แบบนี้

<follow-me>
<a href="https://www.facebook.com/siamhtml">facebook</a>
<a href="https://twitter.com/SuranartN">twitter</a>
</follow-me>

มีข้อสังเกตนิดนึงนะครับว่า ชื่อของ element ใหม่ที่เราสร้างขึ้นเองจะต้องมี “-” คั่นอยู่ด้วยเสมอ เพื่อทำให้ web browser รู้ว่ามันไม่ใช่ element แบบดั้งเดิมนั่นเองครับ

ต่อมาเราก็จะต้องเขียน js เพื่อทำให้ web browser รู้จักกับ element ใหม่นี้ครับ

// สร้าง prototype สำหรับ follow-me element โดย extend มาจาก element แบบปกติ
var followMeProto = Object.create(HTMLElement.prototype);
// เมื่อสร้าง prototype เสร็จแล้วให้ทำอะไร ?
followMeProto.createdCallback = function() {
// สร้าง Shadow Root บน element นี้ แล้วยัด template ลงไป
var root = this.createShadowRoot();
var template = document.getElementById("follow-me-template");
root.appendChild(document.importNode(template.content, true));
};
// แนะนำให้ browser รู้จัก element ใหม่ที่ชื่อ follow-me
document.registerElement('follow-me', {
// โดยมี prototype เป็น followMeProto ที่เราเตรียมไว้แล้ว
prototype: followMeProto
});
ต่อไปนี้เราก็จะสามารถเรียกใช้ widget ของเราได้ง่ายๆ เพียงแค่ใส่ follow-me tag ในตำแหน่งที่ต้องการจะแสดง widget ครับ
4. HTML Importsมาถึงตรงนี้คาดว่าบางคนอาจจะบ่นว่า โอ้ย แล้วยังงี้หน้าเว็บไม่มี template เต็มไปหมดหรอ ยิ่งมี component เยอะมากๆ template ที่ใช้ก็ยิ่งเยอะตามไปด้วย โค้ดคงจะรกมากๆ เลยไม่ต้องห่วงครับ ปัญหานี้สามารถแก้ได้ง่ายๆ เลย ด้วย HTML Imports ให้เราสร้างไฟล์ html ขึ้นมาใหม่อันนึงแล้วตั้งชื่อมันว่า components.html จากนั้น ในส่วนของ body ก็ให้เราใส่ template ของ follow-me element ลงไปแบบนี้เลยครับ<body>
<template id="follow-me-template">
<style>
.followme {
width: 100%;
padding: 0px 20px 10px;
border: 1px solid #eee;
border-radius: 4px;
}
::content a {
display: block;
text-decoration: none;
}
</style>
<div class="followme">
<h3>Follow me</h3>
<content></content>
</div>
</template>
</body>
จากนั้นกลับมาดูที่หน้าเว็บที่เราจะเอา widget มาใช้ครับ ให้เราเอา template ออกไปได้เลย แล้วค่อยไปเพิ่มโค้ดด้านล่างนี้ใน head tag เพื่อเอาไว้ import ไฟล์ components.html มาใช้งาน<link id="components_link" rel="import" href="components.html">สุดท้ายแล้ว เราต้องมาแก้ js ในส่วนของการดึง template นิดนึงครับ// เก็บเนื้อหาที่ import มา ไว้ใน components
var components_link = document.getElementById('components_link');
var components = components_link.import;
var followMeProto = Object.create(HTMLElement.prototype);followMeProto.createdCallback = function() {
var root = this.createShadowRoot();
// ดึง template ของ follow-me element มาใช้
var template = components.getElementById("follow-me-template");
root.appendChild(document.importNode(template.content, true));
};
document.registerElement('follow-me', {
prototype: followMeProto
});
เพียงเท่านี้ โค้ดของเราก็จะดูเป็นระเบียบมากขึ้นแล้วล่ะครับ หากมี component อื่นๆ อีก เราก็สามารถเก็บ template รวมกันไว้ที่ไฟล์ components.html ได้เลยนะครับ เพียงแต่เราจะต้องตั้งชื่อ id ของ template ให้สื่อหน่อย เวลาดึงมาใช้จะได้ไม่งงครับ
บทสรุปก็น่าจะพอเห็นภาพกันคร่าวๆ แล้วนะครับว่า Web Components คืออะไร และมีวิธีการใช้งานอย่างไร ขอย้ำอีกทีว่าการเขียนแบบที่ผมเล่ามาทั้งหมดนี้ มันคือการเขียนตาม specifications ที่เค้ากำหนดมานะครับ ซึ่งจะว่าไปแล้วมันก็ยังไม่ค่อยนิ่งเท่าไร ผมขอแนะนำให้ติดตามข่าวสารเกี่ยวกับ Web Components รวมไปถึงรายละเอียดในเชิงลึกได้ที่ WebComponents.org ครับส่วนใครที่มองว่า Web Components นั้นค่อนข้างจะใช้ยากไปหน่อยก็ไม่ต้องกังวลไปนะครับ บทความหน้าผมจะมาพูดถึง tool ที่ทำให้เราใช้งาน Web Components ได้สะดวกยิ่งขึ้นอย่าง Polymer ที่จะมาคอยจัดการเกี่ยวกับการสร้าง element ใหม่ให้ จนเราแทบจะไม่ต้องไปยุ่งกับ js เลย อย่าลืมติดตามกันให้ได้นะครับ

--

--

Suranart Niamcome
SiamHTML

Lead Engineer @Tencent (Thailand)