summaryrefslogtreecommitdiff
path: root/book/src/main/scalatex/book/handson/ClientServer.scalatex
blob: 87c959da70757d2244d9c0702c367286a20c8a4c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
@import BookData._

@def lazyload(id: String) = script(raw(s"""
  window.addEventListener("load", function(){
    document.getElementById("$id").src = 'https://hands-on-scala-js.herokuapp.com/'
  })
"""))


@p
  Historically, sharing code across client & server has been a holy-grail for web development. There are many things which have made it hard in the past:

@ul
  @li
    Javascript on the client v.s. PHP/Perl/Python/Ruby/Java on the client
  @li
    Most back-ends make heavy use of C extensions, and front-end code was tightly coupled to the DOM. Even if you manage to port the main language
@p
  There have been some attempts in recent years with more traction: Node.js, for example, has been very successful at running Javascript on the server, the Clojure/Clojurescript community has their own version of cross-built code, and there are a number of smaller, more esoteric platforms.

@p
  Scala.js lets you share code between client and server relatively straightforwardly. As we saw in the previous chapter, where we made a shared module. Let's work to turn that shared module into a working client-server application!

@val server = wd/'examples/'crossBuilds/'clientserver/'server/'src/'main/'scala/'simple
@val client = wd/'examples/'crossBuilds/'clientserver/'client/'src/'main/'scala/'simple

