التحديثات الجزئية التصريحية في HTML: ثورة في تدفق المحتوى خارج الترتيب


كيف تعمل التحديثات الجزئية التصريحية في HTML

لطالما دعم HTML خاصية التدفق (streaming). لا يحتاج الخادم إلى بناء صفحة كاملة في الذاكرة قبل إرسالها إلى المتصفح. يمكنه إرسال HTML الأولي أولاً، ثم إرسال المزيد من الأجزاء (chunks) فور جاهزية كل جزء. يقوم المتصفح بتحليل هذه الأجزاء وعرض الصفحة بالترتيب. هذا أحد الأسباب التي تجعل HTML يبدو سريعًا.

لكن تدفق HTML التقليدي له قاعدة صارمة: يأتي HTML بترتيب المستند. إذا حصل المتصفح على الرأس (header) أولاً، ثم الشريط الجانبي (sidebar)، وأخيرًا المحتوى الرئيسي، فإنه يحلل تلك الأجزاء بهذا الترتيب. إذا تسبب استعلام قاعدة بيانات بطيء في حظر جزء من الصفحة في وقت مبكر، غالبًا ما يضطر الجزء التالي إلى الانتظار حتى يصبح جاهزًا على الخادم.

لقد عملت أطر عمل JavaScript على حل هذه المشكلة لسنوات. تتعامل أطر عمل العرض من جانب الخادم (Server-rendering frameworks) مع الهيكل الأساسي (shell)، وحدود التعليق (suspense boundaries)، وحالة التحميل (loading state)، وتدفق المحتوى المتأخر (late content streaming). تستخدم بعض أطر العمل نصوصًا برمجية مضمنة (inline script) لتصحيح DOM الحالي. تسمح مكتبات مثل HTMX للمطورين بتحديث أجزاء من الصفحة باستخدام HTML مُنشأ من الخادم. لكن هذه الحلول تتطلب JavaScript في مكان ما.

تطرح التحديثات الجزئية التصريحية (Declarative Partial Updates) سؤالًا مختلفًا: ماذا لو كان لدى HTML طريقته الخاصة للقول، "عندما يأتي هذا المحتوى، ضعه هنا"؟ هذه هي الفكرة وراء اقتراح Chrome للتحديثات الجزئية التصريحية. في هذا المقال، ستتعرف على المشكلات التي تهدف التحديثات الجزئية التصريحية إلى حلها، وكيف يعمل بناء الجملة المقترح للعناصر النائبة (placeholder syntax)، وكيف يختلف تدفق HTML خارج الترتيب (out-of-order HTML streaming) عن التدفق العادي، وكيف تتناسب واجهات برمجة تطبيقات إدراج HTML في JavaScript ذات الصلة، ولماذا يجب اعتبارها ميزة تجريبية للمتصفح بدلاً من HTML جاهز للإنتاج.

المشكلة التي تحاول التحديثات الجزئية التصريحية حلها

لنفترض صفحة منتج. يعرف الخادم بالفعل عنوان الصفحة، والتنقل، والتذييل، وتفاصيل المنتج. لكن قسم التوصيات يتطلب استعلامًا بطيئًا لقاعدة البيانات. مع HTML التقليدي المعروض من الخادم، لديك خياران شائعان:

  1. أولاً، ينتظر الخادم حتى يصبح كل شيء جاهزًا، ثم يرسل استجابة HTML كاملة. هذا يبقي الكود بسيطًا، لكن المستخدم ينتظر وقتًا طويلاً قبل رؤية أي شيء مفيد.
  2. ثانيًا، يقوم الخادم بتدفق HTML على مراحل. يرسل الجزء العلوي من الصفحة أولاً، ثم يرسل الباقي فور جاهزيته. يبدو هذا وكأنه يحسن الأداء، لأن المتصفح يبدأ العرض قبل اكتمال الاستجابة بالكامل.

لكن التدفق وحده لا يحل هذه المشكلة تمامًا. لا يزال المتصفح يحلل HTML بشكل تسلسلي. إذا كان هناك جزء توصيات بطيء في بداية المستند، فإن المحتوى بعد هذا الجزء سينتظر خلفه، ما لم تقم بإعادة هيكلة المستند، أو إضافة JavaScript، أو استخدام تجريد إطار عمل.

يصف WICG Patching Explainer قيودين لتدفق HTML التقليدي:

  • يتم تدفق محتوى HTML بترتيب DOM.
  • بعد خطوة تحليل المستند الأولية، لم يعد التدفق نشطًا كما كان من قبل.

تحاول التحديثات الجزئية التصريحية تخفيف القيد الأول. إنها تسمح للخادم بإرسال عنصر نائب أولاً ثم إرسال المحتوى الفعلي في الاستجابة. يطبق المتصفح هذا المحتوى التالي فوق العنصر النائب السابق. لا يتطلب هذا التصحيح أي كود تصحيح DOM مخصص من جانب العميل.

صورة توضيحية للمقال

في الرسم البياني أعلاه، يرسل الخادم أجزاء HTML بالتسلسل. يمكن للمتصفح عرض الأجزاء المبكرة قبل انتهاء الاستجابة، لكن ترتيب العرض لا يزال يتبع ترتيب الاستجابة.

كيف يعمل تدفق HTML التقليدي

قبل دراسة الاقتراح، تحتاج إلى فهم هيكله الأساسي. يرسل الخادم نص استجابة HTTP. يحتوي هذا النص على HTML. يقرأ المتصفح الاستجابة فور وصولها. لا يحتاج إلى انتظار نص الاستجابة بالكامل لتحليل العلامات الأولى. إليك مثال صغير لـ Node.js:

