يعرف كل مطور ويب ذلك الشعور عندما يرى تطبيقه يعمل بشكل مثالي في بيئة تجريبية (staging) محكومة. لكن إطلاق أداة (widget) مدمجة يدمر ذلك الشعور تماماً. فأنت لم تعد مهندس الصفحة، بل أصبحت ضيفاً غير مدعو، تحقن تطبيق React في DOM لا تملكه، وفي تسلسل CSS (CSS cascade) لم تكتبه، وفي بيئة تشغيل قد تعمل ضدك بنشاط. أثناء بناء وإطلاق أداة Clanker Support، تعلمنا أن افتراضات تطوير الويب القياسية تنهار في اللحظة التي يعمل فيها كودك داخل سمة (theme) تخص شخصاً آخر. قد يقوم الموقع المضيف بإعادة ضبط أحجام الخطوط، أو إخفاء عناصر div الفارغة، أو فرض دورة حياة للسكربت (script lifecycle) تبطل إعداداتك قبل أن تتمكن من قراءتها. إليك القواعد الدفاعية التي كتبناها بدمائنا في بيئة الإنتاج.

ملف واحد، نمط فشل واحد

تغريك أدوات التجميع (bundlers) الحديثة بتقسيم الكود (code splitting) والاستيرادات الديناميكية (dynamic imports). قاومها. يجب أن تُرسل الأداة المدمجة كملف واحد على هيئة تعبير دالة مستدعى فوراً (Immediately Invoked Function Expression - IIFE). عندما ينسخ العميل وسم السكربت الخاص بك في قالبهم، فإنهم يتوقعون طلباً واحداً للشبكة. إذا حاول ملفك المجمع (bundle) تحميل مكتبة تحليل ثقيلة أو جزء من نموذج لغوي بشكل كسول (lazy-load)، فقد يفشل الطلب بصمت. قد يكون لدى المضيف سياسة أمان محتوى (Content Security Policy) صارمة، أو مانع إعلانات شرس، أو مسار CDN لا يتوافق مع افتراضات الـ publicPath الخاصة بك. من خلال فرض كل شيء داخل IIFE واحد، فإنك تقضي على المجهول المرتبط بتحميل الأجزاء الثانوية (secondary chunks). إذا أصرت إحدى التبعيات (dependencies) على تحميل أجزائها الداخلية بشكل كسول، فقم بعمل alias لها وقت البناء (build time) لتصبح stub خفيف الوزن. النتيجة هي ملف واحد، ونمط فشل واحد، وجلسة تصحيح أخطاء (debugging) أسهل بكثير عندما يرسل إليك مدير موقع العميل لقطة شاشة لفقاعة دردشة معطلة.

Shadow DOM يتسرب أيضاً

غالباً ما يعامل المطورون الـ Shadow DOM كحصن منيع. صحيح أنه يعزل المحددات (selectors) الخاصة بك عن CSS الصفحة المضيفة، لكنه لا يعزل الوراثة (inheritance). تتدفق خصائص مثل font-family و line-height و color و text-align إلى أسفل شجرة الـ shadow الخاصة بك كما لو لم يكن هناك حدود. متجر Shopify يحتوي على تعريف عالمي لـ font-family: "Comic Sans MS" سيصيب أداة الدعم المصممة بعناية بـ "عدوى" ما لم تقم بتثبيت كل خاصية قابلة للوراثة صراحةً عند عنصر الجذر (root element) الخاص بك. قم بتحديد الخطوط، والتباعد، ومحاذاة النص بقيم ملموسة مباشرة عند مستوى المضيف. افترض أن الصفحة الأب معادية وقم بإعادة ضبط كل ما يهمك. الـ Shadow DOM يحمي فئاتك (classes)، وليس جمالياتك.

خدعة اختفاء الـ div الفارغ

هذا الأمر باغتَنا تماماً. العديد من السمات الشهيرة، بما في ذلك Shopify Dawn، تأتي مع قاعدة CSS تبدو بريئة: div:empty { display: none; }. عندما يتم تحميل (mount) أداتك، فإنها تستهدف عادةً عنصر div مضيفاً يبدأ فارغاً. قبل أن يتم تنفيذ JavaScript الخاص بك ويقوم React بعمل hydrate للعقدة (node)، يكون ذلك الـ div فارغاً حرفياً. تقوم ورقة أنماط (stylesheet) السمة بإخفائه. يعمل السكربت الخاص بك، ويستدعي ReactDOM.createRoot ، ولكن لا يظهر شيء. لا يوجد خطأ في وحدة التحكم (console). العنصر ببساطة توقف عن الوجود في التخطيط (layout). الحل هو القوة الغاشمة والوضوح: قم بتطبيق نمط inline وهو display: block !important على نقطة التحميل (mount point) الخاصة بك. لا تعتمد على مكتبة CSS-in-JS الخاصة بك للتعامل مع هذا لاحقاً. فبحلول الوقت الذي يتم فيه تطبيق أوراق الأنماط الخاصة بك، تكون السمة المضيفة قد انتصرت بالفعل.

تخلَّ عن rem واستخدم px

