การสร้างเว็บแอปพลิเคชันสมัยใหม่ด้วย AngularJS และ Play Framework
เผยแพร่แล้ว: 2022-03-11การเลือกเครื่องมือที่เหมาะสมเพื่อวัตถุประสงค์ที่ถูกต้องนั้นไปได้ไกล โดยเฉพาะอย่างยิ่งเมื่อพูดถึงการสร้างเว็บแอปพลิเคชันที่ทันสมัย พวกเราหลายคนคุ้นเคยกับ AngularJS และการพัฒนาเว็บแอปพลิเคชันฟรอนต์เอนด์นั้นง่ายเพียงใด แม้ว่าหลายคนจะโต้แย้งกับการใช้เฟรมเวิร์กเว็บยอดนิยมนี้ แต่ก็มีมากมายที่จะนำเสนอและเป็นทางเลือกที่เหมาะสมสำหรับความต้องการที่หลากหลาย ในทางกลับกัน ส่วนประกอบที่คุณใช้ในส่วนแบ็คเอนด์จะเป็นตัวกำหนดประสิทธิภาพของเว็บแอปพลิเคชันอย่างมาก เนื่องจากองค์ประกอบเหล่านี้มีอิทธิพลต่อประสบการณ์การใช้งานโดยรวมของผู้ใช้ Play เป็นเว็บเฟรมเวิร์กความเร็วสูงสำหรับ Java และ Scala มันใช้สถาปัตยกรรมน้ำหนักเบา ไร้สัญชาติ และเป็นมิตรกับเว็บ และเป็นไปตามรูปแบบและหลักการของ MVC ที่คล้ายกับ Rails และ Django
ในบทความนี้ เราจะมาดูวิธีที่เราสามารถใช้ AngularJS และ Play เพื่อสร้างแอปพลิเคชันบล็อกอย่างง่ายด้วยกลไกการตรวจสอบสิทธิ์พื้นฐานและความสามารถในการโพสต์และแสดงความคิดเห็น การพัฒนา AngularJS ด้วยสารพัด Twitter Bootstrap จะช่วยให้เราสามารถขับเคลื่อนประสบการณ์แอปพลิเคชันหน้าเดียวบนส่วนหลัง REST API ของ Play
เว็บแอปพลิเคชัน - เริ่มต้นใช้งาน
โครงกระดูกแอปพลิเคชัน AngularJS
แอพ AngularJS และ Play จะอยู่ในไดเร็กทอรีไคลเอนต์และเซิร์ฟเวอร์ตามลำดับ สำหรับตอนนี้ เราจะสร้างไดเร็กทอรี "ไคลเอนต์"
mkdir -p blogapp/client
ในการสร้างโครงร่างแอปพลิเคชัน AngularJS เราจะใช้ Yeoman - เครื่องมือนั่งร้านที่น่าทึ่ง การติดตั้ง Yeoman เป็นเรื่องง่าย การใช้แอปพลิเคชัน AngularJS โครงกระดูกอย่างง่ายเพื่อนั่งร้านน่าจะง่ายยิ่งขึ้น:
cd blogapp/client yo angular
การเรียกใช้คำสั่งที่สองจะตามด้วยตัวเลือกสองสามตัวที่คุณต้องเลือก สำหรับโครงการนี้ เราไม่ต้องการ "Sass (พร้อมเข็มทิศ)" เราจะต้องใช้ Boostrap พร้อมกับปลั๊กอิน AngularJS ต่อไปนี้:
- angular-animate.js
- angular-cookies.js
- angular-resource.js
- angular-route.js
- angular-sanitize.js
- angular-touch.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" หรือ "idealize" แอปพลิเคชันของคุณ โดยเรียกใช้คำสั่งต่อไปนี้:
activator
เมื่อคุณเห็นข้อความแจ้งใหม่ ให้พิมพ์ "eclipse" หรือ "idea" แล้วกด Enter เพื่อเตรียมรหัสแอปพลิเคชันสำหรับ Eclipse หรือ IntelliJ ตามลำดับ
เพื่อความกระชับ เราจะครอบคลุมเฉพาะกระบวนการนำเข้าโครงการไปยัง IntelliJ ในบทความนี้ ขั้นตอนการนำเข้าไปยัง Eclipse ควรจะง่ายพอๆ กัน หากต้องการนำเข้าโปรเจ็กต์ไปยัง IntelliJ ให้เริ่มต้นด้วยการเปิดใช้งานตัวเลือก “โครงการจากแหล่งที่มีอยู่…” ซึ่งอยู่ใต้ “ไฟล์ -> ใหม่” จากนั้นเลือกไฟล์ build.sbt ของคุณและคลิก "ตกลง" เมื่อคลิก "ตกลง" อีกครั้งในกล่องโต้ตอบถัดไป IntelliJ ควรเริ่มนำเข้าแอปพลิเคชัน Play ของคุณเป็นโครงการ SBT
Typesafe Activator ยังมาพร้อมกับอินเทอร์เฟซผู้ใช้แบบกราฟิก ซึ่งคุณสามารถใช้สร้างรหัสแอปพลิเคชันโครงกระดูกนี้ได้
ตอนนี้เราได้นำเข้าแอปพลิเคชัน Play ของเราไปยัง IntelliJ แล้ว เราควรนำเข้าแอปพลิเคชัน AngularJS ของเราไปยังพื้นที่ทำงานด้วย เราสามารถนำเข้าเป็นโครงการแยกต่างหากหรือเป็นโมดูลไปยังโครงการที่มีอยู่ซึ่งมีแอปพลิเคชัน Play อยู่
ที่นี่ เราจะนำเข้าแอปพลิเคชันเชิงมุมเป็นโมดูล ภายใต้เมนู "ไฟล์" เราจะเลือกตัวเลือก "ใหม่ -> โมดูลจากแหล่งที่มีอยู่ ... " จากกล่องโต้ตอบ เราจะเลือกไดเร็กทอรี "ไคลเอนต์" และคลิกที่ "ตกลง" ในสองหน้าจอถัดไป ให้คลิกที่ "ถัดไป" และ "เสร็จสิ้น" ตามลำดับ
วางไข่เซิร์ฟเวอร์ท้องถิ่น
ณ จุดนี้ ควรเริ่มต้นแอปพลิเคชัน AngularJS เป็นงาน Grunt จาก IDE ขยายโฟลเดอร์ไคลเอนต์ของคุณและคลิกขวาที่ Gruntfile.js ในเมนูป๊อปอัปให้เลือก "แสดงงาน Grunt" แผงชื่อ "Grunt" จะปรากฏขึ้นพร้อมกับรายการงาน:
ในการเริ่มให้บริการแอปพลิเคชัน ให้ดับเบิลคลิกที่ “ให้บริการ” สิ่งนี้ควรเปิดเว็บเบราว์เซอร์เริ่มต้นของคุณทันทีและชี้ไปที่ที่อยู่ localhost คุณควรเห็นหน้า Stub AngularJS ที่มีโลโก้ของ Yeoman
ต่อไป เราต้องเปิดแอปพลิเคชันเซิร์ฟเวอร์ส่วนหลังของเรา ก่อนที่เราจะดำเนินการต่อ เราต้องแก้ไขปัญหาสองสามข้อ:
- โดยค่าเริ่มต้น ทั้งแอปพลิเคชัน AngularJS (บูตสแตรปโดย Yeoman) และแอปพลิเคชัน Play จะพยายามทำงานบนพอร์ต 9000
- ในการผลิต ทั้งสองแอปพลิเคชันมีแนวโน้มที่จะทำงานภายใต้โดเมนเดียว และเราอาจใช้ 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 ของเรา ในไฟล์นั้น ค้นหางาน "เชื่อมต่อ" และใส่คีย์/ค่า "พร็อกซี่" ตามหลัง:
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/*" ไปยังแอปพลิเคชัน Back-end 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; } } },
สุดท้าย เราต้องเพิ่มการพึ่งพาใหม่ “'configureProxies: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
ต่อไป เราควรอัปเดตการกำหนดเส้นทางของแอปพลิเคชันของเราเพื่ออ้างอิงมุมมองที่สร้างขึ้นใหม่นี้ และลบตัวควบคุมและมุมมอง "เกี่ยวกับ" ที่สร้างซ้ำซ้อนโดยอัตโนมัติ จากภายในไฟล์ “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 สลับไปที่แท็บ "เครือข่าย" แล้วลองส่งแบบฟอร์มลงชื่อสมัครใช้
เราจะเห็นว่าส่วนแบ็คเอนด์ของ Play จะตอบกลับด้วยหน้าข้อผิดพลาด "ไม่พบการดำเนินการ" คาดว่าจะเป็นเช่นนี้เนื่องจากยังไม่ได้ดำเนินการ แต่ความหมายก็คือการตั้งค่าพร็อกซี Grunt ของเราทำงานอย่างถูกต้อง!
ต่อไป เราจะเพิ่ม "การดำเนินการ" ซึ่งเป็นวิธีการในตัวควบคุมแอปพลิเคชัน Play ในคลาส “Application” ในแพ็คเกจ “app/controllers” ให้เพิ่มวิธีการใหม่ “signup”:
public static Result signup() { return ok("Success!"); }
ตอนนี้เปิดไฟล์ "conf/routes" และเพิ่มบรรทัดต่อไปนี้:
POST /app/signup controllers.Application.signup
สุดท้าย เรากลับไปที่เว็บเบราว์เซอร์ของเรา http://localhost:9000/#/signup การคลิกที่ปุ่ม "ส่ง" ในครั้งนี้จะทำให้ได้สิ่งที่แตกต่างออกไป:
คุณควรเห็นค่าฮาร์ดโค้ดที่ส่งคืน ซึ่งเป็นค่าที่เราเขียนไว้ในวิธีการสมัครใช้งาน หากเป็นกรณีนี้ เราก็พร้อมที่จะก้าวต่อไปเนื่องจากสภาพแวดล้อมการพัฒนาของเราพร้อมและกำลังทำงานสำหรับแอปพลิเคชันทั้ง 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
โมเดลโดเมนบล็อกของเราค่อนข้างเรียบง่าย ก่อนอื่น เรามีผู้ใช้ที่อาจสร้างโพสต์ จากนั้นผู้ใช้ที่ลงชื่อเข้าใช้สามารถแสดงความคิดเห็นแต่ละโพสต์ได้ มาสร้างแบบจำลองอีบีนของเรากันเถอะ
ผู้ใช้
// 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>
สุดท้าย เราต้องอัปเดตตัวควบคุมการลงชื่อสมัครใช้:
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
ณ จุดนี้ คุณควรจะเพิ่มโพสต์ใหม่ได้
กำลังแสดงกระทู้
การเพิ่มข้อความมีค่าเพียงเล็กน้อย หากเราไม่สามารถแสดงได้ เราต้องการทำรายการโพสต์ทั้งหมดบนหน้าหลัก เราเริ่มต้นด้วยการเพิ่มวิธีการใหม่ในตัวควบคุมแอปพลิเคชันของเรา:
// 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>
ตอนนี้ ถ้าคุณโหลดหน้าแรกของแอปพลิเคชัน คุณควรเห็นสิ่งที่คล้ายกันนี้:
เราน่าจะมีมุมมองแยกกันสำหรับโพสต์แต่ละรายการ
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.
อะไรต่อไป?
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)