├── .gitignore ├── Procfile ├── README.md ├── app.json ├── pom.xml ├── src ├── main │ ├── docker │ │ ├── Dockerfile.txt │ │ └── Dockerrun.aws.json │ ├── java │ │ └── demo │ │ │ ├── App.java │ │ │ └── JavaScriptEngine.java │ └── resources │ │ ├── application.yml │ │ ├── jsx │ │ └── tutorial.js │ │ ├── static │ │ └── tutorial.js │ │ └── templates │ │ ├── comments.json │ │ └── index.html └── test │ ├── java │ └── .gitkeep │ └── resources │ └── .gitkeep └── system.properties /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | .module-cache 4 | target -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: java $JAVA_OPTS -jar target/*.jar --server.port=$PORT -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | React.js Tutorial with Server Side Rendering by Spring Boot and Nashorn 2 | 3 | http://facebook.github.io/react/docs/tutorial.html 4 | 5 | [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) 6 | 7 | ## Run Application 8 | 9 | ``` console 10 | $ mvn spring-boot:run 11 | ``` 12 | 13 | Go http://localhost:8080 14 | 15 | ## Build JSX (Optional) 16 | 17 | ``` console 18 | $ jsx --watch src/main/resources/jsx src/main/resources/static/ 19 | ``` 20 | 21 | Of course, `npm install -g react-tools` is required. 22 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React.js + Spring Boot Sample", 3 | "description": "React.js Tutorial with Server Side Rendering by Spring Boot and Nashorn" 4 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | demo 7 | reactjs-tutorial-spring-boot 8 | 1.0-SNAPSHOT 9 | jar 10 | 11 | Spring Boot Docker Blank Project (from https://github.com/making/spring-boot-docker-blank) 12 | 13 | 14 | org.springframework.boot 15 | spring-boot-starter-parent 16 | 1.2.1.RELEASE 17 | 18 | 19 | 20 | UTF-8 21 | demo.App 22 | 1.8 23 | 24 | 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-web 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-thymeleaf 33 | 34 | 35 | org.webjars 36 | react 37 | 0.12.2 38 | 39 | 40 | org.webjars 41 | showdown 42 | 0.3.1 43 | 44 | 45 | org.webjars 46 | jquery 47 | 1.11.2 48 | 49 | 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-starter-actuator 54 | 55 | 56 | org.springframework.boot 57 | spring-boot-starter-test 58 | test 59 | 60 | 61 | 62 | ${project.artifactId} 63 | 64 | 65 | org.springframework.boot 66 | spring-boot-maven-plugin 67 | 68 | 69 | org.springframework 70 | springloaded 71 | ${spring-loaded.version} 72 | 73 | 74 | 75 | 76 | 77 | 78 | maven-resources-plugin 79 | 80 | 81 | copy-resources 82 | validate 83 | 84 | copy-resources 85 | 86 | 87 | ${basedir}/target/ 88 | 89 | 90 | src/main/docker 91 | true 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | com.coderplus.maven.plugins 100 | copy-rename-maven-plugin 101 | 1.0 102 | 103 | 104 | rename-file 105 | validate 106 | 107 | rename 108 | 109 | 110 | ${basedir}/target/Dockerfile.txt 111 | ${basedir}/target/Dockerfile 112 | 113 | 114 | 115 | 116 | 117 | org.apache.maven.plugins 118 | maven-antrun-plugin 119 | 1.7 120 | 121 | 122 | zip-files 123 | package 124 | 125 | run 126 | 127 | 128 | 129 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /src/main/docker/Dockerfile.txt: -------------------------------------------------------------------------------- 1 | FROM dockerfile/java:oracle-java8 2 | 3 | ADD reactjs-tutorial-spring-boot.jar /opt/reactjs-tutorial-spring-boot/ 4 | EXPOSE 8080 5 | WORKDIR /opt/reactjs-tutorial-spring-boot/ 6 | CMD ["java", "-Xms512m", "-Xmx1g", "-jar", "reactjs-tutorial-spring-boot.jar"] -------------------------------------------------------------------------------- /src/main/docker/Dockerrun.aws.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSEBDockerrunVersion": "1", 3 | "Ports": [ 4 | { 5 | "ContainerPort": "8080" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /src/main/java/demo/App.java: -------------------------------------------------------------------------------- 1 | package demo; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.stereotype.Controller; 10 | import org.springframework.ui.Model; 11 | import org.springframework.web.bind.annotation.RequestBody; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RequestMethod; 14 | import org.springframework.web.bind.annotation.ResponseBody; 15 | 16 | import javax.annotation.PostConstruct; 17 | import java.util.Arrays; 18 | import java.util.List; 19 | import java.util.concurrent.CopyOnWriteArrayList; 20 | 21 | @SpringBootApplication 22 | @Controller 23 | public class App { 24 | public static void main(String[] args) { 25 | SpringApplication.run(App.class, args); 26 | } 27 | 28 | @Bean 29 | JavaScriptEngine nashornEngine() { 30 | return new JavaScriptEngine() 31 | .polyfillToNashorn() 32 | .loadFromClassPath("META-INF/resources/webjars/react/0.12.2/react.min.js") 33 | .loadFromClassPath("META-INF/resources/webjars/showdown/0.3.1/compressed/showdown.js") 34 | .loadFromClassPath("static/tutorial.js"); 35 | } 36 | 37 | @Autowired 38 | ObjectMapper objectMapper; 39 | @Autowired 40 | JavaScriptEngine nashorn; 41 | 42 | static final List comments = new CopyOnWriteArrayList<>(); 43 | 44 | @RequestMapping("/") 45 | String hello(Model model) throws JsonProcessingException { 46 | String markup = nashorn.invokeFunction("renderOnServer", String::valueOf, comments); 47 | String initialData = objectMapper.writeValueAsString(comments); 48 | model.addAttribute("markup", markup); 49 | model.addAttribute("initialData", initialData); 50 | return "index"; 51 | } 52 | 53 | @ResponseBody 54 | @RequestMapping(value = "/comments", method = RequestMethod.GET) 55 | List getComments() { 56 | return comments; 57 | } 58 | 59 | @ResponseBody 60 | @RequestMapping(value = "/comments", method = RequestMethod.POST) 61 | List postComments(@RequestBody Comment comment) { 62 | comments.add(comment); 63 | return comments; 64 | } 65 | 66 | @PostConstruct 67 | void init() { 68 | comments.addAll(Arrays.asList( 69 | new Comment("Pete Hunt", "This is one comment"), 70 | new Comment("Jordan Walke", "This is *another* comment"))); 71 | } 72 | } 73 | 74 | class Comment { 75 | public String author; 76 | public String text; 77 | 78 | Comment() { 79 | } 80 | 81 | public Comment(String author, String text) { 82 | this.author = author; 83 | this.text = text; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/demo/JavaScriptEngine.java: -------------------------------------------------------------------------------- 1 | package demo; 2 | 3 | 4 | import javax.script.Invocable; 5 | import javax.script.ScriptEngine; 6 | import javax.script.ScriptEngineManager; 7 | import javax.script.ScriptException; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.InputStreamReader; 11 | import java.nio.charset.Charset; 12 | import java.nio.charset.StandardCharsets; 13 | import java.util.function.Function; 14 | 15 | public class JavaScriptEngine { 16 | 17 | private final ScriptEngine scriptEngine = new ScriptEngineManager() 18 | .getEngineByName("js"); 19 | 20 | public JavaScriptEngine polyfillToNashorn() { 21 | String polyfil = "var global = this;\n" 22 | + "var console = {};\n" 23 | + "console.debug = print;\n" 24 | + "console.warn = print;\n" 25 | + "console.log = print;"; 26 | return eval(polyfil); 27 | } 28 | 29 | public JavaScriptEngine eval(String script) { 30 | try { 31 | this.scriptEngine.eval(script); 32 | } catch (ScriptException e) { 33 | throw new IllegalStateException("Failed to eval " + script + "!", e); 34 | } 35 | return this; 36 | } 37 | 38 | public JavaScriptEngine loadFromClassPath(String file) { 39 | try { 40 | this.scriptEngine.eval(readFromClassPath(file)); 41 | } catch (ScriptException e) { 42 | throw new IllegalStateException("Failed to loadFromClassPath " 43 | + file + "!", e); 44 | } 45 | return this; 46 | } 47 | 48 | public Object invokeFunction(String functionName, Object... args) { 49 | try { 50 | return ((Invocable) this.scriptEngine).invokeFunction(functionName, args); 51 | } catch (ScriptException | NoSuchMethodException e) { 52 | throw new IllegalArgumentException("Failed to invoke " 53 | + functionName, e); 54 | } 55 | } 56 | 57 | public T invokeFunction(String functionName, 58 | Function converter, Object... args) { 59 | return converter.apply(invokeFunction(functionName, args)); 60 | } 61 | 62 | private String readFromClassPath(String path) { 63 | try (InputStream in = getClass().getClassLoader().getResourceAsStream( 64 | path)) { 65 | if (in == null) { 66 | throw new IllegalArgumentException(path + " is not found!"); 67 | } 68 | return copyToString(in, StandardCharsets.UTF_8); 69 | } catch (IOException e) { 70 | throw new IllegalStateException("Failed to read " + path, e); 71 | } 72 | } 73 | 74 | private static String copyToString(InputStream in, Charset charset) throws IOException { 75 | StringBuilder out = new StringBuilder(); 76 | try (InputStreamReader reader = new InputStreamReader(in, charset);) { 77 | char[] buffer = new char[4096]; 78 | int bytesRead = -1; 79 | while ((bytesRead = reader.read(buffer)) != -1) { 80 | out.append(buffer, 0, bytesRead); 81 | } 82 | return out.toString(); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | # See http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html 2 | spring: 3 | thymeleaf.cache: false 4 | main.show-banner: false -------------------------------------------------------------------------------- /src/main/resources/jsx/tutorial.js: -------------------------------------------------------------------------------- 1 | var converter = new Showdown.converter(); 2 | 3 | var CommentList = React.createClass({ 4 | render: function () { 5 | var commentNodes = this.props.data.map(function (comment) { 6 | return ( 7 | {comment.text} 8 | ); 9 | }); 10 | return ( 11 |
12 | {commentNodes} 13 |
14 | ); 15 | } 16 | }); 17 | 18 | var CommentForm = React.createClass({ 19 | handleSubmit: function (e) { 20 | e.preventDefault(); 21 | var author = this.refs.author.getDOMNode().value.trim(); 22 | var text = this.refs.text.getDOMNode().value.trim(); 23 | if (!text || !author) { 24 | return; 25 | } 26 | this.props.onCommentSubmit({author: author, text: text}); 27 | this.refs.author.getDOMNode().value = ''; 28 | this.refs.text.getDOMNode().value = ''; 29 | }, 30 | render: function () { 31 | return ( 32 |
33 | 34 | 35 | 36 |
37 | ); 38 | } 39 | }); 40 | 41 | 42 | var Comment = React.createClass({ 43 | render: function () { 44 | var rawMarkup = converter.makeHtml(this.props.children.toString()); 45 | return ( 46 |
47 |

{this.props.author}

48 | 49 |
50 | ); 51 | } 52 | }); 53 | 54 | var CommentBox = React.createClass({ 55 | loadCommentsFromServer: function () { 56 | $.ajax({ 57 | url: this.props.url, 58 | dataType: 'json', 59 | success: function (data) { 60 | this.setState({data: data}); 61 | }.bind(this), 62 | error: function (xhr, status, err) { 63 | console.error(this.props.url, status, err.toString()); 64 | }.bind(this) 65 | }); 66 | }, 67 | handleCommentSubmit: function (comment) { 68 | var comments = this.state.data; 69 | var newComments = comments.concat([comment]); 70 | this.setState({data: newComments}); 71 | $.ajax({ 72 | url: this.props.url, 73 | dataType: 'json', 74 | type: 'POST', 75 | data: JSON.stringify(comment), 76 | contentType: 'application/json', 77 | success: function (data) { 78 | this.setState({data: data}); 79 | }.bind(this), 80 | error: function (xhr, status, err) { 81 | console.error(this.props.url, status, err.toString()); 82 | }.bind(this) 83 | }); 84 | }, 85 | getInitialState: function () { 86 | return {data: this.props.data || []}; 87 | }, 88 | componentDidMount: function () { 89 | setInterval(this.loadCommentsFromServer, this.props.pollInterval); 90 | }, 91 | render: function () { 92 | return ( 93 |
94 |

Comments

95 | 96 | 97 |
98 | ); 99 | } 100 | }); 101 | 102 | 103 | function renderOnClient(comments) { 104 | var data = comments || []; 105 | React.render( 106 | , 107 | document.getElementById('content') 108 | ); 109 | } 110 | 111 | function renderOnServer(comments) { 112 | var data = Java.from(comments); 113 | return React.renderToString( 114 | 115 | ); 116 | } -------------------------------------------------------------------------------- /src/main/resources/static/tutorial.js: -------------------------------------------------------------------------------- 1 | var converter = new Showdown.converter(); 2 | 3 | var CommentList = React.createClass({displayName: "CommentList", 4 | render: function () { 5 | var commentNodes = this.props.data.map(function (comment) { 6 | return ( 7 | React.createElement(Comment, {author: comment.author}, comment.text) 8 | ); 9 | }); 10 | return ( 11 | React.createElement("div", {className: "commentList"}, 12 | commentNodes 13 | ) 14 | ); 15 | } 16 | }); 17 | 18 | var CommentForm = React.createClass({displayName: "CommentForm", 19 | handleSubmit: function (e) { 20 | e.preventDefault(); 21 | var author = this.refs.author.getDOMNode().value.trim(); 22 | var text = this.refs.text.getDOMNode().value.trim(); 23 | if (!text || !author) { 24 | return; 25 | } 26 | this.props.onCommentSubmit({author: author, text: text}); 27 | this.refs.author.getDOMNode().value = ''; 28 | this.refs.text.getDOMNode().value = ''; 29 | }, 30 | render: function () { 31 | return ( 32 | React.createElement("form", {className: "commentForm", onSubmit: this.handleSubmit}, 33 | React.createElement("input", {type: "text", placeholder: "Your name", ref: "author"}), 34 | React.createElement("input", {type: "text", placeholder: "Say something...", ref: "text"}), 35 | React.createElement("input", {type: "submit", value: "Post"}) 36 | ) 37 | ); 38 | } 39 | }); 40 | 41 | 42 | var Comment = React.createClass({displayName: "Comment", 43 | render: function () { 44 | var rawMarkup = converter.makeHtml(this.props.children.toString()); 45 | return ( 46 | React.createElement("div", {className: "comment"}, 47 | React.createElement("h2", {className: "commentAuthor"}, this.props.author), 48 | React.createElement("span", {dangerouslySetInnerHTML: {__html: rawMarkup}}) 49 | ) 50 | ); 51 | } 52 | }); 53 | 54 | var CommentBox = React.createClass({displayName: "CommentBox", 55 | loadCommentsFromServer: function () { 56 | $.ajax({ 57 | url: this.props.url, 58 | dataType: 'json', 59 | success: function (data) { 60 | this.setState({data: data}); 61 | }.bind(this), 62 | error: function (xhr, status, err) { 63 | console.error(this.props.url, status, err.toString()); 64 | }.bind(this) 65 | }); 66 | }, 67 | handleCommentSubmit: function (comment) { 68 | var comments = this.state.data; 69 | var newComments = comments.concat([comment]); 70 | this.setState({data: newComments}); 71 | $.ajax({ 72 | url: this.props.url, 73 | dataType: 'json', 74 | type: 'POST', 75 | data: JSON.stringify(comment), 76 | contentType: 'application/json', 77 | success: function (data) { 78 | this.setState({data: data}); 79 | }.bind(this), 80 | error: function (xhr, status, err) { 81 | console.error(this.props.url, status, err.toString()); 82 | }.bind(this) 83 | }); 84 | }, 85 | getInitialState: function () { 86 | return {data: this.props.data || []}; 87 | }, 88 | componentDidMount: function () { 89 | setInterval(this.loadCommentsFromServer, this.props.pollInterval); 90 | }, 91 | render: function () { 92 | return ( 93 | React.createElement("div", {className: "commentBox"}, 94 | React.createElement("h1", null, "Comments"), 95 | React.createElement(CommentList, {data: this.state.data}), 96 | React.createElement(CommentForm, {onCommentSubmit: this.handleCommentSubmit}) 97 | ) 98 | ); 99 | } 100 | }); 101 | 102 | 103 | function renderOnClient(comments) { 104 | var data = comments || []; 105 | React.render( 106 | React.createElement(CommentBox, {data: data, url: "comments.json", pollInterval: 2000}), 107 | document.getElementById('content') 108 | ); 109 | } 110 | 111 | function renderOnServer(comments) { 112 | var data = Java.from(comments); 113 | return React.renderToString( 114 | React.createElement(CommentBox, {data: data, url: "comments.json", pollInterval: 2000}) 115 | ); 116 | } -------------------------------------------------------------------------------- /src/main/resources/templates/comments.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"author": "Pete Hunt", "text": "This is one comment"}, 3 | {"author": "Jordan Walke", "text": "This is *another* comment"} 4 | ] -------------------------------------------------------------------------------- /src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello React 5 | 6 | 7 |
8 | 9 | 11 | 13 | 15 | 17 | 24 | 25 | -------------------------------------------------------------------------------- /src/test/java/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/making/reactjs-tutorial-spring-boot/7cee64e5caa1031eb6ae23ff64980c6ea06a3f25/src/test/java/.gitkeep -------------------------------------------------------------------------------- /src/test/resources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/making/reactjs-tutorial-spring-boot/7cee64e5caa1031eb6ae23ff64980c6ea06a3f25/src/test/resources/.gitkeep -------------------------------------------------------------------------------- /system.properties: -------------------------------------------------------------------------------- 1 | java.runtime.version=1.8 --------------------------------------------------------------------------------