الزيتونه :)
خلاصة ما فهمته حتى الآن هو أن مطوري فيسبوك يرون أن التطبيقات المبنية بنظام MVC ستصبح أكثر تعقيداً وتكويناً للأخطاء غير المتوقعة مع الوقت
وجذر المشكلة من وجهة نظرهم يرجع إلي ال Model وخاصة إمكانية التعديل فيه من أي اتجاه، لذلك قرروا أن يكون انسياب البيانات في التطبيق بالكامل ذا اتجاه واحد إجباري وأن يتم تحويل ال Model إلي Read-Only Model مع إضافة بعض الإمكانيات عليه، وتم إعاده تسمية ال Model إلي Store، إذا أردت أن تعرف لماذا أخذوا هذا القرار وكيف سيتم تعديل ال Read-Only Model فتابع القراءة :)
البداية من Angular2
خلاصة ما فهمته حتى الآن هو أن مطوري فيسبوك يرون أن التطبيقات المبنية بنظام MVC ستصبح أكثر تعقيداً وتكويناً للأخطاء غير المتوقعة مع الوقت
وجذر المشكلة من وجهة نظرهم يرجع إلي ال Model وخاصة إمكانية التعديل فيه من أي اتجاه، لذلك قرروا أن يكون انسياب البيانات في التطبيق بالكامل ذا اتجاه واحد إجباري وأن يتم تحويل ال Model إلي Read-Only Model مع إضافة بعض الإمكانيات عليه، وتم إعاده تسمية ال Model إلي Store، إذا أردت أن تعرف لماذا أخذوا هذا القرار وكيف سيتم تعديل ال Read-Only Model فتابع القراءة :)
البداية من Angular2
بعد إصدار Angular2 beta-0 تحمست له كثيراً فقد كنت متابع لأخباره ومعجب بما وصل إليه، وكنا على وشك إعادة بناء إحدى التطبيقات داخل الشركة من جديد، بهدف تقديم أداء وتجربة أفضل لمستخدميه، بعد مناقشات داخل الفريق اتفقنا على الاعتماد على Angular2 وهو -حتى الآن- قرار نراه موفق وسعداء بما حصلنا عليه من تحسن في تجربة تطوير تطبيقات ال JavaScript وجودة الكود نفسه
اثناء التجهيز لإعادة بناء أحد أجزاء التطبيق والذي نعلم انه يحتاج إلي أداء استثنائي، أردنا التعرف على ما يمكن ل Angular2 أن يقدمه لنا، وهو ما وصل بنا إلي شاطئ Angular2 "OnPush" Change Detection Strategy والذي بدوره ساقنا إلي القراءة أكثر عن الفرق بين تعامل Angular2 مع البيانات القابلة للتعديل والبيانات الصماء Mutable data & Immutable data
ثم جاء الدور للتعرف على React & Flux وما الذي يمكنهما تقديمه لتحسين الأداء بشكل استثنائي -كما يسوق React لنفسه- وعلى الرغم اني دخلت في الأصل للتعمق في React وبحث إمكانية استعماله جنباً إلي جنب مع Angular2 إلا أن الحديث عن Flux والمشكلة التي استدعت ابتكاره كان شيقاً جداً لي، وبعد جولة في المقالات والفيديوهات أحببت تلخيص مع وصلت إليه حتى الآن
قبل Flux كانت معظم إطارات العمل (MVC Frameworks) المشهورة، ك Angular, Ember وغيرها، تتنافس من أجل تقديم دعم أفضل للربط التلقائي بين ما يحدث في عالم الكود (Controller) وبين ما يحدث في عالم الواجهة الرسومية (View) وذلك عن طريق توسيط البيانات (Model) بين العالمين وأي تغير في البيانات من ناحية يصل إلي الناحية الأخرى تلقائياً وهو ما يعرف ب Two way data binding
وأصبح من الطبيعي أن تجد أن تعديل صغير في ال Model ينتج عنه عدة تحديثات في أكثر من منطقة في ال View، وعلى فائدة هذه الميزة في الشاشات غير المعقدة، ستجد ان نفس الميزة أصبحت عبء على المطور في الشاشات المعقدة، وستجده يشد شعره متسائلا عن سبب بطئ التطبيق قبل أن يكتشف أن تغيير لون خبر واحد تسبب في إعادة رسم قائمة الأخبار بالكامل والذي بدوره تواصل مع ال server للحصول على بيانات محدثة عن كل خبر، فيبدأ زميلنا المطور بمعالجة الأمر قبل أن يتكرر مرة هنا ومرة هناك ويظل في حالة معالجة دائمة لآثار ال Two way data binding
المشكلة هي نفس مشكلة "الفوضى المرورية" التي تواجهها كل مدن العالم، تخيل مثلا طريق صلاح سالم في مصر -أعاذك الله من وقفته:)- بدون جزيرة في النصف ويمكن لأي سيارة السير في أي اتجاه والدخول والخروج من أي مخرج يمينا أو يسارا، تخيلت؟ احمد الله أن الوضع ليس كذلك :)
صياغتي للمشكلة التي جاء Flux لحلها هي:
مشاركة وتعديل نفس البيانات بين أكثر من منطقة/مكون
وأصبح من الطبيعي أن تجد أن تعديل صغير في ال Model ينتج عنه عدة تحديثات في أكثر من منطقة في ال View، وعلى فائدة هذه الميزة في الشاشات غير المعقدة، ستجد ان نفس الميزة أصبحت عبء على المطور في الشاشات المعقدة، وستجده يشد شعره متسائلا عن سبب بطئ التطبيق قبل أن يكتشف أن تغيير لون خبر واحد تسبب في إعادة رسم قائمة الأخبار بالكامل والذي بدوره تواصل مع ال server للحصول على بيانات محدثة عن كل خبر، فيبدأ زميلنا المطور بمعالجة الأمر قبل أن يتكرر مرة هنا ومرة هناك ويظل في حالة معالجة دائمة لآثار ال Two way data binding
المشكلة هي نفس مشكلة "الفوضى المرورية" التي تواجهها كل مدن العالم، تخيل مثلا طريق صلاح سالم في مصر -أعاذك الله من وقفته:)- بدون جزيرة في النصف ويمكن لأي سيارة السير في أي اتجاه والدخول والخروج من أي مخرج يمينا أو يسارا، تخيلت؟ احمد الله أن الوضع ليس كذلك :)
صياغتي للمشكلة التي جاء Flux لحلها هي:
مشاركة وتعديل نفس البيانات بين أكثر من منطقة/مكون
State sharing & management between multiple view-parts/components
2- ما هو الحل الذي جاء به Flux ؟
هو إلغاء فكرة ال Two way data binding وأصبح هناك إتجاه واحد إجباري للتواصل بين مكونات التطبيق وبعضها البعض، مما استدعى إعادة تنظيم المكونات نفسها لتعمل بسلاسة مع فكرة الإتجاه الواحد الإجباري، ومن أجل ذلك تم تصميم أربع مكونات جوهرية، تساعدها أخرى، لكنها تظل أهم أربع مكون لا يمكن أن يطلق على التطبيق انه متوافق مع Flux بدونها
هو إلغاء فكرة ال Two way data binding وأصبح هناك إتجاه واحد إجباري للتواصل بين مكونات التطبيق وبعضها البعض، مما استدعى إعادة تنظيم المكونات نفسها لتعمل بسلاسة مع فكرة الإتجاه الواحد الإجباري، ومن أجل ذلك تم تصميم أربع مكونات جوهرية، تساعدها أخرى، لكنها تظل أهم أربع مكون لا يمكن أن يطلق على التطبيق انه متوافق مع Flux بدونها
- Store هو الأسم الجديد لل Model سابقاً وأصبح للقراءة فقط، وظيفته الوحيدة هي إستقبال بلاغات من ال Dispatcher المركزي بأن إجراء ما، يتم في التطبيق، وهو داخلياً -ال Store- يقرر إذا كان يحتاج إلي تحديث بياناته -بنفسه- أم لا، ويرمي Event إن احتاج بأنه حدث بياناته الداخلية، يمكن أن يكون هناك أكثر من Store في التطبيق الواحد
- Dispatcher وظيفته الوحيدة توصيل أي إجراء (Action) يتم تنفيذه في التطبيق إلي جميع ال Stores، لا يكون في التطبيق إلا Dispatcher واحد فقط
- Action هو كائن (Object) يحتوي على وصف لإجراء ما، يتم داخل التطبيق ويحتوي على كل ما يلزم من بيانات لاتمام هذا الإجراء
- View يتابع أي تغير حصل في ال Store ويعيد رسم نفسه -إن احتاج- بناء على ذلك، ويبلغ ما يحدث بداخله عن طريق رمي Events
هناك مكونات أخرى مكملة يكاد لا يستغني عنها أي تطبيق Flux، وهي:
- View-Controller وظيفته الاستماع إلي أي Events يتم رميها من ال View وبناء ال Action المطلوب وترحيله إلي ال Dispatcher المركزي
- Action-Creator وظيفته بناء ال Action فعلياً وفعل ما يلزم قبل وبعد بناء ال Action، عادة ما يعتمد ال ViewController عليه لبناء أي Action مطلوب
3- مثال
لو تصورنا أن هناك شاشة تعرض قائمة بالأخبار الحالية في موقع ما، ومطلوب عند الضغط على أي خبر، أن يتم تغيير لون خلفية الصف الخاص به إلي اللون الأحمر، إذا أردنا فعل هذا وفقاً ل Flux فسيكون هذا مثال مبسط جداً للكود
ملاحظة: إذا أردت رؤية الكود وهو يعمل يمكنك تجربته من هذه الوصلة https://embed.plnkr.co/B43YS3
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// The View, Component #1 in Flux core components | |
// Will listen to any change in the store (check line number 9), if store fired an event that any change happened inside the store | |
// view will clear the old HTML elements, build new ones, append the new ones to the body | |
function View(store, newsViewController){ | |
this.store = store; | |
this.viewController = newsViewController; | |
var self = this; | |
this.store.addChangeListener(function(){ | |
self.render(); | |
}); | |
} | |
View.prototype.render = function(){ | |
var self = this; | |
var allNewsItems = this.store.getNewsItems(); | |
var btnAddItem = document.createElement('button'); | |
btnAddItem.innerText = 'Add Item'; | |
btnAddItem.onclick = function(){ | |
var count = self.store.getNewsItemsCount(); | |
var newsId = count + 1; | |
self.viewController.handleAddButtonClick(newsId); | |
}; | |
var ul = document.createElement('ul'); | |
var onElementClick = function(){ | |
var element = this; | |
self.viewController.handleItemClick(element); | |
}; | |
for(var i=0; i < allNewsItems.length; i++){ | |
var newsItem = allNewsItems[i]; | |
var li = document.createElement('li'); | |
li.id = newsItem.id; | |
li.style.backgroundColor = newsItem.color; | |
li.innerText = newsItem.text; | |
li.onclick = onElementClick; | |
ul.appendChild(li); | |
} | |
var body = document.body | |
body.innerHTML = ''; | |
body.appendChild(btnAddItem); | |
body.appendChild(ul); | |
}; | |
// The Store, Component #2 in Flux core components | |
// The most important component (in my opinion) in Flux | |
// Will check every action coming from the dispatcher, then will decide if it (the store) need to update its internal data or not | |
// If internal data changed, it will fire an event that some change happened inside it | |
function Store(dispatcher){ | |
this.dispatcher = dispatcher; | |
var _allNewsItems = [ | |
{id:1, text:'News Item 1', color:''}, | |
{id:2, text:'News Item 2', color:''}, | |
{id:3, text:'News Item 3', color:''} | |
]; | |
this.getNewsItemsCount = function(){ | |
return _allNewsItems.length; | |
}; | |
this.getNewsItems = function(){ | |
// NOTE #0 | |
// remember store data should NOT give any external source the ability to modify its data, | |
// ONLY the store who should | |
var _copyOfNewsItems = []; | |
for(var i = 0; i < _allNewsItems.length; ++i){ | |
var newsItem = _allNewsItems[i]; | |
// Check NOTE #0 again :) | |
var copyOfNewsItem = { | |
id: newsItem.id, | |
text: newsItem.text, | |
color: newsItem.color | |
}; | |
_copyOfNewsItems.push(copyOfNewsItem); | |
} | |
return _copyOfNewsItems; | |
}; | |
var self = this; | |
function onActionDispatched(action){ | |
switch(action.type){ | |
case 'SELECT_NEWS': | |
for (var i = 0; i <_allNewsItems.length; i++) { | |
var newsItem = _allNewsItems[i]; | |
if (newsItem.id == action.newsId) { | |
newsItem.color = action.activeColor; | |
break; | |
} | |
} | |
self.emitChange(); | |
break; | |
case 'ADD_NEWS': | |
_allNewsItems.push({ | |
id: action.newsId, | |
text: action.text, | |
color: '' | |
}); | |
self.emitChange(); | |
break; | |
default: | |
break; | |
} | |
} | |
this.dispatcher.register(onActionDispatched); | |
var _viewsCallbacks = []; | |
this.addChangeListener = function(callback){ | |
_viewsCallbacks.push(callback); | |
}; | |
this.emitChange = function(){ | |
for(var i=0; i < _viewsCallbacks.length; i++){ | |
var callbackToView = _viewsCallbacks[i]; | |
callbackToView(); | |
} | |
}; | |
} | |
// The Dispatcher, Component #3 in Flux core components | |
// Will pass any dispatched action to every store registered | |
function Dispatcher(){ | |
var _storeCallbacks = []; | |
this.register = function(callback){ | |
_storeCallbacks.push(callback); | |
}; | |
this.dispatch = function(action){ | |
for(var i =0; i < _storeCallbacks.length; i++){ | |
var callbackToStore = _storeCallbacks[i]; | |
callbackToStore(action); | |
} | |
}; | |
} | |
// The Action, Component #4 in Flux core components | |
// Holds all the needed data to complete an action in the app | |
function SelectNewsAction(newsId, activeColor){ | |
this.type = 'SELECT_NEWS'; | |
this.newsId = newsId; | |
this.activeColor = activeColor; | |
} | |
function AddNewsAction(newsId, text){ | |
this.type = 'ADD_NEWS'; | |
this.newsId = newsId; | |
this.text = text; | |
} | |
function NewsViewController(newsActionCreators){ | |
this.newsActionCreators = newsActionCreators; | |
} | |
NewsViewController.prototype.handleItemClick = function(element){ | |
var newsId = element.id; | |
this.newsActionCreators.selectNews(newsId); | |
}; | |
NewsViewController.prototype.handleAddButtonClick = function(newsId){ | |
this.newsActionCreators.addNews(newsId); | |
}; | |
function NewsActionCreators(dispatcher){ | |
this.dispatcher = dispatcher; | |
} | |
NewsActionCreators.prototype.selectNews = function(newsId){ | |
// doing anything before dispatching | |
var action = new SelectNewsAction(newsId, 'red'); | |
this.dispatcher.dispatch(action); | |
// doing anything after dispatching | |
}; | |
NewsActionCreators.prototype.addNews = function(newsId){ | |
// doing anything before dispatching | |
var text = 'News Item ' + newsId; | |
var action = new AddNewsAction(newsId, text); | |
this.dispatcher.dispatch(action); | |
// doing anything after dispatching | |
}; | |
var dispatcher = new Dispatcher(); | |
var store = new Store(dispatcher); | |
var actionCreators = new NewsActionCreators(dispatcher); | |
var viewController = new NewsViewController(actionCreators); | |
var view = new View(store, viewController); | |
view.render(); |
مقال رائع ومفيد .. يا ريت يبقي فيه استمراراية
ReplyDeleteGreat work. Thank you.
ReplyDeleteA very good one. Keep up
ReplyDeletegreat content thanks for this hard effort
ReplyDeletevery simple and very clear .good job and keep up the good work :)
ReplyDelete