import http from "node:http"; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const server = http.createServer(async (req, res) => { res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", }); res.write(` <!doctype html> <html> <head> <title>Normal HTML Streaming</title> </head> <body> <h1>Normal HTML Streaming</h1> <p>This part arrives first.</p> `); await sleep(2000); res.write(` <p>This part arrives after two seconds.</p> `); await sleep(2000); res.end(` <p>This part arrives after four seconds.</p> </body> </html> `); }); server.listen(3000, () => { console.log("Server running at http://localhost:3000"); });

ينشئ هذا المثال خادم HTTP صغيرًا باستخدام وحدة http المدمجة في Node.js. عند زيارة الصفحة، يرسل الخادم استجابة في ثلاثة أجزاء HTML منفصلة. يرسل res.write() الأول على الفور هيكل المستند، والعنوان، والفقرة الأولى. ثم يوقف sleep(2000) الخادم لمدة ثانيتين قبل أن يرسل res.write() التالي فقرة أخرى. بعد توقف آخر، يرسل res.end() الفقرة الأخيرة ويغلق مستند HTML. يبدأ المتصفح في عرض الجزء الأول قبل اكتمال الاستجابة بأكملها، ثم يضيف الفقرات اللاحقة مع وصول المزيد من HTML. يوضح هذا أن تدفق HTML البسيط يعمل، لكن المحتوى يُعرض بنفس الترتيب الذي يرسله به الخادم. الآن افتح هذه الصفحة في متصفح:

http://localhost:3000

سترى الجزء الأول من الصفحة قبل اكتمال الاستجابة بأكملها. مع إرسال الخادم المزيد من الأجزاء، يستمر المتصفح في التحميل. هذا السلوك قديم وعملي. لكن لاحظ هيكله. يكتب الخادم الفقرة الأولى، ثم الفقرة الثانية، ثم الفقرة الثالثة. يتلقاها المتصفح بنفس الترتيب. كما يضعها في DOM بنفس الترتيب. يسمح لك التدفق التقليدي بإرسال المحتوى السابق أولاً. لكنه لا يمنحك طريقة أصلية للقول إن هذا الجزء التالي سيكون داخل عنصر نائب سابق. تستهدف التحديثات الجزئية التصريحية هذا الجزء المفقود.

لماذا تعمل أطر العمل بالفعل على تجاوز هذه المشكلة

تنشئ أطر العمل الحديثة بالفعل تجارب حيث يتم عرض أجزاء من الصفحة فور جاهزيتها. تُعد مكونات خادم React (React server components) والعرض من جانب الخادم القائم على التعليق (suspense-based server rendering) أمثلة شائعة. يمكن لإطار العمل إرسال هيكل أساسي أولاً، وعرض محتوى احتياطي (fallback)، ثم تدفق المحتوى الكامل لاحقًا. لكن المتصفح لا يفهم حدود React كـ HTML أصلي. يجب على إطار العمل ترميز بروتوكوله الخاص. كما يشير WICG patching explainer، يستخدم React علامات <script> المضمنة لتدفق المحتوى خارج الترتيب وتعديل DOM الذي تم تحليله بالفعل. يعمل هذا لأن JavaScript لديه وصول كامل إلى DOM. لكن هذا يعني أيضًا أن المتصفح لا يطبق التحديث كـ HTML عادي. يشارك وقت تشغيل إطار العمل (framework runtime) أو النص البرمجي المُنشأ بواسطة إطار العمل في هذا التصحيح.

يحل HTMX فئة ذات صلة من المشكلات بطريقة أخرى. يسمح لك بطلب HTML مُنشأ من الخادم واستبداله في الصفحة. إنه قريب من الناحية المفاهيمية، لأنه يتعامل مع HTML كتنسيق الاستجابة الرئيسي. لكن HTMX هي مكتبة JavaScript. الهدف من التحديثات الجزئية التصريحية هو جعل بعض سلوك تصحيح HTML ميزة للمتصفح نفسه. لا يزال HTMX يتعامل مع العديد من أنماط التفاعل التي تتجاوز هذا الاقتراح.

لقد شاعت Astro بنية الجزر (islands architecture)، حيث يتم احتواء الأجزاء التفاعلية المستقلة من الصفحة بشكل أساسي داخل مستند HTML ثابت. تشير مقالة Chrome حول Declarative Partial Updates إلى بنية الجزر كحالة استخدام لهذا الاقتراح.

لا تحل التحديثات الجزئية التصريحية محل هذه الأدوات. إنها تقترح بدائية متصفح منخفضة المستوى (low-level browser primitive). يمكن لأطر العمل لاحقًا اعتماد هذه البدائية. يمكن للتطبيقات المعروضة من الخادم أيضًا استخدامها مباشرة في حالات أبسط. التغيير الرئيسي هو هذا: بدلاً من إرسال JavaScript الذي يجد عقدة DOM ويعدلها، يرسل الخادم HTML الذي يعلن عن مكان وجود العقدة.

ماذا تضيف التحديثات الجزئية التصريحية إلى HTML

يتكون اقتراح Chrome من جزأين رئيسيين:

  1. الجزء الأول هو العناصر النائبة لـ HTML (HTML placeholders) والتدفق خارج الترتيب (out-of-order streaming) باستخدام تصحيح <template>.
  2. الجزء الثاني هو مجموعة جديدة من أساليب JavaScript لإدراج وتدفق HTML في المستندات الموجودة.

يذكر إعلان Chrome أن واجهات برمجة التطبيقات هذه جاهزة للمطورين للاختبار بدءًا من Chrome 148 باستخدام علامة Experimental Web Platform Features. هذه الحالة مهمة. إنها ليست حاليًا HTML مستقرًا عبر المتصفحات. إنه اقتراح نشط واختبار للتنفيذ. بالنسبة لجزء التدفق التصريحي، يستخدم الاقتراح مفهومين لـ HTML:

  • تعليمات المعالجة للعناصر النائبة (Processing instruction placeholders).
  • عنصر <template> مع السمة for.

مثال مبسط يبدو كالتالي:

<div><?marker name="profile-card"></div> <template for="profile-card"> <article> <h2>Ada Lovelace</h2> <p>Mathematician and early computing pioneer.</p> </article> </template>

تحدد العنصر النائب موقعًا. يحتوي القالب (template) على المحتوى. عندما يحلل المتصفح القالب، فإنه يجد العنصر النائب المطابق ويطبق محتوى القالب هناك. بعد التحليل، يتصرف DOM النهائي هكذا:

<div> <article> <h2>Ada Lovelace</h2> <p>Mathematician and early computing pioneer.</p> </article> </div>

أرسل الخادم العنصر النائب أولاً والمحتوى الفعلي لاحقًا. قام المتصفح بدمجهما بشكل تصريحي. هذه هي الفكرة الأساسية. يحصل HTML على تعليمات تصحيح أصلية بدون أي نصوص برمجية مخصصة.

صورة توضيحية للمقال

في الرسم البياني أعلاه، يتلقى المتصفح ويحلل العناصر النائبة أولاً. تستهدف القوالب اللاحقة تلك العناصر النائبة، لذلك تقوم الصفحة المعروضة بتحديث مناطق محددة دون إلحاق كل HTML المتأخر في الأسفل.

كيف تعمل العناصر النائبة للعلامات (Marker Placeholders)

أبسط عنصر نائب هو <?marker>. تشير العلامة إلى مكان في المستند سيظهر فيه كود HTML لاحقًا. إليك مثال صغير من النموذج المقترح:

<section> <h2>Team</h2> <ul> <?marker name="team-members"> </ul> </section> <template for="team-members"> <li>Ada Lovelace</li> </template>

تربط السمة name للعلامة بالسمة for للقالب. عندما يحلل المتصفح القالب، فإنه يجد العلامة المسماة team-members ويدرج محتوى القالب في موقع تلك العلامة. يتصرف DOM النهائي هكذا:

<section> <h2>Team</h2> <ul> <li>Ada Lovelace</li> </ul> </section>

بينما قد يبدو هذا بسيطًا، فإن التوقيت مهم. يمكن أن يأتي العنصر النائب في بداية الاستجابة. يمكن أن يأتي القالب لاحقًا. هذا يعني أن المتصفح يمكنه عرض هيكل أساسي عامل أولاً، ثم تصحيح هذا الموقع المحدد عندما يرسل الخادم المحتوى المتأخر. هذا يختلف عن التدفق العادي، حيث يتم عرض مستند HTML التالي لاحقًا بالترتيب. تحدد العلامة هدفًا لـ HTML التالي.

لماذا تعليمات المعالجة مهمة

قد يبدو بناء جملة العنصر النائب غير عادي إذا كنت تكتب HTML في الغالب.

<?marker name="profile">

هذا مشابه لبناء جملة تعليمات معالجة XML (XML Processing Instruction). توضح MDN أن تعليمات المعالجة موجودة في DOM، ولكنها تُعامل حاليًا كتعليقات في مستندات HTML. يمنح اقتراح Declarative Partial Updates معنى على مستوى المتصفح لتعليمات معالجة مختارة لتصحيح HTML. هذا هو السبب في أن هذه الميزة تتطلب دعم المتصفح. في HTML المستقر الحالي، كتابة <?marker name="profile"> لا ينشئ نقطة تصحيح في جميع المتصفحات. بدون دعم، فإنه يعامل المحتوى كعلامات مهملة أو تعليقات. لذا يجب أن تعتبر هذا البناء سلوكًا موصى به، وليس سلوك HTML راسخًا.

كيف تعمل العناصر النائبة لنطاق البدء والانتهاء (Start and End Range Placeholders)

تمنحك علامة واحدة نقطة إدراج. لكن العديد من تحديثات واجهة المستخدم تحتاج إلى استبدال منطقة كاملة. على سبيل المثال، قد تعرض الصفحة رسالة تحميل أولاً:

<section> <h2>Recommendations</h2> <?start name="recommendations"> <p>Loading recommendations...</p> <?end> </section>

لاحقًا، يرسل الخادم المحتوى الحقيقي:

<template for="recommendations"> <ul> <li>Advanced CSS Layouts</li> <li>Modern HTML APIs</li> <li>Web Performance Basics</li> </ul> </template>

يستبدل المتصفح النطاق بين <?start> و <?end> بمحتوى القالب. يتصرف DOM النهائي هكذا:

<section> <h2>Recommendations</h2> <ul> <li>Advanced CSS Layouts</li> <li>Modern HTML APIs</li> <li>Web Performance Basics</li> </ul> </section>

هذا قريب من حدود التحميل. يبدأ المستند بمحتوى احتياطي ضروري. ينهي الخادم العمل البطيء لاحقًا. عندما يصل القالب المناسب، يستبدل المتصفح المحتوى الاحتياطي. توجيه المعالجة <?end> في الشرح اختياري، لكنه يجعل الأمثلة أسهل في الفهم. استخدمه عند التدريس أو اختبار الميزة.

علامة مقابل نطاق

استخدم العلامات لإضافة محتوى في موقع محدد. استخدم نطاق بدء وانتهاء لاستبدال محتوى مؤقت. هذا يعني، أضف شيئًا هنا:

<ul> <?marker name="new-item"> </ul>

هذا يقول، استبدل منطقة التحميل هذه بالكامل لاحقًا:

<div> <?start name="profile"> <p>Loading profile...</p> <?end> </div>

الطريقة الثانية أكثر ملاءمة للواجهات الحقيقية، حيث يرى المستخدمون محتوى احتياطيًا ذا معنى أثناء انتظارهم.

كيف تعمل التحديثات المتعددة

لا يجب أن يكون العنصر النائب مجرد تحديث واحد. يمكن للخادم إرسال تصحيح، وترك علامة أخرى، ثم إرسال تصحيح آخر لاحقًا. هذا مهم للقوائم، والموجزات، والإشعارات، والسجلات، والتعليقات، ونتائج البحث. إليك مثال مبسط:

<ul> <?marker name="messages"> </ul> <template for="messages"> <li>First message</li> <?marker name="messages"> </template> <template for="messages"> <li>Second message</li> <?marker name="messages"> </template> <template for="messages"> <li>Third message</li> </template>

يتصرف DOM النهائي هكذا:

<ul> <li>First message</li> <li>Second message</li> <li>Third message</li> </ul>

يضيف كل تصحيح محتوى وينشئ المخرج التالي. هذا النموذج مناسب لتدفق البيانات، حيث يجد الخادم النتائج بمرور الوقت. فكر في صفحة بحث. يجد الخادم النتيجة الأولى بسرعة. تتطلب النتيجة الثانية استدعاء خدمة أخرى. تتطلب النتيجة الثالثة بحثًا في قاعدة البيانات. يضع تدفق HTML التقليدي كل نتيجة حيث تظهر في الاستجابة. يسمح التصحيح التصريحي لكل نتيجة باستهداف مخرج معروف في المستند. هذا لا يعني أن كل قائمة يجب أن تستخدم هذه الميزة. إنه يعني أن HTML سيكون لديه بدائية أصلية لهذا النمط.

كيف تعمل التحديثات المتداخلة (Interleaved Updates)

الجزء الأكثر إثارة للاهتمام هو التداخل. لنفترض أن الصفحة تحتوي على ثلاث مناطق:

  • بطاقة ملف شخصي (profile card).
  • لوحة توصيات (recommendations panel).
  • قائمة إشعارات (notification list).

تتطلب كل منطقة إدارة خادم مختلفة. تكون بطاقة الملف الشخصي جاهزة بعد ثانية واحدة. تكون الإشعارات جاهزة بعد ثانيتين. تكون التوصيات جاهزة بعد أربع ثوانٍ. في التدفق البسيط، يحدد ترتيب مستنداتك ما يراه المستخدم أولاً. مع التصحيح التصريحي، يرسل الخادم العناصر النائبة مبكرًا:

<main> <section> <h2>Profile</h2> <?start name="profile"> <p>Loading profile...</p> <?end> </section> <section> <h2>Recommendations</h2> <?start name="recommendations"> <p>Loading recommendations...</p> <?end> </section> <section> <h2>Notifications</h2> <?start name="notifications"> <p>Loading notifications...</p> <?end> </section> </main>

ثم يرسل الخادم القوالب بترتيب الجاهزية، وليس الترتيب المرئي:

<template for="profile"> <p>Ada Lovelace</p> </template> <template for="notifications"> <ul> <li>You have one new message.</li> </ul> </template> <template for="recommendations"> <ul> <li>Modern HTML APIs</li> <li>Streaming Web Apps</li> </ul> </template>

يصحح المتصفح كل منطقة مستهدفة. يحصل المستخدم على الملف الشخصي أولاً، ثم الإشعار ثانيًا، ثم التوصيات ثالثًا، على الرغم من أن التوصيات تظهر قبل الإشعارات في هيكل المستند الرئيسي. هذا هو السبب في أن "التدفق خارج الترتيب" هو المفتاح. تأتي استجابة HTML كتدفق. لكن التحديث المعروض لا يجب أن يتبع نفس الترتيب المرئي لأجزاء الاستجابة. يصف WICG Explainer التصحيح المتداخل عبر منافذ مختلفة. هذا أحد أقوى الأسباب التي تجعل هذا الاقتراح مهمًا. إنه يمنح HTML الأصلي للمتصفح سلوكًا يربطه المطورون عادةً بالتدفق على مستوى إطار العمل.

مقارنة هذه الميزة بـ React و HTMX و Astro و PHP

ستسمح هذه الميزة بإجراء مقارنات. يمكن أن تساعدك هذه المقارنات في فهم الفكرة الرئيسية، ولكنها قد تكون مربكة أيضًا إذا تعمقت كثيرًا.

React

يدعم عرض خادم React بالفعل واجهة المستخدم المتدفقة وحالة الاحتياطي. لكن نموذج العرض هو نموذج React الخاص. عندما يحتاج React إلى تصحيح محتوى مضاف حديثًا إلى مستند تم تحليله بالفعل، فإنه يستخدم توجيهات JavaScript ومُنشأة بواسطة إطار العمل. تجلب التحديثات الجزئية التصريحية جزءًا منخفض المستوى من هذا المفهوم إلى HTML. إنها لا تحل محل نموذج مكونات React، أو نموذج الحالة، أو معالجة الأحداث، أو المصالحة، أو النظام البيئي.

HTMX

يسمح لك HTMX بطلب HTML من الخادم واستبداله في الصفحة. إنه قريب من الناحية المفاهيمية، لأنه يتعامل مع HTML كتنسيق الاستجابة الرئيسي. لكن HTMX هي مكتبة JavaScript. الهدف من التحديثات الجزئية التصريحية هو جعل بعض سلوك تصحيح HTML ميزة للمتصفح نفسه. لا يزال HTMX يتعامل مع العديد من أنماط التفاعل التي تتجاوز هذا الاقتراح.

Astro

شاعت Astro العرض القائم على الجزر (island-based rendering). يمكن أن تتكون الصفحة من HTML ثابت في الغالب ومناطق تفاعلية معزولة. يستشهد Chrome ببنية الجزر كحالة استخدام، لأن المناطق المستقلة من الصفحة غالبًا ما يتم عرضها في أوقات مختلفة. يمكن أن تساعد التحديثات الجزئية التصريحية الخوادم في تقديم HTML الجزر فور عرض كل منطقة.

PHP

قد يذكرك بناء جملته بـ PHP، حيث تم استخدام كود من جانب الخادم و HTML معًا لعقود. لكن هذا الاقتراح ليس PHP داخل المتصفح. يعمل PHP على الخادم. التحديثات الجزئية التصريحية هي سلوك تحليل المتصفح لـ HTML المتدفق. يمكن إجراء مقارنة مفيدة لسير العمل معه. إنه يسهل تدفق HTML المُنشأ من الخادم إلى مواقع محددة في الصفحة دون الحاجة إلى إرسال تصحيح JavaScript منفصل لكل تحديث.

كيف تتناسب واجهات برمجة تطبيقات إدراج HTML في JavaScript

تحل العناصر النائبة التصريحية جزءًا من المشكلة. إنها تساعد المتصفح في تصحيح HTML المتدفق إلى موقع سابق في نفس المستند. لكن ليس كل تحديث يأتي كجزء من استجابة HTML الأصلية. تجلب العديد من التطبيقات HTML بعد تحميل الصفحة. على سبيل المثال، قد تجلب الصفحة:

  • قسم التعليقات
  • ملخص سلة التسوق
  • قائمة منسدلة للملف الشخصي
  • لوحة الإشعارات
  • معاينة نتائج البحث

اليوم، لدى JavaScript عدة طرق لإدراج HTML في مستند:

element.innerHTML = "<p>Hello</p>"; element.outerHTML = "<section>Hello</section>"; element.insertAdjacentHTML("beforeend", "<p>Hello</p>");

تعمل واجهات برمجة التطبيقات هذه، لكنها لا تتصرف بنفس الطريقة. بعضها يستبدل المحتوى. بعضها يدرج بجانب المحتوى. بعضها يحلل في سياق معين. بعضها يتفاعل مع قواعد التطهير والأمان بشكل مختلف. يتضمن عمل تحديثات Chrome الجزئية التصريحية أيضًا مجموعة مُجددة من واجهات برمجة تطبيقات إدراج وتدفق HTML، بهدف إنشاء اصطلاح تسمية أكثر وضوحًا لأنماط الإدراج الشائعة. إليك الفكرة الأساسية:

الإجراءطريقة ثابتة (Static method)طريقة تدفق (Streaming method)
استبدال أبناء العنصرsetHTML()streamHTML()
استبدال العنصر نفسهreplaceWithHTML()streamReplaceWithHTML()
الإدراج قبل العنصرbeforeHTML()streamBeforeHTML()
الإدراج كأول ابنprependHTML()streamPrependHTML()
الإدراج كآخر ابنappendHTML()streamAppendHTML()
الإدراج بعد العنصرafterHTML()streamAfterHTML()

تخبرك الأسماء بمكان ذهاب HTML. هذا هو التحسين الرئيسي. بدلاً من تذكر كيفية اختلاف innerHTML و outerHTML و insertAdjacentHTML و createContextualFragment()، يصف اسم الطريقة الإجراء.

الإدراج الثابت (Static Insertion)

تقبل الطريقة الثابتة سلسلة HTML كاملة.

const card = document.querySelector("#profile-card"); card.setHTML(` <article> <h2>Ada Lovelace</h2> <p>Mathematician and early computing pioneer.</p> </article> `);

يقوم setHTML() بتحليل السلسلة، وتطهيرها، وإدراج النتيجة في العنصر. تصف MDN setHTML() كطريقة آمنة من XSS لتحليل وتطهير سلسلة HTML قبل تضمينها في DOM. هذا أكثر أمانًا من تعيين HTML غير الموثوق به إلى innerHTML.

الإدراج المتدفق (Streaming Insertion)

لا تتطلب طريقة التدفق سلسلة HTML بأكملها للبدء. إنها تنشئ تدفقًا قابلاً للكتابة (writable stream). ثم يكتب JavaScript أجزاء (fragments) إلى هذا التدفق. مثال مبسط يبدو كالتالي:

const output = document.querySelector("#output"); const writer = output.streamHTMLUnsafe().getWriter(); await writer.write("<p>First streamed chunk</p>"); await writer.write("<p>Second streamed chunk</p>"); await writer.close();

هذا النموذج مهم لأن التدفق هو أحد نقاط قوة HTML. لا يجب على المتصفح دائمًا انتظار استجابة كاملة قبل إدراج علامات ذات معنى. يعرض Chrome أيضًا هذا النمط مع fetch():

const output = document.querySelector("#output"); const response = await fetch("/comments"); response.body .pipeThrough(new TextDecoderStream()) .pipeTo(output.streamHTMLUnsafe());

هنا، يتدفق نص الاستجابة من الشبكة. يحول TextDecoderStream البايتات إلى نص. يتم توجيه النتيجة إلى تدفق HTML للعنصر.

لماذا اسم Unsafe مهم

تحتوي بعض الطرق على إصدارات Unsafe. على سبيل المثال:

setHTMLUnsafe(); streamHTMLUnsafe(); appendHTMLUnsafe(); streamAppendHTMLUnsafe();

الاسم مقصود. تحذر MDN من أن setHTMLUnsafe() يحلل الإدخال كـ HTML ويكتب النتيجة إلى DOM. إذا لم تمرر مطهرًا (sanitizer)، فلن يتم استخدام أي مطهر. استخدم الإصدارات الآمنة للمحتوى غير الموثوق به. اعتبر الإصدارات غير الآمنة أداة منخفضة المستوى، خاصة في الحالات التي يكون لديك فيها تحكم كامل في مصدر HTML أو تمرر مطهرًا مناسبًا. بالنسبة للعرض التوضيحي في هذا المقال، ابقِ مدخلات المستخدم بعيدًا عن سلسلة HTML. الهدف هو تعليم سلوك التدفق، وليس إدراج HTML غير آمن.

صورة توضيحية للمقال

في الرسم البياني أعلاه، يبدأ التصحيح التصريحي داخل المستند المتدفق. يبدأ إدراج JavaScript المتدفق بعد أن يختار كود JavaScript عنصرًا ويوجه استجابة متدفقة إليه.

كيفية بناء عرض توضيحي صغير لتدفق Node.js

الآن دعنا نبني عرضًا توضيحيًا صغيرًا. ستنشئ خادم Node.js واحدًا بثلاثة مسارات (routes):

المسار (Route)الغرض (Purpose)
/normal-streamيعرض تدفق HTML العادي بالترتيب
/partial-updates-demoيعرض بناء جملة التحديث الجزئي التصريحي المقترح
/stream-html-api-demoيعرض بناء جملة واجهة برمجة تطبيقات إدراج HTML المتدفقة في JavaScript

يستخدم العرض التوضيحي وحدة http المدمجة في Node.js. لا يوجد Express. لا يوجد إطار عمل للواجهة الأمامية. لا توجد خطوة بناء. هذا يبقي التركيز على تدفق HTML.

إنشاء المشروع

أنشئ مجلدًا جديدًا:

mkdir html-partial-updates-demo cd html-partial-updates-demo

أنشئ ملف package.json:

{ "name": "html-partial-updates-demo", "version": "1.0.0", "type": "module", "scripts": { "dev": "node server.js" } }

أنشئ ملف server.js:

import http from "node:http"; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const server = http.createServer(async (req, res) => { if (req.url === "/normal-stream") { return normalStream(req, res); } if (req.url === "/partial-updates-demo") { return partialUpdatesDemo(req, res); } if (req.url === "/stream-html-api-demo" || req.url === "/comments-stream") { return streamHtmlApiDemo(req, res); } res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", }); res.end(` <!doctype html> <html> <head> <title>HTML Partial Updates Demo</title> </head> <body> <h1>HTML Partial Updates Demo</h1> <ul> <li><a href="/normal-stream">Normal HTML streaming</a></li> <li><a href="/partial-updates-demo">Declarative partial updates demo</a></li> <li><a href="/stream-html-api-demo">Streaming HTML API demo</a></li> </ul> </body> </html> `); }); async function normalStream(req, res) { res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", }); res.write(` <!doctype html> <html> <head> <title>Normal HTML Streaming</title> </head> <body> <h1>Normal HTML Streaming</h1> <p>This paragraph arrives immediately.</p> `); await sleep(1500); res.write(` <p>This paragraph arrives after 1.5 seconds.</p> `); await sleep(1500); res.end(` <p>This paragraph arrives after 3 seconds.</p> <p> <a href="/">Back to home</a> </p> </body> </html> `); } async function partialUpdatesDemo(req, res) { res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", }); res.write(` <!doctype html> <html> <head> <title>Declarative Partial Updates Demo</title> <style> body { font-family: system-ui, sans-serif; max-width: 760px; margin: 40px auto; line-height: 1.5; } section { border: 1px solid #ddd; border-radius: 12px; padding: 16px; margin-bottom: 16px; } .loading { color: #666; } </style> </head> <body> <h1>Declarative Partial Updates Demo</h1> <p> This page sends placeholders first. Then the server sends matching templates when each piece of content is ready. </p> <section> <h2>Profile</h2> <?start name="profile"> <p class="loading">Loading profile...</p> <?end> </section> <section> <h2>Recommendations</h2> <?start name="recommendations"> <p class="loading">Loading recommendations...</p> <?end> </section> <section> <h2>Notifications</h2> <?start name="notifications"> <p class="loading">Loading notifications...</p> <?end> </section> `); await sleep(1000); res.write(` <template for="profile"> <p><strong>Ada Lovelace</strong></p> <p>Mathematician and early computing pioneer.</p> </template> `); await sleep(1000); res.write(` <template for="notifications"> <ul> <li>You have one new message.</li> <li>Your weekly report is ready.</li> </ul> </template> `); await sleep(2000); res.end(` <template for="recommendations"> <ul> <li>Modern HTML APIs</li> <li>Streaming Web Apps</li> <li>Web Performance Basics</li> </ul> </template> <p> <a href="/">Back to home</a> </p> </body> </html> `); } async function streamHtmlApiDemo(req, res) { if (req.url === "/comments-stream") { res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", }); res.write(`<article><p>First streamed comment.</p></article>`); await sleep(1000); res.write(`<article><p>Second streamed comment.</p></article>`); await sleep(1000); return res.end(`<article><p>Third streamed comment.</p></article>`); } res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", }); res.end(` <!doctype html> <html> <head> <title>Streaming HTML API Demo</title> </head> <body> <h1>Streaming HTML API Demo</h1> <button id="load-comments">Load comments</button> <section id="comments"></section> <p> <a href="/">Back to home</a> </p> <script> const button = document.querySelector("#load-comments") const comments = document.querySelector("#comments") button.addEventListener("click", async () => { const response = await fetch("/comments-stream") response.body .pipeThrough(new TextDecoderStream()) .pipeTo(comments.streamHTMLUnsafe()) }) </script> </body> </html> `); } server.listen(3000, () => { console.log("Server running at http://localhost:3000"); });

