├── .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 |
--------------------------------------------------------------------------------