นักพัฒนาเว็บทุกคนย่อมรู้ซึ้งถึงความรู้สึกของการเฝ้าดูแอปพลิเคชันของตนแสดงผลได้อย่างสมบูรณ์แบบในสภาพแวดล้อม staging ที่ควบคุมได้ แต่การส่งมอบ embedded widget จะทำลายความสบายใจนั้นไปจนหมดสิ้น คุณไม่ใช่สถาปนิกของหน้าเว็บอีกต่อไป แต่คุณคือแขกที่ไม่ได้รับเชิญ ที่ต้องฉีด React application เข้าไปใน DOM ที่คุณไม่ได้เป็นเจ้าของ, CSS cascade ที่คุณไม่ได้เขียนขึ้นเอง และ runtime environment ที่อาจจงใจขัดขวางคุณ ในขณะที่สร้างและส่งมอบ Clanker Support widget เราได้เรียนรู้ว่าสมมติฐานการพัฒนาเว็บแบบมาตรฐานจะพังทลายลงทันทีเมื่อโค้ดของคุณรันอยู่ภายใต้ theme ของคนอื่น เว็บไซต์เจ้าบ้าน (host site) อาจรีเซ็ตขนาดฟอนต์, ซ่อน div ที่ว่างเปล่า หรือบังคับใช้ script lifecycle ที่ทำให้การตั้งค่าของคุณใช้งานไม่ได้ก่อนที่คุณจะได้อ่านมันเสียอีก และนี่คือกฎการป้องกันที่เราเขียนขึ้นจากบทเรียนอันเจ็บปวดใน production

หนึ่งไฟล์ หนึ่งรูปแบบความล้มเหลว

Bundler สมัยใหม่มักจะล่อใจคุณด้วย code splitting และ dynamic imports จงต้านทานมันไว้ Embedded widget ต้องถูกส่งมอบในรูปแบบ single-file Immediately Invoked Function Expression (IIFE) เมื่อลูกค้าคัดลอก script tag ของคุณไปใส่ใน template ของพวกเขา พวกเขาคาดหวังการร้องขอเครือข่าย (network request) เพียงครั้งเดียว หาก bundle ของคุณพยายามทำ lazy-load ไลบรารีการ parse ที่มีขนาดใหญ่หรือ chunk ของ language model การ fetch อาจล้มเหลวโดยไม่แจ้งเตือน เว็บไซต์เจ้าบ้านอาจมี Content Security Policy ที่เข้มงวด, มี ad blocker ที่ดุดัน หรือมี CDN path ที่ไม่ตรงกับสมมติฐาน publicPath ของคุณ การบังคับทุกอย่างให้อยู่ใน IIFE เดียวกันจะช่วยกำจัดความไม่แน่นอนของการโหลด chunk รอง หาก dependency ดึงดันที่จะทำ lazy-load ส่วนประกอบภายในของมันเอง ให้ทำ alias มันที่ build time ให้เป็น lightweight stub แทน ผลลัพธ์ที่ได้คือ artifact เดียว, รูปแบบความล้มเหลวเดียว และการทำ debugging ที่ง่ายขึ้นมาก เมื่อผู้ดูแลเว็บไซต์ของลูกค้าส่งอีเมลพร้อมภาพหน้าจอของ chat bubble ที่พังมาให้คุณ

Shadow DOM ก็รั่วไหลได้เช่นกัน

นักพัฒนามักมองว่า Shadow DOM เป็นป้อมปราการที่ไม่มีใครเจาะเข้าได้ มันช่วยแยก selector ของคุณออกจาก CSS ของหน้า host ได้จริง แต่ไม่ได้แยกการสืบทอด (inheritance) คุณสมบัติอย่าง font-family, line-height, color, และ text-align จะไหลลงสู่ shadow tree ของคุณราวกับว่าไม่มีขอบเขตนั้นอยู่ ร้านค้า Shopify ที่มีการประกาศ font-family: "Comic Sans MS" แบบ global จะเข้ามาทำลาย support widget ที่คุณออกแบบมาอย่างดี เว้นแต่คุณจะกำหนด (pin) ทุกคุณสมบัติที่สืบทอดได้ไว้ที่ root element ของคุณอย่างชัดเจน จงกำหนด typography, spacing และ text alignment ของคุณเองด้วยค่าที่แน่นอน (concrete values) ตั้งแต่ระดับ host สมมติไว้ก่อนว่าหน้าเว็บหลักนั้นเป็นศัตรู และรีเซ็ตทุกอย่างที่คุณให้ความสำคัญ Shadow DOM ปกป้อง class ของคุณ แต่ไม่ได้ปกป้องความสวยงาม (aesthetics) ของคุณ

มายากลการหายตัวไปของ Empty Div

