All Projects → d-oliveros → Isomorphine

d-oliveros / Isomorphine

Licence: mit
Require server-side modules from the browser, remotely.

Programming Languages

javascript
184084 projects - #8 most used programming language

Projects that are alternatives of or similar to Isomorphine

Jobeir
Building the future of tech jobs search
Stars: ✭ 47 (-28.79%)
Mutual labels:  isomorphic
Laravel Localization Loader
Laravel Localization loader for webpack. Convert Laravel Translation strings to JavaScript Objects.
Stars: ✭ 58 (-12.12%)
Mutual labels:  webpack-loader
Blitz3d msvc2017
Blitz3D Source for MSVC Community Edition 2017
Stars: ✭ 64 (-3.03%)
Mutual labels:  rpc
Finagle
A fault tolerant, protocol-agnostic RPC system
Stars: ✭ 8,126 (+12212.12%)
Mutual labels:  rpc
Retc
An application used to convert razer effects to multiple output sdks.
Stars: ✭ 54 (-18.18%)
Mutual labels:  rpc
Kubemq
KubeMQ is Enterprise-grade message broker native for Docker and Kubernetes
Stars: ✭ 58 (-12.12%)
Mutual labels:  rpc
Jose
Universal "JSON Web Almost Everything" - JWA, JWS, JWE, JWT, JWK with no dependencies
Stars: ✭ 1,029 (+1459.09%)
Mutual labels:  isomorphic
Discordipc
Connect locally to the Discord client using IPC for a subset of RPC features like Rich Presence and Activity Join/Spectate
Stars: ✭ 66 (+0%)
Mutual labels:  rpc
Tatala Rpc
An easy-to-use RPC middleware, cross language and platform. Right now, there are Java and C# client available.
Stars: ✭ 55 (-16.67%)
Mutual labels:  rpc
Nettyrpc
NettyRPC is high performance java rpc server base on Netty,using kryo,hessian,protostuff support message serialization.
Stars: ✭ 1,131 (+1613.64%)
Mutual labels:  rpc
Universal React Demo
ES6 demo of a simple but scalable React app with react-router, code splitting, server side rendering, and tree shaking.
Stars: ✭ 50 (-24.24%)
Mutual labels:  isomorphic
Html Loader
HTML Loader
Stars: ✭ 1,067 (+1516.67%)
Mutual labels:  webpack-loader
Koot
Koot是基于React和Koa的WEB全栈技术解决方案。
Stars: ✭ 59 (-10.61%)
Mutual labels:  isomorphic
Ng Router Loader
Webpack loader for NgModule lazy loading using the angular router
Stars: ✭ 47 (-28.79%)
Mutual labels:  webpack-loader
Haskell Capnp
Cap'n Proto for Haskell
Stars: ✭ 65 (-1.52%)
Mutual labels:  rpc
Bootstrap Loader
Load Bootstrap styles and scripts in your Webpack bundle
Stars: ✭ 1,038 (+1472.73%)
Mutual labels:  webpack-loader
Danf
Danf is a Node.js full-stack isomorphic OOP framework allowing to code the same way on both client and server sides. It helps you to make deep architectures and handle asynchronous flows in order to help in producing scalable, maintainable, testable and performant applications.
Stars: ✭ 58 (-12.12%)
Mutual labels:  isomorphic
Hprose Golang
Hprose is a cross-language RPC. This project is Hprose for Golang.
Stars: ✭ 1,143 (+1631.82%)
Mutual labels:  rpc
Core Geth
A highly configurable Go implementation of the Ethereum protocol.
Stars: ✭ 66 (+0%)
Mutual labels:  rpc
Nettythrift
Thrift on Netty, support TCP/HTTP/WebSocket at same port. support multiple Protocols at same time. multil Simple Clients with Connection Pool.
Stars: ✭ 60 (-9.09%)
Mutual labels:  rpc

Isomorphine

Isomorphine is a webpack loader that lets you access server-side modules in the browser. It works by injecting an rpc routing layer behind the scenes. This lets you require and run server-side code from the browser, without doing AJAX requests or express routes.

When requiring a server-side entity from the browser, the module is provided as a mirror of the server-side entity. This mirror will automatically transport any method call to the server, and resolve the results in the browser seamlessly. When requiring a module from the server's execution context, the module is resolved as-is, without any mirroring or routing whatsover.

