วันเสาร์ที่ 19 เมษายน พ.ศ. 2557

AngularJS + JSONP + Golang

เนื่องจากผมไม่ใช่คนเขียนเวปโดยธรรมชาติ ความรู้เรื่องเวปจึงน้อยมาก แต่ช่วงที่ผ่านมา เจองานที่ต้องใช้เวปในการนำเสนอข้อมูล จริงๆ คือทำ รายงานนั่นแหละ คือใช้ Crystal report ก็ได้แหละ แต่ปัญหาคือ ยิ่ง Crystal Report ยิ่งตายหยั๋งเขียด...

สุดท้ายบากหน้ามาหยุดที่ทำเวปให้ Query ข้อมูลแทน เนื่องจากโครงการที่ทำไว้นั้น server เป็น Golang RESTFul อยู่แล้ว (อ่านใน blog ก่อนหน้านี้นะครับ) เลยต่อยอดเลย

ผมเปิด port ใหม่
ไม่ใช่เรื่องอยากอะไรสำหรับ Golang ก็แค่ Go routine แต่ผมใช้  Sync ตามนี้

wg := &sync.WaitGroup{}
wg.Add(2)
go func() {
   server.StartREST()
   wg.Done()
}()
go func() {
   server.StartReport()
   wg.Done()
}()
wg.Wait()

ด้วย code ด้านบน ใน function StartREST() ก็จะไป ListenAndServe port 8080 ส่วน StartReport จะไป ListenAndServer port 8088 สำหรับ FileServer เลย

แค่นี้เราก็จะได้ server 2 ports มาใช้

