Scala.js basic tutorial
Với step-by-step tutorial chúng ta sẽ bắt đầu với việc setup Scala.js sbt project và kết thúc với việc tương tác với user và viết unit testing. Code được viết trong tutorial này cũng được public trên GitHub với mỗi step là 1 commit, các bạn có thể tham khảo thêm ở repo: scalajs-tutorial Để đi ...
Với step-by-step tutorial chúng ta sẽ bắt đầu với việc setup Scala.js sbt project và kết thúc với việc tương tác với user và viết unit testing. Code được viết trong tutorial này cũng được public trên GitHub với mỗi step là 1 commit, các bạn có thể tham khảo thêm ở repo: scalajs-tutorial
Để đi được hết tutorial này, bạn cần download và cài đặt sbt(version >= 0.13.0), lưu ý là bạn chỉ cần đọc phần hướng dẫn cài đặt là đủ dùng để follow tutorial này. Bạn cũng cần download và cài đặt Node.js
Đầu tiên bạn tạo một folder chứa sbt project.
sbt Setup
Để setup Scala.js trong sbt project, chúng ta cần làm 2 việc:
- Thêm Scala.js sbt plugin vào build
- Enable plugin trong project
Thêm Scala.js sbt plugin như một one-liner trong project/plugins.sbt (tất cả file names chúng ta viết trong tutorial có liên quan tới project root):
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.14")
Chúng ta cũng setup basic project settings và enable plugin trong sbt build file(build.sbt trong project root directory):
enablePlugins(ScalaJSPlugin) name := "Scala.js Tutorial" scalaVersion := "2.12.0" // or any other Scala version >= 2.10.2
Cuối cùng, chúng ta cần project/build.properties để xác định sbt version(>= 0.13.7)
sbt.version=0.13.13
Đó là tất cả những thứ chúng ta cần để configure build
Tại thời điểm này nếu bạn thích sử dụng IDE là Eclipse hay IDEA, bạn phải dùng sbteclipse để generate một Eclipse project hoặc import sbt build từ IDEA.Lưu ý rằng để compiling và running app của bạn, bạn sẽ cần sử dụng sbt từ command line.
HelloWorld application
Quá quen thuộc phải không ạ, ứng dụng mà chúng ta nghe tên hoài không chán.Để bắt đầu chúng ta sẽ add một thư mục TutorialApp vào trong package tutorial.webapp.Chúng ta tạo một file src/main/scala/tutorial/webapp/TutorialApp.scala:
package tutorial.webapp import scala.scalajs.js.JSApp object TutorialApp extends JSApp { def main(): Unit = { println("Hello world!") } }
Giống như chúng ta mong đợi, kết quả sẽ in ra “HelloWorld” khi run chương trình.Chúng ra thực hiện run như sau:
$ sbt > run [info] Compiling 1 Scala source to (...)/scala-js-tutorial/target/scala-2.12/classes... [info] Fast optimizing (...)/scalajs-tutorial/target/scala-2.12/scala-js-tutorial-fastopt.js [info] Running tutorial.webapp.TutorialApp Hello world! [success] (…).
Đơn giản phải không ạ, chúng ta đã compiled và run thành công app Scala.js đầu tiên.Thực ra code đã được run bởi một trình thông dịch JavaScript: Nếu bạn không tin là vậy (thỉnh thoảng chúng ta hay như thế =))) bạn có thể sử dụng last command trong sbt:
> last (…) [info] Running tutorial.webapp.TutorialApp [debug] with JSEnv ExternalJSEnv for Node.js [debug] Starting process: node [success] (…)
Và như thế thì hiện tại code của bạn đã được thực thi bởi Node.js
Source maps in Node.js: Để có được stack traces trên Node.js, bạn cần cài đặt source-map-support pakage.
npm install source-map-support
Như vậy chúng ta đang có một JavaScript app đơn giản, chúng ta sẽ sử dụng nó trong HTML page.Để làm được việc đó, chúng ta cần 2 step:
- Generate một JavaScript file bên ngoài compiled code
- Create một HTML page include file đó và gọi application
Generate JavaScript
Để generate một JavaScript file sử dụng sbt phải sử dụng fastOptJS:
> fastOptJS [info] Fast optimizing (...)/scala-js-tutorial/target/scala-2.12/scala-js-tutorial-fastopt.js [success] (…)
Nó sẽ optimize và generate file target/scala-2.12/scala-js-tutorial-fastopt.js bao gồm JavaScript code. (Có thể [info] sẽ không xuất hiện nếu đã run chương trình và chương trình không có sự thay đổi).
Create the HTML Page
Để load phần JavaScript đã create, bạn sẽ cần một HTML file, Create file scalajs-tutorial-fastopt.html (hoặc bất cứ tên nào mà bạn thích ví dụ như index-dev.html) trong project root với content dưới đây.Chúng ta sẽ đi vào chi tiết ngay sau đây:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>The Scala.js Tutorial</title> </head> <body> <!-- Include Scala.js compiled code --> <script type="text/javascript" src="./target/scala-2.12/scala-js-tutorial-fastopt.js"></script> <!-- Run tutorial.webapp.TutorialApp --> <script type="text/javascript"> tutorial.webapp.TutorialApp().main(); </script> </body> </html>
Script tag đầu tiên includes generate code(chú ý rằng bạn phải theo Scala version 2.12 đến 2.10 hoặc 2.11, ở đây nếu bạn sử dụng Scala 2.10.x hoặc 2.11.x thay vì 2.12.x ). Nếu script tag thứ 2, chúng ta sẽ get TutorialApp object.Chú ý rằng (): TutorialApp là một function trong JavaScript, object initialization code cần chạy trên lần đầu truy cập vào object.Sau đó chúng ta có thể gọi main method một cách đơn giản trên TutorialApp object. Vì TutorialApp extends JSApp, chính object và main method được tự động tạo JavaScript.Điều đó không đúng một cách toàn bộ.Tiếp tục đọc tutorial hoặc thao khảo chi tiết Export Scala.js API to JavaScript. Nếu bạn mở trang HTML mới nhất trên trình duyệt yêu thích của mình, bạn sẽ không nhìn thấy gì cả.Println trong main method đi thẳng đến JavaScript console, nơi mà không được show mặc định trong browser.Tuy nhiên, nếu bạn mở JavaScript console bạn sẽ nhìn thấy HelloWorld message.
Như ở step trước đã show, running JavaScript trong một trang HTML không có gì đặc biệt hữu dụng nếu bạn không tương tác với page.Đó chính là ý nghĩa của DOM API.
Adding the DOM Library
Để sử dụng được DOM, tốt nhất là statically typed Scala.js.Để add nó vào sbt project, add dòng dưới đây vào build.sbt của bạn:
libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "0.9.1"
sbt-savvy folk sẽ chú ý %%% thay vì %%. Điều đó có nghĩa chúng ta sử dụng một Scala.js library và không phải là Scala library thường.Hãy tham khảo Dependencies guide để xem thêm chi tiết.Nhớ reload build file nếu sbt đã running.
> reload [info] Loading global plugins from (...) [info] Loading project definition from (...)/scala-js-tutorial/project [info] Set current project to Scala.js Tutorial (in build (...)/scala-js-tutorial/)
Nếu bạn đang sử dụng một IDE plugin, bạn cũng sẽ phải regenerate project file để autocompletion hoạt động.
Using the DOM Library
Bây giờ chúng ta sẽ add DOM library, hãy phỏng theo ví dụ HelloWorld của chúng ta để add một thẻ <p> vào body của trang, tốt hơn là việc in trong console. Trước tiên, chúng ta sẽ import những thư viện sau:
import org.scalajs.dom import dom.document
dom là root của JavaScript DOM và tương ứng với global scope của JavaScript.Chúng ta cũng add thêm document(tương tứng với document trong JavaScript) cho tiện lợi.
Và bây giờ chúng ta tạo một method cho phép chúng ta append một thẻ <p> với một text cho một node:
def appendPar(targetNode: dom.Node, text: String): Unit = { val parNode = document.createElement("p") val textNode = document.createTextNode(text) parNode.appendChild(textNode) targetNode.appendChild(parNode) }
Thay thế việc gọi println bằng cách gọi appendPar trong main method:
def main(): Unit = { appendPar(document.body, "Hello World") }
Rebuild the JavaScript
Để rebuild JavaScript, rất đơn giản chúng ta gọi lại fastOptJS:
> fastOptJS [info] Compiling 1 Scala source to (...)/scala-js-tutorial/target/scala-2.12/classes... [info] Fast optimizing (...)/scala-js-tutorial/target/scala-2.12/scala-js-tutorial-fastopt.js [success] (…).
Như bạn thấy ở log, sbt tự động phát hiện vì vậy sources phải được recompliled trước khi fast optimizing.
Bây giờ bạn có thể reload lại HTML trên trình duyệt và bạn có thể nhìn thấy dòng message xinh xắn “Hello World”.
Gọi lại fastOptJS mỗi lần bạn thay đổi file source code là cũng hơi vướng, may mắn thay sbt có khả năng quan sát files của bạn và recompiled nếu cần.
> ~fastOptJS [success] (...) 1. Waiting for source changes... (press enter to interrupt)
Từ thời điểm này trong tutorial chúng ta sẽ gỉả dụ bạn có một sbt với command running, vì vậy chúng ta không cần lo ngại đến việc rebuilding mỗi lần.
Step này cho chúng ta thấy cách để add một button và phản ứng với các sự kiện trên nó bằng cách sử dụng DOM(Chúng ta sẽ sử dụng jQuery ở bước sau).Chúng ta muốn add một button mà sẽ add một thẻ <p> khác vào body khi nó được clicked. Chúng ta bắt đầu bằng việc add một method vào TutorialApp, cái mà sẽ được gọi khi click button.
@JSExport def addClickedMessage(): Unit = { appendPar(document.body, "You clicked the button!") }
Bạn sẽ phải chú ý đến chú thích @JSExport.Nó cho chúng ta biết rằng Scala.js compiler để cho phép medthod có thể được gọi từ JavaScript.Chúng ta cũng phải import chú thích sau:
import scala.scalajs.js.annotation.JSExport
Để tìm hiểu nhiều hơn về việc làm thế nào để call Scala.js method từ JavaScript, xem thêm trong Export Scala.js API to JavaScript guide. Vì hiện tại chúng ta có một method được gọi từ JavaScript, tất cả những gì chúng ta phải làm là add một button vào HTML của chúng ta và set cho chúng onclick attribute(chắc chắn rằng ta đã add button trước <script> tag):
<button id="click-me-button" type="button" onclick="tutorial.webapp.TutorialApp().addClickedMessage()">Click me!</button>
Reload trang HTML của bạn(nhớ rằng sbt compiles code của bạn một cách tự động) và try click button.Nó sẽ add một đoạn text có nội dung sau: “You clicked the button!” với mỗi lần bạn click vào nó.
Mở rộng ra một chút web application có một xu hướng để setup phản hồi tới event trong JavaScript chứ không phải là xác định các thuộc tính. Chúng ta sẽ thay đổi app hiện tại của chúng ta để sử dụng mô hình này với sự giúp đỡ của jQuery.Chúng ta cũng thay thế tất cả những thứ đang sử dụng DOM API bằng jQuery
Depending on jQuery
Cũng giống như đối với DOM, có một thư viện cho jQuery tồn tại trong Scala.js.Thay thế dòng libraryDependencies += .. trong build.sbt của bạn bằng:
libraryDependencies += "be.doeraene" %%% "scalajs-jquery" % "0.9.1"
Vì chúng ta sẽ không trực tiếp sử dụng DOM, chúng ta không cần các thư viện cũ nữa.Lưu ý rằng thư viện jQuery phụ thuộc vào DOM, nhưng chúng ta không phải quan tâm về điều này, sbt sẽ tự động chăm sóc nó. Đừng quên reload sbt configuration lúc này nhé:
- Nhấn enter để bỏ qua ~fastOptJS command
- Gõ reload
- Start ~fastOptJS lần nữa.
Một lần nữa chắc chắn rằng bạn đã update IDE project của bạn nếu bạn đang sử dụng plugin.
Using jQuery
Trong TutorialApp.scala, Remove imports cho DOM và add import cho jQuery:
import org.scalajs.jquery.jQuery
Nó cho phép bạn dễ dàng truy cập jQuery object(thường được gọi là $ trong JavaScript) trong code của bạn. Bây giờ chúng ta có thể remove appendPar và thay thế tất cả lời gọi nó bằng cách đơn giản sau:
jQuery("body").append("<p>[message]</p>")
[message] ở đâu thì đó chính là string được passed tới appendPar. Nếu bạn cố gắng reload webpage bây giờ, nó sẽ không hoạt động(thường thì một TypeError được báo cáo ở trong console).Vấn đề là chúng ta không included chính jQuery library.
Adding JavaScript libraries
Một lựa chọn để include jquery.js từ nguồn bên ngoài ví dụ như jsDelivr.
<script type="text/javascript" src="http://cdn.jsdelivr.net/jquery/2.1.1/jquery.js"></script>
Điều này rất dễ trở nên cồng kềnh, nếu bạn dựa trên nhiều libraries.Scala.js sbt plugin cung cấp một cơ chế cho thư viện từ khai báo thư viện JavaScript mà chúng dựa trên và gói chúng lại thành một file.Tất cả những gì bạn phải làm là active chúng, và sau đó include file. Trong build.sbt của bạn, set:
skip in packageJSDependencies := false jsDependencies += "org.webjars" % "jquery" % "2.1.4" / "2.1.4/jquery.js"
Sau khi reloading và rerunning fastOptJS, nó sẽ create scala-js-tutorial-jsdeps.js bao gồm tất cả các thư viện JavaScript bên cạnh main JavaScript file.Chúng ta có thể include file đó một cách đơn giản và không cần lo lắng về JavaScript libraries nữa:
<!-- Include JavaScript dependencies → <script type="text/javascript" src="./target/scala-2.12/scala-js-tutorial-jsdeps.js"></script>
Setup UI in Scala.js
Chúng ta vẫn muốn được thoát khỏi onclick attribute của <button>.Sau khi remove attribute, chúng ta add setupUI method, Trong đó chúng ta sử dụng JQuery để add một event handler vào button.Chúng ta cũng move “Hello World” message vào trong function đó.
def setupUI(): Unit = { jQuery("#click-me-button").click(addClickedMessage _) jQuery("body").append("<p>Hello World</p>") }
Vì chúng ta không gọi addClickedMessage từ JavaScript nữa, chúng ta có thể remove chú thích @JSExport (và import tương ứng). Cuối cùng chúng ta add lời gọi cuối cùng tới jQuery trong main method, để thực hiện setupUI, DOM chỉ được load một lần:
def main(): Unit = { jQuery(setupUI _) }
Thêm nữa, chúng ta không gọi trực tiếp setupUI từ JavaScript, chúng ta không cần export nó(mặc dù jQuery sẽ gọi nó).
Bây giờ chúng ta có một app có giao diện người dùng được thiếp lập hoàn toàn từ bên trong Scala.js.Bước tiếp theo sẽ giúp chúng ta test ứng dụng.
Ở trong section này chúng ta cùng thảo luận cách để test một ứng dụng sử dụng uTest , một testing framework rất nhỏ mà nó compiles thành cả Scala.js và Scala.JVM.Như phần lưu ý bên trên, framework này cũng là một sự lựa chọn tốt để test libraries có được biên dịch.Thao khảo thêm cross compilation guide
Supporting the DOM
Trước khi chúng ta bắt đầu viết test, cái mà chúng ta có thể chạy qua sbt console, Đầu tiên chúng ta phải giải quyết một vấn đề khác.Nhớ task run, nếu bạn thử invoke nó ngay bây giờ, bạn sẽ nhìn thấy những thứ sau:
> run [info] Running tutorial.webapp.TutorialApp [error] TypeError: (0 , $m_Lorg_scalajs_jquery_package$(...).jQuery$1) is not a function [error] at $c_Ltutorial_webapp_TutorialApp$.main__V (.../scalajs-tutorial/src/main/scala/tutorial/webapp/TutorialApp.scala:9:12) [error] at $c_Ltutorial_webapp_TutorialApp$.$$js$exported$meth$main__O (https:/raw.githubusercontent.com/scala-js/scala-js/v0.6.13/library/src/main/scala/scala/scalajs/js/JSApp.scala:18:4) [error] at $c_Ltutorial_webapp_TutorialApp$.main (.../scalajs-tutorial/src/main/scala/tutorial/webapp/TutorialApp.scala:7:8) [error] ... [trace] Stack trace suppressed: run last compile:run for the full output. [error] (compile:run) org.scalajs.jsenv.ExternalJSEnv$NonZeroExitException: Node.js exited with code 1 [error] Total time: 1 s, completed Oct 13, 2016 3:06:00 PM
Điều cơ bản ở đây là jQuery(được tự động include bởi jsDependencies) không thể load đúng, bởi vì không có DOM tồn tại ở trong Node.js.Để khiến DOM tồn tại, add dòng dưới đây vào build.sbt:
jsDependencies += RuntimeDOM
Nó sẽ sử dụng thư viện jsdom để mô phỏng một DOM trong Node.js.Lưu ý rằng bạn cần cài đặt riêng nó sử dụng:
$ npm install jsdom
Sau khi reload, bạn có thể invoke run thành công:
> run [info] Running tutorial.webapp.TutorialApp [success] (…)
Giống như các thư viện dependencies khác, jsDependencies += RuntimeDOM áp dụng transitively: nếu bạn dựa trên thư viện dựa trên DOM, sau đó dựa trên DOM là tốt nhất.
Ngoài Node.js với jsdom, bạn có thể sử dụng PhantomJS hoặc Selenium . Bạn có thể tìm ra thông tin về nó ở trong tài liệu documentation about JavaScript environments
Adding uTest
Sử dụng testing framework trên Scala.js không khác lắm với trên JVM.Nó thường dùng tới 2 settings trong build.sbt file.Cho uTest thì có:
libraryDependencies += "com.lihaoyi" %%% "utest" % "0.4.4" % "test" testFrameworks += new TestFramework("utest.runner.Framework")
Bây giờ chúng ta đã sẵn sàng add thêm testsuite đơn giản đầu tiên (src/test/scala/tutorial/webapp/TutorialTest.scala):
package tutorial.webapp import utest._ import org.scalajs.jquery.jQuery object TutorialTest extends TestSuite { // Initialize App TutorialApp.setupUI() def tests = TestSuite { 'HelloWorld { assert(jQuery("p:contains('Hello World')").length == 1) } } }
Đoạn test trên sử dụng jQuery để kiểm chứng xem trang của chúng ta có bao gồm chính xác một phần tử <p> mà nó bao chứa text “Hello World” sau khi UI được setup.
Để run test trên, đơn giản như sau:
> test [info] Compiling 1 Scala source to (...)/scalajs-tutorial/target/scala-2.12/test-classes... [info] Fast optimizing (...)/scalajs-tutorial/target/scala-2.12/scala-js-tutorial-test-fastopt.js [info] ------------------Starting Suite tutorial.webapp.TutorialTest------------------ [info] tutorial.webapp.TutorialTest.HelloWorld Success [info] tutorial.webapp.TutorialTest Success [info] -----------------------------------Results----------------------------------- [info] tutorial.webapp.TutorialTest Success [info] HelloWorld Success [info] [info] Tests: 2 [info] Passed: 2 [info] Failed: 0 [success] (…)
Như vậy chúng ta đã tạo thành công một test đơn giản.Giống như run, 'test' sử dụng Node.js để thực thi test của bạn.
A more complex test
Chúng ta cũng muốn test function của button.Để làm được điều đó chúng ta đối mặt với vấn đề nhỏ khác: button không tồn tại khi testing, vì test bắt đầu với một cây DOM rỗng.Để giải quyết vấn đề đó, chúng ta tạo button trong setupUI method và remove nó từ HTML:
jQuery("""<button type="button">Click me!</button>""") .click(addClickedMessage _) .appendTo(jQuery("body")) Nó mang lại những lợi thế đầy bất ngờ khác: chúng ta không cần đưa cho nó một ID nữa nhưng trực tiếp sử dụng jQuery object để cài đặt onclick handler. Bây giờ chúng ta khai báo ButtonClick test phải ở dưới HelloWorld test: 'ButtonClick { def messageCount = jQuery("p:contains('You clicked the button!')").length val button = jQuery("button:contains('Click me!')") assert(button.length == 1) assert(messageCount == 0) for (c <- 1 to 5) { button.click() assert(messageCount == c) } }
Sau khi khai báo một helper method để đếm số messages, chúng ta lấy lại button từ DOM và kiểm chứng chính xác chúng ta có một button và không có messages nào.Trong vòng lặp, chúng ta mô phỏng một click trên button và kiểm chứng số lượng messages được tăng lên.
Bạn có thể gọi lại test task một lần nữa:
> test [info] Compiling 1 Scala source to (...)/scalajs-tutorial/target/scala-2.12/test-classes... [info] Fast optimizing (...)/scalajs-tutorial/target/scala-2.12/scala-js-tutorial-test-fastopt.js [info] ------------------Starting Suite tutorial.webapp.TutorialTest------------------ [info] tutorial.webapp.TutorialTest.HelloWorld Success [info] tutorial.webapp.TutorialTest.ButtonClick Success [info] tutorial.webapp.TutorialTest Success [info] -----------------------------------Results----------------------------------- [info] tutorial.webapp.TutorialTest Success [info] HelloWorld Success [info] ButtonClick Success [info] [info] Tests: 3 [info] Passed: 3 [info] Failed: 0 [success] (…)
Và như vậy chúng ta hoàn thành phần testing của tutorial này
Ở đây chúng ta nói tới 2 vấn đề bạn phải làm khi triển khai ứng dụng trên production
Full Optimization
Size là rất quan trọng cho JavaScript code trên nền web.Để nén compiled code nhiều hơn nữa, Scala.js sbt plugin sử dụng sự tối ưu nâng cao của Google Closure Compiler.Để run full optimizations, đơn giản chúng ta sử dụng fullOptJS task:
> fullOptJS [info] Full optimizing (...)/scala-js-tutorial/target/scala-2.12/scala-js-tutorial-opt.js [info] Closure: 0 error(s), 0 warning(s) [success] (…)
Lưu ý nó có thể sẽ tốn thời gian trong một dự án lớn hơn(hàng chục giây).Đó là lí do tại sao chúng ta không sử dụng fullOptJS trong khi phát triển và thay bằng fastOptJS.Nếu bạn muốn run và `test full-optimized version từ sbt, bạn cần thay đổi stage sử dụng sbt setting sau đây:
> set scalaJSStage in Global := FullOptStage
(Mặc định thì stage là FastOptStage).
Compression
Nếu bạn đáp ứng ứng dụng Scala.js của bạn từ web server, bạn nên bổ sung thêm file nén .js.Bước này có thể giảm size của ứng dụng xuống còn chỉ là 20% so với file gốc. Cài đặt dựa trên server stack của bạn.Một lựa chọn chung là sử dụng sbt-web https://github.com/sbt/sbt-web, sbt-web-scalajs https://github.com/vmunier/sbt-web-scalajs, và sbt-gzip https://github.com/sbt/sbt-gzip, nếu bạn có một Play hoặc Akka-http server.
Automatically Creating a Launcher
Trước khi create file HTML khác mà nó includes full optimized JavaScript, chúng ta sẽ giới thiệu tính năng của sbt plugin.Vì sbt plugin có khả năng phát hiện JSApp object của application, không cần phải lặp lại điều này trong HTML file.Nếu bạn add setting dưới đây vào build.sbt, sbt sẽ create một scala-js-tutorial-launcher.js file mà nó gọi main method:
persistLauncher := true
Trong trang HTML của bạn, chúng ta có thể includes file này thay vì chạy thủ công:
<!-- Run JSApp --> <script type="text/javascript" src="./target/scala-2.12/scala-js-tutorial-launcher.js"></script>
Nếu chúng ta đổi tên JSApp object, chúng ta không cần phải thay đổi HTML của chúng ta nữa.Lưu ý rằng launcher generate chỉ hoạt động nếu bạn có một single JSApp object.Nếu bạn có nhiều JSApp object nhưng vẫn muốn tạo ra launcher, bạn cần set JSApp object một cách rõ ràng. Có thể xem Compiling , Running, Linking , Optimizing guid để hiểu thêm chi tiết.
Putting it all Together
Bây giờ chúng ta create một file HTML hoàn chỉnh scalajs-tutorial.html mà nó includes fully optimized code:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title