You can securely share and use server-side code in the browser (for example your database models) and eliminate data fetching boilerplate. The server-side modules will not be required directly in the browser, so you can require modules containing browser-incompatible libraries.

It does not expose server-side code. It also provides a security mechanism for remote procedure calls (RPCs), and supports promises & async/await.

You don't need to do HTTP requests and endpoints anymore. You can skip your application's routing layer, and focus on your application's purpose.

Summary

Requirements

  • Node
  • Webpack

Installation

npm install isomorphine

Then you must add isomorphine as a webpack loader.

Usage

Isomorphine has only one method: isomorphine.proxy()

isomorphine.proxy() - Creates an object exposing the modules in the directory where it was called. This is similar to require-all, but also enables each module to be used in the browser.

var isomorphine = require('isomorphine');

// This will provide the entities in this folder,
// similar to 'require-all' but in a browser-compatible way.
module.exports = isomorphine.proxy();

Each file in the current directory represents a property in the resulting object. Each file must export a function either in module.exports or via export default. Directories are scanned recursively (See require-all). When using these modules from the browser through isomorphine, function calls will be proxied to the server.

This will let you use any server-side entity remotely, through the object created with isomorphine.proxy(). Just require this morphine object from the browser or the server, and take anything you need out of it. For example:

/**
 * Suppose the file we defined above is in ./models/index.js,
 * and the User model is located in ./models/User.
 */
var User = require('./models').User;

$('#button').on('click', function() {

  // When called from the browser, isomorphine will transport this function call
  // to the server, process the results, and resolve this callback function.
  // When called from the server, the function is called directly.
  User.create({ name: 'someone' }, function(err, user) {
    console.log('Im the browser, and I created a user in the db!');
  });
});

You also need to mount isomorphine's RPC interface on your current express-based app, by doing:

var express = require('express');
var morphine = require('./models'); // Suppose this is the file we created above

var app = express();

// Mounts the rpc layer middleware. This will enable remote function calls
app.use(morphine.router);

app.listen(3000, function() {
  console.log('App listening at port 3000');
});

Alternatively, you can start isomorphine's router as a stand-alone http server by doing:

var morphine = require('./models'); // Suppose this is the file we created above

// This will enable remote function calls, and must be run in the server
morphine.router.listen(3000, function() {
  console.log('RPC interface listening at port 3000');
});

By default, isomorphine will make the remote procedure calls to the same host and port of the client's current window.location. To manually specify the host and port of your API server, you can do:

var morphine = isomorphine.proxy();

morphine.config({
  host: 'api.mysite.com', // default is the current browser location hostname
  port: '3000'            // default is the current browser location port
});

Remember to read the caveats for common gotchas, and the section below for a more in-depth explanation.

How It Works

Isomorphine detects server-side entities by scanning the file structure recursively, and building objects whose methods represent files in the server. Each file has to export a function (via module.exports or export default). Read the caveats for common gotchas.

The internal behavior of isomorphine.proxy() differs depending on whether its being ran in the browser or the server:

  • When called from the server: isomorphine.proxy() requires all files in the current directory (similar to require-all) and also creates an express-based router that will handle remote procedure calls (RPCs) to the methods in these entities.

  • When called from the browser: isomorphine.proxy() creates a mirror to the server-side entities. The mirror is preprocessed and injected by webpack, so you must add isomorphine as a webpack loader. No router is created in the browser, and no server-side modules are actually require()'d in the browser.

In this example, we will be using a fictitious server-side model called User, written in vanilla ES5. We will be splitting each model method in its own file. Please note that only files that export a function can be used in the browser. Promises and ES7 async/await are also supported.

// in /models/User/create.js

module.exports = function createUser(username, callback) {
  var user = {
    _id: 123,
    type: 'user',
    name: username,
    created: new Date()
  };

  // It doesn't matter how you handle your data layer.
  // Isomorphine is framework-agnostic, so you can use anything you want, like
  // mongoose, sequelize, direct db drivers, or whatever really.
  db.create(user, callback);
}
// in /models/User/delete.js

module.exports = function deleteUser(userId, callback) {
  db.remove({ _id: userId }, callback);
}

To make the these functions available in the browser, you have to create an isomorphic proxy to this folder by using isomorphine.proxy().

isomorphine.proxy() will make these modules available in the browser, without breaking your bundle due to incompatible server-only libraries, bloating your bundle's size, or exposing your server's code. In this example, we will create an isomorphic proxy with isomorphine.proxy(), and export it in the index file of the /models directory, thus exposing all the models in the browser.