ينشئ هذا الملف خادم العرض التوضيحي الرئيسي. يستورد وحدة http المدمجة في Node.js، ثم يحدد دالة مساعدة صغيرة sleep() للتدفق المتأخر، ثم ينشئ خادم HTTP بثلاثة مسارات مخططة. عندما يتطابق عنوان URL للطلب مع /normal-stream أو /partial-updates-demo أو /stream-html-api-demo، يمرر الخادم الطلب إلى الدالة المقابلة. إذا زار المستخدم الصفحة الجذرية، يعيد الخادم صفحة HTML بسيطة تحتوي على روابط للعروض التوضيحية الثلاثة. أخيرًا، يبدأ server.listen(3000) الخادم على المنفذ 3000، حتى تتمكن من فتح العرض التوضيحي في متصفحك على http://localhost:3000.

شغل الخادم:

npm run dev

ثم افتح في المتصفح:

http://localhost:3000

لديك الآن ثلاثة عروض توضيحية.

اختبار مسار التدفق العادي (Normal Streaming Route)

افتح هذا المسار:

http://localhost:3000/normal-stream

راقب المتصفح. تظهر الفقرة الأولى مبكرًا. تظهر الفقرة الثانية لاحقًا. تظهر الفقرة الثالثة أخيرًا. هذا مفيد، لكنه لا يزال تدفقًا بالترتيب. يعرض المتصفح بنفس الترتيب الذي يرسله الخادم.

