├── .gitignore ├── .jshintrc ├── README.md ├── client └── app.js ├── config.example.js ├── gulpfile.js ├── index.js ├── package.json ├── public └── .gitignore ├── server ├── server.js └── views │ └── base.tmpl └── shared ├── actions.js ├── components └── app.tag ├── reducers ├── activeRoute.js └── index.js └── routes.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | config.js 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // JSHint Default Configuration File (as on JSHint website) 3 | // See http://jshint.com/docs/ for more details 4 | 5 | "maxerr" : 50, // {int} Maximum error before stopping 6 | 7 | // Enforcing 8 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) 9 | "camelcase" : false, // true: Identifiers must be in camelCase 10 | "curly" : true, // true: Require {} for every new block or scope 11 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 12 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 13 | "freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. 14 | "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 15 | "indent" : 4, // {int} Number of spaces to use for indentation 16 | "latedef" : false, // true: Require variables/functions to be defined before being used 17 | "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` 18 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 19 | "noempty" : true, // true: Prohibit use of empty blocks 20 | "nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters. 21 | "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) 22 | "plusplus" : false, // true: Prohibit use of `++` and `--` 23 | "quotmark" : false, // Quotation mark consistency: 24 | // false : do nothing (default) 25 | // true : ensure whatever is used is consistent 26 | // "single" : require single quotes 27 | // "double" : require double quotes 28 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 29 | "unused" : true, // Unused variables: 30 | // true : all variables, last function parameter 31 | // "vars" : all variables only 32 | // "strict" : all variables, all function parameters 33 | "strict" : true, // true: Requires all functions run in ES5 Strict Mode 34 | "maxparams" : false, // {int} Max number of formal params allowed per function 35 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 36 | "maxstatements" : false, // {int} Max number statements per function 37 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 38 | "maxlen" : false, // {int} Max number of characters per line 39 | "varstmt" : false, // true: Disallow any var statements. Only `let` and `const` are allowed. 40 | 41 | // Relaxing 42 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 43 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 44 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 45 | "eqnull" : false, // true: Tolerate use of `== null` 46 | "es5" : true, // true: Allow ES5 syntax (ex: getters and setters) 47 | "esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`) 48 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 49 | // (ex: `for each`, multiple try/catch, function expression…) 50 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 51 | "expr" : false, // true: Tolerate `ExpressionStatement` as Programs 52 | "funcscope" : false, // true: Tolerate defining variables inside control statements 53 | "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') 54 | "iterator" : false, // true: Tolerate using the `__iterator__` property 55 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 56 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 57 | "laxcomma" : false, // true: Tolerate comma-first style coding 58 | "loopfunc" : false, // true: Tolerate functions being defined in loops 59 | "multistr" : false, // true: Tolerate multi-line strings 60 | "noyield" : false, // true: Tolerate generator functions with no yield statement in them. 61 | "notypeof" : false, // true: Tolerate invalid typeof operator values 62 | "proto" : false, // true: Tolerate using the `__proto__` property 63 | "scripturl" : false, // true: Tolerate script-targeted URLs 64 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 65 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 66 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 67 | "validthis" : false, // true: Tolerate using this in a non-constructor function 68 | 69 | // Environments 70 | "browser" : false, // Web Browser (window, document, etc) 71 | "browserify" : false, // Browserify (node.js code in the browser) 72 | "couch" : false, // CouchDB 73 | "devel" : true, // Development/debugging (alert, confirm, etc) 74 | "dojo" : false, // Dojo Toolkit 75 | "jasmine" : false, // Jasmine 76 | "jquery" : false, // jQuery 77 | "mocha" : true, // Mocha 78 | "mootools" : false, // MooTools 79 | "node" : true, // Node.js 80 | "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) 81 | "phantom" : false, // PhantomJS 82 | "prototypejs" : false, // Prototype and Scriptaculous 83 | "qunit" : false, // QUnit 84 | "rhino" : false, // Rhino 85 | "shelljs" : false, // ShellJS 86 | "typed" : false, // Globals for typed array constructions 87 | "worker" : false, // Web Workers 88 | "wsh" : false, // Windows Scripting Host 89 | "yui" : false, // Yahoo User Interface 90 | 91 | // Custom Globals 92 | "globals" : {} // additional predefined global variables 93 | } 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Universal Rendering - Proof of Concept 2 | This lightweight application aims to showcase how easy it can be to get an isomorphic app started. 3 | Check out the [demo](https://isomorphic-proof-of-concept.github.pablosichert.de/)! Uses 4 | - [Riot](http://riotjs.com/) as view engine (client and server side) 5 | - [Redux](http://rackt.org/redux/) for application state handling 6 | - [Page.js](https://visionmedia.github.io/page.js/) for client side routing 7 | - [Express](http://expressjs.com/) for server side routing 8 | 9 | ## Quick Installation and Start 10 | ``` 11 | > git clone https://github.com/PabloSichert/isomorphic-proof-of-concept 12 | > cp config.example.js config.js 13 | > npm install 14 | > gulp 15 | > npm start 16 | ``` 17 | -------------------------------------------------------------------------------- /client/app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import {createStore} from 'redux'; 4 | import app from '../shared/components/app.tag'; 5 | import reducers from '../shared/reducers'; 6 | import riot from 'riot'; 7 | import routes from '../shared/routes'; 8 | 9 | const store = createStore(reducers, window.state); 10 | var state = store.getState(); 11 | 12 | riot.mount(app, { 13 | isClient: true, 14 | routes: routes, 15 | store: store, 16 | state: state 17 | }); 18 | -------------------------------------------------------------------------------- /config.example.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const config = { 4 | ports: { 5 | http: 1337 6 | }, 7 | defaultTitle: 'App' 8 | }; 9 | 10 | export default config; 11 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var babelify = require('babelify'); 4 | var browserify = require('browserify'); 5 | var gulp = require('gulp'); 6 | var riotify = require('riotify'); 7 | var source = require('vinyl-source-stream'); 8 | 9 | gulp.task('default', ['bundle-client-files']); 10 | 11 | gulp.task('bundle-client-files', function() { 12 | return browserify({ 13 | debug: true, 14 | entries: ['client/app.js'], 15 | transform: [ 16 | [babelify], 17 | [riotify, {"type": "es6"}] 18 | ] 19 | }) 20 | .bundle() 21 | .pipe(source('app.js')) 22 | .pipe(gulp.dest('public/js')) 23 | ; 24 | }); 25 | 26 | gulp.task('watch', function() { 27 | gulp.watch('shared/components/*', ['bundle-client-files']); 28 | }); 29 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | require('babel/register'); 4 | 5 | require('./server/server.js')(); 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isomorphic-proof-of-concept", 3 | "version": "1.1.0", 4 | "description": "Proof of concept for an isomorphic web application using the Flux pattern. Uses Riot for the view layer and Redux for the Flux stores.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "author": "Pablo Sichert (https://pablosichert.de)", 10 | "license": "MIT", 11 | "dependencies": { 12 | "express": "^4.13.3", 13 | "nunjucks": "^2.1.0", 14 | "page": "^1.6.4", 15 | "redux": "^3.0.2", 16 | "riot": "2.2.x" 17 | }, 18 | "devDependencies": { 19 | "babel": "^5.8.23", 20 | "babelify": "^6.3.0", 21 | "browserify": "^11.2.0", 22 | "gulp": "^3.9.0", 23 | "riotify": "^0.1.2", 24 | "vinyl-source-stream": "^1.1.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import {createStore} from 'redux'; 4 | import config from '../config'; 5 | import express from 'express'; 6 | import http from 'http'; 7 | import nunjucks from 'nunjucks'; 8 | import riot from 'riot'; 9 | import reducers from '../shared/reducers'; 10 | import routes from '../shared/routes'; 11 | import riotApp from '../shared/components/app'; 12 | 13 | export default function() { 14 | var app = express(); 15 | 16 | nunjucks.configure('server/views', { 17 | autoescape: false, 18 | express: app 19 | }); 20 | 21 | app.set('view engine', 'tmpl'); 22 | app.use(express.static('public')); 23 | 24 | app.use((req, res, next) => { 25 | // Default state 26 | req.state = { 27 | activeRoute: routes.home 28 | }; 29 | next(); 30 | }); 31 | 32 | for (let route in routes) { 33 | app.get(routes[route].path, (req, res, next) => { 34 | req.state.activeRoute = routes[route]; 35 | next(); 36 | }); 37 | } 38 | 39 | app.use((req, res) => { 40 | var store = createStore(reducers, req.state); 41 | var html = riot.render(riotApp, {isClient: false, routes: routes, store: store, state: store.getState()}); 42 | 43 | res.render('base', { 44 | html: html, 45 | state: req.state 46 | }); 47 | }); 48 | 49 | http.createServer(app).listen(config.ports.http); 50 | console.log('http server started on port ' + config.ports.http); 51 | } 52 | -------------------------------------------------------------------------------- /server/views/base.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{state.title}} 5 | 6 | 7 | 8 | 9 | {{html}} 10 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /shared/actions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var actionsSet = new Set([ 4 | 'ROUTE_CHANGE' 5 | ]); 6 | 7 | var actionsObj = {}; 8 | 9 | for (let action of actionsSet) { 10 | actionsObj[action] = action; 11 | } 12 | 13 | const actions = actionsObj; 14 | 15 | export default actions; 16 | -------------------------------------------------------------------------------- /shared/components/app.tag: -------------------------------------------------------------------------------- 1 | 2 |

