This post described how to create an application with mithril 0.2.x. Now that ver 1.0 is out, some things are a little differnent.
The example is updated with the current version of mithril, though.
I lately read a blog post about isomorphic javascript applications with ember. It seems pretty popular, a lot of people commented they badly wanted this feature.
Since I found this pretty interessting too since the beginning of my mithril work I already created this feature for my mithril-based application a time ago.
This blog post should give a overview, how to build a isomorphic mithril application.
This is an example app.
There are four major components of an application: model, view, controller and routes. The goal is to keep as much components as possible. Another basic requirement is the use of the same dependency resolver. Since node.js only supports commonjs (and also because it's awesome) we will use this also in frontend by leveraging the great browserify. For conveniance I'll leave the export
-statements out since it's pretty obvious what will be exported. If there are question, leave a comment.
mithril has a very thin controller layer. It's just a function that returns a value that is fed to the view function. So let's start with a simple controller:
function userController() {
var user = {
name: 'Frodo'
};
return {
user: user
};
}
Pretty thing is it's just a simple function without any browser dependencies. If your controller did not depend on any async fetching of data it can be the same on server side. Async fetching will be handeled later in the post.
The view layer in mithril is also pretty strait forward. It's just a function that gets the result of the upper controller and returns a virtual mithril dom-tree. To convert this to a real dom, I wrote a small module called mithril-node-render. It converts a view result to a html-string.
function userView(scope) {
return m('.user', scope.user.name);
}
var scope = userController();
var html = render(userView(scope));
// html === '<div class="user">Frodo</div>'
As you can see, rendering is also completely browser independent and so easy to use server side.
The model layer is by far the most difficult of all layers. It almost ever requires async patterns. Also fetching data server and client side greatly differs.
On client side you fetch data using AJAX. In mithril there is a small helper (m.request
) that simplifies this for you. On the server side, it again grealy differs from project to project. Thats why it's hard to create a general solution for this. I might come up with a wrapper around the different model-layers (like mongoose of bookshelf) later. Currenly I wrote a wrapper arround m.request
and bookshelf that share the same API called store
.
As an example I show the load
function for client and server.
// client store.load
function load(type, id) {
if (!type) {
throw new Error('no type provided to load model');
}
if (!id) {
throw new Error('no id provided to load model');
}
return m.request({
method: 'GET',
url: 'api/' + type + '/' + id),
});
}
//server store.load
function load(type, id) {
var resources = require('../server/rest/resources');
if (!resources[type]) {
throw Error('Resource with type "' + type + '" does not exist');
}
return resources[type].forge({id: id});
}
For both you can load an object by calling
store.load('user', 123).then(function(user) {
// do things with user
});
I simplified the code a little to make it less confusing. Hopefully you understand what's going on here. In real project the methods are slightly more complex to handle some edge cases. Feel free to drop me a line, if you need any assistance on this.
So now you have to make sure, that the browser always uses the browser version of store while the server uses its version. In the controller you want to require allways the same file.
Luckily browserify has a solution for that.
// package.json
{
// ...
"browser": {
"./store/index.js": "./store/client.js"
},
// ...
}
Simply add a browser
-section to the package.json
-file. Key should be the path of server-file realtive to the package.json
-file, value should be the client file. If you then require the server file in a file thats used on the client the defined client file is referenced instead.
I used express-based-webserver but it's pretty easy to do with other frameworks too as long as they share basic routes definition. I first created a module that contains all routes and the appropriate mithril-modules. A mithril-module is simply an object with a controller and a view:
var userModule = {
controller: userController,
view: userView
};
The routes file may look like this:
var routes = {
'/user/:id': require('./modules/user')
};
Fortunatly mithril and express share the route definition so we can use the same routes for client and server.
// client
m.route(document.body , '/', routes);
The mithril router uses /
as base-url so the routes in frontend and backend should be equal.
// server
var app = express();
each(routes, function(module, route) {
app.get(route, function(req, res) {
var scope = module.controller(req.params);
res.end(render(module.view(scope)));
});
});
As you might see, I created an entry in the express-router for every mithril-route. I call the controller once for each request and then call the upper shown render
-function. The result of this is passed to as the response.
One slightly differnece between request handling of mithril and express is the handling of parameters. In express they come as req.params
in mithril there is a method m.route.param
. This has to be ironed out. As you see in the upper code, I simply pass the route-arguments as first parameter. I slightly modified the controller, so it can handle both:
function userController(params) {
var userId = params ? params.id : m.route.param('id');
var user = {
name: 'Frodo'
};
return {
user: user
};
}
This can be improved of cause. I simply did not come up with an elegant sollution for this. Any ideas?
Now we have all basic components together, so let's start packing it all together. In the upper example, you might want to fetch the user before rendering the page server side. You also don't want to send the response until the user is fetched.
Im mithril you might simply write this code:
function userController(params) {
var userId = params ? params.id : m.route.param('id');
var scope = {
user: null
};
store.fetch(user, userId).then(function(user) {
scope.user = user;
});
return scope;
}
Since it rerenders the userView
when the AJAX
-call resolves, you don't have to care about async stuff.
In server side you have to care. The solution for this is not optimal right now. Currently I use an Event-Observer for this.
function userController(params) {
var userId = params ? params.id : m.route.param('id');
var scope = {
user: null
onReady: new Signal()
};
store.fetch(user, userId).then(function(fetchedUser) {
scope.user = fetchedUser;
scope.onReady.dispatch();
});
return scope;
}
The express integration now looks like this
each(routes, function(module, route) {
app.get(route, function(req, res) {
var scope = module.controller(req.params);
if (!scope || !scope.onReady) {
return res.end(base(render(module.view(scope))));
}
scope.onReady.addOnce(function() {
res.end(base(render(module.view(scope))));
});
});
});
So it waits for the dispatch of the onReady
-event on the controller result object. This is a little verbose, especially if you have multiple AJAX-requests to listen to. Maybe anyone of you fellow readers have a better solution for this.
Another slightly change is the wrapping of the response in a base
function
function base(content) {
return [
'<!doctype html><html><head>',
'<link href="/index.css" media="all" rel="stylesheet" type="text/css">',
'<script src="/index.js"></script>',
'</head><body>',
content,
'</body></html>'
].join('');
}
It simply wraps the output in some basic html (including html
and body
-tags).
The described solution is already pretty powerful. You can use most code on client and server side. The only thing you have to care about are the models and your REST-API. Beside some small changes to the controllers and your AJAX-Requests you pretty much can leave your mithril-code as is.
Hopefuly this enables you to create a nice isomorphic mithril application. Here you can find a example project to fork an build your application upon.
Looks like a really good starting-point, @StephanHoyer and @jsguy, many thanks!
I'm very new to node, and wondering why there is such a huge performance-difference between StephanHoyer/mithril-isomorphic-example and misojs from jsquy? Using apache bench, I clock the former at about 1800-1900 request/second at max, while misojs only loads at about 170.. Is this all due to the fact that misojs packs a lot more dependencies, or is there something else at work here? In this benchmark express.js gets 367 req/sec whilst reading from a database, which makes me suspect there must be something wrong with misojs. I did get an error when running the server at first (a file was missing), but after downloading it manually, the server started up fine, and the todo app was working. I might add that it gets a bit higher when I run it with "NODE_ENV=production node app.js", about 440 req/sec, but still feels very slow compared to 1800.
Also curious about why there is a very long string of what appears to be some kind of encryption key at the end of the .js-file in both misojs and this one? It disappears from the example site when going into NODE_ENV=production, but stays in misojs.
Are the two apps completely independent projects, or does the documentation from misojs in any way apply to StephanHoyer/mithril-isomorphic-example?
I would much appreciate if you could help me understand all of this as I am eager to get started using mithril as isomorphic, but right now it all feels a bit woobly.. :) I guess what I'm really wondering is this: are they production ready yet?
Many thanks again!