BigPipe هي تقنية تم تطويرها بواسطة Facebook لتحسين سرعة تحميل صفحة الويب. لا توجد مقالات تقريبًا مع Node.js على الإنترنت. في الواقع ، ليس فقط Node.js. تطبيقات BigPipe بلغات أخرى نادرة على الإنترنت. بعد فترة طويلة من ظهور هذه التكنولوجيا ، اعتقدت أنه بعد إرسال إطار صفحة الويب بأكمله أولاً ، استخدمت طلبات أخرى أو عدة طلبات AJAX لطلب الوحدات في الصفحة. لم يكن حتى وقت ليس ببعيد علمت أن المفهوم الأساسي لـ BigPipe هو استخدام طلب HTTP واحد فقط ، ولكن يتم إرسال عناصر الصفحة بالترتيب.
سيكون من السهل فهم هذا المفهوم الأساسي. بفضل الميزة غير المتزامنة لـ Node.js ، من السهل تنفيذ BigPipe مع Node.js. ستستخدم هذه المقالة أمثلة خطوة بخطوة لتوضيح أسباب تقنية BigPipe وتطبيق بسيط يعتمد على Node.js.
سأستخدم Express لإظهار. من أجل البساطة ، نختار Jade كمحرك القالب ، ونحن لا نستخدم الميزة الفرعية للمحرك (الجزئي) ، ولكن بدلاً من ذلك نستخدم القالب الفرعي لجعل HTML كبيانات قالب الأصل.
قم أولاً بإنشاء مجلد nodejs-bigpipe واكتب ملف package.json على النحو التالي:
نسخة الكود كما يلي:
{
"الاسم": "Bigpipe-Experiment"
، "الإصدار": "0.1.0"
، "خاص": صحيح
، "التبعيات": {
"Express": "3.xx"
، "توحيد": "آخر"
، "اليشم": "آخر"
}
}
قم بتشغيل تثبيت NPM لتثبيت هذه المكتبات الثلاث. يتم استخدام الدمج لتسهيل استدعاء اليشم.
لنجعل أبسط محاولة أولاً ، ملفان:
app.js:
نسخة الكود كما يلي:
var express = require ('Express')
، سلبيات = مطلوبة ("توحيد")
، اليشم = مطلوب ('اليشم')
، المسار = مطلوب ('المسار')
var app = express ()
app.engine ('Jade' ، cons.jade)
App.set ('Views' ، path.join (__ dirname ، 'Views'))
app.set ('View Engine' ، 'Jade')
app.use (function (req ، res) {
Res.Render ('Layout' ، {
S1: "مرحبًا ، أنا القسم الأول."
، S2: "مرحبًا ، أنا القسم الثاني."
})
})
app.listen (3000)
وجهات النظر/التصميم
نسخة الكود كما يلي:
Doctype HTML
رأس
عنوان مرحبا ، العالم!
أسلوب
قسم {
الهامش: 20 بكسل Auto ؛
الحدود: 1px منقط رمادي ؛
العرض: 80 ٪ ؛
الارتفاع: 150 بكسل ؛
}
القسم#S1! = S1
القسم#S2! = S2
الآثار هي كما يلي:
بعد ذلك ، وضعنا قالب قسمين في ملفين مختلفين:
وجهات النظر/s1.jade:
نسخة الكود كما يلي:
H1 جزئي 1
.Content! = المحتوى
وجهات النظر/s2.jade:
نسخة الكود كما يلي:
H1 جزئي 2
.Content! = المحتوى
أضف بعض الأنماط إلى تصميم. jade
نسخة الكود كما يلي:
القسم H1 {
حجم الخط: 1.5 ؛
الحشو: 10px 20px ؛
الهامش: 0 ؛
Border-Bottom: 1px منقط رمادي ؛
}
القسم div {
الهامش: 10px ؛
}
قم بتغيير جزء app.use () من app.js إلى:
نسخة الكود كما يلي:
var temp = {
S1: Jade.compile (fs.readfilesync (path.join (__ dirname ، 'Views' ، 'S1.Jade')))))))
، S2: Jade.compile (fs.readfilesync (path.join (__ dirname ، 'Views' ، 'S2.jade'))))))
}
app.use (function (req ، res) {
Res.Render ('Layout' ، {
S1: Temp.S1 ({Content: "مرحبًا ، أنا القسم الأول."})
، S2: Temp.S2 ({Content: "مرحبًا ، أنا القسم الثاني."})
})
})
قلنا من قبل ، "يتم الانتهاء من HTML بعد التقديم باستخدام القالب الفردي كبيانات القالب الأصل" ، مما يعني أن الطريقتين temp.s1 و temp.s2 سيقومان بإنشاء رمز HTML للملفين S1.Jade و S2.Jade ، ثم استخدام هاتين الرمزتين كقيمتين للمتغيرين S1 و S2 في Layout.Jade.
الآن تبدو الصفحة هكذا:
بشكل عام ، يتم الحصول على بيانات القسمين بشكل منفصل - سواء كان ذلك عن طريق الاستعلام عن قاعدة البيانات أو طلب RESTful ، نستخدم وظيفتين لمحاكاة مثل هذه العمليات غير المتزامنة.
نسخة الكود كما يلي:
var getData = {
D1: Function (fn) {
setTimeout (fn ، 3000 ، null ، {content: "مرحبًا ، أنا القسم الأول."})
}
، d2: function (fn) {
setTimeout (fn ، 5000 ، null ، {content: "مرحبًا ، أنا القسم الثاني."})
}
}
وبهذه الطريقة ، سيكون المنطق في app.use () أكثر تعقيدًا ، وأبسط طريقة للتعامل معه هي:
نسخة الكود كما يلي:
app.use (function (req ، res) {
getData.d1 (وظيفة (err ، s1data) {
getData.d2 (وظيفة (err ، s2data) {
Res.Render ('Layout' ، {
S1: Temp.S1 (S1Data)
، S2: Temp.S2 (S2Data)
})
})
})
})
سيحصل هذا أيضًا على النتائج التي نريدها ، ولكن في هذه الحالة ، سيستغرق الأمر 8 ثوانٍ كاملة للعودة.
في الواقع ، يوضح منطق التنفيذ أن getData.d2 يبدأ الاتصال بعد نتيجة getData.d1 ، وليس لديهم مثل هذا الاعتماد. يمكننا استخدام مكتبات مثل Async التي تتولى مكالمات JavaScript غير المتزامنة لحل هذه المشكلة ، ولكن دعونا ببساطة كتابتها هنا:
نسخة الكود كما يلي:
app.use (function (req ، res) {
var n = 2
، النتيجة = {}
getData.d1 (وظيفة (err ، s1data) {
Result.S1Data = S1Data
--ن || الكاتب ()
})
getData.d2 (وظيفة (err ، s2data) {
النتيجة. S2Data = S2Data
--ن || الكاتب ()
})
وظيفة الكاتب () {
Res.Render ('Layout' ، {
S1: Temp.S1 (result.s1data)
، S2: Temp.S2 (result.s2data)
})
}
})
هذا يستغرق 5 ثوان فقط.
قبل التحسين التالي ، نضيف مكتبة jQuery ونضع نمط CSS في ملفات خارجية. بالمناسبة ، سنضيف ملف Runtime.js اللازم لاستخدام قالب Jade الذي سنستخدمه لاحقًا ، ونشغله في الدليل الذي يحتوي على app.js:
نسخة الكود كما يلي:
mkdir ثابت
قرص مضغوط
curl http://code.jquery.com/jquery-1.8.3.min.js -o jquery.js
ln -s ../node_modules/jade/runtime.min.js jade.js
وأخذ الكود في علامة النمط في التصميم.
نسخة الكود كما يلي:
رأس
عنوان مرحبا ، العالم!
Link (href = "/static/style.css" ، rel = "STYLESHEET")
البرنامج النصي (src = "/static/jquery.js")
البرنامج النصي (src = "/static/kade.js")
في app.js ، نقوم بمحاكاة سرعات التنزيل إلى ثانيتين ، وإضافتها قبل app.use (وظيفة (req ، res) {:
نسخة الكود كما يلي:
var static = express.static (path.join (__ dirname ، 'static'))
app.use ('/static' ، function (req ، res ، next) {
setTimeout (Static ، 2000 ، REQ ، RES ، التالي)
})
نظرًا للملفات الثابتة الخارجية ، فإن صفحتنا الآن لديها وقت تحميل يبلغ حوالي 7 ثوانٍ.
إذا قمنا بإرجاع جزء الرأس بمجرد تلقينا طلب HTTP ، ثم ينتظر قسمين حتى يتم الانتهاء من العملية غير المتزامنة قبل العودة ، فإن هذا يستخدم آلية ترميز الإرسال المحظورة HTTP. في node.js ، طالما أنك تستخدم طريقة res.write () ، سيتم إضافة ترميز النقل: رأس مكثف تلقائيًا. وبهذه الطريقة ، بينما يقوم المتصفح بتحميل الملف الثابت ، ينتظر خادم العقدة نتيجة المكالمة غير المتزامنة. دعنا أولاً نحذف الخطين من القسم في التصميم. jade:
نسخة الكود كما يلي:
القسم#S1! = S1
القسم#S2! = S2
لذلك ، لا نحتاج إلى إعطاء هذا الكائن في Res.Render () {S1: ... ، S2: ...} ، ولأن Res.Render () سوف يستدعي Res.end () افتراضيًا ، نحتاج إلى تعيين وظيفة رد الاتصال يدويًا بعد الانتهاء من العرض ، واستخدام طريقة Res.Write () فيه. لا يلزم أن يكون محتوى التصميم. jade في وظيفة رد الاتصال الكاتب (). يمكننا إعادته عندما نتلقى هذا الطلب. لاحظ أننا أضفنا يدويًا رأس نوع المحتوى:
نسخة الكود كما يلي:
app.use (function (req ، res) {
Res.Render ('Layout' ، function (err ، str) {
إذا (ERR) RETRESS RES.REQ.NEXT (ERR)
Res.Setheader ('نوع المحتوى' ، 'text/html ؛ charset = utf-8')
Res.Write (Str)
})
var n = 2
getData.d1 (وظيفة (err ، s1data) {
Res.Write ('<section id = "s1">' + temp.s1 (s1data) + '</section>')
--ن || الدقة () ()
})
getData.d2 (وظيفة (err ، s2data) {
Res.Write ('<section id = "s2">' + temp.s2 (s2data) + '</section>')
--ن || الدقة () ()
})
})
الآن تعود سرعة التحميل النهائية إلى حوالي 5 ثوان. في العملية الفعلية ، يتلقى المتصفح أولاً رمز جزء الرأس ويقوم بتحميل ثلاثة ملفات ثابتة. هذا يستغرق ثانيتين. ثم في الثانية الثالثة ، يظهر الجزئي 1 ، ويظهر جزئي 2 في الثانية الخامسة ، وتنتهي صفحة تحميل صفحة الويب. لن أعطي لقطة شاشة ، فإن تأثير لقطة الشاشة هو نفسه لقطات الشاشة في الثواني الخمس السابقة.
ومع ذلك ، من المهم أن نلاحظ أنه يمكن تحقيق هذا التأثير لأن getData.d1 أسرع من getData.d2. وهذا يعني ، والتي يتم إرجاعها في صفحة الويب التي يتم إرجاعها أولاً تعتمد على من الذي يعيد نتيجة الدعوة غير المتزامنة للواجهة وراءها. إذا قمنا بتغيير getData.d1 للعودة في 8 ثوان ، فسنقوم أولاً بإرجاع Partial 2. يتم عكس ترتيب S1 و S2 ، والنتيجة النهائية لصفحة الويب لا تتفق مع توقعاتنا.
تقودنا هذه المشكلة في النهاية إلى BigPipe ، وهي تقنية يمكنها فصل ترتيب العرض لكل جزء من صفحة الويب من ترتيب نقل البيانات.
الفكرة الأساسية هي أولاً نقل الإطار العام لصفحة الويب بأكملها ، والأجزاء التي تحتاج إلى نقلها لاحقًا يتم تمثيلها بواسطة divs فارغة (أو علامات أخرى):
نسخة الكود كما يلي:
Res.Render ('Layout' ، function (err ، str) {
إذا (ERR) RETRESS RES.REQ.NEXT (ERR)
Res.Setheader ('نوع المحتوى' ، 'text/html ؛ charset = utf-8')
Res.Write (Str)
Res.Write ('<section id = "s1"> </section> <section id = "s2"> </section>')
})
ثم اكتب البيانات التي تم إرجاعها إلى JavaScript
نسخة الكود كما يلي:
getData.d1 (وظيفة (err ، s1data) {
Res.Write ('<script> $ ("#S1").
--ن || الدقة () ()
})
تشبه معالجة S2 هذا. في هذا الوقت ، سترى أنه في الثانية الثانية من طلب صفحة الويب ، يظهر مربعان منقطان فارغان ، في الثانية الخامسة ، يظهر الجزئي 2 ، وفي الثامن الثاني ، يظهر جزء واحد ، ويظهر طلب صفحة الويب.
في هذه المرحلة ، أكملنا أبسط صفحة ويب تنفذها تقنية BigPipe.
تجدر الإشارة إلى أنه إذا كان جزء صفحة الويب المراد كتابة له علامات نصية ، مثل تغيير s1.jade إلى:
نسخة الكود كما يلي:
H1 جزئي 1
.Content! = المحتوى
السيناريو
تنبيه ("تنبيه من S1.Jade")
ثم قم بتحديث صفحة الويب وستجد أنه لم يتم تنفيذ جملة التنبيه ، وستكون لصفحة الويب أخطاء. تحقق من رمز المصدر واعرف أنه خطأ ناتج عن السلسلة في <script>. فقط استبدله بـ <// script>
نسخة الكود كما يلي:
Res.Write ('<script> $ ("#S1").
أعلاه نوضح مبادئ BigPipe والطريقة الأساسية لتنفيذ BigPipe مع Node.js. وكيف ينبغي استخدامه في الواقع؟ فيما يلي طريقة بسيطة لإلقاء الطوب والرم ، الرمز كما يلي:
نسخة الكود كما يلي:
var resproto = طلب ('Express/lib/response')
resproto.pipe = دالة (المحدد ، html ، استبدال) {
this.write ('<script>' + '$ ("' + selector + '").' +
(استبدال === صحيح؟
"(" " + html.replace (/"/g ، '//"').
"") </script> ')
}
وظيفة pipename (الدقة ، الاسم) {
res.pipeCount = res.pipeCount || 0
res.pipemap = res.pipemap || {}
if (res.pipemap [name]) return
res.pipeCount ++
res.pipemap [name] = this.id = ['pipe' ، math.random ().
this.res = الدقة
this.name = الاسم
}
resproto.pipename = دالة (اسم) {
إرجاع اسم الأنابيب الجديد (هذا ، الاسم)
}
resproto.pipelayout = دالة (عرض ، خيارات) {
var res = هذا
Object.Keys (خيارات).
إذا كانت (خيارات [مفتاح] eastyof pipename) خيارات [key] = '<span id = "' + stions [key] .id + '"> </span>'
})
res.Render (عرض ، خيارات ، وظيفة (err ، str) {
إذا (ERR) RETRESS RES.REQ.NEXT (ERR)
Res.Setheader ('نوع المحتوى' ، 'text/html ؛ charset = utf-8')
Res.Write (Str)
if (! res.pipeCount) res.end ()
})
}
resproto.pipepartial = الدالة (الاسم ، العرض ، الخيارات) {
var res = هذا
res.Render (عرض ، خيارات ، وظيفة (err ، str) {
إذا (ERR) RETRESS RES.REQ.NEXT (ERR)
Res.Pipe ('#'+res.pipemap [name] ، str ، true)
-res.pipeCount || الدقة () ()
})
}
app.get ('/' ، function (req ، res) {
res.pipelayout ('Layout' ، {
S1: Res.Pipename ('S1Name')
، S2: res.pipename ('s2name')
})
getData.d1 (وظيفة (err ، s1data) {
Res.PipePartial ('S1Name' ، 'S1' ، S1Data)
})
getData.d2 (وظيفة (err ، s2data) {
res.pipepartial ('s2name' ، 'S2' ، S2Data)
})
})
أضف أيضًا قسمين في التصميم. jade:
نسخة الكود كما يلي:
القسم#S1! = S1
القسم#S2! = S2
الفكرة هنا هي أن محتوى الأنابيب يجب وضعه بعلامة Span أولاً ، والحصول على البيانات بشكل غير متزامن وتقديم رمز HTML المقابل قبل إخراجه إلى المتصفح ، واستبدال عنصر SPAN النائب بطريقة jQuery الخاصة بـ JQuery.
رمز هذه المقالة في https://github.com/undozen/bigpipe-on-node. لقد اتخذت كل خطوة في الالتزام. آمل أن تتمكن من تشغيله محليًا واختراقه. نظرًا لأن الخطوات القليلة التالية تتضمن ترتيب التحميل ، يجب عليك حقًا فتح المتصفح بنفسك لتجربة ذلك ولا يمكنك رؤيته من لقطة الشاشة (في الواقع ، يجب تنفيذها باستخدام الرسوم المتحركة GIF ، لكنني كنت كسولًا جدًا للقيام بذلك).
لا يزال هناك مجال كبير للتحسين حول ممارسة BigPipe. على سبيل المثال ، من الأفضل تعيين قيمة زمنية مشدودة لمحتوى الأنابيب. إذا كانت البيانات التي تسمى بشكل غير متزامن تعود بسرعة ، فلن تحتاج إلى استخدام BigPipe. يمكنك إنشاء صفحة ويب مباشرة وإرسالها. يمكنك الانتظار حتى يتجاوز طلب البيانات فترة زمنية معينة قبل استخدام BigPipe. بالمقارنة مع AJAX ، فإن استخدام BigPipe لا يحفظ فقط عدد الطلبات من المتصفح إلى خادم Node.js ، ولكن أيضًا يحفظ عدد الطلبات من خادم Node.js إلى مصدر البيانات. ومع ذلك ، دعونا نشارك أساليب التحسين والممارسة المحددة بعد أن تستخدم شبكة Snowball BigPipe.