Skip to content

Instantly share code, notes, and snippets.

@StephanHoyer
Last active March 29, 2022 11:46
Show Gist options
  • Save StephanHoyer/bddccd9e159828867d2a to your computer and use it in GitHub Desktop.
Save StephanHoyer/bddccd9e159828867d2a to your computer and use it in GitHub Desktop.
Isomorphic applications with mithril

Attention

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.

Isomorphic applications with mithril

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.

TL;DR

This is an example app.

the components

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.

the controller layer

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

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

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.

the routing layer

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?

using models in controller

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).

summary

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.

@Niklas81
Copy link

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!

@StephanHoyer
Copy link
Author

@Niklas81 The long cryptic string at the end of the file is the base64 encoded source map. It's generated by browserify to make development easier.

The performance issue is probably like you mention the more advanced stack of misojs. I personally like simple unopiniated solutions. Thats why I created the most simple example as a starting point.

I build a full blown editor for learning material (OER) with this approach and I still pretty happy with it.

@Niklas81
Copy link

@StephanHoyer thank you for those clarifications!

I've been playing around with it a bit now, and it's really quite nice. However, I do think it's a huge problem that I can't access m.route.param and use it in the logic of how the view is rendered. As I see it, solving this it's a matter of forwarding request data from express so it can be accessed in the template, e.g. home.js

req.params provides only parameters extracted from e.g. /:var1/:var2 so if you use that route and enter /hello/world?fname=donald&lname=duck it will only give you var1=hello and var2=world but ignore fname and lname. If however you use req.query instead, that will provide you with two objects; console.log(req.query) would in this case give you:

{ var1: 'hello', var2: 'world' }
{ fname: 'donald', lname: 'duck' }

Then you would somehow have to be able to access this in the view, perhaps using something as described here: http://stackoverflow.com/questions/6331776/accessing-express-js-req-or-session-from-jade-template

...and then preferably be able to feed this into m.route.param. One other option might be some sort of identifier that lets you determine if the rendering is done by the server or the client, e.g.:
if (render = 'server') var reqdata('var1') = serverdata('var1'); else var reqdata('var1') = m.route.param('var1');
This identifier would be quite useful in either case, not just to resolve this particular issue.

I have successfully managed to manipulate the server-side rendered code from within the view, allowing me to do operations on it with e.g. jquery or cheerio, so I really think it should be possible to do it the other way around. However I'm quite new to both node, express and mithril, so I feel this is well beyond my capacity. But perhaps this might work as a starting point for fixing this for someone more skilled? Anyone up for the challenge? :)

Oh, and while I'm at it, variadic routes doesn't work either, that is routes defined as :var... that lets you use variables that contain slashes.

@Niklas81
Copy link

On another note: I'll be using an external API that will sit on api.mydomain.com, and this will be a php server. Will this isomorphic setup be able to access that asyncronous data so it can render the pages based on content from the database? Or must this be a node server or else it won't be accessible?

@StephanHoyer
Copy link
Author

It's possible to have async stuff rendered. It's a little tricky though, since express does not really know, when it should send the response.

I currently solve this by having a done-callback function that have to be called in order to send the response.

See the example app here: https://github.com/StephanHoyer/mithril-isomorphic-example/blob/master/client/pages/second.js

@Niklas81
Copy link

Ok, thanks for pointing me in the right direction on this, regarding the done-callback. Possibly more follow-up questions once I start working on that :)

I have managed to access req.params/req.query/m.route.param from the view both on client and server by hacking it a bit. My view function in second.js is now view(scope,query), and then from web.js I call render() with an additional parameter containing the query object, e.g. render(module.view(scope,req.params)). Then within the view, instead of e.g. m.route.param('test') I call a custom function GET('test',query), defined as such:

function GET(name,query){
  if (typeof document !== 'undefined')
    return m.route.param(name);
  else
    return query[name];
}

Works great both on client and server. So now I also have an identifier to determine if it's rendered on the server or in the client, if (typeof document !== 'undefined') then it's on client.

But as I'm very new to Mithril, I don't really know how its diffing tool and the auto-redraw mechanism work. So I don't know if this approach would break this? Any idea?

@StephanHoyer
Copy link
Author

If you use browserify you can simply check process.browser. It's true in browsers and undefined in node

@Niklas81
Copy link

Thanks! And this way of calling m.route.param indirectly (through my GET() function) won't break the diffing/redraw mechanism of Mithril?

@Niklas81
Copy link

If in server/web.js you replace app.get(route, function(req, res, next) { with app.get(route.replace('...', '/*'), function(req, res, next) {, you'll be able to use Mithril's 'variadic routes' while still using one shared routes.js.

And if you install merge, npm install merge, then var merge = require('merge'); and then replace req.params in web.js with merge(req.query,req.params), then params used by controller will behave in the same way on the client and the server; say if the type of resource you're calling is a function of the url and querystring (e.g. ?var1=dsds&varb=2), then you can do something like this in the controller: store.load(process.browser ? m.route.param('anna') : params['anna'], 123).then(function(dog) {
Of course you'd like to put this in a function like GET() mentioned in one of my previous comments.

Just a couple of tips in case this might be helpful for anyone else.

@epicmonkey
Copy link

@StephanHoyer Am I missing something obvious, how can I skip API request in the client's controller for the second page when data is already loaded and rendered by the server side (e.g. controller/page state)? Right now if I refresh /second-page page, data will be a) loaded from the server b) re-rendered by client's API request.

@dontwork
Copy link

Does this example rely on bookshelf?

@StephanHoyer
Copy link
Author

@epicmonkey: thats up to you. I saw someone doing it lately.
@dontwork: I think not.

@abdollahpour
Copy link

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:

// module1
controller1 = ()=>
{
    return new Promise( r =>
        {
            // resolve it here
        }
    )
}

// module2
controller2 = () =>
{
    let promise3 = new Promise( r =>
        // something async here
    )
    let promise4 = controller1()
    return Promise.all([promise3, promise4]).then( values =>
        {
            // finalize the result
        }
    )
}

// And finally in express
router.get('/', (req, res) =>
    {
        controller2().then(ctrl =>
            let html = render(view2(ctrl))
            res.send(html)
        )
    }
)

It works perfect for me :)

@ezramorse
Copy link

I'm a bit new here, but wanted to say thanks to Stephan. I saw this a few months ago and went to work on a turn-key isomorphic javascript framework that uses some of these concepts, but pairs them with socket.io. I've used it for a while to quickly prototype some start-up projects (and I've found it extremely easy to spin up significant projects), but didn't have time to document anything until today. I'd love your thoughts and welcome any help in shaping this!

https://github.com/ezramorse/mens

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment