بناء تطبيقات الويب الحديثة باستخدام AngularJS و Play Framework

نشرت: 2022-03-11

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

تطبيقات الويب مع angularjs و play framework

في هذه المقالة ، سوف نلقي نظرة على كيفية استخدام AngularJS و Play لبناء تطبيق مدونة بسيط بآلية مصادقة أساسية والقدرة على إنشاء منشورات وتعليقات. سيسمح لنا تطوير AngularJS ، مع بعض الأشياء الجيدة على Twitter Bootstrap ، بتشغيل تجربة تطبيق من صفحة واحدة أعلى الواجهة الخلفية REST API القائمة على Play.

تطبيقات الويب - الشروع في العمل

AngularJS Application Skeleton

ستوجد تطبيقات AngularJS و Play في دلائل العميل والخادم وفقًا لذلك. في الوقت الحالي ، سننشئ دليل "العميل".

 mkdir -p blogapp/client

لإنشاء هيكل تطبيق AngularJS ، سنستخدم Yeoman - أداة سقالات مذهلة. تثبيت Yeoman سهل. ربما يكون استخدامه لسقالة تطبيق AngularJS البسيط للهيكل العظمي أسهل:

 cd blogapp/client yo angular

سيتبع تشغيل الأمر الثاني بعض الخيارات التي تحتاج إلى الاختيار من بينها. بالنسبة لهذا المشروع ، لا نحتاج إلى "Sass (مع البوصلة)". سنحتاج إلى Boostrap جنبًا إلى جنب مع الإضافات AngularJS التالية:

  • الزاوي - animate.js
  • الزاوي ملفات تعريف الارتباط. js
  • angular-Resource.js
  • الزاوية-route.js
  • الزاوي- sanitize.js
  • الزاوي اللمس. js

في هذه المرحلة ، بمجرد الانتهاء من تحديداتك ، ستبدأ في رؤية إخراج NPM و Bower على جهازك الطرفي. عند اكتمال التنزيلات وتثبيت الحزم ، سيكون لديك هيكل تطبيق AngularJS جاهز للاستخدام.

لعب إطار تطبيق الهيكل العظمي

تتضمن الطريقة الرسمية لإنشاء تطبيق Play جديد استخدام الأداة Typesafe Activator. قبل أن تتمكن من استخدامه ، يجب عليك تنزيله وتثبيته على جهاز الكمبيوتر الخاص بك. إذا كنت تستخدم نظام التشغيل Mac OS وتستخدم Homebrew ، فيمكنك تثبيت هذه الأداة بسطر واحد من الأوامر:

 brew install typesafe-activator

يعد إنشاء تطبيق Play من سطر الأوامر أمرًا سهلاً للغاية:

 cd blogapp/ activator new server play-java cd server/

الاستيراد إلى IDE

لاستيراد التطبيق في IDE مثل Eclipse أو IntelliJ ، تحتاج إلى "eclipsify" أو "تحسين" تطبيقك. للقيام بذلك ، قم بتشغيل الأمر التالي:

 activator

بمجرد أن ترى مطالبة جديدة ، اكتب إما "eclipse" أو "idea" واضغط على Enter لإعداد رمز التطبيق لـ Eclipse أو IntelliJ ، على التوالي.

للإيجاز ، سنغطي فقط عملية استيراد المشروع إلى IntelliJ في هذه المقالة. يجب أن تكون عملية استيراده إلى Eclipse بسيطة بنفس القدر. لاستيراد المشروع إلى IntelliJ ، ابدأ بتنشيط خيار "المشروع من المصادر الحالية ..." الموجود ضمن "ملف -> جديد". بعد ذلك ، حدد ملف build.sbt الخاص بك وانقر فوق "موافق". عند النقر فوق "موافق" مرة أخرى في مربع الحوار التالي ، يجب أن يبدأ IntelliJ في استيراد تطبيق Play الخاص بك كمشروع SBT.

يأتي Typesafe Activator أيضًا مع واجهة مستخدم رسومية ، والتي يمكنك استخدامها لإنشاء كود التطبيق الهيكلي هذا.

الآن بعد أن قمنا باستيراد تطبيق Play الخاص بنا إلى IntelliJ ، يجب علينا أيضًا استيراد تطبيق AngularJS الخاص بنا إلى مساحة العمل. يمكننا استيراده إما كمشروع منفصل أو كوحدة نمطية إلى المشروع الحالي حيث يوجد تطبيق Play.

هنا ، سنقوم باستيراد تطبيق Angular كوحدة نمطية. ضمن قائمة "ملف" ، سنحدد الخيار "جديد -> وحدة من مصادر موجودة ...". من مربع الحوار ، سنختار دليل "العميل" ونضغط على "موافق". في الشاشتين التاليتين ، انقر فوق "التالي" و "إنهاء" ، على التوالي.

إنتاج الخوادم المحلية

