Zorium v1.0.0 (╯°□°)╯︵ ┻━┻
The CoffeeScript Web Framework
Zorium (source) is the web framework we use at Clay.io.
Today it has finally reached version v1.0.0 (╯°□°)╯︵ ┻━┻
This has been one of the most challenging projects I've worked on, with many great lessons learned. What follows is how Zorium came to be what it is today, and an explanation of design decisions in the framework.
With Zorium (or any framework), the code is the easy part. The hard part is the design and architecture of an application. This is where Zorium stands out from the rest. Just start with Zorium Seed.
For the full documentation: https://zorium.org
Special thanks to Matt Esch and Jake Verbaten, without whom Zorium would not exist.
Example
Because every framework should start with code:
z = require 'zorium'
class AppComponent
constructor: ->
@state = z.state
name: 'Zorium'
render: =>
{name} = @state.getValue()
z 'div.zorium',
z 'p.text',
"The Future is #{name}"
z.render document.createElement('div'), new AppComponent()
# <div class="zorium"><p class="text">The Future is Zorium</p></div>
Notice:
- Components are just CoffeeScript classes
- There is no magic pre-processor (i.e. JSX)
- It's simple, intuitive, and idiomatic. No magic.
API Overview
The two most important methods in Zorium are z()
and z.state()
.
z()
is simply an extension of virtual-hyperscript, which understands Zorium components.
The most difficult implementation detail here was constructing an in-memory cached tree for efficient rendering.
Eventually this was solved with Thunks and traversing the virtual-tree (source).
# Component composition example
class Brick
render: ({name}) ->
z '.z-brick'.
"Hello #{name}"
class House
constructor: ->
@$brick = new Brick()
render: =>
z '.z-house',
z @$brick, {name: 'Zorium'}
z.state()
creates an instance of Rx.BehaviorSubject(),
with a set()
method for diffing the current value (and also some lazy-ness to make async more efficient).
Originally a simpler model using observ-struct was used, however it did not provide a declarative syntax for dealing with complex streams of data.
e.g. We want to display the number of likes of the users favorite game.
- If the user changes their favorite game, the value should update.
- If another user likes the game, the value should update.
# Note that we assume the models update themselves and emit streams.
z.state {
numLikes: Models.User.getMe().flatMapLatest ({favoriteGameId}) ->
return Models.Game.getById favoriteGameId
.map (game) ->
return game.numLikes
}
Zorium Paper
Zorium Paper was the first npm-installable set of components for Zorium. Here's what it looks like to use it:
# npm install zorium-paper
Button = require 'zorium-paper/button'
class Clicker
constructor: ->
@$button = new Button()
render: ->
z '.z-clicker',
z @$button,
$content: 'Click Me'
isRaised: true
The key to using an npm module and maintaining efficient css (separable styles for production) is to use webpack. Webpack lets Zorium components declaratively define styles which can be efficiently loaded using webpack without resorting to inefficient inlining or other duplication.
Just having webpack is not enough though. Without shadow-dom we don't have true CSS isolation. In order to let developers use semantic classes without conflicting with each other, a namespace pattern was developed:
if window?
require './index.styl'
class BigDrawer
render: ->
z '.z-big-drawer', # namespace
z '.blue', 'blue'
// index.styl
// namespace
.z-big-drawer
// direct children only
> .blue
background: blue
User-land components should use the z-
prefix, while library authors should namespace under a different prefix.
Server Side Rendering
Efficient server-side rendering is non-trivial. It requires application code to be written statelessly, and thus needs to be considered from the start of a project. In fact, the state issue was so elusive, it wasn't until after Zorium v1.0.0-rc15 that I realized a critical mistake I had made regarding concurrent requests.
After realizing my mistake, I decided to take a page from React, and implement a simpler z.renderToString()
(with async support).
Note that the server simply acts as a pre-renderer. Do not write server-side business logic here.
# server-side
app = express()
app.use (req, res, next) ->
z.renderToString z new App(), {req, res}
.then (html) ->
res.send '<!DOCTYPE html>' + html
.catch (err) ->
if err.html
log.error err
res.send '<!DOCTYPE html>' + err.html
else
next err
The built in router, z.router
, attempts to expose an express-like api. Note the difference in where new App()
is called, as client-side you want a stateful App for transitions.
$app = new App()
z.router.use (req, res) ->
{path, query} = req
res.send z $app, {req, res}
Sometimes it will be necessary to split code between node and client-side. Use window?
to check:
Promise = if window?
window.Promise
else
# Avoid webpack include
_Promise = 'promiz'
require _Promise
Zorium's boldest claim is probably that you can achieve nearly 100% code re-use for client and server-side code. Normally with other frameworks database requests are made and injected at different initialization points within the application.
This doesn't scale well for complex nested structures and requires duplicate effort in marshalling data. Zorium uses lazy-states and a platform-agnostic request library to create a seamless transition from client-side code to server-side code.
This area is probably the most complex because developers usually don't have to worry about state in this way. (e.g. creating singletons client-side is fine, but server-side will cause elusive caching bugs)
Comparison to other virtual-dom libraries
The Zorium framework is a collection of patterns, which are expressed in Zorium Seed. However, as a library is has some key differences from other popular virtual-dom libraries.
- React - The Facebook-backed library that started it all.
- React source-code is a convoluted mess. Extending it is nearly impossible. [1]
- The Flux architecture is unnecessarily complex. [2]
- React still has some clear deficiencies with complex animations. [3]
- The React API makes questionable design choices. [4][5][6]
- React source-code is a convoluted mess. Extending it is nearly impossible. [1]
- Mithril - A micro vritual-dom library which seemed quite promising.
- We initially used this at Clay for a few months (in a fork), but eventually got rid of it entirely.
- Source-code is quite complicated, making extending difficult.
- Manual state-management and rendering updates hurt scalability as our projects grew.
- Lack of structured best-practices lead to a lot of guessing and implementation mistakes.
Both React and Mithril use different virtual-dom implementations. Zorium uses the virtual-dom library, which is the standard behind many other virtual-dom frameworks (e.g. Mercury).
1: Seriously, this is incomprehensible. (vs z.render() for example)
2: Because Zorium supports RxJS Observables in state, async code is declaratively directional (and streaming) and thus avoids the data-binding issues Flux is said to address.
3: Inter-component communication with animations is nearly impossible (without an event-bus and side-effects), whereas Zorium simply uses RxJS observables to handle complex communications in a declarative way.
4: React.createClass()
(and extending React.Component) means non-idiomatic initialization. (vs. explicitly calling new Component()
)
5: JSX magic is not idiomatic code.
6: Mixins are also a mistake, they are opaque and imperative.