تسلسل الكائنات المعقدة في JavaScript

نشرت: 2022-03-11

أداء الموقع والتخزين المؤقت للبيانات

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

تخزن العمليات بيانات عملها في الذاكرة. إذا كان خادم الويب يعمل في عملية واحدة (مثل Node.js / Express) ، فيمكن بسهولة تخزين هذه البيانات مؤقتًا باستخدام ذاكرة تخزين مؤقت تعمل في نفس العملية. ومع ذلك ، فإن خوادم الويب المتوازنة التحميل تمتد عبر عمليات متعددة ، وحتى عند العمل مع عملية واحدة ، قد ترغب في استمرار ذاكرة التخزين المؤقت عند إعادة تشغيل الخادم. يتطلب هذا حل تخزين مؤقت خارج العملية مثل Redis ، مما يعني أن البيانات تحتاج إلى تسلسل بطريقة ما ، وإلغاء التسلسل عند قراءتها من ذاكرة التخزين المؤقت.

يعد التسلسل وإلغاء التسلسل أمرًا سهلاً نسبيًا لتحقيقه في اللغات المكتوبة بشكل ثابت مثل C #. ومع ذلك ، فإن الطبيعة الديناميكية لجافا سكريبت تجعل المشكلة أكثر تعقيدًا. بينما قدم ECMAScript 6 (ES6) الفئات ، لم يتم تحديد الحقول الموجودة في هذه الفئات (وأنواعها) حتى تتم تهيئتها - والتي قد لا تكون عند إنشاء مثيل للفئة - ولم يتم تحديد أنواع الحقول والوظائف التي تم إرجاعها على الإطلاق في المخطط. علاوة على ذلك ، يمكن تغيير بنية الفصل بسهولة في وقت التشغيل - يمكن إضافة الحقول أو إزالتها ، ويمكن تغيير الأنواع ، وما إلى ذلك. في حين أن هذا ممكن باستخدام الانعكاس في C # ، يمثل الانعكاس "الفنون المظلمة" لتلك اللغة ، و يتوقع المطورون كسر الوظيفة.

لقد واجهت هذه المشكلة في العمل منذ بضع سنوات عندما كنت أعمل في فريق Toptal الأساسي. كنا نبني لوحة تحكم رشيقة لفرقنا ، والتي يجب أن تكون سريعة ؛ وإلا فلن يستخدمه المطورون ومالكو المنتجات. لقد سحبنا البيانات من عدد من المصادر: نظام تتبع العمل لدينا ، وأداة إدارة المشروع لدينا ، وقاعدة البيانات. تم إنشاء الموقع في Node.js / Express ، وكان لدينا ذاكرة تخزين مؤقت لتقليل المكالمات إلى مصادر البيانات هذه. ومع ذلك ، فإن عملية التطوير التكرارية السريعة تعني أننا نشرنا (وبالتالي أعدنا تشغيلنا) عدة مرات في اليوم ، مما أدى إلى إبطال ذاكرة التخزين المؤقت وبالتالي فقد العديد من مزاياها.

كان الحل الواضح هو وجود ذاكرة تخزين مؤقت خارج المعالجة مثل Redis. ومع ذلك ، بعد إجراء بعض الأبحاث ، وجدت أنه لا توجد مكتبة تسلسل جيدة لجافا سكريبت. تقوم الطرق المضمنة JSON.stringify / JSON.parse بإرجاع بيانات من نوع الكائن ، وتفقد أي وظائف في النماذج الأولية للفئات الأصلية. هذا يعني أنه لا يمكن ببساطة استخدام الكائنات التي تم إلغاء تسلسلها "في مكانها" داخل تطبيقنا ، الأمر الذي يتطلب بالتالي إعادة هيكلة كبيرة للعمل مع تصميم بديل.

متطلبات المكتبة

من أجل دعم التسلسل وإلغاء تسلسل البيانات التعسفية في JavaScript ، مع التمثيلات غير المتسلسلة والأصول القابلة للاستخدام بالتبادل ، كنا بحاجة إلى مكتبة تسلسل بالخصائص التالية:

  • يجب أن يكون للتمثيلات غير المتسلسلة نفس النموذج الأولي (الوظائف ، المحصلات ، المحددات) مثل الكائنات الأصلية.
  • يجب أن تدعم المكتبة أنواع التعقيد المتداخلة (بما في ذلك المصفوفات والخرائط) ، مع تعيين النماذج الأولية للكائنات المتداخلة بشكل صحيح.
  • يجب أن يكون من الممكن إجراء تسلسل وإلغاء تسلسل نفس الكائنات عدة مرات - يجب أن تكون العملية خاملة.
  • يجب أن يكون تنسيق التسلسل قابلاً للنقل بسهولة عبر TCP وقابل للتخزين باستخدام Redis أو خدمة مماثلة.
  • يجب أن يكون الحد الأدنى من التغييرات في التعليمات البرمجية مطلوبًا لتمييز فئة على أنها قابلة للتسلسل.
  • يجب أن تكون إجراءات المكتبة سريعة.
  • من الناحية المثالية ، يجب أن يكون هناك طريقة ما لدعم إلغاء تسلسل الإصدارات القديمة من الفصل ، من خلال نوع من التعيين / تعيين الإصدار.

