├── .gitignore ├── README.md ├── nodejs ├── app │ ├── browser.coffee │ ├── developer_list.cjsx │ ├── miniprofile.cjsx │ ├── pagination.cjsx │ ├── routes.coffee │ └── server.coffee ├── gulpfile.coffee ├── gulpfile.js └── package.json ├── php ├── composer.json ├── composer.lock ├── composer.phar └── index.php └── screenshots ├── screenshot1.png └── screenshot2.png /.gitignore: -------------------------------------------------------------------------------- 1 | nodejs/node_modules 2 | nodejs/npm-debug.log 3 | nodejs/app/public/js 4 | php/vendor 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # isomorphic-post-code 2 | 3 | ### What's going on here? 4 | This little repo provides the companion code for the [isomorphic React in a PHP application](http://ericescalante.com/2015/06/07/isomorphic/) post. It contains a small node.js app and a tiny PHP app. 5 | 6 | I wrote the code for this post using coffeescript, I love it's simplicity and terseness (specially since I've been working on Rails for almost a year). As soon as react 0.14 comes out I'll do a rewrite using ES2015 (via Babel). 7 | 8 | ### Setup instructions 9 | To get started, clone this repo then cd into the `isomorphic-post-code` directory. You will find inside a `nodejs` folder and a `php` fodler. 10 | #### Node.js 11 | Follow these steps to set up the node/react app: 12 | ``` 13 | cd nodejs 14 | npm install 15 | gulp 16 | ``` 17 | This will start the node server up, create the javascript bundle for the browser and inform you that it's ready to serve connections on ports 3000 and 3001. 18 | #### PHP 19 | Now, open a new terminal tab and go to the `isomorphic-post-code` directory as well. Then type the following to get the PHP side of things going. 20 | ``` 21 | cd php 22 | curl -sS https://getcomposer.org/installer | php 23 | php composer.phar install 24 | php -S localhost:8000 25 | ``` 26 | This will start a PHP server serving the index.php file by default. 27 | 28 | ### Playing with the node app 29 | Now, to try the node/react app, point your browser to `http://localhost:3000/developers/page/1`. You should see this: 30 | 31 | ![Alt text](/screenshots/screenshot1.png?raw=true "node.js app") 32 | 33 | Things to try out: 34 | * Check on the traffic tab that only the first page is served as full document, the rest should come via ajax. 35 | * Disable javascript, pagination should still work, this time as full page loads. 36 | * View the page source, you can find the first list of developers as JSON string. 37 | * Go directly to a specific page by typing the url like `http://localhost:3000/developers/page/666`, the behaviour should be the same. 38 | * Bookmark the pages with your favourite developer avatars :) 39 | * If you're really into hardcore experiences, visit the page with [lynx](http://lynx.browser.org/)! 40 | 41 | ### Now fiddle with the PHP one! 42 | Go to `http://localhost:8000/developers/page/1` and you should see now the output of `index.php`: 43 | 44 | ![Alt text](/screenshots/screenshot2.png?raw=true "PHP app") 45 | 46 | Things to try out: 47 | * All of the above 48 | 49 | Feel free to create an issue if something does not work quite as it should :) 50 | -------------------------------------------------------------------------------- /nodejs/app/browser.coffee: -------------------------------------------------------------------------------- 1 | React = require 'react' 2 | Router = require 'react-router' 3 | routes = require './routes' 4 | 5 | Router.run routes, Router.HistoryLocation, (Handler, state) -> 6 | if window.initialPage == parseInt state.params.page 7 | React.render , document.getElementById('app') 8 | else 9 | component = state.routes[1].handler 10 | component.fetchData state.params, (err, data) -> 11 | React.render , document.getElementById('app') 12 | -------------------------------------------------------------------------------- /nodejs/app/developer_list.cjsx: -------------------------------------------------------------------------------- 1 | React = require 'react' 2 | request = require 'request' 3 | MiniProfile = require './miniprofile.cjsx' 4 | Pagination = require './pagination.cjsx' 5 | 6 | DeveloperList = React.createClass 7 | displayName: 'DeveloperList' 8 | statics: 9 | fetchData: (params, callback) -> 10 | 'Fetching from Github' 11 | options = 12 | url: "https://api.github.com/users?since=#{params.page*30}" 13 | headers: 14 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/21.0' 15 | withCredentials:false 16 | request options, (error, response, body) -> 17 | return callback null, false if response.statusCode != 200 18 | callback null, JSON.parse body 19 | render: -> 20 | profiles = [] 21 | this.props.data.forEach (user, index) -> 22 | profiles.push 23 | 24 |
25 |
26 | {profiles} 27 |
28 | 29 |
30 | 31 | module.exports = DeveloperList -------------------------------------------------------------------------------- /nodejs/app/miniprofile.cjsx: -------------------------------------------------------------------------------- 1 | React = require 'react' 2 | 3 | MiniProfile = React.createClass 4 | displayName: 'MiniProfile' 5 | render: -> 6 | user = this.props.user 7 |
8 | 9 |
10 | {user.login} 11 |
12 |
13 | 14 | module.exports = MiniProfile -------------------------------------------------------------------------------- /nodejs/app/pagination.cjsx: -------------------------------------------------------------------------------- 1 | React = require 'react' 2 | cx = require 'classnames' 3 | Link = require('react-router').Link 4 | 5 | Pagination = React.createClass 6 | displayName: 'Pagination' 7 | render: -> 8 | prevClasses = cx 9 | 'glyphicon': true 10 | 'glyphicon-chevron-left': true 11 | 'btn btn-primary': true 12 | 'hidden': this.props.currentPage == 1 13 |
14 | 15 | 17 | 18 | 19 | 21 | 22 |
23 | 24 | module.exports = Pagination -------------------------------------------------------------------------------- /nodejs/app/routes.coffee: -------------------------------------------------------------------------------- 1 | React = require('react') 2 | Route = require('react-router').Route 3 | 4 | module.exports = [ 5 | 6 | 7 | 8 | ] 9 | -------------------------------------------------------------------------------- /nodejs/app/server.coffee: -------------------------------------------------------------------------------- 1 | require 'coffee-react/register' 2 | React = require 'react' 3 | Router = require 'react-router' 4 | routes = require './routes' 5 | dnode = require 'dnode' 6 | express = require 'express' 7 | app = express() 8 | gutil = require 'gulp-util' 9 | 10 | regularPort = 3000 11 | dnodePort = 3001 12 | 13 | # HTTP server 14 | app.get '/developers/page/*', (req, res) -> 15 | router = Router.create location: req.url, routes: routes 16 | router.run (Handler, state) -> 17 | gutil.log "Serving to browser via #{regularPort}" 18 | component = state.routes[1].handler 19 | component.fetchData state.params, (err, data) -> 20 | reacOutput = React.renderToString(React.createElement(Handler, data: data, params: state.params)) 21 | res.send(getHtml(reacOutput, data, state)) 22 | 23 | #static files 24 | app.use express.static __dirname+'/public' 25 | 26 | server = app.listen regularPort, -> 27 | gutil.log "HTTP: Listening on port #{regularPort}" 28 | 29 | getHtml = (reacOutput, data, state) -> 30 | response = '' 31 | response += "" 32 | response += "" 33 | response += '
' 34 | response += reacOutput 35 | response += '
' 36 | response += "" 37 | 38 | # dnode server 39 | dserver = dnode 40 | renderIndex: (remoteParams, remoteCallback) -> 41 | router = Router.create location: '/developers/page/'+remoteParams.page, routes: routes 42 | router.run (Handler, state) -> 43 | gutil.log "Serving to PHP via #{dnodePort}" 44 | component = state.routes[1].handler 45 | component.fetchData state.params, (err, data) -> 46 | reacOutput = React.renderToString(React.createElement(Handler, data: data, params: state.params)) 47 | remoteCallback(getHtml(reacOutput, data, state)) 48 | 49 | dserver.listen dnodePort 50 | gutil.log "dnode: Listening on port #{dnodePort}" 51 | -------------------------------------------------------------------------------- /nodejs/gulpfile.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | plugins = require('gulp-load-plugins')() 3 | source = require 'vinyl-source-stream' 4 | browserify = require 'browserify' 5 | watchify = require 'watchify' 6 | 7 | 8 | gulp.task 'server', -> 9 | plugins.developServer.listen path:'./app/server.coffee', plugins.livereload.listen 10 | 11 | gulp.task 'browserify', -> 12 | bundler = browserify 13 | entries: ['./app/browser.coffee'] 14 | extensions: ['.coffee', '.js'] 15 | debug: true 16 | cache: {}, packageCache: {} 17 | bundler.transform 'coffee-reactify' 18 | 19 | watcher = watchify bundler 20 | watcher 21 | .on 'update', -> 22 | updateStart = Date.now() 23 | watcher 24 | .bundle().on 'error', plugins.util.log 25 | .pipe source 'bundle.js' 26 | .pipe gulp.dest './app/public/js/' 27 | plugins.util.log 'Done browserifying' 28 | .bundle() 29 | .pipe source 'bundle.js' 30 | .pipe gulp.dest './app/public/js/' 31 | 32 | 33 | gulp.task 'default',['server', 'browserify'], -> 34 | sources = [ './app/*.coffee', './app/*.cjsx'] 35 | reloadServer = (file) -> 36 | plugins.util.log 'Server restarted' 37 | plugins.developServer.restart (error) -> 38 | if !error then plugins.livereload.changed file.path 39 | gulp.watch sources 40 | .on 'change', reloadServer 41 | -------------------------------------------------------------------------------- /nodejs/gulpfile.js: -------------------------------------------------------------------------------- 1 | require('coffee-script/register'); 2 | require('./gulpfile.coffee'); 3 | -------------------------------------------------------------------------------- /nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isomorphic-post-code", 3 | "version": "1.0.0", 4 | "description": "Sample code for the ismorphic react with php post.", 5 | "main": "server.coffee", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/ericescalante/isomorphic-post-code.git" 12 | }, 13 | "keywords": [ 14 | "reactjs", 15 | "node", 16 | "php", 17 | "dnode", 18 | "chilaquiles", 19 | "chocolate" 20 | ], 21 | "author": "Eric Escalante", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/ericescalante/isomorphic-post-code/issues" 25 | }, 26 | "homepage": "https://github.com/ericescalante/isomorphic-post-code", 27 | "dependencies": { 28 | "browserify": "^9.0.8", 29 | "browserify-shim": "^3.8.7", 30 | "classnames": "~1.2.1", 31 | "coffee-react": "^3.2.0", 32 | "coffee-reactify": "^3.0.0", 33 | "coffee-script": "*", 34 | "coffeeify": "^1.0.0", 35 | "dnode": "^1.2.0", 36 | "express": "^4.12.4", 37 | "gulp": "^3.8.11", 38 | "gulp-cjsx": "^3.0.0", 39 | "gulp-coffee": "^2.3.1", 40 | "gulp-develop-server": "^0.4.2", 41 | "gulp-livereload": "^3.8.0", 42 | "gulp-load-plugins": "^1.0.0-rc.1", 43 | "gulp-util": "^3.0.4", 44 | "react": "^0.13.3", 45 | "react-router": "^0.13.3", 46 | "react-tools": "^0.13.2", 47 | "request": "^2.55.0", 48 | "vinyl-source-stream": "^1.1.0", 49 | "watchify": "^3.1.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /php/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "dnode/dnode": "*" 4 | } 5 | } -------------------------------------------------------------------------------- /php/composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "93548a4b4640772d20fc0762b1471a2a", 8 | "packages": [ 9 | { 10 | "name": "dnode/dnode", 11 | "version": "v0.2.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/bergie/dnode-php.git", 15 | "reference": "048deef50afe95e21ef25c026bd79b2917920f3b" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/bergie/dnode-php/zipball/048deef50afe95e21ef25c026bd79b2917920f3b", 20 | "reference": "048deef50afe95e21ef25c026bd79b2917920f3b", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "evenement/evenement": "~1.0", 25 | "php": ">=5.3.0", 26 | "react/socket": "0.3.*" 27 | }, 28 | "type": "library", 29 | "autoload": { 30 | "psr-0": { 31 | "DNode": "src" 32 | } 33 | }, 34 | "notification-url": "https://packagist.org/downloads/", 35 | "license": [ 36 | "MIT" 37 | ], 38 | "authors": [ 39 | { 40 | "name": "Igor Wiedler", 41 | "email": "igor@wiedler.ch", 42 | "homepage": "http://wiedler.ch/igor/" 43 | }, 44 | { 45 | "name": "Henri Bergius", 46 | "email": "henri.bergius@iki.fi", 47 | "homepage": "http://bergie.iki.fi/" 48 | } 49 | ], 50 | "description": "DNode RPC protocol for PHP 5.3", 51 | "homepage": "https://github.com/bergie/dnode-php", 52 | "keywords": [ 53 | "dnode", 54 | "nodejs", 55 | "rpc" 56 | ], 57 | "time": "2014-01-31 09:12:55" 58 | }, 59 | { 60 | "name": "evenement/evenement", 61 | "version": "v1.0.0", 62 | "source": { 63 | "type": "git", 64 | "url": "https://github.com/igorw/evenement.git", 65 | "reference": "fa966683e7df3e5dd5929d984a44abfbd6bafe8d" 66 | }, 67 | "dist": { 68 | "type": "zip", 69 | "url": "https://api.github.com/repos/igorw/evenement/zipball/fa966683e7df3e5dd5929d984a44abfbd6bafe8d", 70 | "reference": "fa966683e7df3e5dd5929d984a44abfbd6bafe8d", 71 | "shasum": "" 72 | }, 73 | "require": { 74 | "php": ">=5.3.0" 75 | }, 76 | "type": "library", 77 | "autoload": { 78 | "psr-0": { 79 | "Evenement": "src" 80 | } 81 | }, 82 | "notification-url": "https://packagist.org/downloads/", 83 | "license": [ 84 | "MIT" 85 | ], 86 | "authors": [ 87 | { 88 | "name": "Igor Wiedler", 89 | "email": "igor@wiedler.ch", 90 | "homepage": "http://wiedler.ch/igor/" 91 | } 92 | ], 93 | "description": "Événement is a very simple event dispatching library for PHP 5.3", 94 | "keywords": [ 95 | "event-dispatcher" 96 | ], 97 | "time": "2012-05-30 15:01:08" 98 | }, 99 | { 100 | "name": "react/event-loop", 101 | "version": "v0.3.4", 102 | "target-dir": "React/EventLoop", 103 | "source": { 104 | "type": "git", 105 | "url": "https://github.com/reactphp/event-loop.git", 106 | "reference": "235cddfa999a392e7d63dc9bef2e042492608d9f" 107 | }, 108 | "dist": { 109 | "type": "zip", 110 | "url": "https://api.github.com/repos/reactphp/event-loop/zipball/235cddfa999a392e7d63dc9bef2e042492608d9f", 111 | "reference": "235cddfa999a392e7d63dc9bef2e042492608d9f", 112 | "shasum": "" 113 | }, 114 | "require": { 115 | "php": ">=5.3.3" 116 | }, 117 | "suggest": { 118 | "ext-libev": "*", 119 | "ext-libevent": ">=0.0.5" 120 | }, 121 | "type": "library", 122 | "extra": { 123 | "branch-alias": { 124 | "dev-master": "0.3-dev" 125 | } 126 | }, 127 | "autoload": { 128 | "psr-0": { 129 | "React\\EventLoop": "" 130 | } 131 | }, 132 | "notification-url": "https://packagist.org/downloads/", 133 | "license": [ 134 | "MIT" 135 | ], 136 | "description": "Event loop abstraction layer that libraries can use for evented I/O.", 137 | "keywords": [ 138 | "event-loop" 139 | ], 140 | "time": "2013-07-21 02:23:09" 141 | }, 142 | { 143 | "name": "react/socket", 144 | "version": "v0.3.4", 145 | "target-dir": "React/Socket", 146 | "source": { 147 | "type": "git", 148 | "url": "https://github.com/reactphp/socket.git", 149 | "reference": "19bc0c4309243717396022ffb2e59be1cc784327" 150 | }, 151 | "dist": { 152 | "type": "zip", 153 | "url": "https://api.github.com/repos/reactphp/socket/zipball/19bc0c4309243717396022ffb2e59be1cc784327", 154 | "reference": "19bc0c4309243717396022ffb2e59be1cc784327", 155 | "shasum": "" 156 | }, 157 | "require": { 158 | "evenement/evenement": "1.0.*", 159 | "php": ">=5.3.3", 160 | "react/event-loop": "0.3.*", 161 | "react/stream": "0.3.*" 162 | }, 163 | "type": "library", 164 | "extra": { 165 | "branch-alias": { 166 | "dev-master": "0.3-dev" 167 | } 168 | }, 169 | "autoload": { 170 | "psr-0": { 171 | "React\\Socket": "" 172 | } 173 | }, 174 | "notification-url": "https://packagist.org/downloads/", 175 | "license": [ 176 | "MIT" 177 | ], 178 | "description": "Library for building an evented socket server.", 179 | "keywords": [ 180 | "Socket" 181 | ], 182 | "time": "2014-02-17 22:32:00" 183 | }, 184 | { 185 | "name": "react/stream", 186 | "version": "v0.3.4", 187 | "target-dir": "React/Stream", 188 | "source": { 189 | "type": "git", 190 | "url": "https://github.com/reactphp/stream.git", 191 | "reference": "feef56628afe3fa861f0da5f92c909e029efceac" 192 | }, 193 | "dist": { 194 | "type": "zip", 195 | "url": "https://api.github.com/repos/reactphp/stream/zipball/feef56628afe3fa861f0da5f92c909e029efceac", 196 | "reference": "feef56628afe3fa861f0da5f92c909e029efceac", 197 | "shasum": "" 198 | }, 199 | "require": { 200 | "evenement/evenement": "1.0.*", 201 | "php": ">=5.3.3" 202 | }, 203 | "suggest": { 204 | "react/event-loop": "0.3.*", 205 | "react/promise": "~1.0" 206 | }, 207 | "type": "library", 208 | "extra": { 209 | "branch-alias": { 210 | "dev-master": "0.3-dev" 211 | } 212 | }, 213 | "autoload": { 214 | "psr-0": { 215 | "React\\Stream": "" 216 | } 217 | }, 218 | "notification-url": "https://packagist.org/downloads/", 219 | "license": [ 220 | "MIT" 221 | ], 222 | "description": "Basic readable and writable stream interfaces that support piping.", 223 | "keywords": [ 224 | "pipe", 225 | "stream" 226 | ], 227 | "time": "2014-02-16 19:48:52" 228 | } 229 | ], 230 | "packages-dev": [], 231 | "aliases": [], 232 | "minimum-stability": "stable", 233 | "stability-flags": [], 234 | "prefer-stable": false, 235 | "prefer-lowest": false, 236 | "platform": [], 237 | "platform-dev": [] 238 | } 239 | -------------------------------------------------------------------------------- /php/composer.phar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericescalante/isomorphic-post-code/2d294c88cb2911553862e13bf6a8c8852b5e6a09/php/composer.phar -------------------------------------------------------------------------------- /php/index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sample PHP page rendering Node/ReactJS 5 | 6 | 7 | 8 | 9 |
10 |
11 |

Sample PHP page

12 |

The content below is rendered server-side with PHP/node/react via dnode.

13 |

Subsequent pagination is done with react client-side via Ajax.

14 |

This could be a Drupal, Wordpress, or any other kind of PHP app :)

15 |
16 | 17 |
18 | array_pop($path)); 24 | $loop = new React\EventLoop\StreamSelectLoop(); 25 | $dnode = new DNode\DNode($loop); 26 | $dnode->connect(3001, function($remote, $connection) use($options) { 27 | $remote->renderIndex($options, function($result) use ($connection) { 28 | echo $result; 29 | $connection->end(); 30 | }); 31 | }); 32 | $loop->run(); 33 | 34 | ?> 35 |
36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /screenshots/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericescalante/isomorphic-post-code/2d294c88cb2911553862e13bf6a8c8852b5e6a09/screenshots/screenshot1.png -------------------------------------------------------------------------------- /screenshots/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericescalante/isomorphic-post-code/2d294c88cb2911553862e13bf6a8c8852b5e6a09/screenshots/screenshot2.png --------------------------------------------------------------------------------