// in /models/index.js
var isomorphine = require('isomorphine');

// This will be our main isomorphic gateway to this folder
module.exports = isomorphine.proxy();

We are calling isomorphine.proxy() in the index.js file of the ./models folder, thus providing all the models in the ./models folder to the browser, as mirror entity maps.

Based on this example structure, the object created by isomorphine.proxy() is:

// The browser gets this ->
{
  User: {
    create: [func ProxiedMethod],
    delete: [func ProxiedMethod]
  },
  config: [func] // This method sets the host and port of your API server
}

// Whereas the server gets this ->
{
  User: {
    create: [func createUser],
    delete: [func deleteUser]
  },
  config: [func emptyFunction] // This method does nothing in the server
  router: [func] // this is the RPC API router that must be mounted in your app
}

// /models/User/create.js and /models/User/delete.js are not actually being
// required in the browser, so don't worry about browser-incompatible modules

You can use this fictitious User model in the browser by doing this, for example:

// in /client.js, running in the browser

// We can interact with this server-side entity (User)
// without manually having to do any HTTP requests.
//
// If you were to require this model directly from the browser, you'd be requiring
// your whole data layer, probably including your database initialization files
// and other modules that are *not* browser-compatible.
//
// By requiring 'User' through '../models/index.js', which is the file where
// we put 'isomorphine.proxy()', we are actually requiring an object
// which mirrors the methods in the real server-side model.
//
// No server-side modules are actually required. Webpack generates an entity
// map before running this code, so it already knows the API surface area of
// your server-side model.
//
// Remember not to require 'User' directly. You need to require User through
// the gateway we created above. Otherwise, you'd be requiring all the model
// dependencies in the browser.
//
var User = require('../models').User;

// eg. using jquery
$('#button').on('click', function() {

  // When called from the browser, the browser serialized each parameter sent
  // to the function call, and sends the serialized payload via a HTTP request
  // to Isomorphine's endpoint in the server.
  //
  // Isomorphine serializes the result of the function call in the server,
  // returns the resulting values, deserializes the values, and calls
  // this callback function with the resulting values.
  User.create('someUser99', function(err, user) {

    window.alert('User created');

    // uhh, lets delete the user better!
    User.delete(user._id, function(err) {
      window.alert('User deleted');
    });
  });
});

To make this work, you must mount the express-based router created by isomorphine.proxy() in your app:

// in /server.js
var express = require('express');

// suppose the file we defined above is in ./models/index.js
var morphine = require('./models');

var app = express();

// Mounts the rpc layer middleware. This will enable remote function calls
app.use(morphine.router);

// you can mount other middleware and routes in the same app
// app.get('/login', mySecureLoginCtrl);

app.listen(3000, function() {
  console.log('Server listening at port 3000');
});

// Alternatively, if you don't wish to mount isomorphine's middleware in your app,
// you can just start listening for RPCs by doing
var morphine = require('./models');
morphine.router.listen(3000, function() {
  console.log('Server listening at port 3000');
});

Multiple arguments in exported functions are supported. You can define and use functions as you would normally do:

// in /client.js, running in the browser
var User = require('./models').User;

User.edit(user._id, { name: 'newName' }, 'moreargs', (err, editedUser, stats) => {
  console.log('User edited. Server said:');
  console.log(editedUser);
  console.log(stats);
});

Your server's files will not be exposed in the browser, nor they will get added to the bundled file.

It also supports promises and ES7 async/await

// in /client.js, running in the browser
import { User } from '../models';

$('#button').on('click', async () => {

  // using promise-based server-side entities, and async/await in the browser
  const user = await User.create('someUser99');
  window.alert('User created');

  // lets delete this user
  await User.delete(user._id);
  window.alert('User deleted');
});

Other than reducing boilerplate code, it really shines in isomorphic applications, where you need the same piece of code running in the browser and the server, for example when doing server-side rendering of a react application.

import { User } from '../models';

// In the browser, calling 'User.get()' will actually make a HTTP request
// to the server, which will make the actual function call,
// serialize the results back to the browser, and resolve the promise with the value(s).
//
// In the server, 'User.get()' will be called directly
// without doing any HTTP requests or any routing whatsoever.
//
// This same piece of code can be run seamlessly in the browser and the server
export default class MyComponent extends React.Component {
  async componentDidMount() {
    const user = await User.get(123);
    this.setState({ user });
  }
}