اختبار مسار التحديثات الجزئية التصريحية (Declarative Partial Updates Route)

افتح هذا المسار:

http://localhost:3000/partial-updates-demo

في متصفح يدعم الميزة التجريبية، تعرض الصفحة أولاً ثلاث مناطق تحميل. بعد ثانية واحدة، يتم تحديث منطقة الملف الشخصي. بعد ثانية أخرى، يتم تحديث منطقة الإشعارات. بعد ثانيتين إضافيتين، يتم تحديث منطقة التوصيات. لاحظ الترتيب. يظهر قسم التوصيات قبل الإشعارات في المستند. لكن الخادم يرسل قالب الإشعارات قبل قالب التوصيات. هذه هي النقطة. لم يعد هيكل المستند وترتيب التدفق بحاجة إلى أن يكونا متماثلين. يستخدم المتصفح السمة for في كل <template> للعثور على تعليمات المعالجة المطابقة للعنصر النائب. يوضح هذا المثال الفكرة الأساسية وراء تدفق HTML خارج الترتيب.

اختبار مسار واجهة برمجة تطبيقات تدفق HTML (Streaming HTML API Route)

افتح هذا المسار:

http://localhost:3000/stream-html-api-demo

انقر على الزر. يجلب المتصفح /comments-stream. يرسل الخادم HTML التعليقات في أجزاء. يوجه العميل نص الاستجابة المتدفق إلى العنصر #comments. هذا ليس هو نفسه تصحيح العنصر النائب التصريحي. هنا، يبدأ JavaScript الطلب ويختار العنصر المستهدف. تحسن واجهة برمجة التطبيقات الجديدة كيفية إدراج JavaScript لـ HTML المتدفق. يحسن بناء جملة العنصر النائب كيفية إعلان HTML نفسه عن التصحيحات اللاحقة أثناء تدفق المستند. حافظ على فصل هاتين الفكرتين. إنهما مرتبطتان، لكنهما تحلان أجزاء مختلفة من قصة التحديث الجزئي.

