Mọi lập trình viên web đều biết cảm giác khi nhìn thấy ứng dụng của mình hiển thị hoàn hảo trong một môi trường staging được kiểm soát. Nhưng việc triển khai một widget nhúng sẽ phá hủy hoàn toàn sự thoải mái đó. Bạn không còn là kiến trúc sư của trang web nữa. Bạn trở thành một vị khách không mời, đang tiêm một ứng dụng React vào một DOM mà bạn không sở hữu, một chuỗi cascade CSS mà bạn không soạn thảo, và một môi trường runtime có thể đang tích cực chống lại bạn. Trong quá trình xây dựng và triển khai widget Clanker Support, chúng tôi đã học được rằng những giả định phát triển web tiêu chuẩn sẽ sụp đổ ngay khoảnh khắc mã của bạn chạy bên trong giao diện (theme) của người khác. Trang web chủ có thể đặt lại kích thước font chữ, ẩn các thẻ div trống, hoặc áp dụng một vòng đời script làm mất hiệu lực cấu hình của bạn trước khi bạn kịp đọc nó. Dưới đây là những quy tắc phòng thủ mà chúng tôi đã phải viết bằng xương máu trong môi trường production.
Một File, Một Chế Độ Lỗi
Các trình đóng gói (bundlers) hiện đại thường cám dỗ bạn bằng việc chia nhỏ mã (code splitting) và import động (dynamic imports). Hãy cưỡng lại chúng. Một widget nhúng phải được đóng gói dưới dạng một Immediately Invoked Function Expression (IIFE) duy nhất. Khi khách hàng sao chép thẻ script của bạn vào template của họ, họ mong đợi chỉ có một yêu cầu mạng duy nhất. Nếu bundle của bạn cố gắng tải chậm (lazy-load) một thư viện phân tích nặng hoặc một phần của mô hình ngôn ngữ, việc fetch có thể thất bại một cách âm thầm. Trang web chủ có thể có Chính sách Bảo mật Nội dung (Content Security Policy) nghiêm ngặt, một trình chặn quảng cáo quyết liệt, hoặc một đường dẫn CDN không khớp với các giả định publicPath của bạn. Bằng cách ép mọi thứ vào một IIFE duy nhất, bạn loại bỏ được những yếu tố không xác định khi tải các chunk phụ. Nếu một dependency nhất quyết phải lazy-load các thành phần nội bộ của nó, hãy alias nó tại thời điểm build thành một stub nhẹ. Kết quả là một artifact duy nhất, một chế độ lỗi duy nhất, và một phiên debug dễ dàng hơn nhiều khi quản trị viên trang web của khách hàng gửi cho bạn ảnh chụp màn hình của một bong bóng chat bị lỗi.
Shadow DOM Cũng Bị Rò Rỉ
Các lập trình viên thường coi Shadow DOM như một pháo đài bất khả xâm phạm. Nó thực sự cô lập các selector của bạn khỏi CSS của trang web chủ, nhưng nó không cô lập tính kế thừa. Các thuộc tính như font-family, line-height, color, và text-align sẽ chảy xuống cây shadow của bạn như thể ranh giới đó không hề tồn tại. Một cửa hàng Shopify với khai báo font-family: "Comic Sans MS" toàn cục sẽ "lây nhiễm" vào widget hỗ trợ được thiết kế tỉ mỉ của bạn, trừ khi bạn chốt chặt mọi thuộc tính có thể kế thừa ngay tại phần tử gốc. Hãy thiết lập kiểu chữ (typography), khoảng cách (spacing) và căn lề văn bản (text alignment) của riêng bạn với các giá trị cụ thể ngay tại cấp độ host. Hãy mặc định rằng trang web cha là một môi trường không thân thiện và thiết lập lại (reset) mọi thứ mà bạn quan tâm. Shadow DOM bảo vệ các class của bạn, chứ không bảo vệ tính thẩm mỹ của bạn.
Màn Biến Mất Của Các Thẻ Div Trống
Điều này đã khiến chúng tôi hoàn toàn bất ngờ. Nhiều theme phổ biến, bao gồm cả Shopify Dawn, đi kèm với một quy tắc CSS trông có vẻ vô hại: div:empty { display: none; }. Khi widget của bạn mount, nó thường nhắm vào một thẻ div chủ bắt đầu ở trạng thái trống. Trước khi JavaScript của bạn thực thi và React hydrate node đó, thẻ div đó thực sự trống rỗng. Stylesheet của theme sẽ ẩn nó đi. Script của bạn chạy, gọi ReactDOM.createRoot, và chẳng có gì xuất hiện cả. Không có lỗi nào trong console. Phần tử đó đơn giản là đã ngừng tồn tại trong layout. Cách khắc phục là phải mạnh bạo và rõ ràng: áp dụng một inline style là display: block !important cho điểm mount của bạn. Đừng dựa vào thư viện CSS-in-JS của bạn để xử lý việc này sau đó. Vào thời điểm các stylesheet của bạn được áp dụng, theme của trang chủ đã giành chiến thắng rồi.
Từ Bỏ rem để dùng px
Trong một ứng dụng thông thường, các đơn vị tương đối như rem là lựa chọn có trách nhiệm. Nhưng trong một widget nhúng, chúng là một rủi ro. Một giá trị rem được tính toán dựa trên kích thước font chữ html gốc của tài liệu chủ, chứ không phải của widget của bạn. Nếu trang web chủ đặt html { font-size: 10px; } hoặc sử dụng mẹo 62.5% cũ, toàn bộ thang đo kiểu chữ và khoảng cách của bạn sẽ bị thay đổi mà không có cảnh báo trước. Một line-height 1.6rem thoải mái có thể bị thu hẹp xuống còn 16px, hoặc padding của bạn có thể co lại thành những mảnh vụn không thể đọc nổi. Bởi vì bạn không thể dự đoán hoặc kiểm soát kích thước gốc của trang chủ, pixel là đơn vị trung thực duy nhất cho một widget nhúng. Chúng hiển thị ở cùng một kích thước vật lý bất kể các giả định của trang web xung quanh. Hãy đánh đổi sự linh hoạt về khả năng truy cập (accessibility) trên lý thuyết của rem để lấy sự tin cậy thực tế của px khi bạn đang sống bên trong chuỗi cascade của một trang web khác.
Đọc Cấu Hình Trước Khi Nó Biến Mất
Nếu bạn truyền cấu hình cho widget thông qua các thuộc tính data trên thẻ script, bạn phải đọc chúng một cách đồng bộ. Trình duyệt cung cấp document.currentScript để một script có thể tự kiểm tra thẻ của chính nó, nhưng tham chiếu này chỉ mang tính tạm thời. Nếu bạn đợi DOMContentLoaded hoặc bất kỳ ranh giới bất đồng bộ nào, document.currentScript sẽ trở thành null. Cấu hình của bạn sẽ biến mất. Hãy đọc các thuộc tính đó ngay lập tức ở cấp cao nhất (top level) trong quá trình thực thi script. Hãy thu thập API key, widget ID và chủ đề màu sắc ngay tại thời điểm đó, lưu chúng vào một closure hoặc biến module, và chỉ sau đó mới tiến hành khởi chạy React.
Hãy để URL của Script quyết định API Origin
Việc hardcode một URL API production vào bundle là một sai lầm sẽ gây ra hệ lụy trên nhiều môi trường khác nhau. Thay vào đó, hãy suy ra API origin từ thuộc tính src của chính phần tử script. Nếu widget được tải từ https://cdn.staging.example.com/widget.js, các lệnh gọi API của nó nên mặc định là https://api.staging.example.com. Nếu một nhà phát triển chèn thẻ script vào một tệp HTML cục bộ được chạy từ localhost:3000, bản build cục bộ nên điều hướng các yêu cầu đến một máy chủ cục bộ. Quy ước này loại bỏ nhu cầu về các bản build riêng biệt cho từng môi trường, các feature flag, hoặc việc cấu hình thủ công từ người dùng nhúng. Nó hoạt động một cách tự nhiên, vì vị trí hạ tầng được ngụ ý bởi vị trí phân phối.
Hãy coi Cache Headers như một "phao cứu sinh" cho các bản Hotfix
Người dùng chỉ sao chép thẻ script của bạn một lần vào mẫu footer của họ và sau đó quên bẵng đi. Bạn không thể gửi email cho năm nghìn thương gia và yêu cầu họ cập nhật tham số truy vấn phiên bản (version query parameter). Điều này có nghĩa là các cache header của bạn là một phần trong chiến lược ứng phó sự cố. Hãy đặt max-age ngắn cho widget bundle của bạn để khi bạn tung ra một bản sửa lỗi quan trọng, nó sẽ được lan truyền trong vòng vài giờ thay vì vài tuần. Sự tiện lợi của một tài nguyên được cache lâu dài không đáng để đánh đổi bằng sự bất lực khi biết rằng hàng ngàn trang web đang chạy một phiên bản lỗi mà bạn không thể thu hồi. Hãy chấp nhận chi phí lưu lượng CDN. Sự tỉnh táo của bạn phụ thuộc vào điều đó.
Đảo ngược CSP cho các bản nhúng iframe
Nếu bạn cung cấp tùy chọn nhúng dựa trên iframe, Content Security Policy (CSP) của bạn sẽ cần một tư duy đảo ngược so với các ứng dụng web tiêu chuẩn. Thông thường, bạn có thể cấm framing để ngăn chặn clickjacking. Nhưng đối với một widget, bạn phải cho phép nó. Hãy đặt frame-ancestors * để bất kỳ trang web nào cũng có thể chứa iframe của bạn. Sau đó, hãy trở nên cực kỳ khắt khe với mọi thứ khác. Hãy thắt chặt script-src, style-src, và connect-src bên trong chính sách iframe đó. Bạn đang chủ động phơi bày mình trước toàn bộ web thông qua vector framing, vì vậy bạn phải đảm bảo rằng mã chạy bên trong iframe không có kẽ hở để hoạt động sai trái nếu trang chủ cố gắng thao túng nó.
Tư duy của một "Khách"
Xây dựng các thành phần nhúng đòi hỏi một cách tiếp cận khác so với việc xây dựng các ứng dụng web tiêu chuẩn. Trong ứng dụng của chính bạn, bạn sở hữu container, routing, pipeline build và các style toàn cục. Trong một bản nhúng, bạn không sở hữu gì cả. Trang chủ là bất kỳ đâu, thường là cũ kỹ, đôi khi là không thân thiện và luôn nằm ngoài tầm kiểm soát của bạn. Mọi giả định đều phải mang tính phòng thủ. Hãy xác định rõ ràng những gì bạn muốn, kiểm tra môi trường một cách chủ động và thiết kế để sẵn sàng cho những lỗi hỏng hóc mà bạn không thể nhìn thấy trước. Widget Clanker Support hoạt động được như ngày hôm nay không phải vì web có thể dự đoán được, mà vì chúng tôi đã ngừng tin tưởng vào điều đó.