@sect{A Client-Server Setup}
  @p
    Getting started with client-server integration, let's go with the simplest configuration possible: a Spray server and a Scala.js client. Most of the other web-frameworks (@lnk.misc.Play, @lnk.misc.Scalatra, etc.) will have more complex configurations, but the basic mechanism of wiring up Scala.js to your web framework will be the same. Our project will look like this:

  @hl.bash
    $ tree
    .
    ├── build.sbt
    ├── client
    │   ├── shared/main/scala/simple/FileData.scala
    │   └── src/main/scala/simple/Client.scala
    ├── project
    │   └── build.sbt
    └── server
        ├── shared -> ../client/shared
        └── src/main/scala/simple
                           ├── Page.scala
                           └── Server.scala

  @p
    First, let's do the wiring in @code{build.sbt}:

  @hl.ref(wd/'examples/'crossBuilds/'clientserver/"build.sbt")

  @p
    We have two projects: @code{client} and @code{server}, one of which is a Scala.js project (indicated by the presence of @hl.scala{scalaJSSettings}). Both projects share a number of settings: the presence of the @code{shared/} folder, which shared code can live in (similar to what we saw in @sect.ref{Cross Publishing Libraries}) and the settings to add @lnk.github.Scalatags and @lnk.github.uPickle to the build. Note that those two dependencies use the triple @code{%%%} instead of the double @code{%%} to declare: this means that for each dependency, we will pull in the Scala-JVM or Scala.js version depending on whether it's being used in a Scala.js project. Note also the @hl.scala{packageArchetype.java_application} setting, which isn't strictly necessary depending on what you want to do with the application, but this example needs it as part of the deployment to Heroku.

  @p
    The @code{client} subproject is uneventful, with a dependency on the by-now-familiar @code{scalajs-dom} library. The @code{server} project, on the other hand, is interesting: it contains the dependencies required for us to set up out Spray server, and one additional thing: we add the output of @code{fastOptJS} from the client to the @code{resources} on the server. This will allow the @code{server} to serve the compiled-javascript from our @code{client} project from its resources.

  @p
    Next, let's kick off the Spray server in our Scala-JVM main method:


  @hl.ref(server/"Server.scala")

  @p
    This is a not-very-interesting @lnk("spray-routing", "http://spray.io/documentation/1.2.2/spray-routing/") application: we set up a server on @code{localhost:8080}, have the root URL serve the main page on GET, and have other GET URLs serve resources. This includes the @code{js-fastopt.js} file that is now in our resources because of our @code{build.sbt} config earlier! We also add a POST route to allow the client ask the server to list files various directories.

  @p
    The HTML template @hl.scala{Page.skeleton} is not shown above; I put it in a separate file for neatness:

  @hl.ref(server/"Page.scala")

  @p
    This is a typical @lnk.github.Scalatags HTML snippet. Note that since we're serving it directly from the server in Scala code, we do not need to leave a @code{.html} file somewhere on the filesystem! We can declare all HTML, including the skeleton of the page, in Scalatags. Otherwise it's the same as what we saw in earlier chapters: A simple HTML page which includes a script tag to run our Scala.js application.
  @p
    Lastly, we'll set up the Scala.js main method, which we are calling in the @hl.html{<script>} tag above to kick off the client-side application.

  @hl.ref(client/"Client.scala")

  @p
    Again this is a simple Scala.js application, not unlike what we saw in earlier chapters. However, there is one difference: earlier, we made our Ajax calls to @code{api.openweathermap.org/...}. Here, we're making it to @code{/ajax}: the same server the page is served from!

  @p
    You may have noticed in both client and server, we have made reference to a mysterious @hl.scala{FileData} type which holds the name and size of each file. @hl.scala{FileData} is defined in the @code{shared/} folder, so it can be accessed from both Scala-JVM and Scala.js:


  @hl.ref(wd/'examples/'crossBuilds/'clientserver/'client/'shared/'main/'scala/'simple/"FileData.scala")

  @p
    Now, if we go to the browser at @code{localhost:8080}, we should see our web-page!

  @iframe(id:="heroku1", width:="100%", height:="350px", "frameBorder".attr:="0")
  @lazyload("heroku1")


  @p
    This is a real, live example running on a @lnk("Heroku server", "https://hands-on-scala-js.herokuapp.com/"). Feel free to poke around and explore the filesystem on the server, just to convince yourself that this actually works and is not just a mock up.

@sect{Client-Server Reflections}
  @p
    By now you've already set up your first client-server application. However, it might not be immediately clear what we've done and why it's interesting! Here are some points to consider.

  @sect{Shared Templating}

    @p
      In both the client code and the server code, we made use of the same Scalatags HTML generation library. This is pretty neat: transferring rendering logic between client and server no longer means an annoying/messy rewrite! You can simply C&P the Scalatags snippet over. That means it's easy if you want to e.g. shift the logic from one side to the other in order to optimize for performance or time-to-load or other things.
    @p
      One thing to take note of is that we're actually using subtly @i{different} implementations of Scalatags on both sides: on the server, we're importing from @hl.scala{scalatags.Text}, while on the client we're using @hl.scala{scalatags.JsDom}. The @hl.scala{Text} backend renders directly to Strings, and is available on both Scala-JVM and Scala.js. The @hl.scala{JsDom} backend, on the other hand, renders to @lnk.dom.HTMLElement-s which only exist on Scala.js. Thus while on the client you can do things like attach event listeners to the rendered @lnk.dom.HTMLElement objects, or checking their runtime @code{.value}, on the server you can't. And that's exactly what you want!

  @sect{Shared Code}
    @p
      One thing that we skimmed over is the fact that we could easily define our @hl.scala{case class FileData(name: String, size: Long)} in the @code{shared/} folder, and have it instantly and consistently available on both client and server. This perhaps does not seem so amazing: we've already done many similar things earlier when we were building Cross-platform Modules. Nevertheless, in the context of web development, it is a relatively novel idea to be able to ad-hoc share bits of code between client and server.
    @p
      Sharing code is not limited to class definitions: @i{anything} can be shared. Objects, classes, interfaces/traits, functions and algorithms, constants: all of these are things that you will likely want to share at some point or another. Traditionally, people have simply re-implemented the same code twice in two languages, or have resorted to awkward Ajax calls to push the logic to the server. With Scala.js, you no longer need to do so: you can easily, create ad-hoc bits of code which are available on both platforms.

  @sect{Boilerplate-free Serialization}
    @p
      The Ajax/RPC layer is one of the more fragile parts of web applications. Often, you have your various Ajax endpoints written once on the server, have a set of routes written to connect those Ajax endpoints to URLs, and client code (traditionally Javascript) made calls to those URLs with "raw" data: basically whatever you wanted, packed in an ad-hoc mix of CSV and JSON and raw-strings.

    @p
      This has always been annoying boilerplate, and Scala.js removes it. With @lnk.github.uPickle, you can simply call @hl.scala{upickle.write(...)} and @hl.scala{upickle.read[T](...)} to convert your collections, primitives or case-classes to and from JSON. This means you do not need to constantly re-invent different ways of making Ajax calls: you can just fling the data right across the network from client to server and back again.

@sect{What's Left?}
  @p
    We've built a small client-server web application with a Scala.js web-client that makes Ajax calls to a Scala-JVM web-server running on Spray. We performed these Ajax calls using uPickle to serialize the data back and forth, so serializing the arguments and return-value was boilerplate-free and correct.

  @p
    However, there is still some amount of duplication in the code. In particular, the definition of the endpoint name "list" is duplicated 4 times:

  @hl.ref(server/"Server.scala", """path("ajax" / "list")""", "")
  @hl.ref(server/"Server.scala", "list(", "")
  @hl.ref(server/"Server.scala", "def list", "")
  @hl.ref(client/"Client.scala", "ajax/list", "")

  @p
    Three times on the server and once on the client! What's worse, two of the appearances of @hl.scala{"list"} are in string literals, which are not checked by the compiler to match up with themselves or the name of the method @hl.scala{list}. Apart from this, there is one other piece of duplication that is unchecked: the type being returned from @hl.scala{list} (@hl.scala{Seq[FileData]}) is being repeated on the client in @hl.scala{upickle.read[Seq[FileData]]} in order to de-serialize the serialized data. This leaves three opportunities for error wide-open:

  @ul
    @li
      You could change the string literals @hl.scala{"list"} and forget to change the method-name @hl.scala{list}, thus confusing future maintainers of the code.
    @li
      You could change one of literal @hl.scala{"list"}s but forget to change the other, thus causing an error at run-time (e.g. a 404 NOT FOUND response)
    @li
      You could update the return type of the @hl.scala{list} method and forget to update the @hl.scala{upickle.read} deserialization call on the client, resulting in a deserialization failure at runtime.

  @p

  @p
    Neither of these scenarios is great! Although we've already made great progress in making our client-server application type-safe (via Scala.js on the client) and DRY (via shared code in @code{shared/}) we still have this tiny bit of annoying, un-checked duplication and danger lurking in the code-base. The basic problem is that what is normally called the "routing layer" in the web application is still unsafe, and so these silly errors can go un-caught and blow up on unsuspecting developers at run-time. Let's see how we can fix it.

@sect{Autowire}

  @p
    @lnk("Autowire", "https://github.com/lihaoyi/autowire") is a library that turns your request routing layer from a fragile, hand-crafted mess into a solid, type-checked, boilerplate-free experience. Autowire basically turns what was previously a stringly-typed, hand-crafted Ajax call and route:

  @hl.ref(client/"Client.scala", "ajax/list", "")

  @p
    Into a safe, type-checked function call:

  @val client2 = wd/'examples/'crossBuilds/'clientserver2/'client/'src/'main/'scala/'simple
  @val server2 = wd/'examples/'crossBuilds/'clientserver2/'server/'src/'main/'scala/'simple
  @hl.ref(client2/"Client.scala", ".call()", "")

  @p
    Let's see how we can do that.

  @sect{Setting up Autowire}

    @p
      To begin with, Autowire requires you to provide three things:

    @ul
      @li
        An @hl.scala{autowire.Server} on the Server, set up to feed the incoming request into Autowire's routing logic
      @li
        An @hl.scala{autowire.Client} on the Client, set up to take a serialized request and send it across the network to the server.
      @li
        An interface (A Scala @hl.scala{trait}) which defines the interface between these two

    @p
      Let's start with our client-server interface definition

    @hl.ref(wd/'examples/'crossBuilds/'clientserver2/'client/'shared/'main/'scala/'simple/"Shared.scala")

    @p
      Here, you can see that in addition to sharing the @hl.scala{FileData} class, we are also creating an @hl.scala{Api} trait which contains the signature of our @hl.scala{list} method. The exact name of the trait doesn't matter. We need it to be in @code{shared/} so that the code in both client and server can reference it.

    @p
      Next, let's look at modifying our server code to make use of Autowire:

    @hl.ref(server2/"Server.scala")

    @p
      Now, instead of hard-coding the route @hl.scala{"ajax" / "list"}, we now take in any route matching @hl.scala{"ajax" / Segments}, feeding the resultant path segments into the @hl.scala{Router} object:

    @hl.ref(server2/"Server.scala", "path(")
    @p
      The @hl.scala{Router} object in turn simply defines how you intend the objects to be serialized and deserialized:

    @hl.ref(server2/"Server.scala", "object Router", "object Server")

    @p
      In this case using uPickle. Note how the @hl.scala{route} call explicitly states the type (here @hl.scala{Api}) that it is to generate routes against; this ensures that only methods which you explicitly put in your public interface @hl.scala{Api} are publically reachable.

    @p
      Next, let's look at the modified client code:

    @hl.ref(client2/"Client.scala")

    @p
      There are two main modifications here: the existence of the new @hl.scala{Ajaxer} object, and the modification to the Ajax call-site. Let's first look at @hl.scala{Ajaxer}:

    @hl.ref(client2/"Client.scala", "object Ajaxer", "@JSExport")

    @p
      Like the @hl.scala{Router} object, @hl.scala{Ajaxer} also defines how you perform the serialization and deserialization of data-structures, again using uPickle. Unlike the @hl.scala{Router} object, @hl.scala{Ajaxer} also defines how the out-going Ajax call gets sent over the network. Here we're doing it using the @hl.scala{Ajax.post} method.

    @p
      Lastly, let's look at the modified callsite for the ajax call itself:

    @hl.ref(client2/"Client.scala", "def update", "")

    @p
      There are a few things of note here:

    @ul
      @li
        The previous call to @hl.scala{Ajax.post} with the path as a string has been replaced by calling @hl.scala{Ajaxer[Api].list(...).call()}, since the logic of actually performing the POST is specified once-and-only-once in the @hl.scala{Ajaxer} object.
      @li
        While @hl.scala{Ajax.post} returned a @hl.scala{Future[dom.XMLHttpRequest]} and left us to call @hl.scala{upickle.read} and deserialize the data ourselves, @hl.scala{Ajaxer[Api].list(...).call()} now returns a @hl.scala{Future[Seq[FileData]]}! Thus we don't need to worry about making a mistake in the deserialization logic when we write it by hand.

    @p
      Other than that, nothing much has changed. If you've done this correctly, the web application will look and behave exactly as it did earlier!

    @iframe(id:="heroku2", width:="100%", height:="350px", "frameBorder".attr:="0")
    @lazyload("heroku2")


    @p
      So why did we do this in the first place?

  @sect{Why Autowire?}
    @p
      Overall, this set up requires some boilerplate to define the @hl.scala{Ajaxer} and @hl.scala{Router} objects, as well as the @hl.scala{Api} trait. However, these can be defined just once and used over and over; while it might be wasteful/unnecessary for making a single Ajax call, the cost is much less amortized over a number of Ajax calls. In a non-trivial web application with dozens of routes being called all over the place, spending a dozen lines setting up things up-front isn't a huge cost.

    @p
      What have we gotten in exchange? It turns out that by using Autowire, we have eliminated the three failure modes described earlier, that could:

    @ul
      @li
        It is impossible for the route and the endpoint method-name to diverge accidentally: if the endpoint is called @hl.scala{list}, the requests will go through the @code{/list} URL. No room for discussion, or to make a mistake
      @li
        You cannot accidentally rename the route on the server without changing the client, or vice versa. Attempts to do so will cause a compilation error, and even your IDE should highlight it as red. Try it out!

      @li
        There is no chance of messing up the serialization/deserialization code, e.g. writing a response of type A on the server and trying to read a data-structure of type B on the client. You have no opportunity to make an error: you pass arguments to the Ajax call, and they are serialized/deserialized automatically, such that by the time you get access to the value on the server, it is already of the correct type! The same applies to serializing/deserializing the return-value on the client. There is simply no place for you as a developer to accidentally make a mistake!

    @p
      Although the functionality of the web application is the same, it is mostly in terms of @i{safety} that we have made the biggest gains. All of the common failure modes described earlier have been guarded against, and you as a developer will have a hard time trying to make a mal-formed Ajax call. It's worth taking some time to poke at the source code to see the boundaries of the type-safety provided by autowire, as it is a very different experience from the traditional "route it manually" approach to making interactive client-server applications.

@hr

@p
  Hopefully this chapter has given you a glimpse of how a basic client-server application works using Scala.js. Although it is specific to a Spray server, there isn't any reason why you couldn't set up an equivalent thing for your Play, Scalatra or whichever other web framework that you're using.

@p
  It's probably worth taking a moment to play around with the existing client-server system you have set up. Ideas for improvement include:

@ul
  @li
    Try adding additional functionality to the client-server interface: what about making it show the contents of a file if you've entered its full path? This can be added as a separate Ajax call or as part of the existing one.
  @li
    How about setting up the build.sbt so it serves the fully-optimized Scala.js blob, @code{client-opt.js}? This is probably what you want before deployment into production, and the same technique as we used to serve the fast-optimized version applies here too.
  @li
    What if you wanted to use another server rather than Spray? How about trying to set up a Play or Scalatra server to serve our Scala.js application code?