Please read the caveats for common gotchas, and the section below for working examples.

Examples

Also go to Wiselike to see it running in a production environment, and ask me anything here!

Webpack Configuration

In order for Isomorphine to work, you need to specify Isomorphine as a webpack loader in your webpack.config.js file. The main isomorphine package contains the webpack loader so you don't need to install anything else.

module.exports = {
  entry: {...},

  module: {
    preLoaders: [{ loaders: ['isomorphine'] }]
  },
  ...
};

For instructions and a usage guide, please read Usage

Promise / ES7 Support

Isomorphine supports promises, async/await, and callback-based functions.

// Promise support
module.exports = function getUser() {
  return db.findAsync({ _id: 123 }); // supposing this returns a promise
}
// Another promise support example
module.exports = function readSomeFile() {
  return new Promise((resolve, reject) => {
    fs.readFile('somefile.txt', function(err, file) {
      if (err) return reject(err);
      resolve(file);
    });
  });
}
// ES7 async/await support
export default async function getUser() {
  const user = await MyUserModel.find({ _id: 123 });

  // await doMoreStuff()...

  return user;
}
// callback-based support
module.exports = function getUser(uid, callback) {
  MyUserModel.find({ _id: uid }, (err, user) => {
    if (err) return callback(err);

    console.log('Got a user!');

    callback(null, user);
  });
}

RPC Context

Allowing any API endpoint to be called from the browser, needs a proper validation mechanism to avoid getting exploited easily.

When a call to a server-side function is done from the browser, a special context is passed to the function call. A special xhr flag and the request object req are passed as the function's context, in this.xhr and this.req:

/**
 * When an endpoint is called from the browser, 'this.xhr' will be true,
 * and you'll be able to access the request object in 'this.req'.
 */
module.exports = function createUser(username, callback) {
  if (this.xhr) {
    console.log('This function is being called remotely!');
    console.log('Request is', this.req);
  }

  myUserModel.create(username, callback);
}

You can use this context to validate incoming requests. Please note, Isomorphine is unobtrusive and comes with no security middleware by default (other than this mechanism).

You must implement your own security mechanism yourself in an earlier middleware stage (using cookies or redis sessions or JWT or w/e):

module.exports = function deleteUser(userId, callback) {

  // Suppose I have a previous middleware step that adds `isAdmin`
  // to the request object, based on the user's session or else
  if (this.xhr && !this.req.isAdmin) {
    return callback(401);
  }

  myUserModel.delete(userId, callback)
}

If the function is not being called remotely, this.req will be null, so make sure to validate this.xhr before trying to do something with the request object this.req.

Caveats

  • Your files must export a function in module.exports (or export default if using ES6 syntax) if you want to be able to call them from the browser. There is currently no support for exporting objects yet. If anyone figures out a way to determine the exports of a module without requiring its dependencies I'll be happy to merge the PR :D

  • Your modules have to be required through the isomorphine proxy. You can not require a server-side entity directly. If you do, you will be importing all the server-side code to the browser's bundle, and possibly breaking your app due to browser-incompatible modules, like fs, express, mongo, database drivers, etc.

  • When a function is called directly from the server (eg when called by a cron job), there's no this.req object being passed to the function calls, so you must validate sensitive paths in an earlier stage.

  • Also, please note that the file where you create the isomorphic proxy, has to be browser-compatible. Isomorphine works by proxying the methods contained in the specified directory, but the file itself will be required as-is in the browser.

Comparison

The two examples below achieve the same result. They both create a web server, register a route that returns a user by user ID, starts listening at port 3000, and call this endpoint from the browser.

One example uses isomorphine, while the other one uses the common approach of building an API route, calling a model from a controller, and building a client-side wrapper for data-fetching logic.

With isomorphine:
// in ./api/User/get.js

// Dummy User.get() method
module.exports = function(id, callback) {
  var user = {
    id: id,
    name: 'Some User'
  };

  callback(null, user);
};
// in ./api/index.js
var isomoprhine = require('isomorphine');

// make the API entities callable from the browser
var morphine = isomorphine.proxy();

// start listening for RPC calls
morphine.router.listen(3000, function() {
  console.log('Interface listening at port 3000');
});

module.exports = morphine;
// in ./client.js
var User = require('./api').User;
var userId = 123;

// just use the model and be happy
User.get(userId, function(err, user) {
  console.log('User is', user);
});
Without isomorphine:
// in ./api/User/get.js

