diff --git a/reactjs-server-side-rendering/README.md b/reactjs-server-side-rendering/README.md new file mode 100644 index 000000000..2e8d411db --- /dev/null +++ b/reactjs-server-side-rendering/README.md @@ -0,0 +1,13 @@ +### Vert.x + React.js SSR + +This projects aims at showing [Vert.x](http://vertx.io) and [react.js]https://facebook.github.io/react/) server side rendering capabilities. + +To run the project, simply run `npm install && npm start` from the project's root directory, then point your browser at [http://localhost:8080](http://localhost:8080) to get started. + +The example shows how to reuse `react.js` code in your server and use it to prerender the start HTML page. + +It also shows how to use `webpack` and `babel` in order to overcome the limitations of Nashorn with respect to javascript language level. + +The client code (Browser) lives under `src/client` and the server code under `src/server`. All code under `src/shared` is shared by both client and server (typically your react application logic). + +For a more productive development workflow, the `package.json` file also has a `watch` script using `vert.x`, if run, it will start your application and on file save it will reload the application for you. diff --git a/reactjs-server-side-rendering/package.json b/reactjs-server-side-rendering/package.json new file mode 100644 index 000000000..cdf2879b5 --- /dev/null +++ b/reactjs-server-side-rendering/package.json @@ -0,0 +1,44 @@ +{ + "name": "vertx-reactjs-server-side-rendering", + "version": "1.0.0", + "description": "Example showing how to mix react.js and vert.x and server side rendering", + + "mainVerticle": "server.js", + + "scripts": { + "clean": "rm -Rf .vertx", + + "install": "node ./webpack.config.js", + "postinstall": "mvn -f .vertx/pom.xml package", + + "build": "./node_modules/.bin/webpack", + "build:release": "npm run clean && ./node_modules/.bin/webpack -p && npm run clean", + + "prestart": "npm run build:release", + "start": "java -jar run.jar", + + "watch": "npm run start -- --redeploy=\"src/**\" --on-redeploy=\"npm run watch\"" + }, + + "author": "Paulo Lopes", + "license": "MIT", + + "dependencies": { + "react": "^15.4.2", + "react-dom": "^15.4.2", + "react-router": "^3.0.2" + }, + + "devDependencies": { + "babel-core": "^6.22.1", + "babel-loader": "^6.2.10", + "babel-preset-es2015": "^6.22.0", + "babel-preset-react": "^6.22.0", + "webpack": "^2.2.0" + }, + + "javaDependencies": { + "io.vertx:vertx-lang-js": "3.4.1", + "io.vertx:vertx-web": "3.4.1" + } +} diff --git a/reactjs-server-side-rendering/src/client/index.js b/reactjs-server-side-rendering/src/client/index.js new file mode 100644 index 000000000..1da990e6e --- /dev/null +++ b/reactjs-server-side-rendering/src/client/index.js @@ -0,0 +1,9 @@ +import React from 'react'; +import { render } from 'react-dom'; +import { Router, browserHistory } from 'react-router'; +import routes from '../shared/components/routes'; + +render ( + , + document.getElementById('app') +); diff --git a/reactjs-server-side-rendering/src/server/index.js b/reactjs-server-side-rendering/src/server/index.js new file mode 100644 index 000000000..b009c1405 --- /dev/null +++ b/reactjs-server-side-rendering/src/server/index.js @@ -0,0 +1,77 @@ +const Router = require("vertx-web-js/router") +const StaticHandler = require("vertx-web-js/static_handler") +const posts = require('../shared/posts') + +import React from 'react'; +import {renderToString} from 'react-dom/server' +import {match, RouterContext} from 'react-router' +import routes from '../shared/components/routes' + +const app = Router.router(vertx); + +app.get('/api/post').handler((ctx) => { + ctx.response() + .putHeader("content-type", "application/json") + .end(JSON.stringify(posts)); +}); + +app.get('/api/post/:id/*').handler((ctx) => { + const id = ctx.request().getParam('id') + + const post = posts.filter(p => p.id == id) + + if (post) { + ctx.response() + .putHeader("content-type", "application/json") + .end(JSON.stringify(post[0])) + } else { + ctx.fail(404); + } +}); + +app.get().handler((ctx) => { + match({routes: routes, location: ctx.request().uri()}, (err, redirect, props) => { + + if (err) { + ctx.fail(err.message); + } else if (redirect) { + ctx.response() + .putHeader("Location", redirect.pathname + redirect.search) + .setStatusCode(302) + .end(); + } else if (props) { + const routerContextWithData = ( + { + return + }} + /> + ); + const appHtml = renderToString(routerContextWithData) + + ctx.response() + .putHeader("content-type", "text/html") + .end(` + + + + + Universal Blog + + +
${appHtml}
+ + + `) + } else { + ctx.next() + } + }); +}); + +app.get().handler(StaticHandler.create().handle) + +vertx.createHttpServer().requestHandler(app.accept).listen(8080) + +console.log('Server listening: http://127.0.0.1:8080/') \ No newline at end of file diff --git a/reactjs-server-side-rendering/src/shared/components/App.js b/reactjs-server-side-rendering/src/shared/components/App.js new file mode 100644 index 000000000..467bc5fb7 --- /dev/null +++ b/reactjs-server-side-rendering/src/shared/components/App.js @@ -0,0 +1,70 @@ +import React from 'react' +import Post from './Post' +import {Link, IndexLink} from 'react-router' + +const allPostsUrl = '/api/post'; + +class App extends React.Component { + constructor(props) { + super(props); + this.state = { + posts: props.posts || [] + } + } + + componentDidMount() { + const request = new XMLHttpRequest(); + request.open('GET', allPostsUrl, true); + request.setRequestHeader('Content-type', 'application/json'); + + request.onload = () => { + if (request.status === 200) { + this.setState({ + posts: JSON.parse(request.response) + }); + } + }; + + request.send() + } + + render() { + const posts = this.state.posts.map((post) => { + const linkTo = `/${post.id}/${post.slug}`; + + return ( +
  • + {post.title} +
  • + ) + }); + + const {postId, postName} = this.props.params; + let postTitle, postContent; + if (postId && postName) { + const post = this.state.posts.filter(p => p.id == postId)[0]; + if (post) { + postTitle = post.title; + postContent = post.content; + } + } + + return ( +
    + Home +

    Posts

    +
      + {posts} +
    + + {postTitle && postContent ? ( + + ) : ( + this.props.children + )} +
    + ) + } +} + +export default App diff --git a/reactjs-server-side-rendering/src/shared/components/Home.js b/reactjs-server-side-rendering/src/shared/components/Home.js new file mode 100644 index 000000000..09b0f5034 --- /dev/null +++ b/reactjs-server-side-rendering/src/shared/components/Home.js @@ -0,0 +1,7 @@ +import React from 'react'; + +const Home = () => (

    Welcome to the Universal Blog!

    Check out the latest posts!

    ); + + +export default Home; + diff --git a/reactjs-server-side-rendering/src/shared/components/Post.js b/reactjs-server-side-rendering/src/shared/components/Post.js new file mode 100644 index 000000000..0a006c21e --- /dev/null +++ b/reactjs-server-side-rendering/src/shared/components/Post.js @@ -0,0 +1,5 @@ +import React from 'react'; + +const Post = ({title, content}) => (

    {title}

    {content}

    ); + +export default Post; diff --git a/reactjs-server-side-rendering/src/shared/components/routes.js b/reactjs-server-side-rendering/src/shared/components/routes.js new file mode 100644 index 000000000..433f78f0e --- /dev/null +++ b/reactjs-server-side-rendering/src/shared/components/routes.js @@ -0,0 +1,11 @@ +import React from 'react'; +import { Route, IndexRoute } from 'react-router'; +import App from './App'; +import Home from './Home'; + +module.exports = ( + + + + +); diff --git a/reactjs-server-side-rendering/src/shared/posts.js b/reactjs-server-side-rendering/src/shared/posts.js new file mode 100644 index 000000000..bdefef79d --- /dev/null +++ b/reactjs-server-side-rendering/src/shared/posts.js @@ -0,0 +1,32 @@ +module.exports = [ + { + id: 0, + title: 'Building a Universal JavaScript App', + slug: 'buiding-a-universal-javascript-app', + content: 'Street art 8-bit photo booth, aesthetic kickstarter organic raw denim hoodie non kale chips pour-over occaecat. Banjo non ea, enim assumenda forage excepteur typewriter dolore ullamco. Pickled meggings dreamcatcher ugh, church-key brooklyn portland freegan normcore meditation tacos aute chicharrones skateboard polaroid. Delectus affogato assumenda heirloom sed, do squid aute voluptate sartorial. Roof party drinking vinegar franzen mixtape meditation asymmetrical. Yuccie flexitarian est accusamus, yr 3 wolf moon aliqua mumblecore waistcoat freegan shabby chic. Irure 90\'s commodo, letterpress nostrud echo park cray assumenda stumptown lumbersexual magna microdosing slow-carb dreamcatcher bicycle rights. Scenester sartorial duis, pop-up etsy sed man bun art party bicycle rights delectus fixie enim. Master cleanse esse exercitation, twee pariatur venmo eu sed ethical. Plaid freegan chambray, man braid aesthetic swag exercitation godard schlitz. Esse placeat VHS knausgaard fashion axe cred. In cray selvage, waistcoat 8-bit excepteur duis schlitz. Before they sold out bicycle rights fixie excepteur, drinking vinegar normcore laboris 90\'s cliche aliqua 8-bit hoodie post-ironic. Seitan tattooed thundercats, kinfolk consectetur etsy veniam tofu enim pour-over narwhal hammock plaid.' + }, + { + id: 1, + title: 'Learning React', + slug: 'learning-react', + content: 'excepteur typewriter dolore ullamco. Pickled meggings dreamcatcher ugh, church-key brooklyn portland freegan normcore meditation tacos aute chicharrones skateboard polaroid. Delectus affogato assumenda heirloom sed, do squid aute voluptate sartorial. Roof party drinking vinegar franzen mixtape meditation asymmetrical. Yuccie flexitarian est accusamus, yr 3 wolf moon aliqua mumblecore waistcoat freegan shabby chic. Irure 90\'s commodo, letterpress nostrud echo park cray assumenda stumptown lumbersexual magna microdosing slow-carb dreamcatcher bicycle rights. Scenester sartorial duis, pop-up etsy sed man bun art party bicycle rights delectus fixie enim. Master cleanse esse exercitation, twee pariatur venmo eu sed ethical. Plaid freegan chambray, man braid aesthetic swag exercitation godard schlitz. Esse placeat VHS knausgaard fashion axe cred. In cray selvage, waistcoat 8-bit excepteur duis schlitz. Before they sold out bicycle rights fixie excepteur, drinking vinegar normcore laboris 90\'s cliche aliqua 8-bit hoodie post-ironic. Seitan tattooed thundercats, kinfolk consectetur etsy veniam tofu enim pour-over narwhal hammock plaid.' + }, + { + id: 2, + title: 'Expert Node', + slug: 'expert-node', + content: 'franzen mixtape meditation asymmetrical. Yuccie flexitarian est accusamus, yr 3 wolf moon aliqua mumblecore waistcoat freegan shabby chic. Irure 90\'s commodo, letterpress nostrud echo park cray assumenda stumptown lumbersexual magna microdosing slow-carb dreamcatcher bicycle rights. Scenester sartorial duis, pop-up etsy sed man bun art party bicycle rights delectus fixie enim. Master cleanse esse exercitation, twee pariatur venmo eu sed ethical. Plaid freegan chambray, man braid aesthetic swag exercitation godard schlitz. Esse placeat VHS knausgaard fashion axe cred. In cray selvage, waistcoat 8-bit excepteur duis schlitz. Before they sold out bicycle rights fixie excepteur, drinking vinegar normcore laboris 90\'s cliche aliqua 8-bit hoodie post-ironic. Seitan tattooed thundercats, kinfolk consectetur etsy veniam tofu enim pour-over narwhal hammock plaid.' + }, + { + id: 3, + title: 'Debugging Node Apps', + slug: 'debugging-node-apps', + content: 'accusamus, yr 3 wolf moon aliqua mumblecore waistcoat freegan shabby chic. Irure 90\'s commodo, letterpress nostrud echo park cray assumenda stumptown lumbersexual magna microdosing slow-carb dreamcatcher bicycle rights. Scenester sartorial duis, pop-up etsy sed man bun art party bicycle rights delectus fixie enim. Master cleanse esse exercitation, twee pariatur venmo eu sed ethical. Plaid freegan chambray, man braid aesthetic swag exercitation godard schlitz. Esse placeat VHS knausgaard fashion axe cred. In cray selvage, waistcoat 8-bit excepteur duis schlitz. Before they sold out bicycle rights fixie excepteur, drinking vinegar normcore laboris 90\'s cliche aliqua 8-bit hoodie post-ironic. Seitan tattooed thundercats, kinfolk consectetur etsy veniam tofu enim pour-over narwhal hammock plaid.' + }, + { + id: 4, + title: 'Exploring ES2015', + slug: 'exploring-es2015', + content: 'voluptate sartorial. Roof party drinking vinegar franzen mixtape meditation asymmetrical. Yuccie flexitarian est accusamus, yr 3 wolf moon aliqua mumblecore waistcoat freegan shabby chic. Irure 90\'s commodo, letterpress nostrud echo park cray assumenda stumptown lumbersexual magna microdosing slow-carb dreamcatcher bicycle rights. Scenester sartorial duis, pop-up etsy sed man bun art party bicycle rights delectus fixie enim. Master cleanse esse exercitation, twee pariatur venmo eu sed ethical. Plaid freegan chambray, man braid aesthetic swag exercitation godard schlitz. Esse placeat VHS knausgaard fashion axe cred. In cray selvage, waistcoat 8-bit excepteur duis schlitz. Before they sold out bicycle rights fixie excepteur, drinking vinegar normcore laboris 90\'s cliche aliqua 8-bit hoodie post-ironic. Seitan tattooed thundercats, kinfolk consectetur etsy veniam tofu enim pour-over narwhal hammock plaid.' + }, +] diff --git a/reactjs-server-side-rendering/webpack.config.js b/reactjs-server-side-rendering/webpack.config.js new file mode 100644 index 000000000..67ff5f4b8 --- /dev/null +++ b/reactjs-server-side-rendering/webpack.config.js @@ -0,0 +1,173 @@ +var _package = require('./package.json'); +var fs = require('fs'); +var path = require('path'); +var webpack = require('webpack'); + +var javaDependencies = _package.javaDependencies || {}; + +if ('install' === process.env.npm_lifecycle_event) { + // generate pom.xml file + var pom = + '\n' + + '\n' + + '\n' + + ' 4.0.0\n' + + ' pom\n' + + '\n' + + ' ' + _package.name + '\n' + + ' ' + _package.name + '\n' + + ' ' + _package.version + '\n' + + '\n' + + ' ' + _package.name + '\n' + + ' ' + (_package.description || '') + '\n' + + '\n' + + ' \n'; + + for (dep in javaDependencies) { + if (javaDependencies.hasOwnProperty(dep)) { + pom += + ' \n' + + ' ' + dep.split(':')[0] + '\n' + + ' ' + dep.split(':')[1] + '\n' + + ' ' + javaDependencies[dep] + '\n' + + ' \n'; + } + } + + pom += + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' org.apache.maven.plugins\n' + + ' maven-dependency-plugin\n' + + ' 2.10\n' + + ' \n' + + ' \n' + + ' unpack-dependencies\n' + + ' package\n' + + ' \n' + + ' unpack-dependencies\n' + + ' \n' + + ' \n' + + ' **/*.js\n' + + ' ${project.basedir}/../node_modules\n' + + ' false\n' + + ' true\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' org.apache.maven.plugins\n' + + ' maven-shade-plugin\n' + + ' 2.3\n' + + ' \n' + + ' \n' + + ' package\n' + + ' \n' + + ' shade\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' io.vertx.core.Launcher\n' + + ' ' + _package.mainVerticle + '\n' + + ' \n' + + ' \n' + + ' \n' + + ' META-INF/services/io.vertx.core.spi.VerticleFactory\n' + + ' \n' + + ' \n' + + ' ${project.basedir}/../run.jar\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + '\n'; + + // mkdir -p .vertx + fs.mkdir(path.resolve(__dirname, '.vertx'), function (err) { + if (!err || (err && err.code === 'EEXIST')) { + // generate pom.xml + fs.writeFile(path.resolve(__dirname, '.vertx/pom.xml'), pom, function (err) { + if (err) { + console.error(err); + process.exit(1); + } + }); + } else { + if (err) { + console.error(err); + process.exit(1); + } + } + }); +} + +// exclude vert.x modules +var vertxModules = [ + function (context, request, callback) { + if (/^vertx-js\//.test(request)) { + return callback(null, 'commonjs ' + request); + } + callback(); + } +]; + +for (dep in javaDependencies) { + if (javaDependencies.hasOwnProperty(dep)) { + var mavenDep = dep.split(':'); + // exclude the meta-package + if (mavenDep[1] !== 'vertx-lang-js') { + vertxModules.push(function (context, request, callback) { + if (new RegExp('^' + mavenDep[1] + '-js/').test(request)) { + return callback(null, 'commonjs ' + request); + } + callback(); + }); + } + } +} + +var vertxConfig = { + + entry: path.resolve(__dirname, 'src/server/index.js'), + + output: { + filename: _package.mainVerticle + }, + + externals: vertxModules, + + module: { + loaders: [ + { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' } + ] + } +}; + +var webConfig = { + + entry: path.resolve(__dirname, 'src/client/index.js'), + + devtool: 'source-map', + + output: { + filename: 'webroot/bundle.js' + }, + + module: { + loaders: [ + { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' } + ] + } +}; + + +module.exports = [vertxConfig, webConfig]; \ No newline at end of file