دعم المتصفح والحالة الحالية

التحديثات الجزئية التصريحية هي ميزة تجريبية. يقول Chrome إن واجهات برمجة التطبيقات هذه جاهزة لاختبار المطورين بدءًا من Chrome 148 من خلال علامة Experimental Web Platform Features. هذا يعني أنه يجب عليك قراءة هذا الاقتراح كعمل منصة نشط، وليس كـ HTML إنتاجي مستقر. لاختبار السلوك الأصلي في Chrome، قم بتمكين هذه العلامة:

chrome://flags/#enable-experimental-web-platform-features

ثم أعد تشغيل المتصفح. يصف Blink developer thread الميزة كطريقة لتدفق محتوى HTML خارج الترتيب وتحديث مستند موجود بتدفق من التصحيحات المشفرة. يحتوي الاقتراح أيضًا على WICG explainer و WHATWG HTML pull request. هذا يخبرك بمكانة الاقتراح:

  • لديه مسار تنفيذ حقيقي في Chrome.
  • لديه شرح في WICG.
  • لديه مناقشة توحيد في WHATWG.

إنه ليس معيارًا نهائيًا عبر المتصفحات بعد. لذا فإن التحديثات الجزئية التصريحية هي مجموعة مقترحة من واجهات برمجة تطبيقات المتصفح لتصحيح وتدفق HTML الأصلي. جعلها Chrome متاحة لاختبار المطورين خلف علامة.