في هذه المرحلة ، يجب أن يكون من الممكن بدء تطبيق AngularJS كمهمة Grunt من IDE. قم بتوسيع مجلد العميل الخاص بك وانقر بزر الماوس الأيمن على Gruntfile.js. في القائمة المنبثقة ، حدد "إظهار مهام Grunt". ستظهر لوحة بعنوان "Grunt" مع قائمة من المهام:

إنتاج الخوادم المحلية

لبدء خدمة التطبيق ، انقر نقرًا مزدوجًا على "خدمة". يجب أن يفتح هذا على الفور متصفح الويب الافتراضي الخاص بك ويوجهه إلى عنوان مضيف محلي. يجب أن تشاهد صفحة AngularJS ذات كعب عالٍ عليها شعار Yeoman.

بعد ذلك ، نحتاج إلى تشغيل خادم التطبيق الخلفي الخاص بنا. قبل أن نتمكن من المضي قدمًا ، يجب أن نعالج مشكلتين:

  1. بشكل افتراضي ، يحاول كل من تطبيق AngularJS (الذي تم تشغيله بواسطة Yeoman) وتطبيق Play التشغيل على المنفذ 9000.
  2. في الإنتاج ، من المرجح أن يتم تشغيل كلا التطبيقين ضمن مجال واحد ، ومن المحتمل أن نستخدم Nginx لتوجيه الطلبات وفقًا لذلك. ولكن في وضع التطوير ، عندما نقوم بتغيير رقم المنفذ لأحد هذه التطبيقات ، فإن متصفحات الويب ستتعامل معها كما لو كانت تعمل على مجالات مختلفة.

للتغلب على هاتين المشكلتين ، كل ما نحتاج إلى القيام به هو استخدام وكيل Grunt بحيث تكون جميع طلبات AJAX إلى تطبيق Play وكيلاً. مع هذا ، سيكون كلا خادمي التطبيقات متاحين في نفس رقم المنفذ الظاهر.

دعنا أولاً نغير رقم المنفذ لخادم تطبيق Play إلى 9090. للقيام بذلك ، افتح نافذة "تشغيل / تصحيح التكوينات" بالنقر فوق "تشغيل -> تحرير التكوينات". بعد ذلك ، قم بتغيير رقم المنفذ في الحقل "Url To Open". انقر فوق "موافق" للموافقة على هذا التغيير وإغلاق النافذة. يجب أن يؤدي النقر فوق الزر "تشغيل" إلى بدء عملية حل التبعية - ستبدأ سجلات هذه العملية في الظهور.

إنتاج الخوادم المحلية

بمجرد الانتهاء من ذلك ، يمكنك الانتقال إلى http: // localhost: 9090 على متصفح الويب الخاص بك ، وفي بضع ثوانٍ ستتمكن من رؤية تطبيق Play الخاص بك. لتكوين وكيل Grunt ، نحتاج أولاً إلى تثبيت حزمة Node.js صغيرة باستخدام NPM:

 cd blogapp/client npm install grunt-connect-proxy --save-dev

