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

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