ผมเลือก AngularJS มาทำ FrontEnd (ซ่า...ไม่เข้าเรื่อง) คือมันง่ายดีครับ เคยใช้ครั้งนึงตอนคิดจะทำ DropBox Clone (ตอนนั้นใช้ชื่อว่า DropBag แหะๆ เป็นงานทดลอง OpenResty + MongoDB's GridFS นะครับ) ผมเริ่ม code AngrulaJS ง่ายๆ เพื่อทดลองเรียก REST API ให้ส่ง JSON กลับมา Render พบว่า เกิดปัญหา Cross Domain Policy ขึ้น เนื่องจาก API ที่่เรียกอยู่อีก port ส่วนตัว Front-end ที่ใช้ Angular อยู่อีก port เพียงเท่านี้ก็ Cross Domain แล้วครับ ไม่ต้องถึงกับ คนละ IP

ประเด็นที่ทำให้ Blog นี้ขึ้นมา
ผมพยายามหาทางออกอยู่นาน ตอนแรกก็ย้าย Front end ไปไว้ที่ port เดียวกับ REST API ก็พบปัญหาเนื่องจาก ใช้ go-json-rest ของ Antoine แล้วต้องแก้ หลายอย่าง อีกทั้งยั้งมีพวก css, js ที่ต้องถูกเรียก load จาก browser อีกหลายไฟล์ ต้องแยกแบบเดิม แล้วใช้ JSONP

 รู้ว่า JSONP ใช้สำหรับแก้ปัญาเรื่อง CrossDomain หลายคนบอกไว้เช่นนั้น แต่ปัญหาคือ ผมไม่เคลียร์ ผมเริ่มหาข้อมูลว่า AngularJS  ใช้ JSONP ยังไง เรียก server แบบไหนยังไง สุดท้าย พบว่า มันต้อง Wrap JSON result ของเราด้วยชื่อ callback function ที่ front end ส่งไป แล้วส่งกลับมาในรูปแบบของ Javascript เช่น

http://192.168.1.1:8080/api/report.json?callback=jsonp_callback

ข้อมูลที่ส่งกลับจะต้องอยู่ในรูปแบบ

jsonp_callback([{"sequence":1,"data":"abc"},{"sequence":2,"data":"abc"}]);

ก่อนหน้านั้น REST API ของผมจะส่งข้อมูลกลับมาเป็น

[{"sequence":1,"data":"abc"},{"sequence":2,"data":"abc"}]

ทำไมต้องเป็นแบบนี้
มันคือการหลอก browser policy ว่ามันคือ javascript นะเว๊ย ไม่ใช่ data ไม่ผิด policy แล้วตัว framework ก็จะเรียกใช้ function jsonp_callback ซึ่งก็จะได้ข้อมูลที่ส่งจาก server มา

ได้ความเช่นนั้น ก็ลองค้นหาว่าพวกเรามีใครเคยเขียนๆ ไว้รึเปล่า พบว่ามี... คนถาม โดยคำตอบทั้งหมดคือ ไม่ต้องคำถาม ส่วนมากจะตอบว่าคือ JSON ซึ่งไม่ใช่

Implementation
เริ่มจาก server, Golang ใน function ที่ส่งค่า JSON กลับไปให้ Front-end ก่อนหน้าผมใช้ go-json-rest ด้วย

w.WriteJSON(&results) 

โดย results คือ array ของ struct

type result struct {
  Sequence int
  data string
}

w คือ *rest.Response

ผมเริ่มจาก Marshal results มาเป็น byte array (encoding) จากนั้น Query callback value จาก FormValue ที่ front end ส่งมา ซึ่งก็คือ callback value นั่นล่ะครับ แล้วเอามาต่อๆ กัน (concatenate) โดยทดลองแบบง่ายๆ ตามนี้

baJson, _ := json.Marshal(results)
strJson := r.Request.FormValue("callback")
strJson +=  "("
strJson += string(baJson)  // baJson เป็น byteArray
strJson += ");"
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Write([]byte(strJson)) 

ผมลัพท์ที่ได้ จะมี callback wrap data ของเรามาในรูปแบบ javascript function

ส่วน front-end ผมหน้าตาแบบนี้ครับ เป็น AngularJS แบบง่ายๆ

index.html
<html ng-app="MyReport">
  <head>
    <script src="http://code.angularjs.org/1.2.9/angular.min.js" ng:autobind></script>
    <script src="/assets/js/myreport.js"></script>
  </head>
  <body>
    <div ng:controller="MyCtrl" id="Ctrl">
      <button ng:click="calldata()">call data</button><br/>
      <h2 ng-show="data">Data from callback</h2>
      <pre>{{data}}</pre>    
    </div>
  </body>
</html>

myreport.js
angular.module('MyReport', []);

function MyCtrl($scope, $http) {
  var url = "http://192.168.1.1:8080?callback=jsonp_callback";

  $scope.calldata= function() {
    $http.jsonp(url).then(
      function(s) { $scope.success = JSON.stringify(s); }, 
        function(e) { $scope.error = JSON.stringify(e); } 
      );
  }
}

function jsonp_callback(data) {
    var el = document.getElementById('Ctrl');
    var scope = angular.element(el).scope();
    scope.$apply(function() {
        scope.data = JSON.stringify(data);
    });
}

อธิบายได้ว่า พอ user click ที่ปุ่ม call data จะไปเรียก function $scope.calldata (อยู่ใน AngularJS controller scope MyCtrl) ก็จะไปเรียก $http.jsonp ที่เป็น method ของ AngularJS ใน url ที่เราส่งไปจะมี formvalue callback กำหนดให้ server wrap data มาในชื่อ function ที่เรากำหนด กรณีนี้ใช้ชื่อว่า jsonp_callback

เมื่อ result ถูกส่งกลับมาที่ front-end, javascript จะทำการ execute callback function jsonp_callback ซึ่งจะต้องอยู่นอก Angular's controller scope คือต้องเป็น execute global scope นั่นเอง

ปัญหาเลยเกิดต่อมาว่า แล้วจะจับ  data ส่งกับไปที่ controller scope ของ Angular ยังไง ก็เลยต้องกำหนด ID ของ ng:controller ขึ้นมา ("Ctrl") แล้ว getElementById จากนั้นก็ query scope จาก method ที่ Angular มีให้จะได้ scope แล้วทำการ $apply data เข้าไปที่ scope element ของ Angular อีกครั้ง (ดู function jsonp_callback นะครับ)

นี่คือ JSONP ซึ่งมันคนละเรื่องกับ JSON นะครับ


วันอังคารที่ 15 เมษายน พ.ศ. 2557

Web server in 3 lines with Golang

ผมเริ่มเขียน Golang ด้วยโจทย์ที่ต้องเขียน RESTFul Service สำหรับ  Enterprise mobile application เชื่อมกับ MSSQL ที่มีอยู่แล้ว บน Windows เลยเลือก Golang เพราะสะดวกในการ deploy สุด

คำถามที่ทำให้ตัดสินใจในการเลือกใช้ Golang ตอนนั้นก็คือ ใช้อะไรก็ได้ ที่พอ build แล้วส่งไปให้ support copy ไปวางบน server เรียกโปรแกรมแล้วใช้ได้เลย

Golang นี่แหละครับ ทำมาแล้ว...

พอเขียน Rest service จบ ก็เริ่มมี Requirement เพิ่มมาว่า อยากได้ report ก็วางแผนว่าจะทำเป็น Web page นี่แหละ ให้เลือกตัวเลือกที่จะทำรายงาน แล้วส่งไป query ผ่าน REST API ที่มีอยู่แล้ว ออกมาแสดงผล

ปัญหาคือจะเอาอะไรมาทำ file server?

ถ้าเลือก NGINX หรือ Apache เราคงต้องเป็นคนไปติดตั้งให้แน่ๆ Support เอาไม่อยู่

Martini
Golang web application framework ที่พึ่งเกิดและได้รับความนิยมอย่างรวดเร็จ น่าสนใจตรง Martini บอกว่า สามารถ support static file out of the box เลย คือเพิ่มไฟล์ใหม่เข้าไปปุ๊ป ก็เรียกผ่าน http ได้เลย --- น่าสนใจ ---

แต่งานที่ทำอยู่ใช้  go-rest-json ของ Antonie กับ default SQL framework ของ Go เอง แค่นี้ก็ทำงานได้ดีอยู่แล้ว และด้วยความฝักไฝ่ใน Benchmark, Go pure นี่ทิ้ง Revel ขาดเลยนะครับ แล้ว Martini ทำความเร็วสู้ Gorlilla ไม่ได้ด้วย เลยสองจิตสองใจที่จะใช้ Martini

ตัดสินใจเขียนเอง
ต้องยอมรับว่า เอาเข้าจริงๆ ที่ผ่านมาผม focus ที่ project มากจนไม่ได้ดู net/http ว่าทำอะไรได้ขนาดไหน วันนี้เลยไล่ดูหน่อยพบว่า

จริงๆ แล้ว net/http นี่มันสมบูรณ์แบบนะครับ ที่ว่ากันว่า ไม่ต้องใช้ external framework นี่เรื่องจริง

เลยลองเขียน static file server ออกมาดูได้ตามนี้

package main
import "net/http"
func main()  { http.ListenAndServe(":8080", http.FileServer(http.Dir("/Users/McDuck/Home"))) }