เรื่องนี้ทำให้เราตั้งตัวไม่ติดเลยทีเดียว หลายธีมยอดนิยม รวมถึง Shopify Dawn มาพร้อมกับกฎ CSS ที่ดูเหมือนไม่มีอะไร: div:empty { display: none; } เมื่อ widget ของคุณ mount มันมักจะเล็งไปที่ host div ที่เริ่มต้นด้วยความว่างเปล่า ก่อนที่ JavaScript ของคุณจะทำงานและ React จะทำการ hydrate node นั้น div ดังกล่าวจะว่างเปล่าจริงๆ และ stylesheet ของธีมก็จะซ่อนมันไว้ สคริปต์ของคุณทำงาน เรียก ReactDOM.createRoot แต่กลับไม่มีอะไรปรากฏขึ้น ไม่มี error ใน console เลย องค์ประกอบนั้นแค่หายไปจาก layout เฉยๆ วิธีแก้คือต้องใช้แรงกระแทก (brute force) และความชัดเจน: ให้ใส่ inline style เป็น display: block !important ที่ mount point ของคุณ อย่าฝากความหวังไว้กับ CSS-in-JS library ว่าจะจัดการเรื่องนี้ในภายหลัง เพราะกว่าที่ stylesheet ของคุณจะทำงาน ธีมของเจ้าบ้านก็ชนะไปเรียบร้อยแล้ว

เลิกใช้ rem แล้วหันมาใช้ px แทน

ในแอปพลิเคชันปกติ หน่วยสัมพัทธ์อย่าง rem คือทางเลือกที่รับผิดชอบต่อผู้ใช้ แต่ใน embedded widget มันคือภาระ ค่า rem จะถูกคำนวณเทียบกับขนาดฟอนต์ของ html root ของเอกสารเจ้าบ้าน ไม่ใช่ของ widget ของคุณ หากหน้า host ตั้งค่า html { font-size: 10px; } หรือใช้เทคนิค 62.5% แบบเก่า สเกล typography และ spacing ทั้งหมดของคุณจะเปลี่ยนไปโดยไม่มีการแจ้งเตือน ค่า line height ที่อ่านสบายๆ อย่าง 1.6rem อาจจะยุบเหลือเพียง 16px หรือ padding ของคุณอาจจะหดจนเหลือเพียงเส้นบางๆ ที่อ่านไม่ออก เนื่องจากคุณไม่สามารถคาดเดาหรือควบคุมขนาด root ของ host ได้ พิกเซล (pixels) จึงเป็นหน่วยเดียวที่ซื่อสัตย์ที่สุดสำหรับ embedded widget พวกมันจะแสดงผลด้วยขนาดทางกายภาพที่เท่าเดิมเสมอ โดยไม่สนใจสมมติฐานของหน้าเว็บรอบข้าง จงแลกความยืดหยุ่นด้าน accessibility ในทางทฤษฎีของ rem กับความน่าเชื่อถือในทางปฏิบัติของ px เมื่อคุณต้องไปอาศัยอยู่ภายใต้ cascade ของเว็บไซต์อื่น

อ่าน Config ของคุณก่อนที่มันจะหายไป

หากคุณส่งค่าคอนฟิกูเรชันไปยัง widget ของคุณผ่าน data attributes บน script tag คุณต้องอ่านค่าเหล่านั้นแบบ synchronous เบราว์เซอร์มี document.currentScript ให้เพื่อให้สคริปต์สามารถตรวจสอบแท็กของตัวเองได้ แต่การอ้างอิงนี้เป็นเพียงชั่วคราว หากคุณรอ DOMContentLoaded หรือขอบเขตแบบ asynchronous ใดๆ document.currentScript จะกลายเป็น null และคอนฟิกูเรชันของคุณจะหายวับไป จงอ่าน attribute เหล่านั้นทันทีที่ระดับบนสุด (top level) ของการทำงานสคริปต์ เก็บค่า API key, widget ID และ color theme ไว้ในตอนนั้นเลย จากนั้นจึงนำไปเก็บไว้ใน closure หรือตัวแปรโมดูล แล้วจึงค่อยเริ่มกระบวนการ booting React

ให้ URL ของสคริปต์เป็นตัวกำหนด API Origin

