-
Notifications
You must be signed in to change notification settings - Fork 48
Kontraktor 4 React JSX
Kontraktor-Http comes with:
- jnpm: npm-reimplementation for automatic download and resolvement of es6 imports
- a JSX transpiler (for .jsx files)
- built in bundling+minification (optional: transpilation to es-5 with integrated google clojure compiler)
- a "live editing" / "hot reloading" feature which let's you modify .jsx files without losing application / component state
see example here
Development requires an up-to-date chrome/safari/firefox browser. I am testing with chrome dev tools, so its recommended to use chrome for development.
Requires a jdk 8, no node/npm installation is required.
In development mode, http4k does not bundle single jsx files in order to ease debugging. Transpiled files are available in a (virtual) debug folder. E.g. chrome:
As one can see, jsx code is expanded in the transpiled files.
In production mode (same as usual) http4k just inlines and minifies everything, so the example reduces to a single request (index.html) with a size of 100kb.
Kontraktor provides a pure java implementation of npm. Once an import is discovered inside a .jsx file, jnpm automatically installs required libraries and their dependencies in a 'node_modules' subdirectory on demand.
differences to npm:
- only a flat 'node_modules' directory is used/searched. Cascaded/global 'node_module' directories are not supported.
- jnpm does not read/write a 'package.json' file. Instead it reads 'jnpm.kson' where one can place version boundaries for specific npm modules (see example project). It's possible to use npm later on as jnpm's creates exactly the same folder structure as npm.
- in order to "make clean", just delete your 'node_module' and press refresh on the browser, jnpm will automatically download and reinstall all missing packages then.
- jnpm never updates node modules automatically (it sticks with the version found in local 'node_modules'). See 'make clean' above on how to update you local npm modules to newest versions.
Once the first request is done to a .jsx based webapp, jnpm will try to install missing libraries found in import
statements. E.g. ìmport React, {Component} from 'react'
will install react to configured node_modules dir if not already present.
Example: a http4k webserver config with jsx transpilation + http+websocket based API.
Http4K.Build("localhost", 8080)
.resourcePath("/")
.elements("./src/main/web/client","./src/main/web/lib","./src/main/web/node_modules")
.transpile("jsx",
new JSXIntrinsicTranspiler(DEVMODE)
.configureJNPM("./src/main/web/node_modules","./src/main/web/jnpm.kson")
.autoJNPM(true)
)
.allDev(DEVMODE)
.buildResourcePath()
.httpAPI("/api", app)
.serType(SerializerType.JsonNoRef)
.setSessionTimeout(10_000) // extra low to showcase session resurrection
.buildHttpApi()
.websocket("/ws",app)
.serType(SerializerType.JsonNoRef)
.sendSid(true)
.buildWebsocket()
.build();
Example: jnpm.kson config
JNPMConfig {
versionMap:
{
react : "~15.6.1"
react-dom : "~15.6.1"
material-ui: "~0.19.0"
}
repo: "http://registry.npmjs.org/"
transformFunction: "React.createElement"
}
In devmode, kontraktor supports a blazing fast live editing feature for react apps. A downside of this is, that breakpoints done from browser's debug tools won't work anymore. Use the debugger;
statement (via live reloading) to workaround this (AKA: write 'debugger;' where your want to debug, then save for live-reload).
The following limits apply to kontraktor-http's hot reloading
- only top level definitions can be live reloaded (classes, functions, objects)
- Live Editing is enabled only for application files (code in node_modules directory is not instrumented).
- top level object structures are overwritten, if you want to keep module-local application state, mark objects/functions by setting `statefulObject._kNoHMR=true'.
- need to use 'debugger;' statement to set breakpoints
- a module is re-executed on hot reload. If you do initial stuff (set-up network connections, init your store) in a module's init code, ensure this kind of stuff is not re-executed on hot-reload by testing
if ( typeof _kHMR === 'undefined') { // code here is executed once, not on module-hot-reload}
. - a new import statement does not require a full refresh if it has been loaded in your app before (So adding a function to a loaded module and importing it from another module works, creating a completely new .jsx file requries a full refresh).
- use React 16's ErrorBoundaries in order to avoid "blank screen" in case you 'hot-reload' an error destroying your component tree (React 16 only). Simply add
componentDidCatch(error, info) {console.log(error, info);}
to your major container components. - static methods of classes are not hot reloaded currently
In order to enable hot-reload:
- enable it on the transpiler
.hmr(DEVMODE)
- publish kontraktors devmode only filewatcher with
.hmrServer(DEVMODE)
- Important: In order to avoid losing state by redefining your main app with each reload, put your root render statement in a hot-reload detection condition like:
if ( typeof _kHMR === 'undefined' ) { // only load once, not when doing hot reloading
global.app = <App/>;
// required for hot reloading
window._kreactapprender = ReactDOM.render(global.app,document.getElementById("root"));
}
- set global
window._kreactapprender
to the result ofReactDOM.render
(see above). - add
import {KClient} from 'kontraktor-client
statement to your index.jsx
Unfortunately currently function scope is lost upon patching, with the following
function createTextEditor(width) {
return (value,rowcfg,rowdata,props) => {
return <Form><TextArea style={{width: width}} value={value}/></Form>;
}
}
export const editText = createTextEditor(400);
you'll get an 'width is undefined' error. To workaround this, add
createTextEditor._kNoHMR = true;
editText._kNoHMR = true;
This way the code will execute without error, however live editing won't work on those functions.
Browser's do not yet implement ES6 import's and even then, without broad adaption of http 2.0 packaging and bundling will be required for web apps (to many sync http-get requests).
The intrinsic jsx transpiler only supports a subset (sufficient for frontend apps) of the official spec.
Namely:
- The export keyword is ignored, instead kontraktor automatically exports all top level objects of a .jsx file.
- default exports are supported
- the
*
operator in imports is not supported
Example for import/export syntaxes understood by kontraktor-jsx
import { HashRouter, Route, Switch, Redirect } from 'react-router-dom'
import { AppActions,Store as AppStore } from "./store.jsx";
import React, {Component} from 'react';
export class AppStore extends EventEmitter { .. }
export default FeedsState;
In addition, the jsx parser does not provide too detailed error messages, so its recommended to use a jsx validating IDE for editing (e.g. intellij).
In development mode, kontraktor relies on es-6 browser support (e.g. chrome, firefox, ..), there are some exceptions:
-
object spread polyfill (really important for react) can be turned off (in case browsers implement it)
inJSXParser.SHIM_OBJ_SPREAD
. Note '...' is only polyfilled for objects not arrays or argument lists. - For all ES6 features beyond object spread and import/export you rely on the browser implementation (e.g. ...rest) in development mode, for production use clojure compiler integration to transpile down to es5.
The code for klibmap lookup and spread polyfill can be tweaked by overriding JSXIntrinsicTranspiler.getInitialShims()
.
When running in production mode, first request triggers bundling. The result is cached in memory, so changes to sources won't apply.
As this might be error prone (e.g. jnpm suddenly fetcing missing npm modules or so), its possible to generate a single bundle by setting a "distribution" directory in the 'resourcepath' builder.
// (PRODMODE only: look (and create if not present) static build artefact for budled index.html [avoids rebundling on first request in prodmode]
// Warning: you need to delete this file in order to force a rebuild then
.productionBuildDir(new File("./dist") )
If a prebuilt bundle is present, kontraktor-http will deliver this one instead of building one (in production mode). If the bundle is not present, kontraktor-http will generate and save it.
Add .jsPostProcessors(new ClojureJSPostProcessor()) // uses google clojure transpiler to ES5 (PRODMODE only)
to resourcepath builder.
By default kontraktor-http will just bundle and minify, so relies on ES6 support of browser then (fine with newer chrome/firefox/safari/edge).
The following builder example configures:
- a resourcepath pointing to the 'classpath' of your react/js frontend.
- registers a transpiler for .jsx files
- defines two communication endpoints to let you app talk to the java backend a) http long-poll b) websockets
- sets up a 'distribution' directory to get a statically saved build
- enables on-demand jnpm module installation (devmode)
- enables hot-reload (attention: some tweaks to app code reqired, see above)
- sets a session timeout for kontraktors built in actor-per client session handling
Http4K.Build("localhost", 8080)
.resourcePath("/")
.elements("./src/main/web/client","./src/main/web/lib","./src/main/web/node_modules")
.transpile("jsx",
new JSXIntrinsicTranspiler(DEVMODE)
.configureJNPM("./src/main/web/node_modules","./src/main/web/jnpm.kson")
.autoJNPM(true)
.hmr(DEVMODE) // support required for hot reloading
)
.allDev(DEVMODE)
.jsPostProcessors(new ClojureJSPostProcessor()) // uses google clojure transpiler to ES5 (PRODMODE only)
// (PRODMODE only: look (and create if not present) static build artefact for budled index.html [avoids rebundling on first request in prodmode]
// Warning: you need to delete this file in order to force a rebuild then
.productionBuildDir(new File("./dist") )
.buildResourcePath()
.httpAPI("/api", app)
.serType(SerializerType.JsonNoRef)
.setSessionTimeout(10_000) // extra low to showcase session resurrection
.buildHttpApi()
.websocket("/ws",app)
.serType(SerializerType.JsonNoRef)
.sendSid(true)
.buildWebsocket()
.hmrServer(DEVMODE) // hot reloading file tracking server
.build();
Recommendation: If one makes use of kontraktor's "java-npm" implementation (or directly uses npm), definition of "klibmap" is superfluous.
Kontraktor-jsx supports a global map
of [library file name] => () => libraryObject
in order to "fake" imported library objects.
For javascript libraries exporting objects to global window
object, it might be required to
construct a 'virtual' library object.
Example (see 'events' entry in klibmap):
<!doctype html>
<html lang="" style="height: 100%;">
<head>
...
</head>
<body>
<div id="root" style="align-items: stretch; width: 100%;"></div>
<!-- manual import as es6 imports are shimmed for jsx files only -->
<script src="kontraktor-common.js"></script>
<script src="kontraktor-client.js"></script>
<script>
// in order to make UMD builds work with 'import' we need a map of library filename to library Object,
// as libs arent loaded yet, provide lambdas
klibmap = {
react: () => React,
history: () => History,
reactRouterDom: () => ReactRouterDOM,
events: () => { return { // events exposes global objects, so we need to construct a virtual lib object
EventEmitter: EventEmitter,
}}
};
</script>
<script src="index.jsx"></script>
<!-- take care: in dev mode index.jsx is loaded deferred (side effect of document.write -->
</body>
</html>
an import statement like import {Component} from 'react.js'
then simply looks up
klibmap['react']['Component']
. Note lib name is taken without the .js extension.
The polyfill still provides proper scope encapsulation: No definitions are leaked to the browsers
window
object,
Disclaimer: I am only testing under Linux, so there might be path related issues on wind or mac. Pls report.