// Dummy User.get() method
module.exports = function(id, callback) {
  var user = {
    id: id,
    name: 'Some User'
  };

  callback(null, user);
};
// in ./api/index.js
var User = require('./User');

var express = require('express');
var bodyParser = require('body-parser');
var http = require('http');

var app = express();

app.use(bodyParser.urlencoded());
app.use(bodyParser.json());

app.get('/api/user/:id', function(req, res, next) {
  var userId = req.params.id || 123;

  User.get(userId, function(err, user) {
    if (err) {
      return next(err);
    }
    res.set('Content-Type', 'application/json');
    res.send(user);
  });
});

var server = http.createServer(app);
server.listen(3000, function() {
  console.log('Server listening at port 3000');
});
// in ./client.js
var request = require('request');
var userId = 1;

// unnecesary boilerplate code
function getUser(id, callback) {
  request.get(`/api/user/${id}`, function(err, res) {
    if (err) {
      return callback(err);
    }
    var user = res.data;
    callback(null, user);
  });
}

// gets the user
getUser(userId, function(err, user) {
  console.log('User is', user);
});
With Isomorphine (condensed):
var isomoprhine = require('isomorphine');
var morphine = isomorphine.proxy();
morphine.router.listen(3000, function() {
  console.log('Interface listening at port 3000');
});
module.exports = morphine;
var User = require('./api').User;
var userId = 123;
User.get(userId, function(err, user) {
  console.log('User is', user);
});
Without Isomorphine (condensed):
var User = require('./User');
var express = require('express');
var bodyParser = require('body-parser');
var http = require('http');
var app = express();
app.use(bodyParser.urlencoded());
app.use(bodyParser.json());
app.get('/api/user/:id', function(req, res, next) {
  var userId = req.params.id || 123;
  User.get(userId, function(err, user) {
    if (err) {
      return next(err);
    }
    res.set('Content-Type', 'application/json');
    res.send(user);
  });
});
var server = http.createServer(app);
server.listen(3000, function() {
  console.log('Server listening at port 3000');
});
var request = require('request');
var userId = 1;
function getUser(id, callback) {
  request.get(`/api/user/${id}`, function(err, res) {
    if (err) {
      return callback(err);
    }
    var user = res.data;
    callback(null, user);
  });
}
getUser(userId, function(err, user) {
  console.log('User is', user);
});

This example took 11 lines of code using Isomorphine. Doing this the traditional way took 35 lines. And its only one route

If we were to add more CRUD routes to our model, each route would require:

  1. A controller
  2. Some request validation
  3. A method in your model (maybe)
  4. Some wrapper in your client-side application

Plus having to mantain these new components. Multiply this for each route you are currently mantaining, and you'll realize there has to be a better way to streamline your application's development.

With isomorphine, you can just call the server-side model directly. The model is already being supplied to the browser, so you would only require:

  1. A new method in your model

No need to re-write the data-fetching layer in the client application, or get parameters out of request object. heck, you don't even need to define and mantain routes. If you need to access the request object for validation purposes or else, you can access it through this.req as specified here.

Disclaimer: I'm not saying Isomorphine is the best fit for every case. You should have your routes and middleware in place to serve the client and views, and to handle special routes like auth actions, and should be providing everything you need to correctly authenticate remote calls to your methods. Starting isomorphine directly from morphine.router.listen() is not recommended, as Isomorphine is only intended to handle RPCs to server methods. It is not meant to server as a full-blown HTTP server or application stack.

Philosophy

Isomorphine proposes an endpoint-less API approach in an attempt to further abstract the barriers between the server and the browser. It is meant to increase code reusability between the server and the browser, specially in an isomorphic full-stack javascript environment.

The idea is to encapsulate the routing layer within javascript's native syntax for importing and exporting modules, while providing a middleware interface to let you mount its RPC handler the way you want. This massively reduces development times, as you don't have to worry about connecting the browser and server together, so you can focus solely in your application's purpose.

The original idea was to use ES6's Proxy in the browser, to proxy any function call from any property of any object existing in any file in the server. Unfortunately, I quickly found out that there was no out-of-the-box support for Proxy in most of the major browsers. This led to the idea of using Webpack to pre-generate a map of server-side entities based on filenames. While this work, full support for any type of export will probably come after a wider adoption of ES6's Proxy in the browser, or a more advanced webpack loader.

Tests

mocha test

Cheers.

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].