การ Hardcode URL ของ production API ลงใน bundle ของคุณเป็นความผิดพลาดที่จะส่งผลกระทบเป็นทวีคูณในสภาพแวดล้อมต่างๆ แทนที่จะทำเช่นนั้น ให้ดึงค่า API origin มาจาก attribute src ของตัวสคริปต์เอง หาก widget โหลดมาจาก https://cdn.staging.example.com/widget.js การเรียก API ของมันควรจะเปลี่ยนไปใช้ https://api.staging.example.com โดยอัตโนมัติ หากนักพัฒนาใส่ script tag ลงในไฟล์ HTML ท้องถิ่นที่รันจาก localhost:3000 ตัว local build ก็ควรจะส่งคำขอไปยังเซิร์ฟเวอร์ในเครื่อง แนวทางนี้ช่วยลดความจำเป็นในการทำ build แยกตามสภาพแวดล้อม, การใช้ feature flags หรือการตั้งค่าด้วยตนเองจากผู้ใช้ที่นำไปฝัง (embed user) มันทำงานได้ทันที เพราะตำแหน่งของโครงสร้างพื้นฐาน (infrastructure) ถูกกำหนดโดยตำแหน่งที่สคริปต์ถูกส่งไป

ปฏิบัติต่อ Cache Headers เหมือนเป็นเส้นตายสำหรับการทำ Hotfix

ผู้ใช้จะคัดลอก script tag ของคุณไปวางไว้ใน footer template เพียงครั้งเดียวแล้วก็ลืมมันไป คุณไม่สามารถส่งอีเมลหาพ่อค้าห้าพันรายเพื่อขอให้พวกเขาอัปเดต version query parameter ได้ นั่นหมายความว่า cache headers ของคุณคือส่วนหนึ่งของกลยุทธ์การตอบสนองต่ออุบัติการณ์ (incident response strategy) จงตั้งค่า max-age ให้สั้นสำหรับ widget bundle ของคุณ เพื่อที่ว่าเมื่อคุณส่งตัวแก้ไขที่สำคัญ (critical fix) ออกไป มันจะแพร่กระจายไปทั่วภายในไม่กี่ชั่วโมง ไม่ใช่เป็นสัปดาห์ ความสะดวกสบายของการมี asset ที่แคชไว้นานๆ นั้นไม่คุ้มเลยกับความอัมพาตที่คุณต้องเผชิญเมื่อรู้ว่ามีเว็บไซต์นับพันกำลังรันเวอร์ชันที่เสียซึ่งคุณไม่สามารถเรียกคืนได้ ยอมรับค่าใช้จ่ายด้านทราฟฟิกของ CDN เสียเถอะ เพราะสุขภาพจิตของคุณขึ้นอยู่กับมัน

ปรับเปลี่ยน CSP สำหรับการฝังแบบ iframe

หากคุณมีตัวเลือกการฝังแบบ iframe นโยบาย Content Security Policy (CSP) ของคุณจำเป็นต้องคิดกลับด้านจากแนวคิดของแอปพลิเคชันเว็บทั่วไป โดยปกติคุณอาจจะสั่งห้ามการทำ framing เพื่อป้องกัน clickjacking แต่สำหรับ widget คุณต้องอนุญาตให้ทำได้ จงตั้งค่า frame-ancestors * เพื่อให้เว็บไซต์ใดๆ ก็ตามสามารถโฮสต์ iframe ของคุณได้ จากนั้นจงเข้มงวดอย่างที่สุดกับเรื่องอื่นๆ จำกัดสิทธิ์ script-src, style-src, และ connect-src ให้รัดกุมภายในนโยบายของ iframe นั้น คุณกำลังจงใจเปิดตัวเองสู่เว็บในวงกว้างผ่านช่องทาง framing ดังนั้นคุณต้องมั่นใจว่าโค้ดที่รันอยู่ภายใน iframe จะไม่มีช่องว่างให้ทำงานผิดพลาดหากหน้าเว็บโฮสต์พยายามจะเข้ามาแทรกแซงหรือควบคุมมัน

แนวคิดแบบ "แขก" (The Guest Mindset)

การสร้าง embed ต้องใช้ท่าทีที่แตกต่างจากการสร้างแอปพลิเคชันเว็บมาตรฐาน ในแอปพลิเคชันของคุณเอง คุณเป็นเจ้าของทั้ง container, routing, build pipeline และ global styles แต่ในการทำ embed คุณไม่ได้เป็นเจ้าของอะไรเลย หน้าเว็บโฮสต์นั้นคาดเดาไม่ได้ มักจะเก่าแก่ บางครั้งก็เป็นอันตราย และอยู่นอกเหนือการควบคุมของคุณเสมอ ทุกข้อสันนิษฐานต้องเป็นแบบป้องกันไว้ก่อน (defensive) ระบุสิ่งที่คุณต้องการอย่างชัดเจน ตรวจสอบสภาพแวดล้อมอย่างรวดเร็ว (eagerly) และออกแบบเพื่อรองรับความเสียหายที่คุณมองไม่เห็น Clanker Support widget ทำงานได้ในวันนี้ ไม่ใช่เพราะเว็บนั้นคาดเดาได้ แต่เป็นเพราะเราเลิกเชื่อว่ามันจะคาดเดาได้ต่างหาก