Current route: {JSON.stringify(opts.state.activeRoute)}

3 | 8 |
9 |

{opts.state.activeRoute.title}

10 |
11 | Content for "Home" 12 |
13 |
14 | Content for "Page 1" 15 |
16 |
17 | Content for "Page 2" 18 |
19 |
20 | 21 | 47 |
48 | -------------------------------------------------------------------------------- /shared/reducers/activeRoute.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import routes from '../routes'; 4 | import {ROUTE_CHANGE} from '../actions'; 5 | 6 | function activeRoute(state = routes.home, action) { 7 | switch (action.type) { 8 | case ROUTE_CHANGE: 9 | return action.activeRoute; 10 | default: 11 | return state; 12 | } 13 | } 14 | 15 | export default activeRoute; 16 | -------------------------------------------------------------------------------- /shared/reducers/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import {combineReducers} from 'redux'; 4 | 5 | export default combineReducers({ 6 | activeRoute: require('./activeRoute') 7 | }); 8 | -------------------------------------------------------------------------------- /shared/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default { 4 | 'home': { 5 | path: '/', 6 | title: 'Home' 7 | }, 8 | 'page1': { 9 | path: '/page-1', 10 | title: 'Page 1' 11 | }, 12 | 'page2': { 13 | path: '/page-2', 14 | title: 'Page 2' 15 | } 16 | }; 17 | --------------------------------------------------------------------------------