Entrypoint Example
An Entrypoint
is a method on the client-side, that you must call in order to start your webapp.
A typical webapp will be served like this:
<script type="text/javascript" src="/my-app.js"></script>
<script type="text/javascript">
MyExampleApp.start();
</script>
Why?
Generating this stuff manually isn't a huge deal, but in this example we'll use the Entrypoint
API
for a few advantages:
- Everything is DRY — no chance to accidentally call the wrong function
- The server can provide custom data to initialise our app — just need to provide a codec, ser/deser and JS plumbing handled automatically
- We avoid a typical AJAX call to initialise our app — faster and better experience for users
- HTML is generated for us — things like escaping handled automatically
Shared Definition
We'll start with our entrypoint definition, which is cross-compiled for JVM and JS.
package japgolly.webapputil.examples.entrypoint
import boopickle.DefaultBasic._
import japgolly.webapputil.boopickle._
import japgolly.webapputil.entrypoint._
object EntrypointExample {
// The name of our app, as it will appear in the JS global namespace when loaded.
// This is final because its referenced via @JSExportTopLevel on Frontend
final val Name = "MyExampleApp"
// In this example, our app will request the user's username as soon as it starts up.
// The server will provide this to the client.
final case class InitialData(username: String)
// This is our codec typeclass from InitialData to binary and back
implicit val picklerInitialData: Pickler[InitialData] =
implicitly[Pickler[String]].xmap(InitialData.apply)(_.username)
// Finally, our entrypoint definition.
//
// The Pickler[InitialData] we created above is pulled in implicitly.
// (Note: Binary is just one supported format, and not at all a necessity.)
//
val defn = EntrypointDef[InitialData](Name)
}
Frontend
Next we'll create our Scala.js frontend.
package japgolly.webapputil.examples.entrypoint
import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.html_<^._
import japgolly.webapputil.entrypoint._
import japgolly.webapputil.examples.entrypoint.EntrypointExample.InitialData
import scala.scalajs.js.annotation.JSExportTopLevel
@JSExportTopLevel(EntrypointExample.Name) // Instruct Scala.js to make this available in
// the global JS namespace.
object Frontend extends Entrypoint(EntrypointExample.defn) {
// Because the line above extends Entrypoint, all we need to do now is implement a
// run method that takes the decoded InitialData value.
override def run(i: InitialData): Unit = {
// Render a simple React component
val reactApp = Component(i)
reactApp.renderIntoDOM(`#root`) // `#root` is a helper to find DOM with id=root
}
val Component = ScalaComponent.builder[InitialData]
.render_P { i => <.div(s"Hello @${i.username} and nice to meet you!") }
.build
}
Backend
Because we initialise our webapp with a username, the backend server needs to generate different HTML depending on who the request is for.
A few important things are out of scope for this demo:
- how to retrieve a username depends on your app
- how to serve HTML depends on your choice of web server
- how to serve the frontend JS depends on your app and your choice of web server!
In our example below we simply focus on InitialData => Html
.
package japgolly.webapputil.examples.entrypoint
import japgolly.webapputil.entrypoint._
// Here we demonstrate how a backend web server can generate HTML to have the client
// invoke the entrypoint and start the app.
object Backend {
private val invoker = EntrypointInvoker(EntrypointExample.defn)
// It's as simple as this: provide an input value, get HTML.
//
// You can expect to see something like this:
//
// <script type="text/javascript" src="data:application/javascript;base64,…"></script>
//
// which is morally equivalent to:
//
// <script>
// MyExampleApp(encoded(initialData))
// </script>
//
// We'll look at this in more detail in the next section.
//
def generateHtml(i: EntrypointExample.InitialData): Html =
invoker(i).toHtmlScriptTag
// The same as above, except instead of having the invocation occur immediately,
// it schedules it to run on window.onload.
//
// This is morally equivalent to:
//
// <script>
// window.onload = function() {
// MyExampleApp(encoded(initialData))
// }
// </script>
//
// We'll look at this in more detail in the next section, too.
//
def generateHtmlToRunOnWindowLoad(i: EntrypointExample.InitialData): Html =
invoker(Js.Wrapper.windowOnLoad, i).toHtmlScriptTag
}
Backend Test
The only real value in this test is that our Pickler[InitialData]
serialises and deserialises correctly.
Everything else is just to demonstrate how exactly how the generated HTML works.
package japgolly.webapputil.examples.entrypoint
import boopickle._
import japgolly.microlibs.testutil.TestUtil._
import japgolly.univeq.UnivEq
import japgolly.webapputil.binary.BinaryData
import utest._
object BackendTest extends TestSuite {
import EntrypointExample.InitialData
// Declare that == is fine for testing equality of InitialData instances
private implicit def univEqInitialData: UnivEq[InitialData] = UnivEq.derive
// Sample data
private val initData = InitialData(username = "someone123")
// The base64 encoding of `initData` after binary serialisation
private val initData64 = "CnNvbWVvbmUxMjM="
// Helper to parse BinaryData into InitialData
private def deserialiseInitialData(b: BinaryData): InitialData =
UnpickleImpl(EntrypointExample.picklerInitialData).fromBytes(b.unsafeByteBuffer)
override def tests = Tests {
// =================================================================================
// Verify the initData64 deserialises to initData
"pickler" - assertEq(
deserialiseInitialData(BinaryData.fromBase64(initData64)),
initData)
// =================================================================================
// Let's start with our Backend.generateHtml() method
"generateHtml" - {
// Verify the total HTML output
val js64 = "TXlFeGFtcGxlQXBwLm0oIkNuTnZiV1Z2Ym1VeE1qTT0iKQ=="
assertEq(
Backend.generateHtml(initData).asString,
s"""<script type="text/javascript" src="data:application/javascript;base64,$js64"></script>""")
// Verify the JS after base64 decoding
assertEq(
BinaryData.fromBase64(js64).toStringAsUtf8,
s"""MyExampleApp.m("$initData64")""")
}
// =================================================================================
// As above, but the RunOnWindowLoad variant
"generateHtmlToRunOnWindowLoad" - {
// Verify the total HTML output
val js64 = "d2luZG93Lm9ubG9hZD1mdW5jdGlvbigpe015RXhhbXBsZUFwcC5tKCJDbk52YldWdmJtVXhNak09Iil9Ow=="
assertEq(
Backend.generateHtmlToRunOnWindowLoad(initData).asString,
s"""<script type="text/javascript" src="data:application/javascript;base64,$js64"></script>""")
// Verify the JS after base64 decoding
assertEq(
BinaryData.fromBase64(js64).toStringAsUtf8,
s"""window.onload=function(){MyExampleApp.m("$initData64")};""")
}
}
}