Lightweight, Lowly Opinionated, Isomorphic Javascript Framework for Rapid Prototyping Reactive Web Apps
MENS is a complete toolkit for quickly building fast isomorphic javascript web applications, such that there is little to no differentiation between coding on the frontend and the backend. This framework has utilities to handle asynchronous data, sharing of session variables for client-side rendering, JSX (or just HTML) templating and event-driven programming (via socket.io). MENS takes an extreme KISS approach by simply bridging together a variety of useful, synergistic javascript libraries to create an elegant isomorphic mithril javascript server.
Use Cases: Social media web applications with interactive communities, multi-user administration tools, mobile applications, etc. (anything requiring interactivity, speed, reactive interfaces and high concurrency).
npm install mens
node node_modules/mens/example/server.js
Or for a more dynamic example (loud):
node node_modules/mens/worse_example/server.js
var
mens = require("mens"),
path = require("path");
var server = mens({
port: 80,
components: __dirname+path.sep+'components',
routes: __dirname+path.sep+'routes.js'
});
Such that components is a subdirectory of js/jsx files pairs and routes exports a key/value object of routes to isomorphic mithril javascript components.
Name | Default | Description |
---|---|---|
logLevel | 3 | Integer 1-4, with 4 being the most verbose |
tz | 'America/Los_Angeles' | Timezone for logging and utilities |
port | 80 | Httpd Port to listen on |
sessionStore | null | Accepts session storage object for adapting REDIS, etc. |
sessionSecret | 'kneeboard fat' | Session Secret |
sessionName | 'sid' | Session Name |
static | null | Path to static assets (goes to express) |
modeler | null | fn(data, session, callback) to lookup a record matching data, which is returned to the client using callback(result) |
socketHandler | null | fn(socket.io client, session) for binding custom events when a new socket.io client connects |
components | null | Path to a components directory |
settings | null | Full Path to a routes definition file (key/values of routes to components) or a shared settings file exporting an object {routes, title, meta, links} |
template | null | Full Path to the main wrapper template, containing an HTML wrapper with <!--TITLE-->, <!--META-->, <!--LINK-->, <!--SESSION--> and <!--MENS--> tags. The <!--MENS--&rt; tag should be within an element of id "mens-content" |
customJS | null | Full Path to custom javascript to run after components are defined |
minify | true | Flag to toggle off uglification of the source js served to the client |
Note: Many included javascript files should contain full paths, because these files will both be required and read into memory for compiling the client source.
By passing a components directory into the mens constructor, this directory will be recursively searched for js and jsx files. JSX files will be compiled into views and JS files will stored as controllers. Consider the following example, assuming components argument has the value "components":
file: components/hello/world.jsx
<div>
<h1>Hello {ctrl.world()}</h1>
</div>
file: components/hello/world.js
module.exports = function (params) {
m.init(this, params);
this.world = m.prop('World!');
return this;
}
Parsing these files results in:
- m.views.helloWorld
- m.controllers.helloWorld
- m.components.helloWorld == {controller: m.controllers.helloWorld, view: m.views.helloWorld}
By supplying a path/filename in the settings property of the mens constructor, these routes will be setup in the webserver to render the components they are attached to. While you can use the built in templating engine, any valid mithril controller/view component will also work. Assuming settings has the value "routes.js":
file: routes.js
module.exports = {
'/' : m.components.helloWorld,
'/mens' : {
controller: function (params) {
m.init(this, params);
this.world = m.prop('Mens!');
return this;
},
view: m.views.helloWorld
}
}
The routes file can also declare global shared flags (accessible to each route controller and via m.flags), default title tag, default meta tags, and default link tags by using the following format:
var flags = { world: m.prop('world!')};
module.exports = {
'routes' : {
'/' : {
controller: function (params) {
m.init(this, params);
this.flags = flags.world // Or m.flags.world;
return this;
},
view: m.views.helloWorld
}
},
'flags' : flags,
'meta' : [{name: "foo", content: "bar"}],
'links' : [{name: "foo", content: "bar"}]
};
While JSX templates can contain m.component declarations, all asynchronous data variables should be initialized and passed from the route controller into the children components. This allows the route controller to properly control its overall redraw strategy
Components can render asynchronous data on both the server and the client by using m.fetch in the route controller after m.init has been called, with no significant isomorphic considerations. A single fetched "model" should represent all data needed to render a specific route, which is ideal for noSQL databases such as mongoDB.
module.exports = function (params) {
m.init(this, params);
this.datum1 = m.prop(false);
this.datum2 = m.prop(false);
m.fetch(this, {id: 1}, function (d) { console.log('Recieved datum 1:', d) });
m.fetch(this, {id: 2}, function (d) { console.log('Recieved datum 2:', d) }, true); // This data will not be required for rendering on the server
return this;
}
On the server, this controller will not render the item until all required m.fetch calls have resolved. On the client, if the page is rendered isomorphically, the redraw strategy will be "diff" and the page will render instantly (so developers must account for templating missing/pending data with appropriate loading icons and language).
m.fetch Will cache the data described by the second argument for a default of 60 seconds. This value can be overriden by exporting a dataTtl element in the settings file. In the earlier example, data can be invalidated from the server by emitting an invalidateData event to any client, with the object {key: {id: 1}}.
The modeler property passed into the MENS constructor requires fn(data, session, callback), such that data is a description of the data to lookup, session is the wrapped session of the active user and callback is used to deliver the result back to the client/server. The result must be an object with property names matching the controllers'' m.prop properties.
For the previous controller, the following modeler on the server would return appropriate results:
function (data, session, callback) {
if (data.id == 1)
callback({datum1: 'This is fast data'});
else if (data.id == 2)
setTimeout(function () { callback({datum2: 'This is slow data'}) }, 500);
}
Modelers can adapt any type of asynchronous or synchronous data storage, or something generic as detailed above.
Note: When a page is rendered on the server and the client sets up the route, the client must redraw the page in order to bind onclick events. Therefore, the client must have the same data as the server to mount the route. To achieve this, either 1) the served page must contain the model's data or 2) the client must make redundant m.fetch calls. To reduce the size of crawled pages, MENS implements the second approach until there is a mithril solution; an efficient modeler should then cache results to avoid redundant database lookups.
The MENS stack implements socket.io for all communication between the client and the server (as a full replacement for ajax end points). On the client, the socket is exposed in m.socket. Here is an example of a controller that can emit to the server on an onclick={ctrl.doAction}:
modules.exports = function (params) {
m.init(this, params);
this.doAction = function () {
m.socket.emit('someAction', {foo: 'bar'});
}
}
The socketHandler property passed into the MENS constructor requires fn(client, session), where client is the socket.io client and session is the wrapped session of the active user. In order to receive the aforementioned event, the following socketHandler would work:
function (client, session) {
client.on('someAction', function (d) {
// Do Something Now on the server
});
}
The m.fetch utilizes m.poll(event, data, callback), which sends a special event to the server and listens for a 1 time response. In order to poll, the server must have a socket handler setup to properly respond:
Client Side Controller:
modules.exports = function (params) {
m.init(this, params);
this.doAction = function () {
m.poll('someAction', {foo: 'bar'}, function (res) { console.log('Here Is My Response', res); }); // Expects {bar: 'foo'}
}
}
Server Side Socket Handler
function (client, session) {
client.on('someAction', function (d) {
client.emit(d.key, {bar: 'foo'});
});
}
The MENS stack uses Express Session middleware to create the session, and shares this with Socket.io. The session is then wrapped in a mensSession object, which is shared with the client (available at window.session). On the server & client, the session is passed into the route controllers as a parameter and bound to the object when m.init(this, params) is ran in the controller.
The wrapped session allows the server to share session variables seamlessly with the client, which may be needed for rendering (user names or profile images, for example). The session is only mutable on the server and no sensative session information should ever be shared with the client.
Method | Client-Side | Description |
---|---|---|
get(key) | true | Returns the session variable at key. If key is null, all values are returned |
set(key, value) | false | Sets key to value, and emits the update to the client |
clear(key) | false | Clears value at key, and emits the update to the client |
Property | Description |
---|---|
m.socket | Socket.io connection (client only) |
m.web | Global check flag for non-isomorphic compliant code |
m.firstDraw | Internal flag for setting redraw strategy on initial route mounting with required async data (client only) |
m.emitter | Returns a new event-emitter object |
m.init | Allows controllers isomorphic access to page utilities, sessions and global flags. |
m.poll | Creates a bidirectional poll over sockets between the client and server (client only) |
m.fetch | Fetches data from the modeler using m.poll |
Running m.init binds the following properties to the route controllers
Property | Description |
---|---|
session | mensSession wrapper |
flags | Global flags, which should never be mutated outside of a gesture |
setTitle(String) | Updates the document's title |
setMeta(Array) | Updates the route's meta tags. Accepts an array of objects corresponding to the meta tag's properties |
setLinks(Array) | Updates the route's link tags Accepts an array of objects corresponding to the link tag's properties |
- Error Pages
- Event driven m.js refreshes on the client side
- Create practical examples (login/users/acl)
- Solve initial route mounting without redundant modeler polling (or sending data with the page)
Please fork and contribute, and send feedback & examples to [email protected] of any type of isomorphic mithril web application built with the mens stack.
Special thanks for Stephan Hoyer for some mithril isomorphic javascript examples and utilities, as well as Leo Horie for mithril (of course!). Many thanks to the node.js community!