الأمان والتطهير والحواف الحادة

قد تبدو التحديثات الجزئية بسيطة. لكن هناك دائمًا مخاطر أمنية عند إضافة HTML إلى مستند. HTML ليس مجرد نص عادي. يمكن أن يحتوي على روابط، ونماذج، ومعالجات أحداث، ونصوص برمجية، وعناصر مخصصة، وعناصر أخرى يحللها المتصفح. لذا فإن مصدر HTML مهم. إذا كان الخادم الخاص بك يتدفق HTML موثوقًا به تم إنشاؤه بواسطة القالب الخاص بك، فإن نوع المخاطرة يختلف عن نوع المخاطرة عند إضافة HTML المرسل من تعليقات المستخدم، أو ملفه الشخصي، أو رسالة الدردشة.

نطاق القالب (Template Scope)

تشير مقالة Chrome إلى قيد مهم لـ <template for>. يقوم القالب بتحديث تعليمات المعالجة فقط داخل نفس العنصر الأصل. إضافة <template for> مباشرة إلى <body> يمنحه وصولاً إلى المستند بأكمله، بما في ذلك <head>. هذا السلوك قوي، لذا يجب أن تفهم النطاق قبل التصميم حوله. النقطة الرئيسية بسيطة: لا تفترض أن القالب المتأخر يجب أن يصحح أي عنصر نائب في أي مكان بدون قواعد. يتم تحديد هدف التصحيح لأغراض الأمان والقدرة على التنبؤ.

