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

javascript objects

تم تصميم مكتبة Tanagra.js لتكون بسيطة وخفيفة الوزن وهي تدعم حاليًا فصول Node.js و ES6. يدعم التطبيق الرئيسي JSON ويدعم الإصدار التجريبي المخازن المؤقتة لبروتوكول Google.

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

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

تخزن العمليات بيانات عملها في الذاكرة

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

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

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

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

أهم اتجاهات JavaScript (JS) في عام 2021

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

کتابة تسلسل الخاص بنا

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

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

    من الناحية المثالية يجب أن يكون هناك طريقة ما لدعم إلغاء تسلسل الإصدارات القديمة من الفصل من خلال نوع من التعيين / تعيين الإصدار.

تطبيق

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

تم تصميم Tanagra.js ليكون بسيطًا وخفيف الوزن وهو يدعم حاليًا Node.js  (لم يتم اختباره في المتصفح ولكن من الناحية النظرية يجب أن يعمل) وفئات ES6 (بما في ذلك الخرائط). يدعم التطبيق الرئيسي JSON ويدعم الإصدار التجريبي المخازن المؤقتة لبروتوكول Google. تتطلب المكتبة جافا سكريبت قياسي فقط (تم اختباره حاليًا باستخدام 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)

أداء (performance)

لقد قارنت أداء المُسلسلين (المُسلسل 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 }
	}
  },
}

ملخص

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

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

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

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

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

خريطة الطريق

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

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

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

منشور ذات صلة
هز الأشجار (tree shaking) 2 Minutes

شجرة تهز الخير!

محمدباقر عبيات

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

كيف يعمل JSX؟ 2 Minutes

كيف يعمل JSX؟

محمدباقر عبيات

يعرف كل مطور ReactJS عن سحر JSX، يتيح لنا كتابة بعض HTML في جافا سكريبت، […]

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

السلة