├── .gitignore ├── LICENSE.md ├── Makefile ├── README.md ├── client ├── actions.js ├── components │ ├── app │ │ ├── favicon.ico │ │ ├── index.js │ │ └── styles.css │ ├── dev-tools │ │ └── index.js │ ├── homepage │ │ ├── index.js │ │ └── styles.css │ ├── not-found │ │ ├── index.js │ │ └── styles.css │ └── usage │ │ ├── index.js │ │ └── styles.css ├── css │ ├── funcs.js │ ├── global.css │ ├── index.js │ └── vars.js ├── index.js ├── reducers.js ├── router │ ├── index.js │ ├── routes.js │ └── toString.js └── store.js ├── hot.proxy.js ├── modd.conf ├── package.json ├── server ├── data │ └── templates │ │ └── react.html ├── jsrenderer │ ├── api.go │ ├── default_other.go │ ├── default_windows.go │ ├── duktape-renderer.go │ ├── otto-renderer.go │ ├── util.go │ └── v8-renderer.go ├── main.go └── react.go ├── vendor └── vendor.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /bin 3 | /pkg 4 | /src 5 | /vendor/* 6 | !/vendor/vendor.json 7 | /server/bindata.go 8 | /server/rice-box.go 9 | /server/data/static/build 10 | 11 | .pid 12 | .DS_Store 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Original code from "Golang Isomorphic React/Hot Reloadable/Redux/Css-Modules Starter Kit" 2 | is Copyright (C) 2015-2016 Oleg Lebedev 3 | All modified code is Copyright (C) 2016 Augusto Roman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 20 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 21 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TARGET = bin/server 2 | BUNDLE = server/data/static/build/bundle.js 3 | EMBEDDED_DATA = server/rice-box.go 4 | NODE_BIN = $(shell npm bin) 5 | GO_FILES = $(shell find ./server -type f -name "*.go") 6 | APP_FILES = $(shell find client -type f) 7 | GIT_HASH = $(shell git rev-parse HEAD) 8 | LDFLAGS = -X main.commitHash=$(GIT_HASH) 9 | 10 | build: clean $(TARGET) 11 | 12 | $(TARGET): $(EMBEDDED_DATA) $(GO_FILES) 13 | @go build -i -ldflags '$(LDFLAGS)' -o $@ server/*.go 14 | @go build -ldflags '$(LDFLAGS)' -o $@ server/*.go 15 | 16 | $(EMBEDDED_DATA): $(BUNDLE) 17 | @(cd server; rice embed -v) 18 | 19 | $(BUNDLE): $(APP_FILES) $(NODE_BIN)/webpack 20 | @$(NODE_BIN)/webpack --progress --colors --bail 21 | 22 | $(NODE_BIN)/webpack: 23 | npm install 24 | 25 | clean: 26 | @rm -rf server/data/static/build # includes $(BUNDLE) 27 | @rm -f $(TARGET) $(EMBEDDED_DATA) 28 | 29 | lint: 30 | @eslint client || true 31 | @golint $(GO_FILES) || true 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-react-v8-ssr: Work-in-progress 2 | 3 | Go server serving a react-based website using server-side rendering powered by V8. 4 | 5 | --- 6 | 7 | This repo is forked from the awesome [go-starter-kit](https://github.com/olebedev/go-starter-kit). 8 | 9 | I forked that project and am modifying to get a better understanding of how it works. And use some of the tools I'm more familiar with. There are many changes, but some of the major ones include: 10 | * rework the JS rendering engine to use either [v8](https://github.com/augustoroman/v8) or [duktape](https://github.com/olebedev/go-duktape) 11 | On windows, only duktape is allowed and used by default. 12 | * simplify the server code to be an easier-to-understand example 13 | * use [modd](https://github.com/cortesi/modd) instead of [on](https://github.com/olebedev/on) for running the server. 14 | * use [go-rice](https://github.com/GeertJohan/go.rice) instead of [go-bindata](https://github.com/jteeuwen/go-bindata) for embedding the data. 15 | * use [govendor](https://github.com/kardianos/govendor) rather than [srlt](https://github.com/olebedev/srlt) 16 | 17 | ## Installation 18 | 19 | Make sure you have: 20 | * [golang](https://golang.org/) 21 | * [node.js](https://nodejs.org/) with [npm](https://www.npmjs.com/), only to build the application bundle at compile time. 22 | For windows, it's very important that you have a recent npm, otherwise the npm install will fail because the directory paths are too long. 23 | * [GNU make](https://www.gnu.org/software/make/) 24 | 25 | #### Clone the repo 26 | ```bash 27 | $ git clone git@github.com:augustoroman/go-react-v8-ssr.git $GOPATH/src/github.com// 28 | $ cd $GOPATH/src/github.com// 29 | ``` 30 | 31 | #### Install some go-based utilities: 32 | ```bash 33 | $ go get -u github.com/kardianos/govendor 34 | $ go get -u github.com/GeertJohan/go.rice/rice 35 | $ go get -u github.com/cortesi/modd 36 | ``` 37 | 38 | #### Install dependencies: 39 | ```bash 40 | $ govendor sync 41 | $ npm install 42 | ``` 43 | 44 | #### Build V8 45 | See instructions at https://github.com/augustoroman/v8 46 | 47 | #### Add v8 symlinks 48 | ```bash 49 | $ vendor/github.com/augustoroman/v8/symlink.sh 50 | ``` 51 | 52 | ## Run development 53 | 54 | Start dev server: 55 | 56 | ``` 57 | $ modd 58 | ``` 59 | 60 | that's it. Open [http://localhost:5001/](http://localhost:5001/)(if you use default port) at your browser. Now you ready to start coding your awesome project. 61 | 62 | ## Build 63 | 64 | Install dependencies and type `NODE_ENV=production make build`. This rule produces the production webpack build and that is embedded into the go server, then builds the server. You can find the result at `./bin/server`. 65 | 66 | 67 | --- 68 | 69 | ## Project structure 70 | 71 | ##### The server's entry point 72 | ``` 73 | $ tree server 74 | server 75 | ├── main.go <-- main function declared here 76 | ├── react-v8.go 77 | ├── bindata.go <-- this file is gitignored, it will appear at compile time 78 | └── data 79 | ├── static 80 |    | └── build <-- this dir is populated by webpack automatically 81 |    └── templates 82 |    └── react.html 83 | ``` 84 | 85 | The `./server/` is flat golang package. 86 | 87 | ##### The client's entry point 88 | 89 | It's simple React application 90 | 91 | ``` 92 | $ tree client 93 | client 94 | ├── actions.js 95 | ├── components 96 | │   ├── app 97 | │   │   ├── favicon.ico 98 | │   │   ├── index.js 99 | │   │   └── styles.css 100 | │   ├── homepage 101 | │   │   ├── index.js 102 | │   │   └── styles.css 103 | │   ├── not-found 104 | │   │   ├── index.js 105 | │   │   └── styles.css 106 | │   └── usage 107 | │   ├── index.js 108 | │   └── styles.css 109 | ├── css 110 | │   ├── funcs.js 111 | │   ├── global.css 112 | │   ├── index.js 113 | │   └── vars.js 114 | ├── index.js <-- main function declared here 115 | ├── reducers.js 116 | ├── router 117 | │   ├── index.js 118 | │   ├── routes.js 119 | │   └── toString.js 120 | └── store.js 121 | ``` 122 | 123 | The client app will be compiled into `server/data/static/build/`. Then it will be embedded into go package via _go-bindata_. After that the package will be compiled into binary. 124 | 125 | **Convention**: javascript app should declare [_main_](https://github.com/augustoroman/go-react-v8-ssr/blob/master/client/index.js#L4) function right in the global namespace. It will used to render the app at the server side. 126 | 127 | ## License 128 | MIT 129 | -------------------------------------------------------------------------------- /client/actions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * action types 3 | */ 4 | 5 | export const SET_CONFIG = 'SET_CONFIG'; 6 | 7 | /** 8 | * action creators 9 | */ 10 | 11 | export function setConfig(config) { 12 | return { type: SET_CONFIG, config }; 13 | } 14 | -------------------------------------------------------------------------------- /client/components/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/augustoroman/go-react-v8-ssr/25437bb205c74e9e675b827f15d0d2a10abe083c/client/components/app/favicon.ico -------------------------------------------------------------------------------- /client/components/app/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Helmet from 'react-helmet'; 3 | 4 | export default class App extends Component { 5 | 6 | render() { 7 | return
8 | 9 | {this.props.children} 10 |
; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /client/components/app/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #f7f7f7; 3 | &:before { 4 | content: ''; 5 | height: 100%; 6 | display: inline-block; 7 | vertical-align: middle; 8 | } 9 | } 10 | 11 | :global(#app) { 12 | display: inline-block; 13 | vertical-align: middle; 14 | padding: 0 px(100); 15 | text-align: center; 16 | width: 99%; 17 | } 18 | -------------------------------------------------------------------------------- /client/components/dev-tools/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // Exported from redux-devtools 4 | import { createDevTools } from 'redux-devtools'; 5 | 6 | // Monitors are separate packages, and you can make a custom one 7 | import LogMonitor from 'redux-devtools-log-monitor'; 8 | import DockMonitor from 'redux-devtools-dock-monitor'; 9 | 10 | // createDevTools takes a monitor and produces a DevTools component 11 | const DevTools = createDevTools( 12 | // Monitors are individually adjustable with props 13 | // Consult their repositories to learn about those props 14 | 17 | 18 | 19 | ); 20 | 21 | export default DevTools; 22 | -------------------------------------------------------------------------------- /client/components/homepage/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Helmet from 'react-helmet'; 3 | import { Link } from 'react-router'; 4 | import { example, p, link } from './styles'; 5 | 6 | export default class Homepage extends Component { 7 | /*eslint-disable */ 8 | static onEnter({store, nextState, replaceState, callback}) { 9 | // Load here any data. 10 | callback(); // this call is important, don't forget it 11 | } 12 | /*eslint-enable */ 13 | 14 | render() { 15 | return
16 | 24 |

25 | Hot Reloadable
26 | Golang + React + Redux + Css-Modules 27 |
Isomorphic Starter Kit

28 |
29 |

30 | Please take a look at usage page. 31 |

32 |
; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /client/components/homepage/styles.css: -------------------------------------------------------------------------------- 1 | .example { 2 | color: $mainColor; 3 | font-size: px(40); 4 | font-weight: 500; 5 | max-width: px(500); 6 | margin: 0 auto; 7 | } 8 | 9 | .link { 10 | color: $yellow; 11 | position: relative; 12 | &:before { 13 | content: ''; 14 | position: absolute; 15 | height: px(2); 16 | bottom: px(0); 17 | background-color: $yellow; 18 | right: px(-1); 19 | left: @right; 20 | } 21 | 22 | &:hover { 23 | color: $red; 24 | &:before { 25 | background-color: $red; 26 | } 27 | } 28 | } 29 | 30 | .p { 31 | color: $mainColor; 32 | } 33 | 34 | .button { 35 | margin-top: px(50); 36 | width: px(50); 37 | height: @width; 38 | color: #333; 39 | font-size: px(30); 40 | text-align: center; 41 | background-color: $mainColor; 42 | display: inline-block; 43 | border-radius: px(3); 44 | } 45 | -------------------------------------------------------------------------------- /client/components/not-found/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Helmet from 'react-helmet'; 3 | import { IndexLink } from 'react-router'; 4 | import { notFound } from './styles'; 5 | import { link } from '../homepage/styles'; 6 | 7 | export default class NotFound extends Component { 8 | 9 | render() { 10 | return
11 | 12 |

13 | 404 Page Not Found

14 | go home 15 |
; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /client/components/not-found/styles.css: -------------------------------------------------------------------------------- 1 | .notFound { 2 | color: $mainColor; 3 | font-size: px(40); 4 | } 5 | -------------------------------------------------------------------------------- /client/components/usage/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Helmet from 'react-helmet'; 4 | import { IndexLink } from 'react-router'; 5 | import { usage, todo } from './styles'; 6 | import { example, p, link } from '../homepage/styles'; 7 | import { setConfig } from '../../actions'; 8 | 9 | class Usage extends Component { 10 | 11 | /*eslint-disable */ 12 | static onEnter({store, nextState, replaceState, callback}) { 13 | // { credentials: 'same-origin' } means that we should include any cookies 14 | // that correspond to the destination. The default for fetch is to omit 15 | // cookies. See: 16 | // https://developer.mozilla.org/en-US/docs/Web/API/GlobalFetch/fetch 17 | // (section "credentials") 18 | fetch('/api/v1/conf', { credentials: 'same-origin' }).then((r) => { 19 | return r.json(); 20 | }).then((conf) => { 21 | store.dispatch(setConfig(conf)); 22 | callback(); 23 | }); 24 | } 25 | /*eslint-enable */ 26 | 27 | render() { 28 | return
29 | 30 |

Usage:

31 |
32 | // TODO: write an article 33 |
config:
34 |           {JSON.stringify(this.props.config, null, 2)}
35 |
36 |
37 | go home 38 |
39 | Login as alice  |  40 | Login as bob  |  41 | Logout
42 |
; 43 | } 44 | 45 | } 46 | 47 | export default connect(store => ({ config: store.config }))(Usage); 48 | -------------------------------------------------------------------------------- /client/components/usage/styles.css: -------------------------------------------------------------------------------- 1 | .todo { 2 | width: px(400); 3 | margin: 0 auto; 4 | text-align: left; 5 | display: block; 6 | padding-top: px(26); 7 | color: #bbb; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /client/css/funcs.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | px: function(val) { 3 | return val + 'px'; 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /client/css/global.css: -------------------------------------------------------------------------------- 1 | h1,h2,h3,h4,h5,h6,p,ul,ol,li,button,input,strong,small,em { 2 | margin: 0; 3 | padding: 0; 4 | font-size: inherit; 5 | font-weight: inherit; 6 | font-style: inherit; 7 | } 8 | 9 | b { 10 | font-weight: inherit; 11 | } 12 | 13 | i { 14 | font-style: inherit; 15 | } 16 | 17 | ul,ol,li { 18 | display: block; 19 | } 20 | 21 | a { 22 | text-decoration: none; 23 | color: inherit; 24 | } 25 | 26 | button, input, textarea { 27 | border: none; 28 | background: none; 29 | outline: none; 30 | } 31 | 32 | html { 33 | font-family: Helvetica; 34 | } 35 | 36 | body { 37 | font-size: 16px; 38 | } 39 | 40 | 41 | html, body { 42 | height: 100%; 43 | } 44 | 45 | img { 46 | max-width: 100%; 47 | } 48 | 49 | *, :before, :after { 50 | box-sizing: border-box; 51 | } 52 | -------------------------------------------------------------------------------- /client/css/index.js: -------------------------------------------------------------------------------- 1 | require('normalize.css'); 2 | require('./global'); 3 | 4 | /** 5 | * Components. 6 | * Include all css files just if you need 7 | * to hot reload it. And make sure that you 8 | * use `webpack.optimize.DedupePlugin` 9 | */ 10 | require('#app/components/app/styles'); 11 | require('#app/components/homepage/styles'); 12 | require('#app/components/usage/styles'); 13 | require('#app/components/not-found/styles'); 14 | -------------------------------------------------------------------------------- /client/css/vars.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mainColor: '#666', 3 | yellow: '#ffc639', 4 | red: '#fb2c10' 5 | }; 6 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | const router = require('./router'); 2 | 3 | // export main function for server side rendering 4 | global.main = router.renderToString; 5 | 6 | // start app if it in the browser 7 | if(typeof window !== 'undefined') { 8 | // Start main application here 9 | router.run(); 10 | } 11 | -------------------------------------------------------------------------------- /client/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { SET_CONFIG } from './actions'; 3 | 4 | function config(state = {}, action) { 5 | switch (action.type) { 6 | case SET_CONFIG: 7 | return action.config; 8 | default: 9 | return state; 10 | } 11 | } 12 | 13 | export default combineReducers({config}); 14 | -------------------------------------------------------------------------------- /client/router/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Router, browserHistory } from 'react-router'; 4 | import { Provider } from 'react-redux'; 5 | import toString from './toString'; 6 | import { Promise } from 'when'; 7 | import createRoutes from './routes'; 8 | import { createStore, setAsCurrentStore } from '../store'; 9 | 10 | 11 | export function run() { 12 | // init promise polyfill 13 | window.Promise = window.Promise || Promise; 14 | // init fetch polyfill 15 | window.self = window; 16 | require('whatwg-fetch'); 17 | 18 | const store = createStore(window['--app-initial']); 19 | setAsCurrentStore(store); 20 | 21 | render( 22 | 23 | {createRoutes({store, first: { time: true }})} 24 | , 25 | document.getElementById('app') 26 | ); 27 | 28 | } 29 | 30 | // Export it to render on the Golang sever, keep the name sync with - 31 | // https://github.com/olebedev/go-starter-kit/blob/master/src/app/server/react.go#L65 32 | export const renderToString = toString; 33 | 34 | require('../css'); 35 | 36 | // Style live reloading 37 | if (module.hot) { 38 | let c = 0; 39 | module.hot.accept('../css', () => { 40 | require('../css'); 41 | const a = document.createElement('a'); 42 | const link = document.querySelector('link[rel="stylesheet"]'); 43 | a.href = link.href; 44 | a.search = '?' + c++; 45 | link.href = a.href; 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /client/router/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, IndexRoute, Redirect } from 'react-router'; 3 | import App from '#app/components/app'; 4 | import Homepage from '#app/components/homepage'; 5 | import Usage from '#app/components/usage'; 6 | import NotFound from '#app/components/not-found'; 7 | 8 | /** 9 | * Returns configured routes for different 10 | * environments. `w` - wrapper that helps skip 11 | * data fetching with onEnter hook at first time. 12 | * @param {Object} - any data for static loaders and first-time-loading marker 13 | * @returns {Object} - configured routes 14 | */ 15 | export default ({store, first}) => { 16 | 17 | // Make a closure to skip first request 18 | function w(loader) { 19 | return (nextState, replaceState, callback) => { 20 | if (first.time) { 21 | first.time = false; 22 | return callback(); 23 | } 24 | return loader ? loader({store, nextState, replaceState, callback}) : callback(); 25 | }; 26 | } 27 | 28 | return 29 | 30 | 31 | {/* Server redirect in action */} 32 | 33 | 34 | ; 35 | }; 36 | -------------------------------------------------------------------------------- /client/router/toString.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { renderToString } from 'react-dom/server'; 4 | import { match, RouterContext } from 'react-router'; 5 | import Helmet from 'react-helmet'; 6 | import createRoutes from './routes'; 7 | import { createStore, setAsCurrentStore } from '../store'; 8 | 9 | /** 10 | * Handle HTTP request at Golang server 11 | * 12 | * @param {Object} options request options 13 | * @param {Function} cbk response callback 14 | */ 15 | export default function (options, cbk) { 16 | 17 | let result = { 18 | uuid: options.uuid, 19 | app: null, 20 | title: null, 21 | meta: null, 22 | initial: null, 23 | error: null, 24 | redirect: null 25 | }; 26 | 27 | const store = createStore(); 28 | setAsCurrentStore(store); 29 | 30 | try { 31 | match({ routes: createRoutes({store, first: { time: false }}), location: options.url }, (error, redirectLocation, renderProps) => { 32 | try { 33 | if (error) { 34 | result.error = error; 35 | 36 | } else if (redirectLocation) { 37 | result.redirect = redirectLocation.pathname + redirectLocation.search; 38 | 39 | } else { 40 | result.app = renderToString( 41 | 42 | 43 | 44 | ); 45 | const { title, meta } = Helmet.rewind(); 46 | result.title = title.toString(); 47 | result.meta = meta.toString(); 48 | result.initial = JSON.stringify(store.getState()); 49 | } 50 | } catch (e) { 51 | result.error = e.toString(); 52 | } 53 | return cbk(JSON.stringify(result)); 54 | }); 55 | } catch (e) { 56 | result.error = e.toString(); 57 | return cbk(JSON.stringify(result)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /client/store.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore as reduxCreateStore } from 'redux'; 2 | import reducers from './reducers'; 3 | 4 | const middlewares = []; 5 | 6 | // Add state logger 7 | if (process.env.NODE_ENV !== 'production') { 8 | middlewares.push(require('redux-logger')()); 9 | } 10 | 11 | export function createStore(state) { 12 | return reduxCreateStore( 13 | reducers, 14 | state, 15 | applyMiddleware.apply(null, middlewares) 16 | ); 17 | } 18 | 19 | export let store = null; 20 | export function getStore() { return store; } 21 | export function setAsCurrentStore(s) { 22 | store = s; 23 | if (process.env.NODE_ENV !== 'production' 24 | && typeof window !== 'undefined') { 25 | window.store = store; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /hot.proxy.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var webpackDevMiddleware = require('webpack-dev-middleware'); 3 | var webpackHotMiddleware = require('webpack-hot-middleware'); 4 | var proxy = require('proxy-middleware'); 5 | var config = require('./webpack.config'); 6 | 7 | var port = +(process.env.PORT || 5000); 8 | 9 | config.entry = { 10 | bundle: [ 11 | 'webpack-hot-middleware/client?http://localhost:' + port, 12 | config.entry.bundle 13 | ] 14 | }; 15 | 16 | config.plugins.push( 17 | new webpack.optimize.OccurenceOrderPlugin(), 18 | new webpack.HotModuleReplacementPlugin(), 19 | new webpack.NoErrorsPlugin() 20 | ); 21 | 22 | config.devtool = 'cheap-module-eval-source-map'; 23 | 24 | var app = new require('express')(); 25 | 26 | var compiler = webpack(config); 27 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })); 28 | app.use(webpackHotMiddleware(compiler)); 29 | app.use(proxy('http://localhost:' + port)); 30 | 31 | port++ 32 | 33 | app.listen(port, function(error) { 34 | if (error) { 35 | console.error(error); 36 | } else { 37 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /modd.conf: -------------------------------------------------------------------------------- 1 | { 2 | daemon: BABEL_ENV=dev node hot.proxy 3 | } 4 | 5 | client/** { 6 | prep: make server/data/static/build/bundle.js 7 | } 8 | 9 | server/** vendor/** { 10 | prep: make bin/server 11 | daemon +sigterm: bin/server 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "go-starter-kit", 3 | "version": "0.1.0", 4 | "description": "", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "olebedev", 9 | "license": "MIT", 10 | "dependencies": { 11 | "autoprefixer": "^6.3.2", 12 | "autoprefixer-loader": "^3.2.0", 13 | "babel-core": "^6.5.1", 14 | "babel-eslint": "^4.1.8", 15 | "babel-loader": "^6.2.2", 16 | "babel-preset-es2015": "6.5.0", 17 | "babel-preset-react": "6.5.0", 18 | "babel-preset-react-hmre": "1.1.0", 19 | "babel-preset-stage-0": "6.5.0", 20 | "css-loader": "^0.23.1", 21 | "cssrecipes-defaults": "^0.5.0", 22 | "eslint-plugin-react": "^3.16.1", 23 | "expose-loader": "^0.7.1", 24 | "express": "^4.13.4", 25 | "extract-text-webpack-plugin": "^1.0.1", 26 | "file-loader": "^0.8.5", 27 | "history": "2.0.0", 28 | "lodash": "^4.3.0", 29 | "normalize.css": "^3.0.3", 30 | "postcss-functions": "^2.1.0", 31 | "postcss-loader": "^0.8.0", 32 | "precss": "^1.4.0", 33 | "proxy-middleware": "^0.15.0", 34 | "react": "^0.14.7", 35 | "react-dom": "^0.14.7", 36 | "react-helmet": "^3.0.0", 37 | "react-redux": "^4.4.0", 38 | "react-router": "^2.0.0", 39 | "redux": "^3.3.1", 40 | "redux-logger": "^2.6.1", 41 | "style-loader": "^0.13.0", 42 | "stylus-loader": "1.5.1", 43 | "webpack": "^1.12.13", 44 | "webpack-dev-middleware": "^1.5.1", 45 | "webpack-hot-middleware": "^2.7.1", 46 | "whatwg-fetch": "^0.11.0", 47 | "when": "^3.7.7" 48 | }, 49 | "babel": { 50 | "presets": [ 51 | "es2015", 52 | "react", 53 | "stage-0" 54 | ], 55 | "env": { 56 | "dev": { 57 | "presets": [ 58 | "react-hmre" 59 | ] 60 | } 61 | } 62 | }, 63 | "eslintConfig": { 64 | "rules": { 65 | "indent": [ 66 | 2, 67 | 2 68 | ], 69 | "quotes": [ 70 | 2, 71 | "single" 72 | ], 73 | "linebreak-style": [ 74 | 2, 75 | "unix" 76 | ], 77 | "semi": [ 78 | 2, 79 | "always" 80 | ], 81 | "react/jsx-uses-react": 2, 82 | "react/jsx-uses-vars": 2, 83 | "react/react-in-jsx-scope": 2 84 | }, 85 | "env": { 86 | "es6": true, 87 | "browser": true, 88 | "node": true 89 | }, 90 | "ecmaFeatures": { 91 | "jsx": true, 92 | "modules": true 93 | }, 94 | "plugins": [ 95 | "react" 96 | ] 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /server/data/templates/react.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ .HTMLTitle }} 7 | {{ .HTMLMeta }} 8 | 9 | 10 | {{if .Error}} 11 |
25 | 31 |
36 |
{{ .Error }}
38 |
39 | {{end}} 40 |
{{ .HTMLApp }}
41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /server/jsrenderer/api.go: -------------------------------------------------------------------------------- 1 | // Package jsrenderer is an abstract API for server-side react rendering with 2 | // several implementations. 3 | package jsrenderer 4 | 5 | import ( 6 | "errors" 7 | "html/template" 8 | "net/http" 9 | "sync" 10 | ) 11 | 12 | // Renderer is the primary interface for rendering react content. Given a set 13 | // of parameters, it will execute the react js code and return the result and 14 | // error (if any). A given Renderer is typically NOT SAFE for concurrent use. 15 | // Instead, use a Pool. 16 | type Renderer interface { 17 | Render(Params) (Result, error) 18 | } 19 | 20 | // Params describe the options that can be pass to the react rendering code. 21 | type Params struct { 22 | // Url is used directly by react-router to determine what to render. 23 | Url string `json:"url"` 24 | // Headers specifies additional headers to be included for any HTTP requests 25 | // to the local server during rendering. For exampe, if the rendering code 26 | // needs to make an authenticated API call, it's important that the 27 | // authentication be made on behalf of the correct user. 28 | Headers http.Header `json:"headers"` 29 | UUID string `json:"uuid"` 30 | } 31 | 32 | // Result is returned from a successful react rendering. 33 | type Result struct { 34 | // The rendered react content, if any. 35 | Rendered string `json:"app"` 36 | // The URL that the client should be redirected to (if non-empty). 37 | Redirect string `json:"redirect"` 38 | // The title of the page. 39 | Title string `json:"title"` 40 | // Meta HTML tags that should be included on the page. 41 | Meta string `json:"meta"` 42 | // Initial JSON data that should be included on the page, if any. 43 | Initial string `json:"initial"` 44 | } 45 | 46 | func (r Result) HTMLApp() template.HTML { return template.HTML(r.Rendered) } 47 | func (r Result) HTMLTitle() template.HTML { return template.HTML(r.Title) } 48 | func (r Result) HTMLMeta() template.HTML { return template.HTML(r.Meta) } 49 | 50 | // The javascript rendering engine timed out. 51 | var ErrTimeOut = errors.New("Timed out") 52 | 53 | // Pool is a dynamically-sized pool of Renderers that itself implements the 54 | // Renderer interface. You must provide a New() function that will construct 55 | // and initialize a new Renderer when necessary. It will grow the pool as 56 | // necessary to accommodate rendering demands, and will tend to re-use renderers 57 | // as much as possible. A Pool Renderer is safe for concurrent use. 58 | type Pool struct { 59 | New func() Renderer 60 | 61 | mu sync.Mutex 62 | available []Renderer 63 | } 64 | 65 | func (p *Pool) Render(params Params) (Result, error) { 66 | r := p.get() 67 | res, err := r.Render(params) 68 | if err != ErrTimeOut { 69 | p.put(r) // If the engine timed out, throw it away. Otherwise re-use it. 70 | } 71 | return res, err 72 | } 73 | func (p *Pool) get() Renderer { 74 | var r Renderer 75 | 76 | // Check to see if any are available. 77 | p.mu.Lock() 78 | if N := len(p.available); N > 0 { 79 | r = p.available[N-1] 80 | p.available = p.available[:N-1] 81 | } 82 | p.mu.Unlock() 83 | 84 | // Did we get one? If not, allocate a new one. 85 | if r == nil { 86 | r = p.New() 87 | } 88 | 89 | return r 90 | } 91 | 92 | func (p *Pool) put(r Renderer) { 93 | p.mu.Lock() 94 | p.available = append(p.available, r) 95 | p.mu.Unlock() 96 | } 97 | -------------------------------------------------------------------------------- /server/jsrenderer/default_other.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package jsrenderer 4 | 5 | import "net/http" 6 | 7 | // NewDefaultOrDie constructs and initializes a Renderer with the default 8 | // implementation for the current platform. On windows the default 9 | // implementation is the duktape-based renderer. On any other platform, the 10 | // default is the V8-based renderer. 11 | func NewDefaultOrDie(jsCode string, local http.Handler) Renderer { 12 | r, err := NewV8(jsCode, local) 13 | if err != nil { 14 | panic(err) 15 | } 16 | return r 17 | } 18 | -------------------------------------------------------------------------------- /server/jsrenderer/default_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package jsrenderer 4 | 5 | import "net/http" 6 | 7 | // NewDefaultOrDie constructs and initializes a Renderer with the default 8 | // implementation for the current platform. On windows the default 9 | // implementation is the duktape-based renderer. On any other platform, the 10 | // default is the V8-based renderer. 11 | func NewDefaultOrDie(jsCode string, local http.Handler) Renderer { 12 | r, err := NewDukTape(jsCode, local) 13 | if err != nil { 14 | panic(err) 15 | } 16 | return r 17 | } 18 | -------------------------------------------------------------------------------- /server/jsrenderer/duktape-renderer.go: -------------------------------------------------------------------------------- 1 | package jsrenderer 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "time" 9 | 10 | "gopkg.in/olebedev/go-duktape-fetch.v2" 11 | "gopkg.in/olebedev/go-duktape.v2" 12 | ) 13 | 14 | // NewDukTape constructs a Renderer backed by the duktape javascript engine. 15 | // A given Renderer instance is not safe for concurrent usage. 16 | // 17 | // The provided javascript code should be all of the javascript necessary to 18 | // render the desired react page, including all dependencies bundled together. 19 | // It is assumed that the react code exposes a single function: 20 | // 21 | // main(params, callback) 22 | // 23 | // where params corresponds to the Params struct in this package and callback 24 | // is a function that receives a Result struct serialized to a JSON string. 25 | // 26 | // In addition to the javascript code, you should also provide an http.Handler 27 | // to call for any requests to the local server. 28 | func NewDukTape(jsCode string, local http.Handler) (Renderer, error) { 29 | ctx := duktape.New() 30 | // Setup the global console object. 31 | ctx.PevalString(`var console = {log:print,warn:print,error:print,info:print}`) 32 | // Setup the fetch() polyfill. 33 | ah := &addHeaders{local, nil} 34 | fetch.PushGlobal(ctx, ah) 35 | 36 | // Load the rendering code. 37 | if err := ctx.PevalString(jsCode); err != nil { 38 | return nil, fmt.Errorf("Could not load js bundle code: %v", err) 39 | } 40 | ctx.PopN(ctx.GetTop()) 41 | 42 | return duktapeRenderer{ctx, ah}, nil 43 | } 44 | 45 | type duktapeRenderer struct { 46 | ctx *duktape.Context 47 | local *addHeaders 48 | } 49 | 50 | func (d duktapeRenderer) Render(p Params) (Result, error) { 51 | // Update the local addHeaders server to include the current request's cookies. 52 | d.local.Headers = p.Headers 53 | 54 | // Convert the parameters to JSON so we can call main. 55 | data, err := json.Marshal(p) 56 | if err != nil { 57 | panic(err) // should never happen 58 | } 59 | 60 | // Setup the callback function. 61 | ch := make(chan resAndError, 1) 62 | d.ctx.PushGlobalGoFunction("__goServerCallback__", func(ctx *duktape.Context) int { 63 | ch <- parseJsonFromCallback(ctx.SafeToString(-1), nil) 64 | return 0 65 | }) 66 | 67 | // Call main() in the js code to render the react page. 68 | if err := d.ctx.PevalString(`main(` + string(data) + `, __goServerCallback__)`); err != nil { 69 | return Result{}, fmt.Errorf("Could not call main(...): %v", err) 70 | } 71 | 72 | // Wait for a response. 73 | select { 74 | case res := <-ch: 75 | return res.Result, res.error 76 | case <-time.After(time.Second): 77 | // TODO(aroman) Kill the engine here. 78 | return Result{}, errors.New("Timed out") 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /server/jsrenderer/otto-renderer.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package jsrenderer 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/robertkrimen/otto" 12 | ) 13 | 14 | // NewOtto constructs a Renderer backed by the otto pure-go javascript engine. 15 | // 16 | // The otto renderer is included for comparison, but it dies when trying to 17 | // render react cause of there are regular expressions that use look-ahead which 18 | // isn't supported by re2 that otto uses. 19 | // 20 | // This snippet from node_modules/react/lib/ReactChildren.js fails: 21 | // var userProvidedKeyEscapeRegex = /\/(?!\/)/g; <-- this regexp fails 22 | // function escapeUserProvidedKey(text) { 23 | // return ('' + text).replace(userProvidedKeyEscapeRegex, '//'); 24 | // } 25 | // 26 | // Even trying to change this to work with re2 still causes some other errors, 27 | // so I'm not sure exactly what's going on. 28 | // 29 | // Also, there's not yet a fetch implementation for otto that I'm aware of. It 30 | // should be easy to do, however since the rest isn't working I haven't done it 31 | // yet. 32 | func NewOtto(jsCode string, local http.Handler) (Renderer, error) { 33 | ctx := otto.New() 34 | _, err := ctx.Run(jsCode) 35 | if err != nil { 36 | return nil, fmt.Errorf("Could not load js bundle code: %v", err) 37 | } 38 | return &ottoRenderer{ctx, &addHeaders{local, nil}}, err 39 | } 40 | 41 | type ottoRenderer struct { 42 | ctx *otto.Otto 43 | local *addHeaders 44 | } 45 | 46 | func (o *ottoRenderer) Render(p Params) (Result, error) { 47 | o.local.Headers = p.Headers 48 | main, err := o.ctx.Get("main") 49 | if err != nil { 50 | return Result{}, fmt.Errorf("Cannot get main(...) func: %v", err) 51 | } 52 | ch := make(chan resAndError, 1) 53 | _, err = main.Call(main, p, func(call otto.FunctionCall) otto.Value { 54 | ch <- parseJsonFromCallback(call.Argument(0).ToString()) 55 | return otto.UndefinedValue() 56 | }) 57 | if err != nil { 58 | return Result{}, fmt.Errorf("Failed to call main(...): %v", err) 59 | } 60 | select { 61 | case res := <-ch: 62 | return res.Result, res.error 63 | case <-time.After(time.Second): 64 | return Result{}, errors.New("Timed out") 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /server/jsrenderer/util.go: -------------------------------------------------------------------------------- 1 | package jsrenderer 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "net/http" 7 | ) 8 | 9 | type resAndError struct { 10 | Result 11 | error 12 | } 13 | 14 | func parseJsonFromCallback(jsonData string, err error) resAndError { 15 | if err != nil { 16 | return resAndError{error: err} 17 | } 18 | if jsonData == "undefined" { 19 | return resAndError{error: errors.New("No result returned from rendering engine.")} 20 | } 21 | var res struct { 22 | Error string `json:"error"` 23 | Result 24 | } 25 | if err := json.Unmarshal([]byte(jsonData), &res); err != nil { 26 | return resAndError{res.Result, err} 27 | } else if res.Error != "" { 28 | return resAndError{res.Result, errors.New(res.Error)} 29 | } 30 | return resAndError{res.Result, nil} 31 | } 32 | 33 | // addHeaders wraps Server and adds all of the provided headers to any 34 | // request processed by it. This can be used to copy cookies from a client 35 | // request to all fetch calls during server-side rendering. 36 | type addHeaders struct { 37 | Server http.Handler 38 | Headers http.Header 39 | } 40 | 41 | func (a addHeaders) ServeHTTP(w http.ResponseWriter, r *http.Request) { 42 | for key, vals := range a.Headers { 43 | for _, val := range vals { 44 | r.Header.Add(key, val) 45 | } 46 | } 47 | a.Server.ServeHTTP(w, r) 48 | } 49 | -------------------------------------------------------------------------------- /server/jsrenderer/v8-renderer.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package jsrenderer 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | "os" 10 | "sync/atomic" 11 | "time" 12 | 13 | "github.com/augustoroman/v8" 14 | "github.com/augustoroman/v8/v8console" 15 | "github.com/augustoroman/v8fetch" 16 | ) 17 | 18 | var createdCount int32 19 | 20 | // NewV8 constructs a Renderer backed by the V8 javascript engine. A given 21 | // Renderer instance is not safe for concurrent usage. 22 | // 23 | // The provided javascript code should be all of the javascript necessary to 24 | // render the desired react page, including all dependencies bundled together. 25 | // It is assumed that the react code exposes a single function: 26 | // 27 | // main(params, callback) 28 | // 29 | // where params corresponds to the Params struct in this package and callback 30 | // is a function that receives a Result struct serialized to a JSON string. 31 | // For example: 32 | // 33 | // function main(params, callback) { 34 | // result = { app: '
hi there!
', title: 'The app' }; 35 | // callback(JSON.stringify(result)); 36 | // } 37 | // 38 | // In addition to the javascript code, you should also provide an http.Handler 39 | // to call for any requests to the local server. 40 | // 41 | func NewV8(jsCode string, local http.Handler) (Renderer, error) { 42 | ctx := v8.NewIsolate().NewContext() 43 | ah := &addHeaders{Server: local} 44 | v8fetch.Inject(ctx, ah) 45 | num := atomic.AddInt32(&createdCount, 1) 46 | v8console.Config{fmt.Sprintf("Context #%d> ", num), os.Stdout, os.Stderr, true}.Inject(ctx) 47 | _, err := ctx.Eval(jsCode, "bundle.js") 48 | if err != nil { 49 | return nil, fmt.Errorf("Cannot initialize context from bundle code: %v", err) 50 | } 51 | ctx.Eval("console.info('Initialized new context')", "") 52 | return v8Renderer{ctx, ah}, nil 53 | } 54 | 55 | type v8Renderer struct { 56 | ctx *v8.Context 57 | local *addHeaders 58 | } 59 | 60 | func (r v8Renderer) Render(p Params) (Result, error) { 61 | // Update the console log bindings to prefix logs with the current request UUID. 62 | v8console.Config{p.UUID + "> ", os.Stdout, os.Stderr, true}.Inject(r.ctx) 63 | // Update the local addHeaders server to include the current request's cookies. 64 | r.local.Headers = p.Headers 65 | 66 | // Convert the params Go struct into a javascript object. 67 | params, err := r.ctx.Create(p) 68 | if err != nil { 69 | return Result{}, fmt.Errorf("Cannot create params ob: %v", err) 70 | } 71 | 72 | // Setup the callback function for the current handler. 73 | res := make(chan resAndError, 1) 74 | callback := r.ctx.Bind("rendered_result", func(in v8.CallbackArgs) (*v8.Value, error) { 75 | res <- parseJsonFromCallback(in.Arg(0).String(), nil) 76 | return nil, nil 77 | }) 78 | 79 | // Get and call main() in the js code to render the react page. 80 | main, err := r.ctx.Global().Get("main") 81 | if err != nil { 82 | return Result{}, fmt.Errorf("Can't get global main(): %v", err) 83 | } 84 | _, err = main.Call(main, params, callback) 85 | if err != nil { 86 | return Result{}, fmt.Errorf("Call to main(...) failed: %v", err) 87 | } 88 | 89 | // Wait for a response. If it times out, kill the V8 engine and return 90 | // an error. 91 | select { 92 | case resp := <-res: 93 | return resp.Result, resp.error 94 | case <-time.After(time.Second): 95 | r.ctx.Terminate() // TODO(aroman): re-initialize ctx 96 | return Result{}, errors.New("Timed out") 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "html/template" 8 | "log" 9 | "net/http" 10 | "time" 11 | 12 | "github.com/GeertJohan/go.rice" 13 | "github.com/augustoroman/go-react-v8-ssr/server/jsrenderer" 14 | "github.com/augustoroman/sandwich" 15 | "github.com/nu7hatch/gouuid" 16 | ) 17 | 18 | // Values for these are injected during the build process. 19 | var ( 20 | commitHash string 21 | debug bool 22 | ) 23 | 24 | func main() { 25 | log.SetFlags(log.Lshortfile | log.Ltime | log.Lmicroseconds) 26 | 27 | addr := flag.String("addr", ":5000", "Address to serve on") 28 | flag.Parse() 29 | 30 | // Get our static data. Typically this will be embedded into the binary, 31 | // though it depends on how rice is initialize in the build script. 32 | // With the current Makefile, it's compiled into the binary. 33 | templateBox := rice.MustFindBox("data/templates") 34 | staticBox := rice.MustFindBox("data/static") 35 | 36 | // Setup the react rendering handler: 37 | reactTpl := template.Must(template.New("react.html").Parse( 38 | templateBox.MustString("react.html"))) 39 | jsBundle := staticBox.MustString("build/bundle.js") 40 | renderer := &jsrenderer.Pool{New: func() jsrenderer.Renderer { 41 | // Duktape on windows, V8 otherwise. Panics on initialization error. 42 | return jsrenderer.NewDefaultOrDie(jsBundle, http.DefaultServeMux) 43 | }} 44 | react := ReactPage{renderer, reactTpl} 45 | 46 | staticFiles := http.StripPrefix("/static", http.FileServer(staticBox.HTTPBox())) 47 | 48 | // Don't use sandwich for favicon to reduce log spam. 49 | http.Handle("/favicon.ico", http.NotFoundHandler()) 50 | 51 | // Now, setup the actual middleware handling & routing: 52 | mw := sandwich.TheUsual() 53 | // Gzip all the things! If you want to be more selective, then move this 54 | // call to specific handlers below. 55 | mw = sandwich.Gzip(mw) 56 | // A fake authentication middleware. 57 | mw = mw.With(DoAuth) 58 | 59 | // Check out some random API endpoint: 60 | http.Handle("/api/v1/conf", mw.With(ApiConf)) 61 | 62 | // Anything under /static/ goes here: 63 | http.Handle("/static/", mw.With(staticFiles.ServeHTTP)) 64 | 65 | // All other requests will get handling by this: 66 | http.Handle("/", mw.With(NewUUID, react.Render)) 67 | fmt.Println("Serving on ", *addr) 68 | if err := http.ListenAndServe(*addr, nil); err != nil { 69 | log.Fatal(err) 70 | } 71 | } 72 | 73 | func NewUUID(e *sandwich.LogEntry) (*uuid.UUID, error) { 74 | u, err := uuid.NewV4() 75 | if err == nil { 76 | e.Note["uuid"] = u.String() 77 | } 78 | return u, err 79 | } 80 | 81 | type User string 82 | 83 | func DoAuth(w http.ResponseWriter, r *http.Request, e *sandwich.LogEntry) User { 84 | var u User 85 | if login := r.FormValue("login"); login == "-" { 86 | deleteCookie(w, r, "user") 87 | u = "" 88 | } else if login != "" { 89 | replaceCookie(w, r, &http.Cookie{ 90 | Name: "user", Value: login, HttpOnly: true}) 91 | u = User(login) 92 | } else if c, err := r.Cookie("user"); err == nil && c.Value != "" { 93 | u = User(c.Value) 94 | } else { 95 | deleteCookie(w, r, "user") 96 | u = "" 97 | } 98 | e.Note["user"] = string(u) 99 | return u 100 | } 101 | 102 | // deleteCookie removes a cookie from the client (via setting an expired cookie 103 | // in the response headers) and the local request (by re-encoding all the 104 | // cookies without the offending one). 105 | func deleteCookie(w http.ResponseWriter, r *http.Request, name string) { 106 | replaceCookie(w, r, &http.Cookie{Name: name, HttpOnly: true, MaxAge: -1, Expires: time.Now()}) 107 | } 108 | 109 | // Replace cookie will set the specified cookie both into the response but also 110 | // in the current request, modifying the headers. This is necessary for 111 | // correct server-side rendering since we might alter the cookie (e.g. during 112 | // login) and then use the current request's headers to do the page rendering, 113 | // and we want the page rendering to have the correct cookies. 114 | func replaceCookie(w http.ResponseWriter, r *http.Request, c *http.Cookie) { 115 | cookies := r.Cookies() // Extract existing cookies 116 | r.Header.Del("Cookie") // Delete all cookies 117 | r.AddCookie(c) // Add the new cookie first 118 | for _, e := range cookies { // Add back all other cookies 119 | if e.Name != c.Name { 120 | r.AddCookie(e) 121 | } 122 | } 123 | http.SetCookie(w, c) // Also set cookie on response 124 | } 125 | 126 | func ApiConf(w http.ResponseWriter, r *http.Request, u User) error { 127 | config := struct { 128 | Debug bool `json:"debug"` 129 | Commit string `json:"commit"` 130 | Port int `json:"port"` 131 | Title string `json:"title"` 132 | User User `json:"user"` 133 | ApiPrefix string `json:"api.prefix"` 134 | Path string `json:"duktape.path"` 135 | }{true, commitHash, 5000, "Go Starter Kit", u, "/api", "static/build/bundle.js"} 136 | return json.NewEncoder(w).Encode(config) 137 | } 138 | 139 | func must(b *rice.Box, err error) *rice.Box { 140 | if err != nil { 141 | panic(err) 142 | } 143 | return b 144 | } 145 | -------------------------------------------------------------------------------- /server/react.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "html/template" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/augustoroman/go-react-v8-ssr/server/jsrenderer" 9 | 10 | "github.com/augustoroman/sandwich" 11 | "github.com/nu7hatch/gouuid" 12 | ) 13 | 14 | // ReactPage configures the rendering of a server-side rendered react page. 15 | // It fundamentally consists of two things: the javascript engine that will 16 | // render the react content based on the request parameters, and the go 17 | // template that will wrap the rendered content. The wrapping includes the 18 | // basic HTML document structure as well as links to the react javascript (for 19 | // any future client-side renderings) and links to style sheets as so on. In 20 | // our case, the wrapping also conditionally includes error-rendering. 21 | type ReactPage struct { 22 | r jsrenderer.Renderer 23 | tpl *template.Template 24 | } 25 | 26 | // Render is the actual HTTP handler portion 27 | func (v *ReactPage) Render( 28 | w http.ResponseWriter, 29 | r *http.Request, 30 | UUID *uuid.UUID, 31 | e *sandwich.LogEntry, 32 | ) error { 33 | start := time.Now() 34 | // First, we execute the react javascript to process the main content of 35 | // the page. The Helmet components will also return information about 36 | // titles & meta tags. 37 | res, err := v.r.Render(jsrenderer.Params{ 38 | // Include the URL to be rendered for the react router. 39 | Url: r.URL.String(), 40 | // Pass in the cookies from the client request so that they can be 41 | // added to any child HTTP calls made by the renderer during the server- 42 | // side rendering of the page. That will allow authenticated API calls 43 | // to succeed as if they were made by the client itself. 44 | Headers: http.Header{"Cookie": r.Header["Cookie"]}, 45 | UUID: UUID.String(), 46 | }) 47 | e.Note["react-render"] = time.Since(start).String() 48 | 49 | // Once we have the react content rendered, just pass the result into our 50 | // wrapping template and render that! Easy-peasy! 51 | var templateData = struct { 52 | jsrenderer.Result 53 | UUID string 54 | Error error 55 | }{res, UUID.String(), err} 56 | return v.tpl.ExecuteTemplate(w, "react.html", templateData) 57 | } 58 | -------------------------------------------------------------------------------- /vendor/vendor.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": "", 3 | "ignore": "test", 4 | "package": [ 5 | { 6 | "checksumSHA1": "3eH5XenKCK28s5fpbAeQoPGT3D0=", 7 | "path": "github.com/GeertJohan/go.rice", 8 | "revision": "9fdfd46f9806a9228aae341d65ab75c5235c383c", 9 | "revisionTime": "2016-08-11T09:34:08Z" 10 | }, 11 | { 12 | "checksumSHA1": "xECV8VmnSwtMPugLqB1OAXwOs48=", 13 | "path": "github.com/GeertJohan/go.rice/embedded", 14 | "revision": "9fdfd46f9806a9228aae341d65ab75c5235c383c", 15 | "revisionTime": "2016-08-11T09:34:08Z" 16 | }, 17 | { 18 | "checksumSHA1": "8Cj/rdKxl5vEdhfHouZdGXFRKjY=", 19 | "path": "github.com/augustoroman/sandwich", 20 | "revision": "d8657ec39ab8b95bcb89de6df8d9f197d58a341c", 21 | "revisionTime": "2016-08-27T00:27:47Z" 22 | }, 23 | { 24 | "checksumSHA1": "+OZDFX0VPeKBWNQ7haiMVBTxhc4=", 25 | "path": "github.com/augustoroman/sandwich/chain", 26 | "revision": "d8657ec39ab8b95bcb89de6df8d9f197d58a341c", 27 | "revisionTime": "2016-08-27T00:27:47Z" 28 | }, 29 | { 30 | "checksumSHA1": "nTK5uoTTH3Zi4CNsAeNApMYY6K4=", 31 | "path": "github.com/augustoroman/v8", 32 | "revision": "d5aaaec6245e71ff4ffb0f68d25b64203100b96e", 33 | "revisionTime": "2016-08-27T03:22:54Z" 34 | }, 35 | { 36 | "checksumSHA1": "b9AFO0WnxQH/uqUoK09BHx6+BQs=", 37 | "path": "github.com/augustoroman/v8/v8console", 38 | "revision": "d5aaaec6245e71ff4ffb0f68d25b64203100b96e", 39 | "revisionTime": "2016-08-27T03:22:54Z" 40 | }, 41 | { 42 | "checksumSHA1": "SJiHu8FSO2WKqOlQHQZX6sj+jy0=", 43 | "path": "github.com/augustoroman/v8fetch", 44 | "revision": "a1247688a822b98882e9ed3ab47c1d74a6a5a498", 45 | "revisionTime": "2016-08-27T05:57:28Z" 46 | }, 47 | { 48 | "checksumSHA1": "eeiNBo+Zej0PSas3vs2ezjWezuY=", 49 | "path": "github.com/augustoroman/v8fetch/internal/data", 50 | "revision": "a1247688a822b98882e9ed3ab47c1d74a6a5a498", 51 | "revisionTime": "2016-08-27T05:57:28Z" 52 | }, 53 | { 54 | "checksumSHA1": "8i+beEgcVf0q/I7lTqo2ERZM/OU=", 55 | "path": "github.com/daaku/go.zipexe", 56 | "revision": "a5fe2436ffcb3236e175e5149162b41cd28bd27d", 57 | "revisionTime": "2015-03-29T02:31:25Z" 58 | }, 59 | { 60 | "checksumSHA1": "g+afVQQVopBLiLB5pFZp/8s6aBs=", 61 | "path": "github.com/kardianos/osext", 62 | "revision": "c2c54e542fb797ad986b31721e1baedf214ca413", 63 | "revisionTime": "2016-08-11T00:15:26Z" 64 | }, 65 | { 66 | "checksumSHA1": "O7lIX71vTepwRVcnXbV3Fzq0z5U=", 67 | "path": "github.com/moul/http2curl", 68 | "revision": "b1479103caacaa39319f75e7f57fc545287fca0d", 69 | "revisionTime": "2016-05-20T21:31:28Z" 70 | }, 71 | { 72 | "checksumSHA1": "gcLub3oB+u4QrOJZcYmk/y2AP4k=", 73 | "path": "github.com/nu7hatch/gouuid", 74 | "revision": "179d4d0c4d8d407a32af483c2354df1d2c91e6c3", 75 | "revisionTime": "2013-12-21T20:05:32Z" 76 | }, 77 | { 78 | "checksumSHA1": "32CeEfELP6teo0LoJeqdWN9smLs=", 79 | "path": "github.com/parnurzeal/gorequest", 80 | "revision": "aa4ecb7ff5d4538cfb9878346864be60ec237052", 81 | "revisionTime": "2016-08-20T11:52:03Z" 82 | }, 83 | { 84 | "checksumSHA1": "6thYfOqbpn607QljSl+jOXbuqio=", 85 | "path": "golang.org/x/net/publicsuffix", 86 | "revision": "7394c112eae4dba7e96bfcfe738e6373d61772b4", 87 | "revisionTime": "2016-08-19T20:57:39Z" 88 | }, 89 | { 90 | "checksumSHA1": "sKHFPwLKWnaLi38REvlx/2IYzNI=", 91 | "path": "gopkg.in/olebedev/go-duktape-fetch.v2", 92 | "revision": "a4ba37699f8bae499586df39f367460ab7566f1b", 93 | "revisionTime": "2016-04-18T10:53:35Z" 94 | }, 95 | { 96 | "checksumSHA1": "+IcBxYZj5xqiRqbuSy9I51L1zc0=", 97 | "path": "gopkg.in/olebedev/go-duktape.v2", 98 | "revision": "c8eb77ef5aa5bff7b04fa1b8e70a5a1e5eb0755a", 99 | "revisionTime": "2016-08-24T01:59:21Z" 100 | } 101 | ], 102 | "rootPath": "github.com/augustoroman/go-react-v8-ssr" 103 | } 104 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var autoprefixer = require('autoprefixer'); 4 | var precss = require('precss'); 5 | var functions = require('postcss-functions'); 6 | var ExtractTextPlugin = require("extract-text-webpack-plugin"); 7 | 8 | var postCssLoader = [ 9 | 'css-loader?module', 10 | '&localIdentName=[name]__[local]___[hash:base64:5]', 11 | '&disableStructuralMinification', 12 | '!postcss-loader' 13 | ]; 14 | 15 | var plugins = [ 16 | new webpack.NoErrorsPlugin(), 17 | new webpack.optimize.DedupePlugin(), 18 | new ExtractTextPlugin('bundle.css'), 19 | ]; 20 | 21 | if (process.env.NODE_ENV === 'production') { 22 | plugins = plugins.concat([ 23 | new webpack.optimize.UglifyJsPlugin({ 24 | output: {comments: false}, 25 | test: /bundle\.js?$/ 26 | }), 27 | new webpack.DefinePlugin({ 28 | 'process.env': {NODE_ENV: JSON.stringify('production')} 29 | }) 30 | ]); 31 | 32 | postCssLoader.splice(1, 1) // drop human readable names 33 | }; 34 | 35 | var config = { 36 | entry: { 37 | bundle: path.join(__dirname, 'client/index.js') 38 | }, 39 | output: { 40 | path: path.join(__dirname, 'server/data/static/build'), 41 | publicPath: "/static/build/", 42 | filename: '[name].js' 43 | }, 44 | plugins: plugins, 45 | module: { 46 | loaders: [ 47 | {test: /\.css/, loader: ExtractTextPlugin.extract('style-loader', postCssLoader.join(''))}, 48 | {test: /\.(png|gif)$/, loader: 'url-loader?name=[name]@[hash].[ext]&limit=5000'}, 49 | {test: /\.svg$/, loader: 'url-loader?name=[name]@[hash].[ext]&limit=5000!svgo-loader?useConfig=svgo1'}, 50 | {test: /\.(pdf|ico|jpg|eot|otf|woff|ttf|mp4|webm)$/, loader: 'file-loader?name=[name]@[hash].[ext]'}, 51 | {test: /\.json$/, loader: 'json-loader'}, 52 | { 53 | test: /\.jsx?$/, 54 | include: path.join(__dirname, 'client'), 55 | loaders: ['babel'] 56 | } 57 | ] 58 | }, 59 | resolve: { 60 | extensions: ['', '.js', '.jsx', '.css'], 61 | alias: { 62 | '#app': path.join(__dirname, 'client'), 63 | '#c': path.join(__dirname, 'client/components'), 64 | '#css': path.join(__dirname, 'client/css') 65 | } 66 | }, 67 | svgo1: { 68 | multipass: true, 69 | plugins: [ 70 | // by default enabled 71 | {mergePaths: false}, 72 | {convertTransform: false}, 73 | {convertShapeToPath: false}, 74 | {cleanupIDs: false}, 75 | {collapseGroups: false}, 76 | {transformsWithOnePath: false}, 77 | {cleanupNumericValues: false}, 78 | {convertPathData: false}, 79 | {moveGroupAttrsToElems: false}, 80 | // by default disabled 81 | {removeTitle: true}, 82 | {removeDesc: true} 83 | ] 84 | }, 85 | postcss: function() { 86 | return [autoprefixer, precss({ 87 | variables: { 88 | variables: require('./client/css/vars') 89 | } 90 | }), functions({ 91 | functions: require('./client/css/funcs') 92 | })] 93 | } 94 | }; 95 | 96 | module.exports = config; 97 | --------------------------------------------------------------------------------