تطبيق

لسد هذه الفجوة ، قررت أن أكتب Tanagra.js ، مكتبة تسلسل للأغراض العامة لجافا سكريبت. اسم المكتبة هو إشارة إلى إحدى الحلقات المفضلة لدي من Star Trek: The Next Generation ، حيث يجب أن يتعلم طاقم المؤسسة كيفية التواصل مع جنس غريب غامض لغته غير مفهومة. تدعم مكتبة التسلسل هذه تنسيقات البيانات الشائعة لتجنب مثل هذه المشاكل.

تم تصميم Tanagra.js ليكون بسيطًا وخفيف الوزن ، وهو يدعم حاليًا Node.js (لم يتم اختباره في المتصفح ، ولكن من الناحية النظرية ، يجب أن يعمل) وفصول ES6 (بما في ذلك الخرائط). يدعم التطبيق الرئيسي JSON ، ويدعم الإصدار التجريبي المخازن المؤقتة لبروتوكول Google. تتطلب المكتبة فقط JavaScript قياسي (تم اختباره حاليًا باستخدام ES6 و Node.js) ، مع عدم الاعتماد على الميزات التجريبية ، أو نقل بابل ، أو TypeScript .

يتم تمييز الفئات القابلة للتسلسل على هذا النحو باستدعاء طريقة عند تصدير الفئة:

module.exports = serializable(Foo, myUniqueSerialisationKey)

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

module.exports = serializable(Foo, [Bar, Baz], myUniqueSerialisationKey)

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

أثناء التسلسل ، تنشئ المكتبة بشكل متكرر خريطة عالمية لمفاتيح الفئات ، وتستخدم ذلك أثناء إلغاء التسلسل. (تذكر أن المفتاح متسلسل مع بقية البيانات.) من أجل معرفة نوع فئة "المستوى الأعلى" ، تتطلب المكتبة تحديد ذلك في استدعاء إلغاء التسلسل:

const foo = decodeEntity(serializedFoo, Foo)

توجد مكتبة تجريبية لرسم الخرائط التلقائي تسير في شجرة الوحدة وتقوم بإنشاء التعيينات من أسماء الفئات ، ولكن هذا يعمل فقط للفئات ذات الأسماء الفريدة.

تخطيط المشروع

ينقسم المشروع إلى عدد من الوحدات:

  • tanagra-core - الوظيفة المشتركة التي تتطلبها تنسيقات التسلسل المختلفة ، بما في ذلك وظيفة تمييز الفئات باعتبارها قابلة للتسلسل
  • tanagra-json - تسلسل البيانات إلى تنسيق JSON
  • tanagra-protobuf - تسلسل البيانات إلى تنسيق Google protobuffers (تجريبي)
  • tanagra-protobuf-redis-cache - مكتبة مساعدة لتخزين protobufs المتسلسلة في Redis
  • tanagra-auto-mapper - يمشي في شجرة الوحدة في Node.js لبناء خريطة للفئات ، مما يعني أنه لا يتعين على المستخدم تحديد النوع الذي سيتم إلغاء التسلسل إليه (تجريبي).

لاحظ أن المكتبة تستخدم الهجاء الأمريكي.

مثال على الاستخدام