الإدراج الديناميكي له سلوك مختلف

هناك جانب حاد آخر. إذا قمت بإدراج جزء من HTML ديناميكيًا باستخدام واجهة برمجة تطبيقات مثل setHTML() أو innerHTML، يحلل المتصفح هذا HTML داخل جزء وسيط أولاً. هذا يعني أن التصحيح التصريحي ينطبق داخل هذا الجزء المحلل. لا يبحث تلقائيًا في المستند الموجود بأكمله ويحدث العناصر النائبة القديمة خارج الجزء. هذا التمييز مهم لأن تدفق المستند وإدراج HTML الديناميكي ليسا نفس العملية.

واجهات برمجة تطبيقات HTML الآمنة وغير الآمنة

تفصل واجهات برمجة تطبيقات إدراج JavaScript أيضًا بين الطرق الأكثر أمانًا والأقل مستوى. يقوم setHTML() بتطهير الإدخال قبل إدراجه. setHTMLUnsafe() أقل مستوى. تقول MDN إنه يجب دائمًا تفضيل setHTML() حيثما كان مدعومًا لأنه يزيل دائمًا كيانات HTML غير الآمنة من XSS. لذا فإن القواعد بسيطة:

  • استخدم واجهات برمجة التطبيقات الآمنة للمحتوى غير الموثوق به.
  • تعامل مع واجهات برمجة التطبيقات غير الآمنة كأدوات متقدمة لـ HTML الموثوق به أو HTML المطهر بعناية.
  • لا تقم أبدًا بتدفق HTML الذي يتحكم فيه المستخدم إلى DOM بدون استراتيجية تطهير.

