الدليل الكامل لتعلم Angular JS في يوم واحد
هذه ترجمة (بتصرف) لمقالة Todd Motto على مدوّنته وإليه يعود ضمير المتكلم في المقالة. نُشرت للمرة الأولى على أكاديمية حسوب، وأعيدُ نشرها هنا.
ما هو AngularJS؟
هو إطار عمل JavaScript لطرف العميل يتبع بنية Model-View-Controller/Model-View-View-Model، ويعتبر اليوم مهماً لبناء تطبيقات ويب وحيدة الصفحة (SAP) أو حتى المواقع العادية. يُعتبر Angular JS قفزة كبيرة نحو مستقبل HTML وما يجلبه الإصدار الخامس منها (مع التطورات على صعيد JavaScript بالطبع!)، ويبعث الحياة من جديد في تعاملنا مع الويب الحديث. هذا المقال جولة شاملة في Angular JS مستخلصة من تجاربي ونصائح وممارسات تعلمتها خلال استخدامي.
المصطلحات
ليس عليك أن تبذل جهداً كبيراً لتعلم Angular، وأهم ما يجب معرفته معاني المصطلحات وتبني طريقة MVC، وMVC اختصار لـ_Model-View-Controller_، أي نموذج-طريقة عرض-مُتحكِّم. فيما يلي بعض المصطلحات والواجهات البرمجية الأساسية التي تزوّدنا بها Angular.
MVC
ربما سمعت بهذا الاختصار من قبل، وهو مستخدم في لغات برمجة عديدة كوسيلة لبناء هيكل التطبيقات أو البرامج. وهاك تلخيص سريع لمعناه: - النموذج (Model): بنية البيانات التي تقوم عليها أجزاء التطبيقات، غالباً ما تمثل بصيغة JSON. يفضل أن تكون لديك معرفة مسبقة بـJSON قبل تعلم Angular، لأنها ضرورية للتواصل بين الخادم وطريقة العرض (سنأتي على شرحها في النقطة التالية). على سبيل المثال، لنفترض أن لدينا مجموعة من المستخدمين، يمكن تمثيل بيانات تعريفهم كما يلي:
{
"users" : [{
"name": "أحمد",
"id": "82047392"
},{
"name": "سامر",
"id": "65198013"
}]
}
عادة تُجلب هذه المعلومات من خادم بطلب XMLHttpRequest
، ويقابله في jQuery الإجراء $.ajax
، وفي Angular الكائن $http
. وقد تكون هذه المعلومات مكتوبة ضمن النص البرمجي أثناء تفسير الصفحة (من قاعدة بيانات أو مخزن بيانات). بعد ذلك يكون بإمكانك تعديل هذه المعلومات وإعادة إرسالها.
- طريقة العرض (View): وهو أمر سهل التفسير، فهي ببساطة المُخرج النهائي أو صفحة HTML التي تعرض البيانات (النموذج) على المستخدم مثلاً. باستخدام إطار عمل MVC، تُجلب البيانات من النموذج وتُعرض المعلومات المناسبة في صفحة HTML.
- المُتحكِّم (Controller): وله من اسمه نصيب، فهو يتحكم بالأشياء! ولكن أية أشياء؟ البيانات. المُتحكمات هي الطريقة التي تصل من خلالها بين الخادم و_طريقة العرض_، فتسمح بتحديث البيانات سريعاً من خلال التواصل مع كلا الخادم والعميل.
إعداد مشروع Angular JS (الأساسيات)
يجب أولاً تهيئة الأساسيات التي يقوم عليها مشروعنا. يجب أن نبدأ بتطبيق (ng-app
) الذي يُعرِّف التطبيق (وng
هي بادئة تعني Angular وتسبق عادة كل مكونات Angular JS)، ثم متحكم (Controller) ليتواصل مع طريقة العرض، ثم ربط DOM ولا ننسى تضمين Angular بالطبع! إليك الأساسيات:
نص HTML مع تصريحات ng-*
:
<div ng-app="myApp">
<div ng-controller="MainCtrl">
<!-- محتويات المتحكم -->
</div>
</div>
وحدة Angular مع متحكم:
var myApp = angular.module('myApp', []);
myApp.controller('MainCtrl', ['$scope', function ($scope) {
// أوامر المتحكم
}]);
قبل أن نستبق الأمور، نحتاج لإنشاء وحدة Angular (أو Angular module)، التي ستتضمن كل النص البرمجي المتعلق بالمشروع. هناك أكثر من طريقة للتصريح عن الوحدات، إحداها سَلسلة كل النص البرمجي معاً (لا أفضل هذه الطريقة):
angular.module('myApp', [])
.controller('MainCtrl', ['$scope', function ($scope) {...}])
.controller('NavCtrl', ['$scope', function ($scope) {...}])
.controller('UserCtrl', ['$scope', function ($scope) {...}]);
ولكن الطريقة التي أفضلها، والتي أثبتت أنها الأفضل لكل مشاريع Angular التي صمّمتها هي تعريف الوحدة العامة بشكل منفصل. الطريقة التي تعتمد على تسلسل التصريحات قد تجعلك تنسى إغلاق بعض الأقواس وتجعل قراءة النص البرمجي وتصحيحه أكثر تعقيداً. لذا أُفضّل هذا الأسلوب:
var myApp = angular.module('myApp', []);
myApp.controller('MainCtrl', ['$scope', function ($scope) {...}]);
myApp.controller('NavCtrl', ['$scope', function ($scope) {...}]);
myApp.controller('UserCtrl', ['$scope', function ($scope) {...}]);
بهذه الطريقة أُقسّم النص البرمجي على عدة ملفات، وفي كل ملف أربط مكوّناً من المكونات مع فضاء الأسماء myApp فيصبح تلقائياً جزءاً من تطبيقي. نعم، الأمر كما فهمت، أفضل أن أنشئ ملفاً مستقلاً لكل متحكم ومُرشِد (directive) ومعمل (factory) وأي شيء آخر (ستشكرني على هذا!). فيما بعد يمكنك دمجها معاً وتقليصها لتصبح ملفاً واحدًا (مستخدماً مدير مهام مثل Grunt أو Gulp) فتدفعَه إلى DOM
.
المُتحكّمات (Controllers)
أصبحت تعرف الآن مفهوم MVC وتعلمت طريقة إعداد مشروع جديد، فلنطّلع الآن على الكيفية التي يُطبِّق فيها Angular JS العمل بالمتحكّمات.
بناء على المثال السابق، بإمكاننا الآن أن نخطو خطوة بسيطة نحو عرض بعض البيانات ضمن طريقة العرض مستخدمين متحكّماً. يستخدم Angular تركيب «مقود الدراجة (handlebars)» لقولبة HTML. ببساطة يعني هذا أن بإمكان المتحكمات أن تعرض البيانات في صفحة HTML بأن تستبدل كل عبارة فيها مكتوبة ضمن الأقواس المزدوجة هكذا: {{ data }}
قيمة يُعينها المتحكم. في الحالة المثالية يجب أن لا تحوي صفحة HTML نصاً حقيقيًا أو قيماً مدرجة مسبقاً، ويجب أن تترك هذه المهمة لمتحكمات Angular. فيما يلي مثال يبيّن كيف يمكن عرض نص أو سلسلة حروف String
بسيطة ضمن الصفحة:
<div ng-app="myApp">
<div ng-controller="MainCtrl">
{{ text }}
</div>
</div>
في ملف JavaScript:
var myApp = angular.module('myApp', []);
myApp.controller('MainCtrl', ['$scope', function ($scope) {
$scope.text = 'مرحباً بمعجبي Angular!';
}]);
والناتج النهائي:
أهم مفهوم هنا مفهوم النطاق ($scope
) والذي ستربطه بكل الوظائف ضمن متُحكّم مُعيّن. يُشير $scope
إلى العنصر أو المنطقة الحالية في DOM (فهو لا يساوي this
ضمن النص البرمجي) وبهذا يخلق نطاقاً يحيط بكل البيانات والوظائف ضمن العناصر (DOM elements)، ويعزلها عن العناصر الأخرى، فيبدو وكأنه ينقل مجالات JavaScript العامة/الخاصة إلى DOM، وهذا شيء رائع!
قد يبدو مفهوم النطاق مخيفاً للوهلة الأولى، لكنه طريقك الواصل بين الخادم (أو حتى البيانات المحلية) من جهة وDOM من الجهة الأخرى. يعطيك هذا المثال فكرة عن الطريقة التي «تُدفع» بها البيانات إلى DOM.
لنٌلقِ نظرة على بيانات حقيقية نفترض أننا جلبناها من خادم لنعرض تفاصيل تسجيل دخول المستخدم، سنكتفي في هذه المرحلة باستخدام بيانات جاهزة، وسنتعلم كيفية جلبها من الخادم على هيئة JSON لاحقاً. أولاً سنكتب نص JavaScript:
var myApp = angular.module('myApp', []);
myApp.controller('UserCtrl', ['$scope', function ($scope) {
// لنجعل معلومات المستخدم ضمن عنصر فرعي
$scope.user = {};
$scope.user.details = {
"username": "Todd Motto",
"id": "89101112"
};
}]);
ثم ننتقل إلى DOM لعرض هذه البيانات:
<div ng-app="myApp">
<div ng-controller="UserCtrl">
<p class="username">Welcome, {{ user.details.username }}</p>
<p class="id">User ID: {{ user.details.id }}</p>
</div>
</div>
الناتج:
من المهمّ أن تتذكر أن المتحكمات تستخدم فقط للبيانات ولإنشاء وظائف تتواصل مع الخادم وتجلب أو ترسل بيانات JSON. لا تستخدم المتحكمات لمعالجة DOM (كأن تنقل عنصراً ضمن الصفحة أو تخفيه أو تظهره…)، فمعالجة DOM مهمة المُرشِدات (directives)، وهي ما سنشرحه لاحقاً، المهم أن تتذكر أن موضع jQuery وغيرها من المكتبات التي تتعامل مع DOM ليس ضمن المتحكّمات.
نصيحة من محترف: خلال اطلاعك على وثائق Angular الرسمية، ستلاحظ أن الأمثلة المقدمة تعتمد الأسلوب التالي لإنشاء المتحكمات:
var myApp = angular.module('myApp', []);
function MainCtrl ($scope) {
//...
};
لا تفعل هذا! هذا سيجعل كل الوظائف المُصرّحة تابعةً للنطاق العامّ (global scope) ولا يربطها بشكل جيد مع التطبيق. هذا يعني كذلك أن عمليات التقليص للنص البرمجي والتجارب ستكون أكثر صعوبة. لا تلوّث فضاء الاسماء العام، بل اجعل المتحكمات ضمن التطبيق.
المُرشِدات (Directives)
الُمرشد في أبسط صوره هو نص HTML مٌقولَب، يفضل أن يكون استخدامه متكررًا ضمن التطبيق. توفر المرشدات طريقة سهلة لإدخال أجزاء DOM ضمن التطبيق دون عناء. تعلم استخدام المرشدات ليس أمراً سهلاً على الإطلاق، وإتقانها يتطلب جهداً، ولكن الفقرات التالية ستضعك على بداية الطريق.
إذن، ما فائدة المرشدات؟ إنها مفيدة في عدة أمور، منها إنشاء عناصر DOM، مثل علامات التبويب (tabs) وقوائم التصفح - ففائدتها تعتمد على ما يفعله تطبيقك في الواجهة. لتسهيل الشرح، سأقول ببساطة: إن كنت استعملت ng-show
وng-hide
من قبل، فقد استعملت المرشدات (حتى وإن كان هذان لا يُدرجان أية عناصر DOM).
على سبيل التمرين، سنُنشئ نوعاً خاصًّا من الأزرار ونسميه _customButton
_، يُدرج هذا العنصر بعض العناصر الفرعية التي لا نريد كتابتها في كل مرة. تتنوع طرق التصريح عن المرشدات في DOM، وهي مبينة في النص البرمجي التالي:
<!-- 1: تصريح عن مُرشد كخاصّة (attribute) -->
<a custom-button>انقرني</a>
<!-- 2: كعنصر مخصص (custom elements) -->
<custom-button>انقرني</custom-button>
<!-- 3: كصنف (class) (للتوافق مع النسخ القديمة من IE) -->
<a class="custom-button">انقرني</a>
<!-- 4: كتعليق (comment) (ليس ملائماً لهذا التمرين) -->
<!-- directive: custom-button -->
أفضّل استخدام المرشدات كخواصّ (attributes)، أما العناصر المخصصة (custom elements) فقادمة في النسخ المستقبلية من HTML باسم Web Components، يوفر Angular ما يشبهها، ولكنها قد تنطوي على بعض العيوب والعلل في المتصفحات القديمة. الآن نعرف كيف نصرح عن المرشدات ضمن الصفحة، سننتقل إلى إنشائها ضمن JavaScript. لاحظ أنني سأربطه مع فضاء الأسماء العام myApp؛ في صيغته الأبسط يُكتب المرشد كما يلي:
myApp.directive('customButton', function () {
return {
link: function (scope, element, attrs) {
// هنا اكتب التعليمات التي تعالج DOM أو تتعامل مع أحداثه
}
};
});
عرّفنا المرشد باستخدام الطريقة _.directive()
_، مُرسلين إليها اسم المرشد 'customButton'
. عندما تكتب حرفاً كبيراً بالإنكليزية في اسم المُرشد، فإنه ينبغي استخدام اسم المرشد ضمن DOM بصيغته التي يُفصل بها باستخدام الشرطة (-) بين الحروف الكبيرة (كما في المثال السابق: استخدمنا 'customElement'
في JavaScript و"custom-button"
في HTML).
يُرجع المُرشد كائناً (Object) له عدد من الخصائص. أهم ما يجب تعلّمه منها: restrict
وreplace
وtransclude
وtemplate
وtemplateUrl
وأخيراً link
. لنضف بعضها إلى نصنا البرمجي:
myApp.directive('customButton', function () {
return {
restrict: 'A',
replace: true,
transclude: true,
template: '<a href="" class="myawesomebutton" ng-transclude>' +
'<i class="icon-ok-sign"></i>' +
'</a>',
link: function (scope, element, attrs) {
// هنا اكتب التعليمات التي تعالج DOM أو تتعامل مع أحداثه
}
};
});
الناتج:
تأكد من فحص العنصر (من الأمر Inspect element في المتصفح) لرؤية العناصر الجديدة التي أُدخلت في الصفحة. أعلم أن الرمز لم يظهر ضمن العنصر الجديد، ببساطة لأنني لم أُضمّن Font Awesome ضمن المشروع، ولكن يمكنك فهم كيف تعمل المرشدات. لنتعرف الآن ما تعنيه كل واحدة من خصائص المرشد السابقة الذكر:
* الخاصة restrict
: تُقيّد هذه الخاصة كيفية استخدام المُرشد، كيف نريد أن نستخدمه؟ إن كنت تبني مشروعاً يتطلب دعم النسخ القديمة من IE، فعليك استخدامه كخاصّة (attribute) أو صنف (class). القيمة 'A'
تعني حصر استخدام المرشد بالخواص (attributes) فقط. 'E'
تعني Element و'C'
صنف و'M'
تعليق. القيمة الافتراضية هي 'EA'
(أي عنصر وخاصة).
* الخاصة replace
: تعني استبدال HTML العنصر المصرّح عن المُرشد ضمن الصفحة بالقالب (template) الذي يُحدد في الخاصة template
(مشروحة أدناه).
* الخاصة _transclude_
: تسمح بنسخ المحتوى الأصلي للعنصر المُصرّح عن المُرشد في الصفحة ودمجه ضمن المرشد (عند التنفيذ، ستلاحظ أن العبارة «انقرني» انتقلت إلى المُرشد).
* الخاصة template
: قالب (كذلك المستخدم في المثال) يُدخل إلى الصفحة. يفضّل استخدام القوالب الصغيرة فقط. تُعالج القوالب وتبنى من قبل Angular مما يسمح باستخدام صيغة مقود الدراجة ضمنها.
* الخاصة templateUrl
: مشابهة للسابقة، ولكنها تُجلب من ملف أو من وسم <script>
بدل كتابتها ضمن تعريف المُرشد. كل ما عليك هو تعيين مسار الملف الذي يحوي القالب. يكون هذا الخيار مناسباً عندما تريد الاحتفاظ بالقوالب خارج النص البرمجي لملفات JavaScript:
myApp.directive('customButton', function () {
return {
templateUrl: 'templates/customButton.html'
// directive stuff...
});
وضمن الملف، نكتب:
<!-- inside customButton.html -->
<a href="" class="myawesomebutton" ng-transclude>
<i class="icon-ok-sign"></i>
</a>
ملاحظة: يمكن أن يكون اسم الملف أي شيء وليس من الضروري أن يوافق اسمَ المُرشد.
عند استخدام الأسلوب السابق، سيحتفظ المتصفح بنسخة مُخبأة (cached) من ملف HTML، وهو أمر رائع! الخيار البديل الذي استخدام قالب ضمن وسم <script>
وهنا لا تُخبأ نسخة منه في المتصفح:
<script type="text/ng-template" id="customButton.html">
<a href="" class="myawesomebutton" ng-transclude>
<i class="icon-ok-sign"></i>
</a>
</script>
هنا أخبرنا Angular بأن وسم <script>
هذا هو قالب (ng-template
) وأعطيناه المُعرف. سيبحث Angular عن القالب أو عن ملف html، فاستخدم ما تراه مناسباً. شخصياً، أفضّل إنشاء ملفات html لسهولة تنظيمها ولتحسين الأداء وإبقاء DOM نظيفاً، فقد يستخدم مشروعك مع الوقت عشرات المُرشدات، وترتيبها في ملفات مستقلة يجعل مراجعتها أسهل.
الخدمات (Services)
كثيراً ما تثير الخدمات في Angular ارتباك المطورين؛ ومن خبرتي وأبحاثي، أعتقد أن الخدمات وُضعت كنمط وأسلوب للتصميم أكثر من اختلافها بالوظيفة التي تؤديها. بعد التنقيب في مصدر Angular، وجدت أنها تُعالج وتبنى باستخدام المُجمّع (compiler) ذاته، وكذلك فهي تقدم العديد من الوظائف المشابهة. أنصح باستخدام الخدمات لبناء الكائنات المُتفرِّدة (singletons)، واستخدام المعامل (Factories) لبناء وظائف أكثر تعقيداً كالكائنات الحرفيّة (Object Literals). فيما يلي مثال لاستخدام خدمة توجد ناتج الضرب لعددين:
myApp.service('Math', function () {
this.multiply = function (x, y) {
return x * y;
};
});
يمكنك بعد هذا استخدامها ضمن مُتحكم كما يلي:
myApp.controller('MainCtrl', ['$scope', function ($scope) {
var a = 12;
var b = 24;
// الناتج: 288
var result = Math.multiply(a, b);
}]);
نعم بالطبع إيجاد ناتج الضرب سهل ولا يحتاج خدمة، لكننا نستخدمه لإيصال الفكرة فحسب.
عندما ننشئ خدمة (أو معملاً) نحتاج إلى إخبار Angular عن متطلبات هذه الخدمة، وهو ما يسمى «حقن المتطلبات Dependency Injection» - إن لم تُصرّح عن المتطلبات فلن يعمل المتحكم المعتمد على الخدمة، ويقع خطأ عند التجميع. ربما لاحظت الجزء function ($scope)
ضمن التصريح عن المتحكم أعلاه، وهذا هو ببساطة حقن المتطلبات! ستلاحظ أيضًا [$scope]
قبل الجزء function ($scope)
، وهو ما سأشرحه لاحقاً. فيما يلي طريقة استخدام حقن المتطلبات لإخبار Angular أنك تحتاج إلى الخدمة التي أنشأتها للتو:
// مرر الخدمة Math
myApp.controller('MainCtrl', ['$scope', 'Math', function ($scope, Math) {
var a = 12;
var b = 24;
// يُعطي 288
var result = Math.multiply(a, b);
}]);
المعامل (Factories)
إيضاح فكرة المعامل سهل إذا كنت قد استوعبت فكرة الخدمات، بإمكاننا إنشاء كائن حرفي (Object Literal) ضمن المعمل أو طرائق أكثر تعقيداً:
function ($http) {
return {
get: function(url) {
return $http.get(url);
},
post: function(url) {
return $http.post(url);
},
};
}]);
هنا أنشأت مُغلفات (wrappers) مخصصة لخدمة $http
في Angular المسؤولة عن طلبات XHR
. بعد حقن المتطلبات ضمن المتحكم يمكننا استخدام هذا المعمل بسهولة:
myApp.controller('MainCtrl', ['$scope', 'Server', function ($scope, Server) {
var jsonGet = 'http://myserver/getURL';
var jsonPost = 'http://myserver/postURL';
Server.get(jsonGet);
Server.post(jsonPost);
}]);
إذا أرت طلب التحديثات من الخادم، بإمكانك إنشاء طريقة Server.poll
أو إن كنت تستخدم مقبساً (ٍSocket)، فربما ترغب بإنشاء الطريقة Server.socket
وهكذا… المعامل تسمح لك بتنظيم نصك البرمجي ضمن وحدات يمكن إدراجها ضمن المتحكمات منعاً لتكرار النص البرمجي فيها والحاجة المتكررة لصيانته.
المُرشّحات
تستخدم المرشحات مع مصفوفات (arrays) من البيانات وخارج الحلقات (loops). إن احتجت للمرور على عناصر من مصفوفة بيانات والحصول على بعض منها فقط، فأنت في المكان الصحيح! يمكنك أيضًا استخدام المرشحات لتصفية ما يكتبه المستخدم ضمن حقل إدخال <input>
مثلاً. هناك عدة طرق لاستخدام المرشحات: ضمن متحكم، أو كطريقة مُعرفة. فيما يلي الطريقة الأخيرة:
myApp.filter('reverse', function () {
return function (input, uppercase) {
var out = '';
for (var i = 0; i < input.length; i++) {
out = input.charAt(i) + out;
}
if (uppercase) {
out = out.toUpperCase();
}
return out;
}
});
// Controller included to supply data
myApp.controller('MainCtrl', ['$scope', function ($scope) {
$scope.greeting = 'Todd Motto';
}]);
وفي HTML:
<div ng-app="myApp">
<div ng-controller="MainCtrl">
<p>No filter: {{ greeting }}</p>
<p>Reverse: {{ greeting | reverse }}</p>
</div>
</div>
الناتج:
وهنا نستخدم المُرشح ضمن حلقة ng-repeat
:
<ul>
<li ng-repeat="number in myNumbers |filter:oddNumbers">{{ number }}</li>
</ul>
مثال عن مُرشح ضمن متحكم:
myApp.controller('MainCtrl', ['$scope', function ($scope) {
$scope.numbers = [10, 25, 35, 45, 60, 80, 100];
$scope.lowerBound = 42;
// Does the Filters
$scope.greaterThanNum = function (item) {
return item > $scope.lowerBound;
};
}]);
واستخدامه حلقة ng-repeat
:
<li ng-repeat="number in numbers | filter:greaterThanNum">
{{ number }}
</li>
الناتج:
كان هذا القسم الأكبر مما تحتاج لمعرفته عن AngularJS وواجهاتها البرمجية، ومع أن ما تعلمناه كافٍ لبناء تطبيق Angular، ولكننا إلى الآن لم نسكتشف أغوراها!
ربط البيانات ثنائي الاتجاه
عندما سمعت للمرة الأولى عن ربط البيانات ثنائي الاتجاه لم أفهم ما يعنيه. باختصار يمكن القول إنه حلقة متصلة من البيانات المُزامنة: حدّث النموذج (Model) لتُحدَّث طريقة العرض (View)، أو حدّث طريقة العرض ليُحدَّث النموذج (Model). هذا يعني أن البيانات تبقى محدثة دوماً دون عناء. إن ربطت نموذج ng-model
مع حقل إدخال <input>
وكتبت فيه، فهذا يُنشئ (أو يُحدِّث) نموذجاً في الوقت ذاته.
فيما يلي نقوم بإنشاء حقل <input>
ونربطه بنموذج نسميه myModel
، يمكنني الآن استخدام صياغة مقود الدراجة لعكس هذا النموذج وما يطرأ عليه من تحديثات في طريقة العرض في الوقت ذاته:
<div ng-app="myApp">
<div ng-controller="MainCtrl">
<input type="text" ng-model="myModel" placeholder="Start typing..." />
<p>My model data: {{ myModel }}</p>
</div>
</div>
myApp.controller('MainCtrl', ['$scope', function ($scope) {
// Capture the model data
// and/or initialise it with an existing string
$scope.myModel = '';
}]);
الناتج:
طلبات XHR
/Ajax/$http
وربط JSON
نعرف الآن كيف نرسل بيانات بسيطة ضمن المجال ($scope
)، ونعرف ما يكفي عن كيفية عمل النماذج وربط البيانات ثنائي الجانب، والآن حان الوقت لمحاكاة طلبات XHR
حقيقية للخادم. ليس هذا ضرورياً لمواقع الويب العادية، لكنه مناسب جداً لجلب البيانات في تطبيقات الويب.
عندما تطور تطبيقك على جهازك المحلي، فغالباً ما تستخدم شيئاً مثل Java أو ASP.NET أو PHP أو غيرها لتشغيل خادم محلي. وسواء كنا نتصل بقاعدة بيانات محلية أم بخادم بعيد كواجهة برمجية، فإننا نتبع نفس الخطوات بالضبط.
هنا يأتي دور $http
، صديقك المخلص من اليوم فصاعداً! الطريقة $http
هي مُغلّف wrapper
تقدمه Angular للوصول إلى البيانات من الخادم، وهو سهل الاستخدام للغاية ولا يحتاج لأي خبرة. فيما يلي مثال عن طلب GET
لجلب البيانات من الخادم. الصياغة مشابهة جداً لصياغة jQuery، وهذا يُسهل الانتقال من الأخيرة إلى Angular:
myApp.controller('MainCtrl', ['$scope', '$http', function ($scope, $http) {
$http({
method: 'GET',
url: '//localhost:9000/someUrl'
});
}]);
يُعيد Angular إلينا شيئاً يُصطلح على تسميته الوعد (Promise) ، وهو بديل أسهل استخداماً من الاستدعاءات الراجعة (callbacks). يمكن تركيب الوعود في سلسلة باستخدام النقطة، ويمكننا ربطها مع مستقبلات النجاح والفشل:
myApp.controller('MainCtrl', ['$scope', function ($scope) {
$http({
method: 'GET',
url: '//localhost:9000/someUrl'
})
.success(function (data, status, headers, config) {
// successful data retrieval
})
.error(function (data, status, headers, config) {
// something went wrong :(
});
}]);
سهلة الاستخدام والقراءة. هنا نربط طريقة العرض والخادم بربط نموذج أو تحديثه. لنفترض أن لدينا خادمًا مُعدًّا ولنقم بدفع اسم المستخدم إلى طريقة العرض عن طريق طلب AJAX
.
علينا –لو كنا حريصين على المثالية– أن نصمم بيانات JSON
التي نريدها أولاً. دعونا الآن نُبسط الأمور، ولندع هذا الأمر ليتولاه من يفهم في أمور النهاية الخلفية (backend)، ولكن لنقل أننا تفترض أن نستقبل بيانات مثل هذه:
{
"user": {
"name": "Todd Motto",
"id": "80138731"
}
}
هذا يعني أننا سنحصل على كائن Object
من الخادم (سنسميه data
، وسترى أنه يُمرر إلى مستقبلات الوعد الذي أنشأناه).
علينا الآن أن نربط هذا الكائن بالخاصة data.user
، وضمنها لدينا name
وid
.
يمكن ببساطة الوصول إلى هذه القيم باستخدام data.user.name
للحصول على «Todd Motto» مثلاً.
فيما يلي النص البرمجي (اطلع على التعليقات المضمنة):
myApp.controller('UserCtrl', ['$scope', '$http', function ($scope, $http) {
// create a user Object
$scope.user = {};
// Initiate a model as an empty string
$scope.user.username = '';
// نريد أن نرسل طلباً ونحصل على اسم المستخدم
$http({
method: 'GET',
url: '//localhost:9000/someUrlForGettingUsername'
})
.success(function (data, status, headers, config) {
// عند نجاح الطلب، نُسنِد الاسم إلى النموذج الذي أنشأناه
$scope.user.username = data.user.name;
})
.error(function (data, status, headers, config) {
// وقع خطأ ما! :(
});
}]);
الآن ضمن الصفحة يمكننا ببساطة كتابة:
<div ng-controller="UserCtrl">
<p>{{ user.username }}</p>
</div>
هذا سيعرض اسم المستخدم. لننتقل الآن إلى خطوة أبعد بفهم ربط البيانات التصريحي (Declarative data-binding) حيث تصبح الأمور أكثر إثارة!
ربط البيانات التصريحي
تقوم فلسفة Angular على إنشاء نصوص HTML ديناميكية قادرة على القيام بوظائف بنفسها لم نكن نتوقع أنها ممكنة ضمن المتصفح. هذه هي المهمة التي تقوم بها Angualar على خير وجه. لنتخيل أننا أرسلنا طلب AJAX لجلب قائمة بعناوين البريد الإلكتروني وسطر الموضوع فيها مع تاريخ إرسالها، ولنفترض أننا نريد عرضها ضمن الصفحة. في هذا المكان بالضبط تذهلنا Angular بقدراتها. لننشئ أولاً متحكماً بالبريد الإلكتروني:
yApp.controller('EmailsCtrl', ['$scope', function ($scope) {
// نُنشئ كائناً يدعاً `emails`
$scope.emails = {};
// لنفترض أننا حصلنا على هذه البيانات من الخادم
// هذه **مصفوفة** من **الكائنات**
$scope.emails.messages = [{
"from": "Steve Jobs",
"subject": "I think I'm holding my phone wrong :/",
"sent": "2013-10-01T08:05:59Z"
},{
"from": "Ellie Goulding",
"subject": "I've got Starry Eyes, lulz",
"sent": "2013-09-21T19:45:00Z"
},{
"from": "Michael Stipe",
"subject": "Everybody hurts, sometimes.",
"sent": "2013-09-12T11:38:30Z"
},{
"from": "Jeremy Clarkson",
"subject": "Think I've found the best car... In the world",
"sent": "2013-09-03T13:15:11Z"
}];
}]);
علينا الآن دفعها ضمن الصفحة. هنا نستخدم الربط التصريحي لنُعلن عما سيفعله تطبيقنا: إنشاء أول جزء من عناصر HTML الحيوية. سنستخدم مُرشد ng-repeat
المبني ضمن Angular، والذي سوف يمر على البيانات ويعرض الناتج دون عناء الاستدعاءات الرجعية أو تغيير الحالة، بهذه السهولة:
<ul>
<li ng-repeat="message in emails.messages">
<p>From: {{ message.from }}</p>
<p>Subject: {{ message.subject }}</p>
<p>{{ message.sent | date:'MMM d, y h:mm:ss a' }}</p>
</li>
</ul>
الناتج:
قمت أيضاً باستخدام مُرشّح التاريخ لأبين لك كيف يمكن أن تعرض تواريخ UTC.
اطلع على المُرشدات التي توفرها Angular لتتعرف على القدرات الكاملة للربط التصريحي. بهذا نكون قد عرفنا كيف نصل البيانات بين الخادم وطريقة العرض.
وظائف المجال (Scope functions)
تعتبر وظائف المجال الخطوة التالية في بناء وظائف التطبيق واستكمالاً للربط التصريحي. فيما يلي وظيفة بسيطة تحذف إحدى الرسائل:
myApp.controller('MainCtrl', ['$scope', function ($scope) {
$scope.deleteEmail = function (index) {
$scope.emails.messages.splice(index, 1)
};
}]);
نصيحة من محترف: من الهم أن تفكر في حذف البيانات من النموذج، لا تحذف العناصر أو أي شيئ من الصفحة، دع Angular يتولى هذا بربطه ثنائي الجانب للبيانات، فقط فكر بذكاء واكتب نصاً يستجيب لبياناتك!
ربط الوظائف مع طريقة العرض يمر عبر المُرشدات، هذه المرة نستخدم مُرشد ng-click
:
<a ng-click="deleteEmail($index)">Delete email</a>
هذا يختلف تمامًا عن مستقبلات النقر التقليدية في JavaScript، لأسباب عديدة نشرحها لاحقاً.
لاحظ أنني أيضًا أمرر فهرس العنصر $index
، إذ يعرف Angualr ما العنصر المُراد حذفه (كم يوفر هذا من العناء!؟)
الناتج (احذف بعض الرسائل):
طرائق DOM التصريحية
ننتقل الآن لطرائق DOM، وهي أيضًا مُرشدات تؤدي وظيفة ضمن الصفحة بدونها كنا سنكتب الكثير من النص البرمجي. إحدى الأمثلة المناسبة لإيضاح الفكرة هنا هي إظهار أو إخفاء قسم التنقل ضمن الصفحة باستخدام ng-show
وng-click
، لنرَ بساطة هذا:
<a href="" ng-click="toggle = !toggle">Toggle nav</a>
<ul ng-show="toggle">
<li>Link 1</li>
<li>Link 2</li>
<li>Link 3</li>
</ul>
هنا ندخل عالم MVVM (اختصار Model-View-View-Model)، لاحظ أننا لم نستخدم متحكماً، وسنشرح فكرة MVVM بعد قليل.
الناتج (جرب الإظهار والإخفاء):
التعبيرات (Expressions)
من أفضل ما يقدمه Angular، يقدم بديلاً عن الحاجة لكتابة الكثير من JavaScript والنصوص المكررة.
هل قمت يوماً بكتابة شيء كهذا؟
elem.onclick = function (data) {
if (data.length === 0) {
otherElem.innerHTML = 'No data';
} else {
otherElem.innerHTML = 'My data';
}
};
ربما يكون هذا استدعاءً راجعًا عن طلب GET، ونحتاج لنغير محتوى في الصفحة بناء على البيانات، يقدم Angular هذا بدون الحاجة لكتابة حرف JavaScript!
<p>{{ data.length > 0 && 'My data' || 'No data' }}</p>
سيقوم هذا بتحديث الصفحة تلقائياً دون استدعاءات عند وصول البيانات أو ما شابه. إن لم تتوفر البيانات، سيظهر هذا واضحاً، وإن وجدت فسيظهر كذلك. هناك حالات كثيرة جدًا يممكن فيها لـAngular أن يتولاها عبر ربطه ثنائي الجانب للبيانات الذي يعمل كالسحر!
الناتج:
طرق العرض الديناميكية والتوجيه (Routing)
هي ما تقوم عليه فلسفة تطبيقات الويب (والمواقع) أحادية الصفحة: لديك قسم الترويسة وقسم التذييل وشريط جانبي ثم المحتوى الذي يُحدث تلقائياً بناءً على الرابط الحالي.
يجعل Angular إعداد هذا في منتهى السهولة. تحقن طرق العرض الديناميكة طرقاً معينة بناء على الرابط، عبر استخدام $routeProvider
. فيما يلي إعداد بسيط:
myApp.config(['$routeProvider', function ($routeProvider) {
/**
* $routeProvider
*/
$routeProvider
.when('/', {
templateUrl: 'views/main.html'
})
.otherwise({
redirectTo: '/'
});
}]);
لاحظ أنه عندما (when
) يكون الرابط /
فقط (الصفحة الرئيسية)، ستعرض الصفحة main.html
. من المفيد تسمية طريقة العرض الأساسية main.html
وليس index.html
لأنه سيكون لدينا مسبقاً الصفحة index.html
وهي الصفحة التي تحوي طرق العرض الديناميكية وبقية الأجزاء.
يمكن ببساطة إضافة المزيد من طرق العرض:
myApp.config(['$routeProvider', function ($routeProvider) {
/**
* $routeProvider
*/
$routeProvider
.when('/', {
templateUrl: 'views/main.html'
})
.when('/emails', {
templateUrl: 'views/emails.html'
})
.otherwise({
redirectTo: '/'
});
}]);
بإمكاننا الآن تحميل الصفحة emails.html
ببساطة لتقوم بعرض قائمة الرسائل الإلكترونية. الخلاصة أننا استطعنا أن نبني تطبيقاً معقداً للغاية بجهد ضئيل جداً.
توفر الخدمة $routerProvider
المزيد من الخيارات، لكن ما تعلمناه عنها كافٍ في البداية. هناك أيضاً أشياء مثل مُعترِضات $http
التي تبدأ أحداثاً خلال مسير طلب AJAX، فتتيح لنا عرض مقدار التقدم على سبيل المثال أثناء جلب البيانات.
البيانات الثابتة العامة
في تطبيق Gmail للويب، تُكتب بيانات كثيرة بصيغة JSON ضمن الصفحة (انقر باليمين واختر عرض المصدر في صفحة Gmail). إن قمنا بخطوة مشابهة، أي كتابة البيانات ضمن الصفحة، فهذا سيجعل وقت عرضها أقل وسيبدو التطبيق أكثر سرعة.
عندما أطور تطبيقات مؤسستنا، تُدرج وسوم Java ضمن الصفحة وعندما تُعرض، تُرسل البيانات من الخادم (لا خبرة لدي في Java لذا سأكتب فيما يلي تصريحات وهمية، يمكنك استخدام أي لغة على الخادم إن أحببت). النصوص التالية توضح كيف يمكنك كتابة JSON ضمن الصفحة ثم تمريرها إلى المتحكم لربطها مباشرة:
<!-- ضمن index.html (في نهاية الصفحة بالطبع) -->
<script>
window.globalData = {};
globalData.emails = <javaTagHereToGenerateMessages>;
</script>
سيُنشئ وسم Java الذي اختلقته البينات بينما سيعالج Angular الرسائل فوراً. كل ما عليك هو إعطاؤه البيانات عبر المتحكم:
myApp.controller('EmailsCtrl', ['$scope', function ($scope) {
$scope.emails = {};
// Assign the initial data!
$scope.emails.messages = globalData.emails;
}]);
تقليص الملفات (Minification)
سنتحدث قليلاً عن تقليص حجم النصوص البرمجية التي كتبناها. ربما تكون قد جربت تقليص نصوصك البرمجية التي كتبتها لـAngular وصادفت خطأ. ليس هناك أمور خاصة يتطلبها تقليص حجم هذه الملفات، باستثناء الحاجة لإدراج أسماء المتطلبات ضمن مصفوفة قبل الطريقة المُصرّح عنها، لنوضح أكثر:
myApp.controller('MainCtrl',
['$scope', 'Dependency', 'Service', 'Factory',
function ($scope, Dependency, Service, Factory) {
// code
}]);
بعد التقليص:
myApp.controller('MainCtrl',
['$scope', 'Dependency', 'Service', 'Factory',
function (a,b,c,d) {
// a = $scope
// b = Dependency
// c = Service
// d = Factory
// $scope alias usage
a.someFunction = function () {...};
}]);
عليك أن تحافظ على ترتيب المتطلبات المحقونة في المصفوفة ['_one', '_two']
وضمن مُعاملات الطريقة function(_one, _two)
، وإلا ستسبب لنفسك ولفريق العمل معك مشاكل كثيرة!
الاختلافات بين MVC وMVVM
سننهي مقالتنا العملاقة الآن بشرح سريع يشمل الفروق بين MVC وMVVM:
MVC: التواصل يعتمد على المتحكم (Controller)، لذلك نقول Model-View-Controller
MVVM: يشمل ربط البيانات التصريحي الذي يتواصل، بالمفهوم التقني، مع نفسه؛ أي Model-View-View-Model. يتواصل النموذج مع طريقة العرض، وتتواصل هذه الأخيرة مع النموذج ثانية. يسمح هذا للبيانات بأن تبقى محدّثة على الجانبين دون الحاجة لفعل أي شيء. لا داعي هنا للمتحكم. مثال على هذا: إنشاء حلقة
ng-repeat
دون أن نعتمد على بيانات يُرسلها متحكم:
<li ng-repeat="number in [1,2,3,4,5,6,7,8,9]">
{{ number }}
</li>
يبدو هذا مناسباً للتجارب السريعة، ولكنني أنصح دوماً باستخدام متحكم للحفاظ على تنظيم النص البرمجي.
الناتج:
مكونات الويب في HTML5
قد تبدو هذه الفكرة مكررة، ولكنني سأعيدها هنا لنتحدث عن مكونات الويب.
يسمح Angular بإنشاء عناصر (elements) مخصصة مثل: <myCustomElement></myCustomElement>
في الحقيقة هذا أشبه ما يكون بمستقبل HTML5 التي تقدم فكرة جديدة تُسمى مكونّات الويب (Web Components)، والتي تتركب من عناصر مخصصة ضمن HTML مترافقة مع نص JavaScript ديناميكي، وهذا أمر مثيرٌ للغاية - والأكثر إثارة أنه ممكن اليوم باستخدام Angular! فريق Angular بعيد النظر - شكراً لكم!
تعليقات المجال (Scope)
أعتقد أن تعليقات المجال إضافة جميلة لسياق العمل، فبدل الحاجة لكتابة التعليقات ضمن HTML بالطريقة التالية:
<!-- header -->
<header>
Stuff.
</header>
<!-- /header -->
أصبحنا نتحدث عن طرق العرض والمجالات بدل الصفحة، البيانات ضمن مجال ما لا تُشارك مع مجالات أخرى إلا إن قمت بفعل ذلك عن عمدٍ. لنستغل هذا في تحسين كتابة النص البرمجي بتقسيمه إلى مناطق تسبقها تعليقات:
<!-- scope: MainCtrl -->
<div class="content" ng-controller="MainCtrl">
</div>
<!-- /scope: MainCtrl -->
تصحيح العلل في Angular
تتوفر إضافة جميلة للغاية لمتصفح Chrome ينصح بها فريق Angular، وتسمى Batarang.
برمجة سعيدة!
جدول المحتويات