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.
Thanks for your article. Very useful.
I just have a problem with using Signal for controller! I think it makes everything more complicated. Specially if you wanna use one module in another module.I use Promise instead. It makes everything more simple. I hope I can explain it with this little bit snippets:
It works perfect for me :)