بعد ذلك ، نحتاج إلى تعديل Gruntfile.js. في هذا الملف ، حدد موقع مهمة "connect" ، وأدخل مفتاح / قيمة "proxies" بعدها:

 proxies: [ { context: '/app', // the context of the data service host: 'localhost', // wherever the data service is running port: 9090, // the port that the data service is running on changeOrigin: true } ],

سيعمل Grunt الآن على تفويض جميع الطلبات إلى "/ app / *" إلى تطبيق Play الخلفي. سيوفر هذا علينا الاضطرار إلى إضافة كل مكالمة إلى النهاية الخلفية في القائمة البيضاء. علاوة على ذلك ، نحتاج أيضًا إلى تعديل سلوك حمل الكبد لدينا:

 livereload: { options: { open: true, middleware: function (connect) { var middlewares = []; // Setup the proxy middlewares.push(require('grunt-connect-proxy/lib/utils').proxyRequest); // Serve static files middlewares.push(connect.static('.tmp')); middlewares.push(connect().use( '/bower_components', connect.static('./bower_components') )); middlewares.push(connect().use( '/app/styles', connect.static('./app/styles') )); middlewares.push(connect.static(appConfig.app)); return middlewares; } } },

أخيرًا ، نحتاج إلى إضافة تبعية جديدة "" configProxies: server "إلى مهمة" serve ":

 grunt.registerTask('serve', 'Compile then start a connect web server', function (target) { if (target === 'dist') { return grunt.task.run(['build', 'connect:dist:keepalive']); } grunt.task.run([ 'clean:server', 'wiredep', 'concurrent:server', 'autoprefixer:server', 'configureProxies:server', 'connect:livereload', 'watch' ]); });

عند إعادة تشغيل Grunt ، يجب أن تلاحظ الأسطر التالية في سجلاتك تشير إلى أن الوكيل قيد التشغيل:

 Running "autoprefixer:server" (autoprefixer) task File .tmp/styles/main.css created. Running "configureProxies:server" (configureProxies) task Running "connect:livereload" (connect) task Started connect web server on http://localhost:9000

إنشاء نموذج التسجيل

سنبدأ بإنشاء نموذج تسجيل لتطبيق المدونة الخاص بنا. سيسمح لنا هذا أيضًا بالتحقق من أن كل شيء يعمل كما ينبغي. يمكننا استخدام Yeoman لإنشاء وحدة تحكم في التسجيل وعرض في AngularJS:

 yo angular:controller signup yo angular:view signup

بعد ذلك ، يجب علينا تحديث توجيه تطبيقنا للإشارة إلى هذا العرض الذي تم إنشاؤه حديثًا ، وإزالة وحدة التحكم والعرض "about" الزائدة التي تم إنشاؤها تلقائيًا. من داخل الملف "app / scripts / app.js" ، أزل الإشارات إلى "app / scripts / Controllers / about.js" و "app / views / about.html" ، مع تركه مع:

 .config(function ($routeProvider) { $routeProvider .when('/', { templateUrl: 'views/main.html', controller: 'MainCtrl' }) .when('/signup', { templateUrl: 'views/signup.html', controller: 'SignupCtrl' }) .otherwise({ redirectTo: '/' });

وبالمثل ، قم بتحديث ملف "app / index.html" لإزالة الروابط الزائدة ، وإضافة رابط إلى صفحة الاشتراك:

 <div class="collapse navbar-collapse"> <ul class="nav navbar-nav"> <li class="active"><a href="#/">Home</a></li> <li><a ng-href="#/signup">Signup</a></li> </ul> </div> </div>

أيضًا ، قم بإزالة علامة البرنامج النصي لـ "about.js":

 <!-- build:js({.tmp,app}) scripts/scripts.js --> <script src="scripts/app.js"></script> <script src="scripts/controllers/main.js"></script> <script src="scripts/controllers/signup.js"></script> <!-- endbuild --> </body> </html>

بعد ذلك ، أضف نموذجًا إلى ملف "signup.html" الخاص بنا:

 <form name="signupForm" ng-submit="signup()" novalidate> <div> <label for="email">Email</label> <input name="email" class="form-control" type="email" placeholder="Email" ng-model="email"> </div> <div> <label for="password">Password</label> <input name="password" class="form-control" type="password" placeholder="Password" ng-model="password"> </div> <button type="submit" class="btn btn-primary">Sign up!</button> </form>

نحتاج إلى جعل النموذج تتم معالجته بواسطة وحدة التحكم الزاويّة. من الجدير بالذكر أننا لسنا بحاجة إلى إضافة سمة "ng-controller" على وجه التحديد في وجهات نظرنا ، حيث يقوم منطق التوجيه الخاص بنا في "app.js" بتشغيل وحدة التحكم تلقائيًا قبل تحميل العرض الخاص بنا. كل ما يتعين علينا القيام به لتوصيل هذا النموذج هو الحصول على وظيفة "تسجيل" مناسبة محددة في نطاق $. يجب أن يتم ذلك في ملف “signup.js”:

 angular.module('clientApp') .controller('SignupCtrl', function ($scope, $http, $log) { $scope.signup = function() { var payload = { email : $scope.email, password : $scope.password }; $http.post('app/signup', payload) .success(function(data) { $log.debug(data); }); }; });

الآن دعنا نفتح وحدة تحكم مطوري Chrome ، وانتقل إلى علامة التبويب "الشبكة" ، ونحاول إرسال نموذج الاشتراك.

نموذج الاشتراك في Angular Play

سنرى أن الواجهة الخلفية للتشغيل ترد بشكل طبيعي مع صفحة خطأ "لم يتم العثور على الإجراء". هذا متوقع لأنه لم يتم تنفيذه بعد. ولكن ما يعنيه ذلك أيضًا هو أن إعداد وكيل Grunt يعمل بشكل صحيح!

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

 public static Result signup() { return ok("Success!"); }

افتح الآن ملف "conf / route" وأضف السطر التالي:

 POST /app/signup controllers.Application.signup

أخيرًا ، نعود إلى متصفح الويب الخاص بنا ، http: // localhost: 9000 / # / signup. يجب أن يؤدي النقر فوق الزر "إرسال" هذه المرة إلى شيء مختلف:

نموذج الاشتراك Angular Play في المتصفح

يجب أن ترى القيمة الثابتة التي تم إرجاعها ، القيمة التي كتبناها في طريقة التسجيل. إذا كان هذا هو الحال ، فنحن مستعدون للمضي قدمًا لأن بيئة التطوير لدينا جاهزة وتعمل لكل من تطبيقات Angular و Play.

تحديد نماذج Ebean في Play

قبل تحديد النماذج ، دعنا أولاً نختار مخزن بيانات. في هذه المقالة ، سنستخدم قاعدة بيانات H2 في الذاكرة. لتفعيل ذلك ، ابحث عن الأسطر التالية في ملف "application.conf" وأزل تعليقها:

 db.default.driver=org.h2.Driver db.default.url="jdbc:h2:mem:play" db.default.user=sa db.default.password="" ... ebean.default="models.*"

وأضف السطر التالي:

 applyEvolutions.default=true

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

المستعمل

 // User.java @Entity public class User extends Model { @Id public Long id; @Column(length = 255, unique = true, nullable = false) @Constraints.MaxLength(255) @Constraints.Required @Constraints.Email public String email; @Column(length = 64, nullable = false) private byte[] shaPassword; @OneToMany(cascade = CascadeType.ALL) @JsonIgnore public List<BlogPost> posts; public void setPassword(String password) { this.shaPassword = getSha512(password); } public void setEmail(String email) { this.email = email.toLowerCase(); } public static final Finder<Long, User> find = new Finder<Long, User>( Long.class, User.class); public static User findByEmailAndPassword(String email, String password) { return find .where() .eq("email", email.toLowerCase()) .eq("shaPassword", getSha512(password)) .findUnique(); } public static User findByEmail(String email) { return find .where() .eq("email", email.toLowerCase()) .findUnique(); } public static byte[] getSha512(String value) { try { return MessageDigest.getInstance("SHA-512").digest(value.getBytes("UTF-8")); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } }

مشاركة مدونة

 // BlogPost.java @Entity public class BlogPost extends Model { @Id public Long id; @Column(length = 255, nullable = false) @Constraints.MaxLength(255) @Constraints.Required public String subject; @Column(columnDefinition = "TEXT") @Constraints.Required public String content; @ManyToOne public User user; public Long commentCount; @OneToMany(cascade = CascadeType.ALL) public List<PostComment> comments; public static final Finder<Long, BlogPost> find = new Finder<Long, BlogPost>( Long.class, BlogPost.class); public static List<BlogPost> findBlogPostsByUser(final User user) { return find .where() .eq("user", user) .findList(); } public static BlogPost findBlogPostById(final Long id) { return find .where() .eq("id", id) .findUnique(); } }

أضف تعليقا

 // PostComment.java @Entity public class PostComment extends Model { @Id public Long id; @ManyToOne @JsonIgnore public BlogPost blogPost; @ManyToOne public User user; @Column(columnDefinition = "TEXT") public String content; public static final Finder<Long, PostComment> find = new Finder<Long, PostComment>( Long.class, PostComment.class); public static List<PostComment> findAllCommentsByPost(final BlogPost blogPost) { return find .where() .eq("post", blogPost) .findList(); } public static List<PostComment> findAllCommentsByUser(final User user) { return find .where() .eq("user", user) .findList(); } }

إجراء التسجيل الحقيقي

لنقم الآن بإنشاء أول إجراء حقيقي ، مما يسمح للمستخدمين بالتسجيل:

 // Application.java public static Result signup() { Form<SignUp> signUpForm = Form.form(SignUp.class).bindFromRequest(); if ( signUpForm.hasErrors()) { return badRequest(signUpForm.errorsAsJson()); } SignUp newUser = signUpForm.get(); User existingUser = User.findByEmail(newUser.email); if(existingUser != null) { return badRequest(buildJsonResponse("error", "User exists")); } else { User user = new User(); user.setEmail(newUser.email); user.setPassword(newUser.password); user.save(); session().clear(); session("username", newUser.email); return ok(buildJsonResponse("success", "User created successfully")); } } public static class UserForm { @Constraints.Required @Constraints.Email public String email; } public static class SignUp extends UserForm { @Constraints.Required @Constraints.MinLength(6) public String password; } private static ObjectNode buildJsonResponse(String type, String message) { ObjectNode wrapper = Json.newObject(); ObjectNode msg = Json.newObject(); msg.put("message", message); wrapper.put(type, msg); return wrapper; }

لاحظ أن المصادقة المستخدمة في هذا التطبيق أساسية جدًا ولا يوصى بها للاستخدام الإنتاجي.

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

إذا عدنا إلى تطبيق AngularJS الخاص بنا في متصفح الويب وقمنا بالنقر فوق "إرسال" مرة أخرى ، فسنرى أن الخادم يستجيب الآن بخطأ مناسب - أن هذه الحقول مطلوبة.

إرسال استمارة اللعب

معالجة أخطاء الخادم في AngularJS

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

أولاً ، نحتاج إلى إنشاء قالب خدمة باستخدام Yeoman:

 yo angular:service alerts

بعد ذلك ، أضف هذا الرمز إلى "alerts.js":

 angular.module('clientApp') .factory('alertService', function($timeout) { var ALERT_TIMEOUT = 5000; function add(type, msg, timeout) { if (timeout) { $timeout(function(){ closeAlert(this); }, timeout); } else { $timeout(function(){ closeAlert(this); }, ALERT_TIMEOUT); } return alerts.push({ type: type, msg: msg, close: function() { return closeAlert(this); } }); } function closeAlert(alert) { return closeAlertIdx(alerts.indexOf(alert)); } function closeAlertIdx(index) { return alerts.splice(index, 1); } function clear(){ alerts = []; } function get() { return alerts; } var service = { add: add, closeAlert: closeAlert, closeAlertIdx: closeAlertIdx, clear: clear, get: get }, alerts = []; return service; } );

الآن ، لنقم بإنشاء وحدة تحكم منفصلة مسؤولة عن التنبيهات:

 yo angular:controller alerts
 angular.module('clientApp') .controller('AlertsCtrl', function ($scope, alertService) { $scope.alerts = alertService.get(); });

نحتاج الآن إلى إظهار رسائل خطأ Bootstrap لطيفة بالفعل. أسهل طريقة هي استخدام Angular UI. يمكننا استخدام Bower لتثبيته:

 bower install angular-bootstrap --save

في "app.js" ، قم بإلحاق وحدة Angular UI:

 angular .module('clientApp', [ 'ngAnimate', 'ngCookies', 'ngResource', 'ngRoute', 'ngSanitize', 'ngTouch', 'ui.bootstrap' ])

دعنا نضيف توجيه تنبيه إلى ملف "index.html" الخاص بنا:

 <div class="container"> <div ng-controller="AlertsCtrl"> <alert ng-repeat="alert in alerts" type="{{alert.type}}" close="alert.close()">{{ alert.msg }}</alert> </div> <div ng-view=""></div> </div>

أخيرًا ، نحتاج إلى تحديث وحدة تحكم SignUp:

 angular.module('clientApp') .controller('SignupCtrl', function ($scope, $http, $log, alertService, $location, userService) { $scope.signup = function() { var payload = { email : $scope.email, password : $scope.password }; $http.post('app/signup', payload) .error(function(data, status) { if(status === 400) { angular.forEach(data, function(value, key) { if(key === 'email' || key === 'password') { alertService.add('danger', key + ' : ' + value); } else { alertService.add('danger', value.message); } }); } if(status === 500) { alertService.add('danger', 'Internal server error!'); } }) }; });

الآن إذا أرسلنا النموذج الفارغ مرة أخرى ، فسنرى أخطاء معروضة أعلى النموذج:

أخطاء اللعب الزاوي

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

 yo angular:view dashboard yo angular:controller dashboard

عدّل طريقة الاشتراك في وحدة التحكم "signup.js" بحيث يعيد توجيه المستخدم عند النجاح:

 angular.module('clientApp') .controller('SignupCtrl', function ($scope, $http, $log, alertService, $location) { // .. .success(function(data) { if(data.hasOwnProperty('success')) { $location.path('/dashboard'); } });

أضف مسارًا جديدًا في "apps.js":

 .when('/dashboard', { templateUrl: 'views/dashboard.html', controller: 'DashboardCtrl' })

نحتاج أيضًا إلى تتبع ما إذا كان المستخدم قد قام بتسجيل الدخول. فلننشئ خدمة منفصلة لذلك:

 yo angular:service user
 // user.js angular.module('clientApp') .factory('userService', function() { var username = ''; return { username : username }; });

وأيضًا قم بتعديل وحدة التحكم في التسجيل لتعيين المستخدم إلى الشخص الذي سجل للتو:

 .success(function(data) { if(data.hasOwnProperty('success')) { userService.username = $scope.email; $location.path('/dashboard');; } });

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

المصادقة الأساسية

دعنا ننتقل إلى تطبيق Play الخاص بنا وننفذ إجراءات تسجيل الدخول والخروج. أضف أسطر الأطروحات إلى "Application.java":

 public static Result login() { Form<Login> loginForm = Form.form(Login.class).bindFromRequest(); if (loginForm.hasErrors()) { return badRequest(loginForm.errorsAsJson()); } Login loggingInUser = loginForm.get(); User user = User.findByEmailAndPassword(loggingInUser.email, loggingInUser.password); if(user == null) { return badRequest(buildJsonResponse("error", "Incorrect email or password")); } else { session().clear(); session("username", loggingInUser.email); ObjectNode wrapper = Json.newObject(); ObjectNode msg = Json.newObject(); msg.put("message", "Logged in successfully"); msg.put("user", loggingInUser.email); wrapper.put("success", msg); return ok(wrapper); } } public static Result logout() { session().clear(); return ok(buildJsonResponse("success", "Logged out successfully")); } public static Result isAuthenticated() { if(session().get("username") == null) { return unauthorized(); } else { ObjectNode wrapper = Json.newObject(); ObjectNode msg = Json.newObject(); msg.put("message", "User is logged in already"); msg.put("user", session().get("username")); wrapper.put("success", msg); return ok(wrapper); } } public static class Login extends UserForm { @Constraints.Required public String password; }

بعد ذلك ، دعنا نضيف القدرة على السماح بمكالمات خلفية معينة للمستخدمين المصادق عليهم فقط. أنشئ "Secured.java" بالشفرة التالية:

 public class Secured extends Security.Authenticator { @Override public String getUsername(Context ctx) { return ctx.session().get("username"); } @Override public Result onUnauthorized(Context ctx) { return unauthorized(); } }

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

 yo angular:controller menu
 // menu.js angular.module('clientApp') .controller('MenuCtrl', function ($scope, $http, userService, $location) { $scope.user = userService; $scope.logout = function() { $http.get('/app/logout') .success(function(data) { if(data.hasOwnProperty('success')) { userService.username = ''; $location.path('/login'); } }); }; $scope.$watch('user.username', function (newVal) { if(newVal === '') { $scope.isLoggedIn = false; } else { $scope.username = newVal; $scope.isLoggedIn = true; } }); });

نحتاج أيضًا إلى عرض ووحدة تحكم لصفحة تسجيل الدخول:

 yo angular:controller login yo angular:view login
 <!-- login.html --> <form name="loginForm" ng-submit="login()" novalidate> <div> <label for="email">Email</label> <input name="email" class="form-control" type="email" placeholder="Email" ng-model="email"> </div> <div> <label for="password">Password</label> <input name="password" class="form-control" type="password" placeholder="Password" ng-model="password"> </div> <button type="submit" class="btn btn-primary">Log in</button> </form>
 // login.js angular.module('clientApp') .controller('LoginCtrl', function ($scope, userService, $location, $log, $http, alertService) { $scope.isAuthenticated = function() { if(userService.username) { $log.debug(userService.username); $location.path('/dashboard'); } else { $http.get('/app/isauthenticated') .error(function() { $location.path('/login'); }) .success(function(data) { if(data.hasOwnProperty('success')) { userService.username = data.success.user; $location.path('/dashboard'); } }); } }; $scope.isAuthenticated(); $scope.login = function() { var payload = { email : this.email, password : this.password }; $http.post('/app/login', payload) .error(function(data, status){ if(status === 400) { angular.forEach(data, function(value, key) { if(key === 'email' || key === 'password') { alertService.add('danger', key + ' : ' + value); } else { alertService.add('danger', value.message); } }); } else if(status === 401) { alertService.add('danger', 'Invalid login or password!'); } else if(status === 500) { alertService.add('danger', 'Internal server error!'); } else { alertService.add('danger', data); } }) .success(function(data){ $log.debug(data); if(data.hasOwnProperty('success')) { userService.username = data.success.user; $location.path('/dashboard'); } }); }; });

بعد ذلك ، نقوم بتعديل القائمة بحيث يمكنها عرض بيانات المستخدم:

 <!-- index.html --> <div class="collapse navbar-collapse" ng-controller="MenuCtrl"> <ul class="nav navbar-nav pull-right" ng-hide="isLoggedIn"> <li><a ng-href="/#/signup">Sign up!</a></li> <li><a ng-href="/#/login">Login</a></li> </ul> <div class="btn-group pull-right acc-button" ng-show="isLoggedIn"> <button type="button" class="btn btn-default">{{ username }}</button> <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> <span class="caret"></span> <span class="sr-only">Toggle Dropdown</span> </button> <ul class="dropdown-menu" role="menu"> <li><a ng-href="/#/dashboard">Dashboard</a></li> <li class="divider"></li> <li><a href="#" ng-click="logout()">Logout</a></li> </ul> </div> </div>

الآن إذا قمت بتسجيل الدخول إلى التطبيق ، يجب أن تكون قادرًا على رؤية الشاشة التالية:

تشغيل لقطة الشاشة

إضافة المشاركات

الآن بعد أن أصبح لدينا آليات تسجيل ومصادقة أساسية ، يمكننا البدء في تنفيذ وظيفة النشر. دعنا نضيف طريقة عرض جديدة ووحدة تحكم لإضافة المنشورات.

 yo angular:view addpost
 <!-- addpost.html --> <form name="postForm" ng-submit="post()" novalidate> <div> <label for="subject">Subject</label> <input name="subject" class="form-control" type="subject" placeholder="Subject" ng-model="subject"> </div> <div> <label for="content">Post</label> <textarea name="content" class="form-control" placeholder="Content" ng-model="content"></textarea> </div> <button type="submit" class="btn btn-primary">Submit post</button> </form>
 yo angular:controller addpost
 // addpost.js angular.module('clientApp') .controller('AddpostCtrl', function ($scope, $http, alertService, $location) { $scope.post = function() { var payload = { subject : $scope.subject, content: $scope.content }; $http.post('/app/post', payload) .error(function(data, status) { if(status === 400) { angular.forEach(data, function(value, key) { if(key === 'subject' || key === 'content') { alertService.add('danger', key + ' : ' + value); } else { alertService.add('danger', value.message); } }); } else if(status === 401) { $location.path('/login'); } else if(status === 500) { alertService.add('danger', 'Internal server error!'); } else { alertService.add('danger', data); } }) .success(function(data) { $scope.subject = ''; $scope.content = ''; alertService.add('success', data.success.message); }); }; });

ثم نقوم بتحديث "app.js" ليشمل:

 .when('/addpost', { templateUrl: 'views/addpost.html', controller: 'AddpostCtrl' })

بعد ذلك ، نقوم بتعديل "index.html" لإضافة رابط لعرضنا "addpost" في قائمة لوحة التحكم:

 <ul class="dropdown-menu" role="menu"> <li><a ng-href="/#/dashboard">Dashboard</a></li> <li><a ng-href="/#/addpost">Add post</a></li> <li class="divider"></li> <li><a href="#" ng-click="logout()">Logout</a></li> </ul>

الآن على جانب تطبيق Play ، لنقم بإنشاء وحدة تحكم جديدة Post مع طريقة addPost:

 // Post.java public class Post extends Controller { public static Result addPost() { Form<PostForm> postForm = Form.form(PostForm.class).bindFromRequest(); if (postForm.hasErrors()) { return badRequest(postForm.errorsAsJson()); } else { BlogPost newBlogPost = new BlogPost(); newBlogPost.commentCount = 0L; newBlogPost.subject = postForm.get().subject; newBlogPost.content = postForm.get().content; newBlogPost.user = getUser(); newBlogPost.save(); } return ok(Application.buildJsonResponse("success", "Post added successfully")); } private static User getUser() { return User.findByEmail(session().get("username")); } public static class PostForm { @Constraints.Required @Constraints.MaxLength(255) public String subject; @Constraints.Required public String content; } }

أضف مُدخلاً جديدًا إلى ملف المسارات لتتمكن من التعامل مع الطرق المضافة حديثًا في التوجيه:

 POST /app/post controllers.Post.addPost

في هذه المرحلة ، يجب أن تكون قادرًا على إضافة مشاركات جديدة.

إضافة مشاركات جديدة في Play

عرض المشاركات

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

 // Application.java public static Result getPosts() { return ok(Json.toJson(BlogPost.find.findList())); }

وتسجيله في ملف مساراتنا:

 GET /app/posts controllers.Application.getPosts

بعد ذلك ، في تطبيق AngularJS الخاص بنا ، نقوم بتعديل وحدة التحكم الرئيسية لدينا:

 // main.js angular.module('clientApp') .controller('MainCtrl', function ($scope, $http) { $scope.getPosts = function() { $http.get('app/posts') .success(function(data) { $scope.posts = data; }); }; $scope.getPosts(); });

أخيرًا ، أزل كل شيء من "main.html" وأضف هذا:

 <div class="panel panel-default" ng-repeat="post in posts"> <div class="panel-body"> <h4>{{ post.subject }}</h4> <p> {{ post.content }} </p> </div> <div class="panel-footer">Post by: {{ post.user.email }} | <a ng-href="/#/viewpost/{{ post.id }}">Comments <span class="badge">{{ post.commentCount }}</span></a></div> </div>

الآن إذا قمت بتحميل الصفحة الرئيسية للتطبيق الخاص بك ، فمن المفترض أن ترى شيئًا مشابهًا لهذا:

Angularjs Play تحميل المثال

من المحتمل أيضًا أن يكون لدينا طريقة عرض منفصلة للمشاركات الفردية.

 yo angular:controller viewpost yo angular:view viewpost
 // viewpost.js angular.module('clientApp') .controller('ViewpostCtrl', function ($scope, $http, alertService, userService, $location) { $scope.user = userService; $scope.params = $routeParams; $scope.postId = $scope.params.postId; $scope.viewPost = function() { $http.get('/app/post/' + $scope.postId) .error(function(data) { alertService.add('danger', data.error.message); }) .success(function(data) { $scope.post = data; }); }; $scope.viewPost(); });
 <!-- viewpost.html --> <div class="panel panel-default" ng-show="post"> <div class="panel-body"> <h4>{{ post.subject }}</h4> <p> {{ post.content }} </p> </div> <div class="panel-footer">Post by: {{ post.user.email }} | Comments <span class="badge">{{ post.commentCount }}</span></a></div> </div>

وطريق AngularJS:

 app.js: .when('/viewpost/:postId', { templateUrl: 'views/viewpost.html', controller: 'ViewpostCtrl' })

كما كان من قبل ، أضفنا طريقة جديدة إلى وحدة التحكم في التطبيق لدينا:

 // Application.java public static Result getPost(Long id) { BlogPost blogPost = BlogPost.findBlogPostById(id); if(blogPost == null) { return notFound(buildJsonResponse("error", "Post not found")); } return ok(Json.toJson(blogPost)); }

... وطريق جديد:

 GET /app/post/:id controllers.Application.getPost(id: Long)

الآن إذا انتقلت إلى http: // localhost: 9000 / # / viewpost / 1 ، فستتمكن من تحميل عرض لمنشور معين. بعد ذلك ، دعنا نضيف القدرة على رؤية مشاركات المستخدم في لوحة القيادة:

 // dashboard.js angular.module('clientApp') .controller('DashboardCtrl', function ($scope, $log, $http, alertService, $location) { $scope.loadPosts = function() { $http.get('/app/userposts') .error(function(data, status) { if(status === 401) { $location.path('/login'); } else { alertService.add('danger', data.error.message); } }) .success(function(data) { $scope.posts = data; }); }; $scope.loadPosts(); });
 <!-- dashboard.html --> <h4>My Posts</h4> <div ng-hide="posts.length">No posts yet. <a ng-href="/#/addpost">Add a post</a></div> <div class="panel panel-default" ng-repeat="post in posts"> <div class="panel-body"> <a ng-href="/#/viewpost/{{ post.id }}">{{ post.subject }}</a> | Comments <span class="badge">{{ post.commentCount }}</span> </div> </div>

أضف أيضًا طريقة جديدة إلى Post controller ، متبوعًا بمسار يتوافق مع هذه الطريقة:

 // Post.java public static Result getUserPosts() { User user = getUser(); if(user == null) { return badRequest(Application.buildJsonResponse("error", "No such user")); } return ok(Json.toJson(BlogPost.findBlogPostsByUser(user))); }
 GET /app/userposts controllers.Post.getUserPosts

الآن عند إنشاء منشورات ، سيتم إدراجها في لوحة المعلومات:

الوظائف الجديدة المدرجة على لوحة القيادة

وظيفة التعليق

لتنفيذ وظيفة التعليق ، سنبدأ بإضافة طريقة جديدة في Post controller:

 // Post.java public static Result addComment() { Form<CommentForm> commentForm = Form.form(CommentForm.class).bindFromRequest(); if (commentForm.hasErrors()) { return badRequest(commentForm.errorsAsJson()); } else { PostComment newComment = new PostComment(); BlogPost blogPost = BlogPost.findBlogPostById(commentForm.get().postId); blogPost.commentCount++; blogPost.save(); newComment.blogPost = blogPost; newComment.user = getUser(); newComment.content = commentForm.get().comment; newComment.save(); return ok(Application.buildJsonResponse("success", "Comment added successfully")); } } public static class CommentForm { @Constraints.Required public Long postId; @Constraints.Required public String comment; }

And as always, we need to register a new route for this method:

 POST /app/comment controllers.Post.addComment

In our AngularJS application, we add the following to “viewpost.js”:

 $scope.addComment = function() { var payload = { postId: $scope.postId, comment: $scope.comment }; $http.post('/app/comment', payload) .error(function(data, status) { if(status === 400) { angular.forEach(data, function(value, key) { if(key === 'comment') { alertService.add('danger', key + ' : ' + value); } else { alertService.add('danger', value.message); } }); } else if(status === 401) { $location.path('/login'); } else if(status === 500) { alertService.add('danger', 'Internal server error!'); } else { alertService.add('danger', data); } }) .success(function(data) { alertService.add('success', data.success.message); $scope.comment = ''; $scope.viewPost(); }); };

And finally add the following lines to “viewpost.html”:

 <div class="well" ng-repeat="comment in post.comments"> <span class="label label-default">By: {{ comment.user.email }}</span> <br/> {{ comment.content }} </div> <div ng-hide="user.username || !post"><h4><a ng-href="/#/login">Login</a> to comment</h4></div> <form name="addCommentForm" ng-submit="addComment()" novalidate ng-show="user.username"> <div><h4>Add comment</h4></div> <div> <label for="comment">Comment</label> <textarea name="comment" class="form-control" placeholder="Comment" ng-model="comment"></textarea> </div> <button type="submit" class="btn btn-primary">Add comment</button> </form>

Now if you open any post, you will be able to add and view comments.

View comments screenshot

What's Next?

In this tutorial, we have built an AngularJS blog with a Play application serving as a REST API back-end. Although the application lacks robust data validation (especially on the client side) and security, these topics were out of the scope of this tutorial. It was aiming to demonstrate one of the many possible ways of building an application of this kind. For convenience, the source code of this application has been uploaded to a GitHub repository.

If you find this combination of AngularJS and Play in web application development interesting, I highly recommend you review the following topics further:

  • AngularJS documentation
  • Play documentation
  • Recommended security approach in one page JS app with Play as back-end (contains example)
  • Secure REST API without OAuth
  • Ready Play authentication plug-in (might be not fully usable for single-page JavaScript applications, but can be used as a good example)