في التطبيقات العادية، تُعد الوحدات النسبية مثل rem الخيار المسؤول. أما في الأدوات المدمجة، فهي تمثل عبئاً. تُحسب قيمة rem بناءً على حجم خط الـ html الجذري للمستند المضيف، وليس لأداتك. إذا قامت الصفحة المضيفة بضبط html { font-size: 10px; } أو استخدمت خدعة الـ 62.5% القديمة، فإن مقياس الخطوط والتباعد بالكامل لديك سيتغير دون سابق إنذار. قد يتقلص ارتفاع السطر المريح البالغ 1.6rem إلى 16px ، أو قد يتقلص الـ padding الخاص بك إلى شرائح غير مقروءة. بما أنه لا يمكنك التنبؤ بحجم الجذر للمضيف أو التحكم فيه، فإن البكسل (pixels) هي الوحدة الوحيدة الصادقة للأداة المدمجة. فهي تظهر بنفس الحجم الفيزيائي بغض النظر عن افتراضات الصفحة المحيطة. ضحِّ بمرونة إمكانية الوصول النظرية لـ rem مقابل الموثوقية العملية لـ px عندما تعيش داخل تسلسل CSS لموقع آخر.

اقرأ إعداداتك قبل أن تختفي

إذا قمت بتمرير الإعدادات إلى الـ widget الخاص بك عبر سمات البيانات (data attributes) في وسم الـ script، فيجب عليك قراءتها بشكل متزامن (synchronously). يوفر المتصفح document.currentScript بحيث يمكن للـ script فحص الوسم الخاص به، ولكن هذا المرجع مؤقت (ephemeral). إذا انتظرت DOMContentLoaded أو أي حد غير متزامن (asynchronous boundary)، فسيصبح document.currentScript قيمته null. ستتبخر إعداداتك. اقرأ تلك السمات فوراً في المستوى الأعلى (top level) لتنفيذ الـ script الخاص بك. التقط مفتاح الـ API، ومعرف الـ widget، والسمة اللونية (color theme) في تلك اللحظة، وقم بتخزينها في closure أو متغير module، وبعد ذلك فقط ابدأ بتشغيل React.

اجعل رابط الـ Script يحدد أصل الـ API

إن كتابة رابط الـ API الخاص بالإنتاج بشكل ثابت (Hardcoding) داخل الـ bundle الخاص بك هو خطأ يتضاعف عبر البيئات المختلفة. بدلاً من ذلك، استخلص أصل الـ API (API origin) من سمة src الخاصة بعنصر الـ script نفسه. إذا تم تحميل الـ widget من https://cdn.staging.example.com/widget.js ، فيجب أن تتوجه طلبات الـ API الخاصة به افتراضياً إلى https://api.staging.example.com. إذا قام مطور بوضع وسم الـ script في ملف HTML محلي يتم تشغيله من localhost:3000 ، فيجب أن يوجه البناء المحلي (local build) الطلبات إلى خادم محلي. هذا النهج يلغي الحاجة إلى عمليات بناء خاصة بكل بيئة، أو أعلام الميزات (feature flags)، أو تكوين يدوي من مستخدم التضمين (embed user). إنه يعمل ببساطة، لأن موقع البنية التحتية يُستدل عليه من موقع التوصيل.

تعامل مع ترويسات التخزين المؤقت كطوق نجاة للإصلاحات العاجلة

يقوم المستخدمون بنسخ وسم الـ script الخاص بك مرة واحدة في قالب التذييل (footer template) الخاص بهم وينسون أمره. لا يمكنك مراسلة خمسة آلاف تاجر عبر البريد الإلكتروني لتطلب منهم تحديث معامل استعلام الإصدار (version query parameter). هذا يعني أن ترويسات التخزين المؤقت (cache headers) الخاصة بك هي جزء من استراتيجية الاستجابة للحوادث (incident response strategy). قم بتعيين max-age قصير لحزمة الـ widget الخاصة بك بحيث عندما ترسل إصلاحاً حرجاً، فإنه ينتشر في غضون ساعات وليس أسابيع. إن ميزة الأصول المخزنة مؤقتاً لفترة طويلة لا تستحق شلل المعرفة بأن آلاف المواقع تعمل بإصدار معطل لا يمكنك استرداده. تقبل تكلفة حركة مرور الـ CDN. فسلامتك النفسية تعتمد على ذلك.

اعكس سياسة أمن المحتوى (CSP) من أجل تضمينات الـ iframe

إذا كنت توفر خيار التضمين القائم على الـ iframe، فإن سياسة أمن المحتوى (Content Security Policy) الخاصة بك تتطلب عكس التفكير التقليدي لتطبيقات الويب. عادةً ما قد تمنع التأطير (framing) لمنع هجمات clickjacking. أما بالنسبة للـ widget، فيجب عليك السماح به. قم بتعيين frame-ancestors * حتى يتمكن أي موقع من استضافة الـ iframe الخاص بك. ثم كن صارماً للغاية بشأن كل شيء آخر. قم بتقييد script-src و style-src و connect-src بدقة داخل سياسة الـ iframe تلك. أنت تعرض نفسك عمداً للويب بشكل عام من خلال ناقل التأطير (framing vector)، لذا يجب عليك التأكد من أن الكود الذي يعمل داخل الـ iframe ليس لديه مجال لسوء التصرف إذا حاولت صفحة مضيفة التلاعب به.

عقلية الضيف

يتطلب بناء التضمينات (embeds) وضعية مختلفة عن بناء تطبيقات الويب القياسية. في تطبيقك الخاص، أنت تملك الحاوية (container)، والتوجيه (routing)، وخط أنابيب البناء (build pipeline)، والأنماط العالمية (global styles). أما في التضمين، فأنت لا تملك شيئاً. الصفحة المضيفة عشوائية، وغالباً ما تكون قديمة، وأحياناً عدائية، ودائماً خارج نطاق سيطرتك. يجب أن يكون كل افتراض دفاعياً. حدد ما تعنيه بوضوح، وتحقق من البيئة بنشاط، وصمم لمواجهة الأعطال التي لا يمكنك رؤيتها. يعمل Clanker Support widget اليوم ليس لأن الويب يمكن التنبؤ به، بل لأننا توقفنا عن الوثوق بقدرته على ذلك.