يوضح المثال التالي فئة قابلة للتسلسل ويستخدم وحدة tanagra-json لتسلسلها / إلغاء تسلسلها:

 const serializable = require('tanagra-core').serializable class Foo { constructor(bar, baz1, baz2, fooBar1, fooBar2) { this.someNumber = 123 this.someString = 'hello, world!' this.bar = bar // a complex object with a prototype this.bazArray = [baz1, baz2] this.fooBarMap = new Map([ ['a', fooBar1], ['b', fooBar2] ]) } } // Mark class `Foo` as serializable and containing sub-types `Bar`, `Baz` and `FooBar` module.exports = serializable(Foo, [Bar, Baz, FooBar]) ... const json = require('tanagra-json') json.init() // or: // require('tanagra-protobuf') // await json.init() const foo = new Foo(bar, baz) const encoded = json.encodeEntity(foo) ... const decoded = json.decodeEntity(encoded, Foo)

أداء

لقد قارنت أداء المُسلسلين (المُسلسل JSON والمُسلسل التجريبي لـ protobufs ) مع عنصر تحكم (JSON.parse الأصلي و JSON.stringify). لقد أجريت ما مجموعه 10 تجارب مع كل منها.

لقد اختبرت هذا على جهاز الكمبيوتر المحمول Dell XPS15 2017 الخاص بي بذاكرة 32 جيجا بايت ، ويعمل بنظام التشغيل Ubuntu 17.10.

لقد تسلسلت الكائن المتداخل التالي:

 foo: { "string": "Hello foo", "number": 123123, "bars": [ { "string": "Complex Bar 1", "date": "2019-01-09T18:22:25.663Z", "baz": { "string": "Simple Baz", "number": 456456, "map": Map { 'a' => 1, 'b' => 2, 'c' => 2 } } }, { "string": "Complex Bar 2", "date": "2019-01-09T18:22:25.663Z", "baz": { "string": "Simple Baz", "number": 456456, "map": Map { 'a' => 1, 'b' => 2, 'c' => 2 } } } ], "bazs": Map { 'baz1' => Baz { string: 'baz1', number: 111, map: Map { 'a' => 1, 'b' => 2, 'c' => 2 } }, 'baz2' => Baz { string: 'baz2', number: 222, map: Map { 'a' => 1, 'b' => 2, 'c' => 2 } }, 'baz3' => Baz { string: 'baz3', number: 333, map: Map { 'a' => 1, 'b' => 2, 'c' => 2 } } }, }

كتابة الأداء

طريقة التسلسل افي المؤتمر الوطني العراقي. أول محاكمة (مللي ثانية) StDev. المؤتمر الوطني العراقي. أول محاكمة (مللي ثانية) افي. السابقين. أول محاكمة (مللي ثانية) StDev. السابق. أول محاكمة (مللي ثانية)
جسون 0.115 0.0903 0.0879 0.0256
جوجل بروتوبوفس 2.00 2.748 1.13 0.278
مجموعة التحكم 0.0155 0.00726 0.0139 0.00570

اقرأ

طريقة التسلسل افي المؤتمر الوطني العراقي. أول محاكمة (مللي ثانية) StDev. المؤتمر الوطني العراقي. أول محاكمة (مللي ثانية) افي. السابقين. أول محاكمة (مللي ثانية) StDev. السابق. أول محاكمة (مللي ثانية)
جسون 0.133 0.102 0.104 0.0429
جوجل بروتوبوفس 2.62 1.12 2.28 0.364
مجموعة التحكم 0.0135 0.00729 0.0115 0.00390

ملخص

المسلسل JSON أبطأ بحوالي 6-7 مرات من التسلسل الأصلي. المسلسل التجريبي protobufs أبطأ بحوالي 13 مرة من المسلسل JSON ، أو 100 مرة أبطأ من التسلسل الأصلي.

بالإضافة إلى ذلك ، من الواضح أن التخزين المؤقت الداخلي لمعلومات المخطط / الهيكلية داخل كل مُسلسل له تأثير على الأداء. بالنسبة لمسلسل JSON ، تكون الكتابة الأولى أبطأ بحوالي أربع مرات من المتوسط. بالنسبة للمسلسل protobuf ، يكون أبطأ تسع مرات. لذا فإن كتابة العناصر التي تم تخزين البيانات الوصفية الخاصة بها بالفعل تكون أسرع بكثير في أي من المكتبتين.

لوحظ نفس التأثير للقراءات. بالنسبة لمكتبة JSON ، تكون القراءة الأولى أبطأ بنحو أربع مرات من المتوسط ​​، وبالنسبة لمكتبة protobuf ، فهي أبطأ بنحو مرتين ونصف.

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

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

خريطة الطريق

المكتبة لا تزال في المرحلة التجريبية. تم اختبار المسلسل JSON بشكل جيد ومستقر. إليك خارطة الطريق للأشهر القليلة القادمة:

  • تحسينات في الأداء لكل من المسلسل
  • دعم أفضل لجافا سكريبت قبل ES6
  • دعم لمصممي الديكور ES-Next

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

الصفحة الرئيسية للمشروع
مستودع جيثب