Tutorial for creating a simple Single Page Application with Scala.js and Spray.
- Purpose
- Getting started
- Application structure
- Testing
- Debugging and logging
- SBT build definition
- FAQ
- What next?
This project demonstrates typical design patterns and practices for developing SPAs with Scala.js with special focus on building a complete application. It started as a way to learn more about Scala.js and related libraries, but then I decided to make it more tutorial-like for the greater good :)
The code covers typical aspects of building a SPA using Scala.js but it doesn't try to be an all-encompassing example for all the things possible with Scala.js. Before going through this tutorial, it would be helpful if you already know the basics of Scala.js and have read through the official Scala.js tutorial and the great e-book Hands-on Scala.js by Li Haoyi (lihaoyi).
Fork a copy of the repository and clone it to your computer using Git. Run sbt
in the project folder and after SBT has completed loading the project,
start the server with re-start
. This will compile both the client and server side Scala application, package it and start the server. You can now navigate to
localhost:8080
on your web browser to open the Dashboard view. It should look something like this
The application is really simple, containing only two views (Dashboard and Todo) and you can access these by clicking the appropriate item on the menu. The Todo view looks like this
Now that you have everything up and running, it's time to dive into the details of what makes this application tick. Or if you want to experiment a little
yourself, use the ~fastOptJS
command on SBT prompt and SBT will automatically compile the (client side) application when you modify the source code. Try
changing for example the chart data in js/Dashboard.scala
and reloading the web page.
The application is divided into three folders: js
, jvm
and shared
. As the names imply, js
contains the client code for the SPA, jvm
is the server and
shared
contains code and resources used by both. If you take a quick look at project/build.scala
you will notice the use of
crossProject
to define this Scala.js specific cross-building project structure.
Within each sub-project the usual SBT/Scala directory structure convention is followed.
We'll get to the details of the project build file later on, but let's first take a look at actual client code!
As is typical for SPAs the client consists of a single HTML file and a number of supporting resources (JS and CSS). One of these resources is the actual JavaScript
code generated by Scala.js from the Scala sources. There are two variants of the index.html
one for fast development and the other (index-full.html
) for
a production optimized version. Both are stored under shared
project so that you can access them not only through the server, but also when doing local
development with the JS client only (more about this later). All the relevant resources are under the web
directory to prevent conflicts with other (server)
resources.
In the index.html
you'll find the usual HTML things like links to CSS and JS files. As you can see, the <body>
element is pretty empty, because all the
HTML will be generated by the application itself.
<body onload="SPAMain().main()">
<script src="js/scalajs-spa-jsdeps.js"></script>
<script src="js/scalajs-spa-fastopt.js"></script>
</body>
</html>
Instead of using external JavaScript references to React, jQuery, Bootstrap and to a chart component, the build system combines all these into a single JavaScript file (scalajs-spa-jsdeps.js). See here for details. The last JavaScript reference is the compiled application code.
Once the browser has loaded all the resources, it will call the SPAMain().main()
method defined in the
SPAMain.scala
singleton class. This is the entry point of the application. The class itself is very simple,
@JSExport("SPAMain")
object SPAMain extends JSApp {
@JSExport
def main(): Unit = {
// tell React to render the router in the document body
React.render(MainRouter.routerComponent(), dom.document.body)
}
}
The externally accessible classes and functions are annotated with @JSExport
so the Scala.js compiler knows not to optimize them away and make them available
with those exact names in the global scope.
What main()
does is simply to create a router and instruct React to render it inside the document <body>
tag.
Now at this point you've seen couple of references to React and might wonder what's it about. React is a JavaScript library for building user interfaces, developed by Facebook. You might ask "why use a JavaScript library with Scala.js if Scala is so great" and yes, it might make sense to do a similar library in Scala.js but since it's already there, why not use it. And to sweeten the deal there is a very nice wrapper for React called scalajs-react by David Barri (japgolly).
There are also other Scals.js libraries available for building SPAs but I wanted to go with React, so that's what we'll use in this tutorial :)
A critical feature in a SPA is navigation between "pages" within the application. Of course they are not real pages, since it's a Single Page Application, but from the user point of view it looks like that. A typical example of a SPA is Gmail where the URL in the browser reflects the state of the application.
Since we are not loading new pages from the server, we cannot use the regular browser navigation but need to provide one ourselves. This is called routing and
is provided by many JS frameworks like AngularJS. Scala.js itself is not an application framework so there is no ready made router component provided by it. But
we are lucky to have developers like @japgolly who go through all the pain and suffering to deliver great libraries for the rest of us. In the tutorial I'm using
scalajs-react
router which nicely integrates with scalajs-react
and provides a
seamless way to manage routes and navigate between them.
The way it works is that you basically create route components, register them with the router and off it goes binding your components as the URL changes. The provided examples build all routes within a single class, but in real life we want more modularity. In this tutorial we have a total of two modules/routes/views just to demonstrate how to use the router.
As this is a Single Page Application, all routes are defined under one router, MainRouter
, which extends RoutingRules
trait.
object MainRouter extends RoutingRules {
// register the components and store locations
val dashboardLoc = register(rootLocation(Dashboard.component))
val todoLoc = register(location("#todo", ToDo.component))
Here we just register the two routes with the RoutingRules
. First route is our main route which is attached to the special rootLocation
. The second
route is attached to #todo
path. You could also use "clean" paths without the hash, but then your server must be prepared to server correct content
even when there is a sub-path defined. The hash also makes it easy to work with a local-only setup, when the files are served by the Workbench plugin.
The MainRouter
also provides the base HTML code and integrates a MainMenu
component for the application in its interceptRender
function. SPA tutorial
uses Bootstrap CSS to provide a nice looking layout, but you can use whatever CSS framework you wish just by changing the CSS class definitions.
import japgolly.scalajs.react.vdom.prefix_<^._
override protected def interceptRender(ic: InterceptionR) = {
<.div(
<.nav(^.className := "navbar navbar-inverse navbar-fixed-top")(
<.div(^.className := "container")(
<.div(^.className := "navbar-header")(<.span(^.className := "navbar-brand")("SPA Tutorial")),
<.div(^.className := "collapse navbar-collapse")(
MainMenu(MainMenu.Props(ic.loc, TodoStore.todos))
)
)
),
// currently active module is shown in this container
<.div(^.className := "container")(ic.element)
)
}
See how the code looks just like HTML, except it's type safe and the IDE provides auto-complete! If you insist on having even closer resemblance to HTML,
you can replace the prefix_<^
with all
giving you simple div
and className
tag names. Be warned, however, that this may lead to nasty surprises
down the road because the HTML namespace contains a lot of short, common tag names like a
and id
. The little extra effort from <.
and ^.
pays
off in the long run.
The main menu is just another React component that is given the current location and the router as properties. The contents of the menu is defined statically within the class itself, because the referred locations are anyway all known at compile time. For other kinds of menus you'd want to use a dynamic system, but static is just fine here.
case class MenuItem(label: (Props) => ReactNode, icon: Icon, location: MainRouter.Loc)
private val menuItems = Seq(
MenuItem(_ => "Dashboard", Icon.dashboard, MainRouter.dashboardLoc),
MenuItem(buildTodoMenu, Icon.check, MainRouter.todoLoc)
)
private def buildTodoMenu(props: MenuProps): ReactNode = {
val todoCount = props.todos().count(!_.completed)
Seq(
<.span("Todo "),
if (todoCount > 0) <.span(^.className := "label label-danger label-as-badge", todoCount) else <.span()
)
}
For each menu item we define a function to generate the label, an icon and the location that was registered in the MainRouter
. For Dashboard
the label is simple text, but for Todo we also render the number of open todos.
To render the menu we just loop over the items and create appropriate tags. For links we need to use the router
provided in the properties.
val MainMenu = ReactComponentB[MenuProps]("MainMenu")
.render(P => {
<.ul(^.className := "nav navbar-nav")(
// build a list of menu items
for (item <- menuItems) yield {
<.li((P.activeLocation == item.location) ?= (^.className := "active"),
MainRouter.routerLink(item.location)(item.icon, " ", item.label(P))
)
}
)
})
.build
Ok, we've got the HTML page defined, menu generated and the active component (Dashboard) within the placeholder, what happens next?
Dashboard module is really simple in terms of React components as it contains
no internal state nor backend functionality. It's basically just a placeholder for two other components Motd
and Chart
. The only method is
the render
method which is responsible for rendering the component when it's mounted by React. It also provides fake data for the Chart
component, to keep simple.
val component = ReactComponentB[MainRouter.Router]("Dashboard")
.render(router => {
// create dummy data for the chart
val cp = ChartProps("Test chart", Chart.BarChart, ChartData(Seq("A", "B", "C"), Seq(ChartDataset(Seq(1, 2, 3), "Data1"))))
// get internal links
val appLinks = MainRouter.appLinks(router)
<.div(
// header, MessageOfTheDay and chart components
<.h2("Dashboard"),
Motd(),
Chart(cp),
// create a link to the Todo view
<.div(MainRouter.todoLink("Check your todos!"))
)
}).build
Motd component is a simple React component that fetches Message of the day
from the server and displays it in a panel. The component consists of three different classes: State
for storing component state, Backend
for functionality and Motd
for instantiating the component. The Motd
is not given any properties, so the prop type is Unit
.
val Motd = ReactComponentB[Unit]("Motd")
.initialState(State("loading...")) // show a loading text while message is being fetched from the server
.backend(new Backend(_))
.render((_, S, B) => {
Panel(Panel.Props("Message of the day"), div(S.message),
Button(Button.Props(B.refresh, CommonStyle.danger), Icon.refresh, span(" Update"))
)
})
.componentDidMount(scope => {
scope.backend.refresh()
})
.buildU
A React component is defined through a series of function calls. Each of these calls, like initialState
modifies the type of the component,
meaning you cannot access the state in the render
method unless you have initialized it with initialState
.
In the Todo component earlier, we didn't define state nor backend and therefore the render method had only access to the properties. In this
render
method we have access to all three, denoted by _
(props), S
(state) and B
(backend). Since the component has no properties
(type Unit
) we discard the first parameter using the _
notation. The render method builds a simple hierarchy of other React components
(Panel
and Button
) containing the message from the server.
Of course to actually get the message, we need to request it from the server. To do this automatically when the component is mounted, we hook
a call to refresh()
in the componentDidMount
method. The Backend
class takes care of the actual server Ajax call and after receiving a
response, updates the component state.
def refresh() {
// load a new message from the server
AjaxClient[Api].motd("User X").call().foreach { message =>
t.modState(_ => State(message))
}
}
How the magic of calling the server actually happens is covered in a later chapter.
Sometimes you need to create a link that takes the user to an another module behind a route. To create these links in a type-safe manner,
the tutorial code defines functions inside MainRouter
that return valid links (anchor tags with onClick
hooked to appropriate router handler).
def dashboardLink = router.link(dashboardLoc)
def todoLink = router.link(todoLoc)
def routerLink(loc: Loc) = router.link(loc)
These functions return a ReactTag
that you can assign children just by adding them to the call.
MainRouter.todoLink("Check your todos!")
Although Scala.js provides a superb environment for developing web clients, sometimes it makes sense to utilize the hard work of thousands of JavaScript developers fumbling in the shadows :)
Bootstrap is a popular HTML/CSS/JS framework by Twitter for developing responsive applications. It comes with a lot of styled HTML/CSS components that are easy to use and integrate into your application. Lot of Bootstrap actually doesn't even use JavaScript, all the magic happens in CSS.
This tutorial wraps couple of simple Bootstrap components (button and panel) into React components. Bootstrap uses contextual styles in many components to convey additional meaning. These can be easily represented by a Scala enumeration.
object CommonStyle extends Enumeration {
val default, primary, success, info, warning, danger = Value
}
To define an interactive button, it has to know what to do when it's clicked. In this example we simply give a function in the properties alongside the contextual style. Note that the actual button contents doesn't need to be provided in the properties as it's more convenient to define it through child component(s).
object Button {
case class Props(onClick: () => Unit, style: CommonStyle.Value = CommonStyle.default)
val component = ReactComponentB[Props]("Button")
.render { (P, C) =>
<.button(^.className := s"btn btn-${P.style}", ^.tpe := "button", ^.onClick --> P.onClick())(C)
}.build
def apply(props: Props, children: ReactNode*) = component(props, children)
def apply() = component
}
This time the render method gets two parameters, the properties and the children given to this component. It simply renders a normal
button using Bootstrap CSS and binding onClick
to the handler we defined in the properties. Finally the children are rendered within
the button tag.
Defining a Bootstrap Panel is about as simple.
object Panel {
case class Props(heading: String, style: CommonStyle.Value = CommonStyle.default)
val component = ReactComponentB[Props]("Panel")
.render { (P, C) =>
<.div(^.className := s"panel panel-${P.style}")(
<.div(^.className := "panel-heading")(P.heading),
<.div(^.className := "panel-body")(C)
)
}.build
def apply(props: Props, children: ReactNode*) = component(props, children)
def apply() = component
}
The panel provides no interactivity but this time we define a separate heading
in addition to using the children property.
Custom fonts are a great way to generate scalable icons that look good on all displays. In the tutorial we use Font Awesome icons and a simple wrapper that generates appropriate HTML tags to display the icon.
object Icon {
type Icon = ReactTag
def apply(name: String): Icon = <.i(^.className := s"fa fa-$name")
def adjust = apply("adjust")
def adn = apply("adn")
.
.
def youtubePlay = apply("youtube-play")
def youtubeSquare = apply("youtube-square")
}
If you'd want a nice charting component in your web UI you could go ahead and write a lot of SVG-generating code, but why bother when there are so many components available for your benefit. Scala.js provides many ways to use JavaScript from your own Scala code and some of them are more type-safe than others. A good way is to define a facade for the 3rd party JS module and for any data structures it may expose. This way you can be sure to use it in a type-safe manner.
In the tutorial we are using Chart.js but the same principles apply to practically all JS components out there.
The Chart.js draws the chart onto a HTML5 canvas and is instantiated by following JavaScript code
var ctx = document.getElementById("myChart").getContext("2d");
var myNewChart = new Chart(ctx).Line(data);
To do the same in Scala.js we define a simple facade trait as follows
@JSName("Chart")
class JSChart(ctx: js.Dynamic) extends js.Object {
def Line(data: ChartData): js.Dynamic = js.native
def Bar(data: ChartData): js.Dynamic = js.native
}
Note that this defines only couple of charts available in the Chart.js component, but it's trivial to add more if you need them. We are also
skipping the options
parameter for charts to keep things simple.
To actually instantiate the chart, we need access to the canvas element and with React this is a bit problematic since it builds a virtual-DOM and
updates the real DOM behind the scene. Therefore the canvas element does not exist at the time of render
function call. To work around this problem
we need to build the chart in the componentDidMount
function, which is called after the real DOM has been updated. This function is called with
a scope
parameter that gives us access to the actual DOM node through getDOMNode()
. The chart is built by creating a new instance of Chart
and calling the appropriate chart function.
val Chart = ReactComponentB[ChartProps]("Chart")
.render((P) => {
<.canvas(^.width := P.width, ^.height := P.height)
}).componentDidMount(scope => {
// access context of the canvas
val ctx = scope.getDOMNode().asInstanceOf[HTMLCanvasElement].getContext("2d")
// create the actual chart using the 3rd party component
scope.props.style match {
case LineChart => new JSChart(ctx).Line(scope.props.data)
case BarChart => new JSChart(ctx).Bar(scope.props.data)
case _ => throw new IllegalArgumentException
}
}).build
Chart.js input data is a JavaScript object like below
var data = {
labels: ["January", "February", "March", "April", "May", "June", "July"],
datasets: [
{
label: "My First dataset",
fillColor: "rgba(220,220,220,0.2)",
strokeColor: "rgba(220,220,220,1)",
data: [65, 59, 80, 81, 56, 55, 40]
},
{
label: "My Second dataset",
fillColor: "rgba(151,187,205,0.2)",
strokeColor: "rgba(151,187,205,1)",
data: [28, 48, 40, 19, 86, 27, 90]
}
]
};
To build the same in Scala.js we could directly use js.Dynamic.literal
but that would be very unsafe and cumbersome. A better alternative is to define
a builder function to create it and a facade to access it.
trait ChartData extends js.Object {
def labels: js.Array[String] = js.native
def datasets: js.Array[ChartDataset] = js.native
}
object ChartData {
def apply(labels: Seq[String], datasets: Seq[ChartDataset]): ChartData = {
js.Dynamic.literal(
labels = labels.toJSArray,
datasets = datasets.toJSArray
).asInstanceOf[ChartData]
}
}
In this case defining the ChartData
trait is actually not necessary, since we don't really use it except to enforce type safety. But if you actually
need to access a JavaScript object defined outside your application, this is the way to do it. Defining chart data is now as simple as
val cp = ChartProps("Test chart", Chart.BarChart, ChartData(Seq("A", "B", "C"), Seq(ChartDataset(Seq(1, 2, 3), "Data1"))))
If you need to build/access very complex JavaScript objects, consider an option builder approach like the one in Querki by jducoeur (for example JQueryUIDialog).
Bootstrap is not only a CSS library but also comes with JavaScript to add functionality to components like Dropdown and Modal. The Modal is an especially problematic system as it involves a hidden dialog box that is shown when the modal is activated and hidden afterwards. In a normal Bootstrap application you would define the dialog box HTML as part of your application and just kept it hidden. With React, however, it's easy (and recommended) to create the HTML for the modal just before it's displayed, so that your application can easily control the contents of the dialog box.
Before diving into the integration of the Bootstrap Modal, let's first examine how jQuery components can be integrated in general. We've provided a truly
skeleton jQuery integration,
just enough for the modal to work, so you'll want to use something more complete for
most purposes. The jQuery integration is also briefly explained in Scala.js documentation so we won't go
into the details too much. Basically you need to define a global jQuery
variable, through which you can then access the jQuery functionality. This is done
in the package.scala
for the
components package.
jQuery works by "calling" it with a selector or an element. In this tutorial we are always using a direct DOM element, so the facade only includes that option. For example to attach an event listener to an element, you would call
jQuery(scope.getDOMNode()).on("hidden.bs.modal", null, null, scope.backend.hidden _)
jQuery has an extension mechanism where plugins can add new functions to the jQuery object. For example Bootstrap Modal adds a modal
function. To define such
an extension in Scala.js we create a trait for it and an implicit conversion (just a type cast, really) for it.
trait BootstrapJQuery extends JQuery {
def modal(action: String): BootstrapJQuery = js.native
def modal(options: js.Any): BootstrapJQuery = js.native
}
implicit def jq2bootstrap(jq: JQuery): BootstrapJQuery = jq.asInstanceOf[BootstrapJQuery]
Now whenever we want to call jQuery(e).modal()
the compiler will automatically cast the JQuery
type into BootstrapJQuery
.
Armed with the jQuery integration we can now tackle the Modal itself. One of the problems the Modal poses is that it's dynamically shown and hidden by the Bootstrap code and we need somehow control that. In this tutorial we've chosen a design where the modal doesn't even exist before it's needed and it's shown right after it has been created. This leaves only the hiding part for us to handle.
In the Backend
of the Modal
we define a hide()
function to do just that.
class Backend(t: BackendScope[Props, Unit]) {
def hide(): Unit = {
// instruct Bootstrap to hide the modal
jQuery(t.getDOMNode()).modal("hide")
}
However, because the dialog box itself contains controls that need to actually close the dialog, we need to expose this functionality to the parent component via properties.
// header and footer are functions, so that they can get access to the the hide() function for their buttons
case class Props(header: (Backend) => ReactNode, footer: (Backend) => ReactNode, closed: () => Unit, backdrop: Boolean = true,
keyboard: Boolean = true)
Additionally, the Bootstrap modals are faded in and out, so the parent cannot go ahead and remove the modal HTML from DOM right away, but
it needs to wait for the fade-out to complete. This is accomplished by listening to an event and calling the parent's closed
function afterwards.
// jQuery event handler to be fired when the modal has been hidden
def hidden(e: JQueryEventObject): js.Any = {
// inform the owner of the component that the modal was closed/hidden
t.props.closed()
}
...
// register event listener to be notified when the modal is closed
jQuery(scope.getDOMNode()).on("hidden.bs.modal", null, null, scope.backend.hidden _)
To show the dialog box after it has been created, we again call modal()
via jQuery in componentDidMount
.
.componentDidMount(scope => {
val P = scope.props
// instruct Bootstrap to show the modal
jQuery(scope.getDOMNode()).modal(js.Dynamic.literal("backdrop" -> P.backdrop, "keyboard" -> P.keyboard, "show" -> true))
The Todo module and its React component are a bit more interesting than Dashboard as they provide some interaction. The module contains a TodoList
component, displaying a list of Todo items retrieved from the server. User can then click the checkbox next to the item to indicate if that item
is completed or not. Internally the state of Todo items is maintained by the Todo
module and TodoList
just passively displays them.
Before going into the details of the actual Todo module and related components, let's ponder a while about data flow in a Scala.js React application.
Several JS frameworks out there (for example AngularJS) use mutable state and two-way data binding. In this tutorial, however, we are taking cues from Facebook's Flux, which is an architecture for unidirectional data flow and immutable state. This architecture works especially well in more complex applications, where two-way data binding can quickly lead to all kinds of hard issues. It's also a relatively simple concept, so it works well even in a simple tutorial application like this. Below you can see a diagram of the Flux architecture.
It consists of a Dispatcher that takes in Actions, and dispatches them to all Stores that then inform Views to update themselves with the new data. This kind of message dispatching sounds quite familiar if you've used actor frameworks like Akka before, so we might as well call those parts with a bit different names.
It's not a real actor framework, for example the dispatcher sends all messages to all registered actors, but it's close enough for our purposes. To explain how this works in practice, let's look at the concrete examples in the Todo module.
The TodoList
component renders a checkbox for each Todo, which can be used to mark the Todo as completed.
<.input(^.tpe := "checkbox", ^.checked := item.completed, ^.onChange --> P.stateChange(item.copy(completed = !item.completed))),
Clicking the checkbox calls the stateChange
method, which maps to updateTodo
method in TodoActions
object. The TodoActions
is a
collection of functions to communicate with the server about changes to the todo list.
object TodoActions {
def updateTodo(item: TodoItem) = {
// inform the server to update/add the item
AjaxClient[Api].updateTodo(item).call().foreach { todos =>
MainDispatcher.dispatch(UpdateAllTodos(todos))
}
}
Note that nothing really happens on the client side as a result of clicking the checkbox. All it does is tell the server that the state
of completion has changed on the selected Todo. Only after the call to the server returns with an updated list of todos, is the client updated.
The update happens indirectly by sending an UpdateAllTodos
message to the MainDispatcher
. This message is then dispatched to all registered
actors, which in this case means only the TodoStore
.
// refine a reactive variable
private val items = Var(Seq.empty[TodoItem])
private def updateItems(newItems: Seq[TodoItem]): Unit = {
// check if todos have really changed
if (newItems != items()) {
// use Rx to update, which propagates down to dependencies
items() = newItems
}
}
override def receive = {
case UpdateAllTodos(todos) =>
updateItems(todos)
TodoStore
updates its internal state with the new items (only if there is a real change) and this change is propagated using
ScalaRx to all interested parties. As it happens, there are
actually two separate components observing changes in the TodoStore
. One is the Todo module where we started:
// get todos from the Rx defined in props
TodoList(TodoListProps(P.todos(), TodoActions.updateTodo, item => B.editTodo(Some(item)), B.deleteTodo))
It forces an update on the component, which in turn causes a call to render
to refresh the view. Within the view, the new todos are fetched from
TodoStore
using a reactive variable (Rx
). This change cascades down to TodoList
and to the individual Todo that was originally clicked.
Mission accomplished!
But as we mentioned before, there was another component interested in changes to the Todos. This is the main menu item for Todo, which shows the number of open Todos.
val todoCount = props.todos().count(!_.completed)
Seq(
<.span("Todo "),
if (todoCount > 0) <.span(^.className := "label label-danger label-as-badge", todoCount) else <.span()
)
This is the beauty of unidirectional data flow, where the components do not need to know where the change came from, or who might be interested in the change. All state changes are propagated to interested parties automatically.
Next, let's look how to set up everything for data to flow.
As you can see in the diagram above, there's all kinds of control flow activity going on in the application. Relevant classes are:
MainDispatcher
- Singleton instance of
Dispatcher
that everything is using
TodoStore
- A store implementing both
Actor
for receiving messages fromDispatcher
. Provides reactive variables (Rx
) to propagate changes.
TodoActions
- Helper class to make Ajax calls to the server and initiate updates via the dispatcher
TodoStore
registers itself with the MainDispatcher
in its singleton constructor.
MainDispatcher.register(this)
The Todo
module and MainMenu
register themselves, when they are mounted, as observers to changes in TodoStore
items.
They also initiate a refresh request for the todos.
def mounted(): Unit = {
// hook up to TodoStore changes
val obsTodos = t.props.todos.foreach { _ => t.forceUpdate() }
onUnmount {
// stop observing when unmounted
obsTodos.kill()
}
// dispatch a message to refresh the todos, which will cause TodoStore to fetch todos from the server
MainDispatcher.dispatch(RefreshTodos)
}
The foreach
call returns an Obs
which we can use to observe changes. Whenever todos
change, the function body is executed, forcing an update on
the React component. The actual value of todos
is not used here, but within the view definition.
Note the use of onUnmount to automatically remove the observer when the component is unmounted.
Another, cleaner way to do the same is to use an observer pattern.
abstract class RxObserver[BS <: BackendScope[_,_]](scope: BS) extends OnUnmount {
protected def observe[T](rx:Rx[T]): Unit = {
val obs = rx.foreach( _ => scope.forceUpdate() )
// stop observing when unmounted
onUnmount(obs.kill())
}
}
class Backend(t: BackendScope[Props, State]) extends RxObserver(t) {
def mounted(): Unit = {
// hook up to TodoStore changes
observe(t.props.todos)
}
This does the same thing as before but a bit more elegantly. The RxObserver
trait is independent and can be used for all Backend classes that
have dependencies on reactive variables.
Finally views hook up different callbacks to TodoActions
when something happens to the todos.
For adding new Todo items, the user interface provides a button and a modal dialog box (using the Modal
component described earlier). Editing
an existing item is performed by clicking an Edit button next to the todo description. Both actions open the same dialog. Finally you can also
delete a todo by clicking the Delete button.
TodoForm
is a simple React component built around a Modal
which allows users to edit an existing Todo item, or create a new one
(there is no difference between these two from the component's point of view). It looks like this
The dialog gets an optional item in properties and maintains current item in its state. The submitHandler
callback is used to inform the parent
when the dialog box is closed (or cancelled).
case class Props(item: Option[TodoItem], submitHandler: (TodoItem, Boolean) => Unit)
case class State(item: TodoItem, cancelled: Boolean = true)
Building the component looks a bit complicated, so let's walk through it.
val component = ReactComponentB[Props]("TodoForm")
.initialStateP(p => State(p.item.getOrElse(TodoItem("", "", TodoNormal, false))))
.backend(new Backend(_))
.render((P, S, B) => {
val headerText = if (S.item.id == "") "Add new todo" else "Edit todo"
Modal(Modal.Props(
// header contains a cancel button (X)
header = be => <.span(<.button(^.tpe := "button", ^.className := "close", ^.onClick --> be.hide(), Icon.close), <.h4(headerText)),
// footer has the OK button that submits the form before hiding it
footer = be => <.span(Button(Button.Props(() => {B.submitForm(); be.hide()}), "OK")),
// this is called after the modal has been hidden (animation is completed)
closed = B.formClosed),
<.div(^.className := "form-group",
<.label(^.`for` := "description", "Description"),
<.input(^.tpe := "text", ^.className := "form-control", ^.id := "description", ^.value := S.item.content,
^.placeholder := "write description", ^.onChange ==> B.updateDescription)),
<.div(^.className := "form-group",
<.label(^.`for` := "priority", "Priority"),
// using defaultValue = "Normal" instead of option/selected due to React
<.select(^.className := "form-control", ^.id := "priority", ^.value := S.item.priority.toString, ^.onChange ==> B.updatePriority,
<.option(^.value := TodoHigh.toString, "High"),
<.option(^.value := TodoNormal.toString, "Normal"),
<.option(^.value := TodoLow.toString, "Low")
)
)
)
}).build
State is first initialized with the provided item or with a new empty item. Within the render
method a new Modal
is created and in the properties we assign
couple of button controls. Note how both header
and footer
are actually functions that are given the Modal
's Backend
so that they can call the hide()
function. In the OK button the state is first updated to not cancelled.
The form itself is quite straightforward, with handlers to update internal state as fields change. Note that with React the select
element works a bit
differently from regular HTML5 and you must use value
property to select the option instead of the typical selected
attribute.
When the form closes, the parent's submitHandler
gets called with the item and a flag indicating if the dialog box was cancelled.
def formClosed(): Unit = {
// call parent handler with the new item and whether form was OK or cancelled
t.props.submitHandler(t.state.item, t.state.cancelled)
}
But now it's time to get to the bottom of client-server communications!
Web clients communicate with the server most commonly with Ajax which is 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 as our friend Li Haoyi (lihaoyi) has created and published two great libraries called uPickle and Autowire.
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.
// client side
object AjaxClient extends autowire.Client[String, upickle.Reader, upickle.Writer]{
override def doCall(req: Request): Future[String] = {
dom.extensions.Ajax.post(
url = "/api/" + req.path.mkString("/"),
data = upickle.write(req.args)
).map(_.responseText)
}
def read[Result: upickle.Reader](p: String) = upickle.read[Result](p)
def write[Result: upickle.Writer](r: Result) = upickle.write(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 uPickle for serialization.
// server side
object Router extends autowire.Server[String, upickle.Reader, upickle.Writer]{
def read[Result: upickle.Reader](p: String) = upickle.read[Result](p)
def write[Result: upickle.Writer](r: Result) = upickle.write(r)
}
Now that you have the AjaxClient
set up, calling server is as simple as
import scalajs.concurrent.JSExecutionContext.Implicits.runNow
import autowire._
AjaxClient[Api].getTodos().call().foreach { todos =>
println(s"Got some things to do $todos")
}
Note that you need those two imports to access the Autowire 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)
}
Please check out uPickle 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?
The tutorial server is very simplistic and does not represent a typical Spray application, but it's enough to provide some basic support for the client side. Routing logic on the server side is defined using Spray DSL
startServer("0.0.0.0", port = port) {
get {
pathSingleSlash {
// serve the main page
if(Config.productionMode)
getFromResource("web/index-full.html")
else
getFromResource("web/index.html")
} ~
// serve other requests directly from the resource directory
getFromResourceDirectory("web")
} ~ post {
path("api" / Segments) { s =>
extract(_.request.entity.asString) { e =>
complete {
// handle API requests via autowire
Router.route[Api](apiService)(
autowire.Core.Request(s, upickle.read[Map[String, String]](e))
)
}
}
}
}
}
The main HTML page and related resources are provided directly from the project resources directory (coming from the shared
sub-project, actually).
The interesting part is handling api
requests using Autowire router. Like on the client side, Autowire takes care of the complicated stuff
so you just need to plug it in and let it do its magic. The ApiService
is just a normal class and it doesn't need to concern itself with
serialization or URL request path mappings. It just implements the same Api
as it used on the client side. Simple, eh!
class ApiService extends Api {
var todos = Seq(
TodoItem("1", "Wear shirt that says 'Life'. Hand out lemons on street corner.", TodoLow, false),
TodoItem("2", "Make vanilla pudding. Put in mayo jar. Eat in public.", TodoNormal, false),
TodoItem("3", "Walk away slowly from an explosion without looking back.", TodoHigh, false),
TodoItem("4", "Sneeze in front of the pope. Get blessed.", TodoNormal, true)
)
override def motd(name: String): String = s"Welcome to SPA, $name! Time is now ${new Date}"
override def getTodos(): Seq[TodoItem] = {
// provide some fake Todos
todos
}
// update a Todo
override def updateTodo(item: TodoItem): Unit = {
// TODO, update database etc :)
println(s"Todo item was updated: $item")
if(todos.exists(_.id == item.id)) {
todos = todos.collect {
case i if i.id == item.id => item
case i => i
}
} else {
// add a new item
todos :+= item.copy(id = UUID.randomUUID().toString)
}
}
}
Testing Scala.js application is as easy as testing regular Scala applications, except you have to choose a test framework that is OK with the limitations of the JavaScript environment. Many popular frameworks like ScalaTest and Specs2 depend on JVM features (like reflection) that are not available in the JS land, so Li Haoyi went ahead and created uTest, a simple yet powerful testing framework that works wonderfully well with Scala.js.
To define tests, you just need to extend from TestSuite
and override the tests
method.
object DispatcherTests extends TestSuite {
override def tests = TestSuite {
'test { ... }
}
}
Take a look at DispatcherTests.scala
for some examples of test cases.
To run tests in SBT, you'll need to add a dependency for "com.lihaoyi" % "utest" % "0.3.0"
and configure the test framework with
testFrameworks += new TestFramework("utest.runner.Framework")
. Now you can run the tests using regular test
and testOnly
commands
in the SBT prompt.
Scala.js supports source maps, making debugging your code in the browser quite easy. You can set breakpoints in the original source-code and inspect local variables etc. just like in a real IDE debugger. See the documentation for your browser developer tools for more information.
By default Scala.js generates source-maps using local file paths, but for some reason this doesn't work on (my) Chrome, even though it works just fine in Firefox. To get around this limitation, we'll modify the source-map to use relative URLs and serve source files directly from our web server. To remap source paths, use following configuration in the build file.
scalacOptions ++= Seq({
val a = js.base.toURI.toString.replaceFirst("[^/]+/?$", "")
s"-P:scalajs:mapSourceURI:$a->/srcmaps/"
})
This will replace the local file path with a relative URL starting with /srcmaps/
which we will then use on our server to provide the source files.
In the server code you'll need to add following:
pathPrefix("srcmaps") {
if(!Config.productionMode)
getFromDirectory("../")
else
complete(StatusCodes.NotFound)
}
Note how serving source files is only enabled in development builds. As the server is running in the jvm
directory, the relative path ../
will point
to the directory directly above it, which will allow access to the source file under js/src
. If your project configuration is different, you may
need to change this.
When running the application, you can access the sources through the developer tools window as shown below.
You can set breakpoints and investigate variables to see what's going on in your code. Some of the variable names have funny extensions like completed$1
but this is just due to name mangling by Scala. Below you can see how the debugger has hit a breakpoint and the local variables are displayed automatically.
You might also want to install Facebook React DevTools into your Chrome browser to help visualize active React components in the DevTools window.
Most of us are used to great logging infrastructure available on the server side and sometimes it helps in situations where direct debugging is difficult or plain impossible (for example a customer is using your app). Luckily there are nice logging libraries available also for Javascript that we can utilize through thin facades.
To emulate log4j style API, we'll define a LoggerFactory
providing Logger
instances. These will hook up to the underlying
Javascript library to provide the real functionality. See
Log4JavaScript.scala
for details.
The package object logger
provides a default logger called log
for easy access to logging functionality. If you want to create separate loggers,
you may do so through LoggerFactory.getLogger(name)
method. To create an entry in the log, all you need to do is call the appropriate log-level function
with a message and an optional Exception
.
log.debug(s"User selected ${items.size} items")
log.error("Invalid response from server", ex)
The default logger prints all messages to the browser console, but you can also use a more advanced popup window logger, to analyze log messages with
better granularity and filtering. Use getPopupLogger
to create such logger.
The logging library also provides functionality to send all your log messages to the server, for easier analysis in error situations. It packages each log message into a small JSON object and POSTs it to the specified URL. On the server side we define a path to receive and print those log messages.
path("logging") {
entity(as[String]) { msg =>
ctx =>
println(s"ClientLog: $msg")
ctx.complete(StatusCodes.OK)
}
}
To enable server side logging, call log.enableServerLogging("/logging")
. In the server logs the client log messages will be shown as below:
sharedProjectJVM ClientLog: [{"logger":"Log","timestamp":1425500652089,"level":"INFO","url":"http://localhost:8080/#todo","message":"This message goes to server as well"}]
sharedProjectJVM Sending 4 Todo items
sharedProjectJVM Sending 4 Todo items
sharedProjectJVM Todo item was updated: TodoItem(3,Walk away slowly from an explosion without looking back.,TodoHigh,false)
sharedProjectJVM ClientLog: [{"logger":"Log","timestamp":1425500661456,"level":"DEBUG","url":"http://localhost:8080/#todo","message":"User is editing a todo"}]
sharedProjectJVM ClientLog: [{"logger":"Log","timestamp":1425500664865,"level":"DEBUG","url":"http://localhost:8080/#todo","message":"Todo editing cancelled"}]
sharedProjectJVM ClientLog: [{"logger":"Log","timestamp":1425500668485,"level":"DEBUG","url":"http://localhost:8080/#todo","message":"User is editing a todo"}]
sharedProjectJVM ClientLog: [{"logger":"Log","timestamp":1425500671017,"level":"DEBUG","url":"http://localhost:8080/#todo","message":"User is editing a todo"}]
sharedProjectJVM ClientLog: [{"logger":"Log","timestamp":1425500671751,"level":"DEBUG","url":"http://localhost:8080/#todo","message":"User is editing a todo"}]
sharedProjectJVM ClientLog: [{"logger":"Log","timestamp":1425500672101,"level":"DEBUG","url":"http://localhost:8080/#todo","message":"Todo edited:
TodoItem(3,Walk away slowly from an explosion without looking back.,TodoNormal,false)"}]
Many of the advanced features of the underlying logging library are not exposed in this tutorial project, but you can look at the current implementation and continue from there if you find the missing features useful!
While extensive debug messages are a life saver in development, you don't want to flood your customer's browser console with debug messages. To disable
low level log messages in the production build we'll use a special Scala annotation called @elidable.
It works much like #ifdef
in C/C++ removing the function and all calls to the function in the final byte-code. Therefore there is no performance penalty
whatsoever even if you litter your code with hundreds of log.debug
calls, as they are all totally optimized away.
@elidable(FINEST) def trace(msg: String, e: Exception): Unit
@elidable(FINEST) def trace(msg: String): Unit
@elidable(FINE) def debug(msg: String, e: Exception): Unit
@elidable(FINE) def debug(msg: String): Unit
@elidable(INFO) def info(msg: String, e: Exception): Unit
@elidable(INFO) def info(msg: String): Unit
To control what calls to eliminate and what to keep, use a scalac
command line option -Xelide-below <level>
. This is automatically set to WARNING
in
the release command.
// in settings we have scalacOptions ++= elideOptions.value,
set elideOptions in js := Seq("-Xelide-below", "WARNING")
In the production build all log calls below the WARNING
level will be optimized away.
Since Scala.js is quite new and it's been evolving even rather recently, building Scala.js applications with SBT is not as clear as it could be. Yes, the documentation and tutorials give you the basics, but what if you want something more, like put the output of the Scala.js compiler into another directory?
The build.scala
in this tutorial shows you some typical cases you might run into in your own application.
Scala.js SBT Workbench plugin enables quick development by serving client files straight from SBT
without the need for your own server. The relevant part in build.scala
is
// define where the JS-only application will be hosted by the Workbench plugin
localUrl :=("localhost", 13131),
refreshBrowsers <<= refreshBrowsers.triggeredBy(fastOptJS in Compile),
bootSnippet := "SPAMain().main();"
To use the Workbench plugin, point your browser to http://localhost:13131/jvm/target/scala-2.11/classes/web/index.html. Whenever you run fastOptJS
the plugin will automatically refresh the browser with an updated version. How's that for fast turn-around times!
But since Scala.js compile output is stored under the JS project by default, how can we serve it from the JVM project? The solution is to instruct Scala.js to save its output in a specific directory under the JVM project and then make a copy back to the JS side. For this you need a bit more SBT code to make it work
// configure a specific directory for scalajs output
val scalajsOutputDir = Def.settingKey[File]("directory for javascript files output by scalajs")
// make all JS builds use the output dir defined later
lazy val js2jvmSettings = Seq(fastOptJS, fullOptJS, packageJSDependencies) map { packageJSKey =>
crossTarget in(spaJS, Compile, packageJSKey) := scalajsOutputDir.value
}
// instantiate the JS project for SBT with some additional settings
lazy val spaJS: Project = spa.js.settings(
fastOptJS in Compile := {
// make a copy of the produced JS-file (and source maps) under the spaJS project as well,
// because the original goes under the spaJVM project
// NOTE: this is only done for fastOptJS, not for fullOptJS
val base = (fastOptJS in Compile).value
IO.copyFile(base.data, (classDirectory in Compile).value / "web" / "js" / base.data.getName)
IO.copyFile(base.data, (classDirectory in Compile).value / "web" / "js" / (base.data.getName + ".map"))
base
}
)
// instantiate the JVM project for SBT with some additional settings
lazy val spaJVM: Project = spa.jvm.settings(js2jvmSettings: _*).settings(
// scala.js output is directed under "web/js" dir in the spaJVM project
scalajsOutputDir := (classDirectory in Compile).value / "web" / "js",
// reStart depends on running fastOptJS on the JS project
Revolver.reStart <<= Revolver.reStart dependsOn (fastOptJS in(js, Compile))
)
In addition to the compiled Scala.js code, you need other resources such as the index.html
some CSS and JS files for your application to work.
You should store these under the shared
project resources and let the JS/JVM project know about these extra resource directories.
val sharedSrcDir = "shared"
// copy resources from the "shared" project
unmanagedResourceDirectories in Compile += file(".") / sharedSrcDir / "src" / "main" / "resources",
unmanagedResourceDirectories in Test += file(".") / sharedSrcDir / "src" / "test" / "resources",
WebJars are a wonderful way to simplify inclusion of external resources such as JavaScript libraries and CSS definitions into your own project. Instead of downloading JS/CSS packages like Bootstrap and extracting them within your project (or referring to external CDN served resources), you can just add a dependency to the appropriate WebJar and you're all set! Well, except you aren't, unless you were using something like Play, which this tutorial isn't using. So how do you actually get those resources out of a WebJar and into your web resource directory? The process is a bit different for JavaScript, CSS (LESS) and other resources files.
Scala.js SBT plugin offers a nice and convenient way for extracting JavaScript sources from various
WebJars and concatenating them into a single JavaScript file that you can then refer to in your index.html
. In the tutorial project this means following
configuration in the build.scala
file:
/** Dependencies for external JS libs that are bundled into a single .js file according to dependency order */
val jsDependencies = Def.setting(Seq(
"org.webjars" % "react" % versions.react / "react-with-addons.js" commonJSName "React",
"org.webjars" % "jquery" % versions.jQuery / "jquery.js",
"org.webjars" % "bootstrap" % versions.bootstrap / "bootstrap.js" dependsOn "jquery.js",
"org.webjars" % "chartjs" % versions.chartjs / "Chart.js",
"org.webjars" % "log4javascript" % versions.log4js / "js/log4javascript_uncompressed.js"
))
.jsSettings(
jsDependencies ++= {if (!productionBuild.value) Settings.jsDependencies.value else Settings.jsDependenciesProduction.value},
jsDependencies += RuntimeDOM % "test",
skip in packageJSDependencies := false,
This will produce a file named web/js/scalajs-spa-jsdeps.js
containing all those JavaScript files combined.
For extracting CSS files from WebJars you could use the method described below, but there is bit more convenient method that gives you LESS
processing as a bonus. First we'll need to add the sbt-less plugin into our plugins.sbt
addSbtPlugin("com.typesafe.sbt" % "sbt-less" % "1.0.6")
The projects also need to enable the sbt-web
plugin for this to work.
jvm.enablePlugins(SbtWeb)
js.enablePlugins(SbtWeb)
Because sbt-less
and other sbt-web
plugins are heavily related to the Play framework, we'll need to do some additional configuration to make it work with
our simple tutorial. We'll be storing LESS files under src/main/assets
to keep them separated from directly copied resources.
import com.typesafe.sbt.less.Import._
sourceDirectory in Assets := baseDirectory.value / "src" / "main" / "assets",
LessKeys.compress in Assets := true,
The last line tells the LESS compiler to minify the produced CSS.
Next step is to create a main.less
(yes, it has to be named exactly that) with references to CSS/LESS files inside the WebJars.
Note that due to a bug in sbt-web
we cannot put the mail.less
under the shared
project where it belongs, but
must duplicate it to both JS and JVM projects.
@import "lib/bootstrap/less/bootstrap.less";
@import "lib/font-awesome/less/font-awesome.less";
body {
padding-top: 50px;
}
.label-as-badge {
border-radius: 1em;
}
In this case we import Bootstrap and Font Awesome LESS files. Depending on the WebJar, it may or may not contain LESS files in addition to the CSS file. With
the LESS files you can easily configure the library to your liking by defining CSS variables in your main.less
file.
@import "lib/bootstrap/less/bootstrap.less";
@import "lib/font-awesome/less/font-awesome.less";
@brand-danger: #00534f;
The problem with the sbt-less
plugin is that it saves the compiled CSS file into wrong directory (since we are not using Play), so we need to copy it
to the correct location. This will be covered in the next section.
Sometimes WebJars contain other useful resources, such as the font files for Font Awesome in our case. Just including the WebJar as a dependency will provide us the extracted contents, but it's in the wrong place and doesn't get bundled into the final package. So we will need to copy relevant files where we want them.
To achieve this, we'll define our very own SBT task to do the job.
val copyWebJarResources = TaskKey[Unit]("Copy resources from WebJars")
copyWebJarResources := {
// copy the compiled CSS
val s = streams.value
s.log("Copying webjar resources")
val compiledCss = webTarget.value / "less" / "main" / "stylesheets"
val targetDir = (classDirectory in Compile).value / "web"
IO.createDirectory(targetDir / "stylesheets")
IO.copyDirectory(compiledCss, targetDir / "stylesheets")
// copy font-awesome fonts from WebJar
val fonts = (webModuleDirectory in Assets).value / "webjars" / "lib" / "font-awesome" / "fonts"
IO.createDirectory(targetDir / "fonts")
IO.copyDirectory(fonts, targetDir / "fonts")
}
Without going into the SBT details, this task copies the generated CSS file (and its sourcemap) into web/stylesheets
. It also copies all files found
under the Font Awesome fonts
folder into the appropriate place under our project. If you need to copy other resources, you can use the same code just by
modifying the path definitions.
Finally we need to tell SBT to actually run this task at the appropriate moment. Since we need the compiled CSS output, it has to happen after the fact, but it also has to happen before the application is finally put together so that the files are in place for packaging or running the app.
// run the copy after compile/assets but before managed resources
copyWebJarResources <<= copyWebJarResources dependsOn (compile in Compile, assets in Compile),
managedResources in Compile <<= (managedResources in Compile) dependsOn copyWebJarResources
NOTE! This whole procedure is IMHO a dirty hack, but couldn't find a better solution. If you do know a better way to do this, please create an issue.
For fast development, it's nice to have the SBT console available even while the server is running. So instead of spaJVM/run
you should use
the great Revolver plugin and run the server with re-start
and re-stop
. This way you can start your
server and then instruct SBT to track changes to the client code with ~fastOptJS
and all your client changes are automatically deployed
without restarting your server.
To configure the Revolver you'll need the following
import spray.revolver.RevolverPlugin._
...
jvmSettings(Revolver.settings: _*).
...
// set some basic options when running the project with Revolver
javaOptions in Revolver.reStart ++= Seq("-Xmx1G"),
// configure a specific port for debugging, so you can easily debug multiple projects at the same time if necessary
Revolver.enableDebugging(port = 5111, suspend = false)
So far we have been interested in having a nice and fast development cycle, without worrying about optimization. For your production release you'll want
to have a very optimized and small code-base to make the application perform well for the end user. To build an optimized version of the application
you just need to use fullOptJS
instead of fastOptJS
. This will produce a JavaScript file with -opt.js
extension, so the server must know to serve
this file instead of the non-optimized version.
To achieve this, we need to add a configuration parameter that can be affected from outside the application. Typesafe provides a nice
config library for application configuration data so we'll be using it. The library automatically reads
reference.conf
and application.conf
(amongst a few others) from resources, so all you need to is to add an application.conf
under resources
.
Our configuration is extremely simple:
spatutorial {
productionMode = false
productionMode = ${?PRODUCTION_MODE}
}
These configs can be overridden by having a separate application.conf
in your built application conf-directory, but we are using a system
environment variable to override the single setting. By default productionMode
is false
but if the env variable PRODUCTION_MODE
is set,
it will override the default.
MainApp
loads this configuration into a Config
object.
object Config {
val c = ConfigFactory.load().getConfig("spatutorial")
val productionMode = c.getBoolean("productionMode")
}
The productionMode
parameter is only used to determine which HTML file to serve.
if(Config.productionMode)
getFromResource("web/index-full.html")
else
getFromResource("web/index.html")
Typically JS libraries provided in WebJars come in two variants: normal and minified (.min.js
). The latter is highly compressed and often also optimized
version of the more verbose normal version. For example debug-prints and development time checks have been removed. Therefore it makes sense to use these
pre-packaged minified versions instead of running the minification process yourself.
We need to define a separate list of JS dependencies for the production build, using the .min.js
versions:
val jsDependenciesProduction = Def.setting(Seq(
"org.webjars" % "react" % versions.react / "react-with-addons.min.js" commonJSName "React",
"org.webjars" % "jquery" % versions.jQuery / "jquery.min.js",
"org.webjars" % "bootstrap" % versions.bootstrap / "bootstrap.min.js" dependsOn "jquery.min.js",
"org.webjars" % "chartjs" % versions.chartjs / "Chart.min.js",
"org.webjars" % "log4javascript" % versions.log4js / "js/log4javascript.js"
))
Because packaging the JS libs is a different task in SBT (it doesn't relate to fastOptJS
or fullOptJS
) we need another way to tell SBT to use these
dependencies instead of the normal ones. For that we'll define a settingKey
val productionBuild = settingKey[Boolean]("Build for production")
and set it in the JS project
.jsSettings(
productionBuild := false,
jsDependencies ++= {if (!productionBuild.value) Settings.jsDependencies.value else Settings.jsDependenciesProduction.value},
To do a production build, you'll need to override the setting on SBT command line:
set productionBuild in js := true
If you now run packageJSDependencies
it will use the minified versions and build a smaller -jsdeps.js
file.
Running your app from SBT is fine for development but not so great in production. To build an application package with all the JAR libraries included,
we can use the sbt-native-packager
plugin.
// in plugins.sbt
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.0.0-RC1")
// in build.scala
jvm.enablePlugins(JavaAppPackaging)
This is all you need to enable the stage
command in SBT (under the JVM project), which will build and package the application for you in the
target/universal/stage
directory. You may run the application with bin/scalajs-spa
(or bin\scalajs-spa.bat
in Windows).
We can configure the packager to set the PRODUCTION_BUILD
environment variable automatically in the script with the following settings
NativePackagerKeys.batScriptExtraDefines += "set PRODUCTION_MODE=true",
NativePackagerKeys.bashScriptExtraDefines += "export PRODUCTION_MODE=true",
Even though we have all the pieces to build and package the application, it can be quite tedious to run several SBT commands one after another
to get everything done. That's what computers are really good at, so let's build a special Command
to do the release.
val ReleaseCmd = Command.command("release") {
state => "set productionBuild in js := true" ::
"set elideOptions in js := Seq(\"-Xelide-below\", \"WARNING\")" ::
"sharedProjectJS/test" ::
"sharedProjectJS/fullOptJS" ::
"sharedProjectJS/packageJSDependencies" ::
"sharedProjectJVM/test" ::
"sharedProjectJVM/stage" ::
"set productionBuild in js := false" ::
"set elideOptions in js := Seq()" ::
state
}
and enable it in the root project
commands += ReleaseCmd,
With this command, you can just execute release
under the root
and SBT will run all those individual commands to build
your application package.
No questions asked so far!
- authentication/authorization support
- you tell me!