ماذا يعني هذا لمطوري الويب

تعتبر التحديثات الجزئية التصريحية مهمة لأنها تنقل نمط إطار عمل شائع أقرب إلى منصة الويب. لسنوات، بنى المطورون أنظمة تحديث جزئية باستخدام JavaScript:

  • جلب HTML.
  • البحث عن عقدة DOM.
  • استبدال محتواها.
  • الحفاظ على حالات التحميل متزامنة.
  • تجنب كسر معالجات الأحداث.
  • التعامل مع الأخطاء.
  • التعامل مع الأمان.

تعمل أطر العمل والمكتبات على تحسين سير العمل هذا. لكن المتصفح لا يزال يفتقر إلى لغة أصلية صغيرة للقول: "هذا HTML المتأخر ينتمي إلى هذا العنصر النائب السابق." توفر التحديثات الجزئية التصريحية هذه اللغة. هذا لا يعني أن JavaScript سيختفي. لا تزال بحاجة إلى JavaScript للتفاعل، والحالة، ومعالجة الأحداث، وتدفقات البيانات من جانب العميل، والعديد من سلوكيات التطبيق. هذا لا يعني أيضًا أن أطر العمل ستختفي. React و HTMX و Astro والأدوات المماثلة تحل مشكلات أكبر من تصحيح DOM. النتيجة الأكثر واقعية هي هذه: قد تستخدم أطر العمل وأدوات العرض من جانب الخادم هذه البدائيات داخليًا في النهاية. قد تستخدمها التطبيقات الأصغر المعروضة من جانب الخادم مباشرة لمناطق التحميل البسيطة. قد تكتسب المتصفحات آلية أنظف وأقل مستوى لتدفق HTML إلى مواقع دقيقة. هذا هو الجزء المهم. لا يجعل الاقتراح HTML إطار عمل تطبيق كامل. إنه يمنح HTML بدائية تصحيح تدفق أفضل.

متى ستساعد هذه الميزة

تتناسب التحديثات الجزئية التصريحية مع الصفحات التي يعرف فيها الخادم التخطيط مبكرًا ولكن بعض الأقسام تنتهي لاحقًا. تتضمن الأمثلة الجيدة:

  • صفحات المنتجات ذات كتل التوصيات البطيئة.
  • لوحات المعلومات ذات بطاقات البيانات المستقلة.
  • صفحات البحث حيث تصل النتائج من خدمات متعددة.
  • مواقع التوثيق ذات التنقل أو القوائم الثقيلة.
  • صفحات الملف الشخصي ذات مناطق النشاط والإحصائيات والإشعارات المنفصلة.
  • تطبيقات العرض من جانب الخادم التي تريد حالات تحميل بدون نصوص تصحيح من جانب العميل.

الأنسب هو HTML المُنشأ من الخادم. إذا كان تطبيقك يعرض كل شيء بالفعل على العميل، فلن يغير هذا الاقتراح بنيتك كثيرًا. إذا كان تطبيقك يركز على العرض الأولي السريع، والتدفق التدريجي، والنفقات العامة المنخفضة لـ JavaScript، وواجهات المستخدم المعروضة من جانب الخادم، يصبح هذا الاقتراح أكثر جاذبية.

متى يجب تجنبها اليوم

يجب عليك تجنب الاعتماد الإنتاجي على التحديثات الجزئية التصريحية اليوم. استخدمها من أجل:

  • التعلم.
  • العروض التوضيحية المحلية.
  • تجارب المتصفح.
  • أبحاث إطار العمل.
  • تجارب التحسين التدريجي.

لا تستخدم هذا كمسار وحيد لواجهات المستخدم الإنتاجية المهمة بعد. دعم المتصفح ليس واسع الانتشار بما يكفي بعد. قد يتغير التوصية. الأدوات ليست ناضجة بعد. معظم المستخدمين لن يتركوا العلامة التجريبية ممكّنة. إذا كنت تجربها، فأنشئ محتوى احتياطيًا. على سبيل المثال، يجب أن تظل العناصر النائبة منطقية في الصفحة المعروضة من الخادم حتى لو لم يتم تصحيحها أصلاً. أو، يجب عليك استخدام محتوى احتياطي صغير من JavaScript في تطبيقك حتى يتحسن دعم المتصفح.

الخاتمة

تدفق HTML قديم. تصحيح HTML خارج الترتيب هو الفكرة الجديدة. يسمح التدفق التقليدي للمتصفح بعرض الأجزاء فور وصولها، لكن المستند لا يزال يتبع ترتيب الاستجابة. تقترح التحديثات الجزئية التصريحية طريقة أصلية لوضع HTML المتأخر في عناصر نائبة سابقة. بناء الجملة الأساسي صغير:

<?marker name="target"> <template for="target"> <p>Late content</p> </template>

لمناطق التحميل، يستخدم بناء الجملة نطاقًا:

<?start name="target"> <p>Loading...</p> <?end> <template for="target"> <p>Real content</p> </template>

يمنح هذا HTML نموذج تصحيح أصلي للمتصفح. كما يفتح الباب أمام تقليل JavaScript المخصص في بعض واجهات التدفق المعروضة من الخادم. لكن هذا لا يزال تجريبيًا. أفضل طريقة للتفكير في التحديثات الجزئية التصريحية ليست أن HTML يحل محل أطر العمل. طريقة أفضل هي هذه: HTML يكتسب بدائية منخفضة المستوى كانت أطر العمل والتطبيقات المعروضة من الخادم بحاجة إليها لسنوات. إذا مضى الاقتراح قدمًا، فقد يحصل مطورو الويب على طريقة أنظف لبناء صفحات سريعة، معروضة تدريجيًا حيث يقوم الخادم بتدفق المحتوى فور جاهزية كل جزء.