توفّر العناصر الوكيلة ES2015 (في Chrome 49 والإصدارات الأحدث) واجهة برمجة تطبيقات للتوسّط في JavaScript، ما يتيح لنا اعتراض أو حظر جميع العمليات على عنصر مستهدَف وتعديل طريقة عمل هذا العنصر.
هناك عدد كبير من استخدامات الخوادم الوكيلة، بما في ذلك:
- قطْع التمريرة
- محاكاة الأجسام
- إدارة الموارد
- إعداد الملفات الشخصية أو التسجيل لتصحيح الأخطاء
- الأمان والتحكّم في الوصول
- عقود استخدام العناصر
تحتوي واجهة برمجة التطبيقات Proxy API على مُنشئ وكيل يأخذ عنصرًا مستهدفًا محدّدًا وعنصر معالج.
var target = { /* some properties */ };
var handler = { /* trap functions */ };
var proxy = new Proxy(target, handler);
يتم التحكّم في سلوك الخادم الوكيل من خلال المعالج، الذي يمكنه تعديل السلوك الأصلي للعنصر الهدف بعدة طرق مفيدة. يحتوي المعالِج على طرق فخ اختيارية (مثل .get()
و.set()
و.apply()
) يتمّ استدعاؤها عند تنفيذ العملية المقابلة على الخادم الوكيل.
قطْع التمريرة
لنبدأ بأخذ عنصر عادي وإضافة بعض الوسائط المتوسطة للاعتراض إليه باستخدام واجهة برمجة التطبيقات Proxy API. تذكَّر أنّ المَعلمة الأولى التي يتم تمريرها إلى المنشئ هي الهدف (العنصر الذي يتم إنشاء وكيل له) والمَعلمة الثانية هي المعالِج (الوكيل نفسه). هذا هو المكان الذي يمكننا فيه إضافة أدوات ربط لوظائف الحصول أو الضبط أو أي سلوك آخر.
var target = {};
var superhero = new Proxy(target, {
get: function(target, name, receiver) {
console.log('get was called for:', name);
return target[name];
}
});
superhero.power = 'Flight';
console.log(superhero.power);
عند تشغيل الرمز البرمجي أعلاه في Chrome 49، نحصل على ما يلي:
get was called for: power
"Flight"
كما نرى في الممارسة العملية، أدّى تنفيذ عملية الحصول على السمة أو ضبطها على عنصر الوكيل بشكل صحيح إلى إجراء طلب على مستوى البنية الأساسية للفخّ المقابل في المعالِج. تشمل عمليات معالِج الأخطاء عمليات قراءة السمات وتخصيصها وتطبيق الدوالّ، ويتم إعادة توجيهها كلها إلى مصيدة الأخطاء المقابلة.
يمكن لدالة الالتقاط تنفيذ عملية بشكل عشوائي (مثل إعادة توجيه العملية إلى الكائن المستهدَف) إذا أرادت ذلك. وهذا هو ما يحدث تلقائيًا في حال عدم تحديد فخ. على سبيل المثال، إليك خادم وكيل إعادة توجيه بدون إجراء ينفّذ هذا الإجراء فقط:
var target = {};
var proxy = new Proxy(target, {});
// operation forwarded to the target
proxy.paul = 'irish';
// 'irish'. The operation has been forwarded
console.log(target.paul);
لقد اطّلعنا للتو على التوسّط بين الكائنات العادية، ولكن يمكننا بسهولة التوسّط بين كائن دالة، حيث تكون الدالة هي الهدف. سنستخدم هذه المرة فخ handler.apply()
:
// Proxying a function object
function sum(a, b) {
return a + b;
}
var handler = {
apply: function(target, thisArg, argumentsList) {
console.log(`Calculate sum: ${argumentsList}`);
return target.apply(thisArg, argumentsList);
}
};
var proxy = new Proxy(sum, handler);
proxy(1, 2);
// Calculate sum: 1, 2
// 3
تحديد الخوادم الوكيلة
يمكن رصد هوية الخادم الوكيل باستخدام عوامل مساواة JavaScript (==
و===
). وكما نعلم، عند تطبيق هذه العوامل على عنصرَين، تقارن هويات العنصرَين. يوضّح المثال التالي هذا السلوك. تؤدي مقارنة وكيلَين مختلفَين إلى عرض قيمة خطأ على الرغم من تطابق الاستهدافات الأساسية. على نفس المنوال، يختلف الكائن المستهدَف عن أيّ من العناصر الوكيلة له:
// Continuing previous example
var proxy2 = new Proxy (sum, handler);
(proxy==proxy2); // false
(proxy==sum); // false
من المفترض ألا تتمكّن من التمييز بين العنصر الوكيل والعنصر غير الوكيل، وذلك لكي لا يؤثّر استخدام عنصر وكيل في نتيجة تطبيقك. وهذا هو أحد الأسباب التي تجعل واجهة برمجة التطبيقات Proxy API لا تتضمّن طريقة للتحقّق مما إذا كان العنصر وكيلاً أو لا، ولا تقدّم مصائد لجميع العمليات على العناصر.
حالات الاستخدام
كما ذكرنا، هناك مجموعة كبيرة من حالات استخدام الخوادم الوكيلة. يندرج العديد من العناصر المذكورة أعلاه، مثل التحكّم في الوصول وإعداد الملفات الشخصية، ضمن العناصر العامة التي تُغلِّف عناصر أخرى: وهي عناصر وكيلة تُغلِّف عناصر أخرى في "مساحة" العنوان نفسها. تم ذكر تقنية المحاكاة الافتراضية أيضًا. العناصر الافتراضية هي عناصر وكيلة تحاكي عناصر أخرى بدون الحاجة إلى أن تكون هذه العناصر في مساحة العناوين نفسها. وتشمل الأمثلة الأجسام البعيدة (التي تحاكي الأجسام في مساحات أخرى) والمستقبلات الشفافة (التي تحاكي النتائج التي لم يتم احتسابها بعد).
الخوادم الوكيلة بصفتها معالجات
إنّ إحدى حالات الاستخدام الشائعة لمعالجات الوكيل هي إجراء عمليات التحقّق من الصحة أو التحكّم في الوصول قبل تنفيذ عملية على عنصر مُغلف. ولا تتم إعادة توجيه العملية إلا إذا تم إثبات صحتها. يوضّح مثال التحقّق التالي ذلك:
var validator = {
set: function(obj, prop, value) {
if (prop === 'yearOfBirth') {
if (!Number.isInteger(value)) {
throw new TypeError('The yearOfBirth is not an integer');
}
if (value > 3000) {
throw new RangeError('The yearOfBirth seems invalid');
}
}
// The default behavior to store the value
obj[prop] = value;
}
};
var person = new Proxy({}, validator);
person.yearOfBirth = 1986;
console.log(person.yearOfBirth); // 1986
person.yearOfBirth = 'eighties'; // Throws an exception
person.yearOfBirth = 3030; // Throws an exception
قد تأخذ الأمثلة الأكثر تعقيدًا لهذا النمط في الاعتبار جميع العمليات المختلفة التي يمكن لمعالجات الوكيل اعتراضها. يمكن للمرء أن يتخيل عملية تنفيذ يجب أن تكرّر نمط التحقّق من الوصول وإعادة توجيه العملية في كل فخ.
قد يكون من الصعب تبسيط ذلك بسهولة، لأنّه قد يكون من الضروري إعادة توجيه كل عملية بشكل مختلف. في السيناريو المثالي، إذا كان من الممكن توجيه جميع العمليات بشكل موحّد من خلال فخ واحد فقط، لن يحتاج معالِج الأحداث إلا إلى إجراء عملية التحقّق من الصحة مرة واحدة في الفخّ الواحد. يمكنك إجراء ذلك من خلال تنفيذ معالِج الوكيل نفسه كوكيل. لا يُسمح بمناقشة هذا الموضوع في هذه المقالة.
إضافة الكائن
ومن حالات الاستخدام الشائعة الأخرى للوكلاء توسيع أو إعادة تعريف دلالات العمليات على الكائنات. على سبيل المثال، قد تريد أن يسجِّل معالِج العمليات أو يُرسِل إشعارات إلى المراقبين أو يُلقي استثناءات بدلاً من عرض قيمة غير محدّدة أو يعيد توجيه العمليات إلى استهدافات مختلفة للتخزين. وفي هذه الحالات، قد يؤدي استخدام عنصر وكيل إلى نتيجة مختلفة جدًا عن استخدام العنصر المستهدَف.
function extend(sup,base) {
var descriptor = Object.getOwnPropertyDescriptor(base.prototype,"constructor");
base.prototype = Object.create(sup.prototype);
var handler = {
construct: function(target, args) {
var obj = Object.create(base.prototype);
this.apply(target,obj, args);
return obj;
},
apply: function(target, that, args) {
sup.apply(that,args);
base.apply(that,args);
}
};
var proxy = new Proxy(base, handler);
descriptor.value = proxy;
Object.defineProperty(base.prototype, "constructor", descriptor);
return proxy;
}
var Vehicle = function(name){
this.name = name;
};
var Car = extend(Vehicle, function(name, year) {
this.year = year;
});
Car.prototype.style = "Saloon";
var Tesla = new Car("Model S", 2016);
console.log(Tesla.style); // "Saloon"
console.log(Tesla.name); // "Model S"
console.log(Tesla.year); // 2016
التحكّم في الوصول
التحكّم في الوصول هو حالة استخدام أخرى جيدة للوكلاء. بدلاً من تمرير عنصر مستهدَف إلى قطعة من رمز غير موثوق به، يمكن تمرير الوكيل المرتبط به ملفوفًا في نوع من الأغشية الواقية. بعد أن يرى التطبيق أنّ الرمز غير الموثوق به قد أكمل مهمة معيّنة، يمكنه إلغاء المرجع الذي يفصل الخادم الوكيل عن هدفه. ستوسّع الغشاء هذا الفصل بشكل متكرّر ليشمل جميع العناصر التي يمكن الوصول إليها من الهدف الأصلي الذي تم تحديده.
استخدام ميزة "العرض المطابق" مع الخوادم الوكيلة
Reflect هو عنصر مضمّن جديد يقدّم طرقًا لعمليات JavaScript التي يمكن اعتراضها، وهو مفيد جدًا للعمل مع الخوادم الوكيلة. في الواقع، طرق Reflect هي نفسها طرق معالجات الخادم الوكيل.
لقد كانت اللغات ذات النوع الثابت، مثل Python أو C#، توفّر واجهة برمجة تطبيقات للتفكير منذ فترة طويلة، ولكن لم تكن JavaScript بحاجة إلى واجهة برمجة تطبيقات للتفكير لأنّها لغة ديناميكية. يمكن القول إنّ ES5 تتضمّن حاليًا بعض ميزات النسخ، مثل Array.isArray()
أو Object.getOwnPropertyDescriptor()
التي يمكن اعتبارها نسخًا في لغات أخرى. يقدّم ES2015 واجهة Reflection API التي ستضمّ الطرق المستقبلية لهذه الفئة، ما يسهّل فهمها. وهذا منطقي لأنّ Object مخصّص ليكون نموذجًا أوليًا أساسيًا بدلاً من حزمة لطرق التأمل.
باستخدام Reflect، يمكننا تحسين مثال Superhero السابق من أجل اعتراض الحقل بشكلٍ سليم في مصائد get وset على النحو التالي:
// Field interception with Proxy and the Reflect API
var pioneer = new Proxy({}, {
get: function(target, name, receiver) {
console.log(`get called for field: ${name}`);
return Reflect.get(target, name, receiver);
},
set: function(target, name, value, receiver) {
console.log(`set called for field: ${name} and value: ${value}`);
return Reflect.set(target, name, value, receiver);
}
});
pioneer.firstName = 'Grace';
pioneer.secondName = 'Hopper';
// Grace
pioneer.firstName
النتائج التي يتم عرضها:
set called for field: firstName and value: Grace
set called for field: secondName and value: Hopper
get called for field: firstName
في ما يلي مثال آخر على الحالات التي قد تحتاج فيها إلى إجراء ما يلي:
لتجنُّب إنشاء وكيل جديد يدويًا في كل مرة نريد فيها العمل باستخدام منطق محدّد، يمكنك لفّ تعريف وكيل داخل أسلوب إنشاء مخصّص.
أضِف إمكانية "حفظ" التغييرات، ولكن فقط في حال تعديل البيانات فعليًا (نظريًا بسبب ارتفاع تكلفة عملية الحفظ).
function Customer() {
var proxy = new Proxy({
save: function(){
if (!this.dirty){
return console.log('Not saving, object still clean');
}
console.log('Trying an expensive saving operation: ', this.changedProperties);
},
}, {
set: function(target, name, value, receiver) {
target.dirty = true;
target.changedProperties = target.changedProperties || [];
if(target.changedProperties.indexOf(name) == -1){
target.changedProperties.push(name);
}
return Reflect.set(target, name, value, receiver);
}
});
return proxy;
}
var customer = new Customer();
customer.name = 'seth';
customer.surname = 'thompson';
// Trying an expensive saving operation: ["name", "surname"]
customer.save();
لمزيد من أمثلة Reflect API، يُرجى الاطّلاع على ES6 Proxies من Tagtree.
إضافة polyfill إلى دالة Object.observe()
على الرغم من أنّنا نقول وداعًا للإصدار Object.observe()
، أصبح من الممكن الآن إضافة عناصر polyfill إليه باستخدام العناصر الوكيلة ES2015. كتب "سيمون بلاكويل" مؤخرًا بديلاً لـ Object.observe() يستند إلى الوكيل، وهو جدير بالاطّلاع عليه. كتب إريك أرفيدسون أيضًا نسخة متكاملة المواصفات في عام 2012.
دعم المتصفح
يمكن استخدام بروتوكول ES2015 Proxies في الإصدار 49 من Chrome وOpera وMicrosoft Edge وFirefox. تلقّينا إشارات عامة متضاربة من Safari بشأن هذه الميزة، ولكنّنا ما زلنا متفائلين. يتوفّر تطبيق Reflect في Chrome وOpera وFirefox، ونحن بصدد تطويره ليعمل على Microsoft Edge.
أصدرت Google مجموعة محدودة من العناصر القابلة للاستبدال في واجهة برمجة التطبيقات لـ Proxy. لا يمكن استخدام هذا الإجراء إلا مع الحاويات العامة، لأنّه لا يمكنه تمثيل سوى المواقع المعروفة في وقت إنشاء الوكيل.
مراجع إضافية
- ES6 بالتفصيل: الخوادم الوكيلة وReflect
- MDN: ES6 Proxies
- خوادم الوكيل ES6 وReflect على TagTree
- MDN: عنصر الانعكاس
- نظرة تفصيلية على ميزة ES6 Reflection
- الوكلاء: مبادئ التصميم لواجهات برمجة التطبيقات القوية الموجَّهة للكائنات للتوسّط
- 2ality: Metaprogramming with ES6
- البرمجة الفائقة في ES6 باستخدام Reflect
- ES6 everyday Reflect