├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── es ├── client.jsx ├── components │ ├── App.jsx │ └── Cmp.jsx ├── container.jsx └── ssr.jsx ├── package.json ├── redux ├── actionTypes.js ├── actions.js └── reducers.js ├── server ├── db.js └── index.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceMaps": true, 3 | "presets": [ 4 | "es2015", 5 | ], 6 | "plugins": [ 7 | [ "transform-react-jsx", { "pragma": "h" } ] 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | dist 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Hans Chan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # preact-redux-ssr-example 2 | 3 | [Preact](https://preactjs.com/) [Server-side Rendering](https://github.com/developit/preact-render-to-string) with [Redux](http://redux.js.org/) Example. 4 | 5 | ## Installation 6 | 7 | ``` 8 | npm install 9 | ``` 10 | 11 | ## Development 12 | 13 | ``` 14 | npm run build 15 | npm start 16 | ``` 17 | -------------------------------------------------------------------------------- /es/client.jsx: -------------------------------------------------------------------------------- 1 | import { h, render } from 'preact'; 2 | import container from './container'; 3 | 4 | const data = window.__backend_data__; 5 | 6 | render( 7 | container(data), 8 | document.body, 9 | document.getElementById('app') 10 | ); 11 | -------------------------------------------------------------------------------- /es/components/App.jsx: -------------------------------------------------------------------------------- 1 | // 这里必须 引入 preact.h 2 | 3 | 4 | import { h, Component } from 'preact'; 5 | import { bindActionCreators } from 'redux'; 6 | import { connect } from 'preact-redux'; 7 | import reducers from '../../redux/reducers'; 8 | import * as actions from '../../redux/actions'; 9 | 10 | import Cmp from './Cmp'; 11 | 12 | 13 | // function bindActions(actions) { 14 | // return dispatch => ({ 15 | // ...bindActionCreators(actions, dispatch) 16 | // }); 17 | // } 18 | function bindActions(dispatch) { 19 | return { 20 | actions: bindActionCreators(actions, dispatch) 21 | }; 22 | } 23 | 24 | 25 | class App extends Component { 26 | render({ name, age, actions }) { 27 | return (
28 |

Hello, { name } !

29 |

30 | Your age is { age }. 31 | 32 |

