Autowire and BooPickle
Web clients communicate with the server most commonly with Ajax which is a quite loosely defined collection of techniques. Most notable
JavaScript libraries like JQuery provide higher level access to the low level protocols exposed by the browser. Scala.js provides a nice
Ajax wrapper in dom.extensions.Ajax
(or dom.ext.Ajax
in scalajs-dom 0.8+) but it's still quite tedious to serialize/deserialize objects
and take care of all the dirty little details.
But fear not, there is no need to do all that yourself since our friend Li Haoyi (lihaoyi) has created and published a great library called Autowire. Combined with my very own BooPickle library you can easily handle client-server communication. Note that BooPickle uses binary serialization format, so if you'd prefer a JSON format, consider using uPickle. As the SPA tutorial used to use uPickle for serialization, you can browse the repository history to see the relevant code here and here.
To build your own client-server communication pathway all you need to do is to define a single object on the client side and another on the server side.
import boopickle.Default._
// client side
object AjaxClient extends autowire.Client[ByteBuffer, Pickler, Pickler] {
override def doCall(req: Request): Future[ByteBuffer] = {
dom.ext.Ajax.post(
url = "/api/" + req.path.mkString("/"),
data = Pickle.intoBytes(req.args),
responseType = "arraybuffer",
headers = Map("Content-Type" -> "application/octet-stream")
).map(r => TypedArrayBuffer.wrap(r.response.asInstanceOf[ArrayBuffer]))
}
override def read[Result: Pickler](p: ByteBuffer) = Unpickle[Result].fromBytes(p)
override def write[Result: Pickler](r: Result) = Pickle.intoBytes(r)
}
The only variable specific to your application is the URL you want to use to call the server. Otherwise everything else it automatically generated for you through the magic of macros. The server side is even simpler, just letting Autowire know that you want to use BooPickle for serialization.
import boopickle.Default._
// server side
object Router extends autowire.Server[ByteBuffer, Pickler, Pickler] {
override def read[R: Pickler](p: ByteBuffer) = Unpickle[R].fromBytes(p)
override def write[R: Pickler](r: R) = Pickle.intoBytes(r)
}
Now that you have the AjaxClient
set up, calling the server is as simple as
import scala.concurrent.ExecutionContext.Implicits.global
import boopickle.Default._
import autowire._
AjaxClient[Api].getTodos().call().foreach { todos =>
println(s"Got some things to do $todos")
}
Note that you need those three imports to access the Autowire/BooPickle magic and to provide an execution context for the futures.
The Api
is just a simple trait shared between the client and server.
trait Api {
// message of the day
def motd(name:String) : String
// get Todo items
def getTodos() : Seq[TodoItem]
// update a Todo
def updateTodo(item: TodoItem): Seq[TodoItem]
// delete a Todo
def deleteTodo(itemId: String): Seq[TodoItem]
}
Please check out BooPickle documentation on what it can and cannot serialize. You might need to use something else if your data is complicated. Case classes, base collections and basic data types are a safe bet.
So how does this work on the server side?