33 |
34 |
); 35 | } 36 | } 37 | 38 | export default connect(reducers, bindActions)(App); 39 | -------------------------------------------------------------------------------- /es/components/Cmp.jsx: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | 3 | export default class Clock extends Component { 4 | constructor() { 5 | super(); 6 | // set initial time: 7 | this.state.time = Date.now(); 8 | } 9 | componentDidMount() { 10 | // update time every second 11 | this.timer = setInterval(() => { 12 | const time = Date.now(); 13 | this.setState({ 14 | time, 15 | }); 16 | }, 1000); 17 | } 18 | componentWillUnmount() { 19 | // stop when not renderable 20 | clearInterval(this.timer); 21 | } 22 | render({ setAge }, state) { 23 | const time = new Date(state.time).toLocaleTimeString(); 24 | return ( 25 |
26 |

27 |

It is { time } now!

28 |
); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /es/container.jsx: -------------------------------------------------------------------------------- 1 | // 这里必须 引入 preact.h 2 | import { h } from 'preact'; 3 | import { Provider } from 'preact-redux'; 4 | import { createStore } from 'redux'; 5 | 6 | import App from './components/App'; 7 | import reducers from '../redux/reducers'; 8 | 9 | export default function (initData) { 10 | // Create a new Redux store instance 11 | const store = createStore(reducers, initData); 12 | // universal component 13 | return ( 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /es/ssr.jsx: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | import { h } from 'preact'; 5 | import render from 'preact-render-to-string'; 6 | import container from './container'; 7 | 8 | module.exports = function (initData) { 9 | // Render the component to a string 10 | const html = render(container(initData)); 11 | 12 | return { 13 | html, 14 | state: initData, 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact-ssr", 3 | "version": "1.0.0", 4 | "description": "Preact Server-Side Rendering Example", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "webpack", 8 | "start": "node server/index.js" 9 | }, 10 | "license": "MIT", 11 | "dependencies": { 12 | "babel-register": "^6.11.6", 13 | "express": "^4.14.0", 14 | "preact": "^5.6.0", 15 | "preact-redux": "^1.0.1", 16 | "preact-render-to-string": "^3.0.5", 17 | "redux": "^3.5.2" 18 | }, 19 | "devDependencies": { 20 | "babel-cli": "^6.11.4", 21 | "babel-loader": "^6.2.4", 22 | "babel-plugin-transform-react-jsx": "^6.8.0", 23 | "babel-preset-es2015": "^6.13.0", 24 | "source-map-loader": "^0.1.5", 25 | "webpack": "^1.13.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /redux/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const INCREMENT = 'INCREMENT'; 2 | export const DECREMENT = 'DECREMENT'; 3 | export const RESET = 'RESET'; 4 | -------------------------------------------------------------------------------- /redux/actions.js: -------------------------------------------------------------------------------- 1 | import { INCREMENT, DECREMENT, RESET } from './actionTypes'; 2 | 3 | export function increaseAge() { 4 | return { 5 | type: INCREMENT 6 | }; 7 | } 8 | 9 | export function decreaseAge() { 10 | return { 11 | type: DECREMENT 12 | }; 13 | } 14 | 15 | export function setAge(age) { 16 | return { 17 | type: RESET, 18 | age, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /redux/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { INCREMENT, DECREMENT, RESET } from './actionTypes'; 3 | 4 | function age(state = 0, action) { 5 | switch (action.type) { 6 | case INCREMENT: 7 | return state + 1; 8 | case DECREMENT: 9 | return state - 1; 10 | case RESET: 11 | return action.age; 12 | default: 13 | return state; 14 | } 15 | } 16 | 17 | function name(state = '', action) { 18 | return state; 19 | } 20 | 21 | const reducers = combineReducers({ 22 | age, 23 | name 24 | }); 25 | 26 | export default reducers; 27 | -------------------------------------------------------------------------------- /server/db.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DataBase mocker 3 | */ 4 | 5 | exports.getUser = function (id) { 6 | return { 7 | // id, 8 | name: 'Hans', 9 | age: Math.round(Math.random() * 50), 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const express = require('express'); 6 | const db = require('./db'); 7 | 8 | // Support .jsx on Node runtime 9 | require('babel-register')({ 10 | extensions: ['.jsx', '.js'] 11 | }); 12 | 13 | // Server-side Entry (.jsx) 14 | const ssr = require('../es/ssr'); 15 | 16 | // basic HTTP server via express: 17 | const app = express(); 18 | 19 | const BUNDLE_FILE_URL = '/bundle.client.js'; 20 | const BUNDLE_FILE_PATH = path.join(__dirname, `../dist${BUNDLE_FILE_URL}`); 21 | 22 | // bundle js file 23 | app.get(BUNDLE_FILE_URL, (req, res) => { 24 | fs.readFile(BUNDLE_FILE_PATH, 'utf-8', (err, ctx) => { 25 | res.send(ctx); 26 | }); 27 | }); 28 | 29 | // on each request, render and return a component: 30 | app.get('/', (req, res) => { 31 | const data = db.getUser(); 32 | const ssrResult = ssr(data); 33 | // send it back wrapped up as an HTML5 document: 34 | res.send(` 35 | 36 | 37 | Preact SSR 38 | 39 | 40 | ${ssrResult.html} 41 | 42 | 43 | 44 | `); 45 | }); 46 | 47 | // start server 48 | const PORT = process.env.PORT || 3000; 49 | app.listen(PORT, () => { 50 | console.log(`Preact Server start on ${PORT}`); 51 | }); 52 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | const ENV = process.env.NODE_ENV || 'development'; 4 | 5 | module.exports = { 6 | entry: './es/client.jsx', 7 | output: { 8 | path: './dist', 9 | publicPath: '/', 10 | filename: 'bundle.client.js' 11 | }, 12 | resolve: { 13 | extensions: ['', '.jsx', '.js', '.json', '.less'] 14 | }, 15 | module: { 16 | preLoaders: [ 17 | { 18 | test: /\.jsx?$/, 19 | exclude: /src\//, 20 | loader: 'source-map' 21 | } 22 | ], 23 | loaders: [ 24 | { 25 | test: /\.jsx?$/, 26 | exclude: /node_modules/, 27 | loader: 'babel' 28 | }, 29 | ] 30 | }, 31 | plugins: ([ 32 | new webpack.NoErrorsPlugin(), 33 | new webpack.DefinePlugin({ 34 | 'process.env.NODE_ENV': JSON.stringify(ENV) 35 | }), 36 | ]).concat(ENV==='production' ? [ 37 | new webpack.optimize.DedupePlugin(), 38 | new webpack.optimize.OccurenceOrderPlugin() 39 | ] : []), 40 | devtool: ENV==='production' ? 'source-map' : 'inline-source-map', 41 | }; 42 | --------------------------------------------------------------------------------