├── client ├── components │ ├── index.js │ └── Button │ │ ├── index.less │ │ └── index.jsx ├── page │ ├── Home │ │ ├── index.less │ │ └── index.jsx │ ├── About │ │ ├── index.less │ │ └── index.jsx │ └── index.jsx ├── redux │ ├── types.js │ ├── store.js │ └── reducer.js ├── common │ ├── getData.js │ └── request.js ├── index.js └── router │ └── index.jsx ├── .gitignore ├── server ├── routes │ └── index.js ├── app.js ├── controllers │ ├── user.js │ └── config.js ├── server.prod.js ├── middlewares │ └── clientRoute.js └── server.dev.js ├── views ├── server.html ├── template.html └── index.html ├── README.md └── package.json /client/components/index.js: -------------------------------------------------------------------------------- 1 | 2 | export { default as Button } from './Button/index.jsx' -------------------------------------------------------------------------------- /client/page/Home/index.less: -------------------------------------------------------------------------------- 1 | .box { 2 | color: red; 3 | text-align: center; 4 | padding: 100px 0; 5 | } 6 | -------------------------------------------------------------------------------- /client/redux/types.js: -------------------------------------------------------------------------------- 1 | export default { 2 | ADD_COUNTER: 'ADD_COUNTER', 3 | DEL_COUNTER: 'DEL_COUNTER' 4 | }; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | pids 4 | *.pid 5 | *.seed 6 | coverage 7 | node_modules 8 | dist 9 | bower_components 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /client/page/About/index.less: -------------------------------------------------------------------------------- 1 | .box { 2 | color: #333; 3 | text-align: center; 4 | padding: 100px 0; 5 | } 6 | .button { 7 | width: 80px; 8 | height: 36px; 9 | } 10 | -------------------------------------------------------------------------------- /client/redux/store.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, compose, createStore } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import rootReducer from './reducer'; 4 | 5 | export default createStore(rootReducer, compose(applyMiddleware(thunk))); 6 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router'; 2 | import getUserInfo from '../controllers/user.js'; 3 | 4 | const router = new Router({ prefix: '/api' }); 5 | 6 | router.get('/user/getUserInfo', getUserInfo); 7 | 8 | export default router; 9 | -------------------------------------------------------------------------------- /client/page/index.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { RoutesIndex } from '../router/index.jsx'; 3 | 4 | export default connect( 5 | state => state, 6 | undefined, 7 | undefined, 8 | { pure: false } 9 | )(RoutesIndex); 10 | -------------------------------------------------------------------------------- /client/components/Button/index.less: -------------------------------------------------------------------------------- 1 | .button { 2 | display: inline-block; 3 | color: #fff; 4 | background-color: #2396fa; 5 | outline: 0; 6 | font-size: 14px; 7 | border: none; 8 | padding: 0 10px; 9 | border-radius: 4px; 10 | transition: all 0.5s cubic-bezier(0.645, 0.045, 0.355, 1); 11 | cursor: pointer; 12 | } 13 | -------------------------------------------------------------------------------- /views/server.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /views/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | My Blog 6 | 7 | 8 | 9 | 10 |
<%- root %>
11 | 12 | 13 | -------------------------------------------------------------------------------- /client/common/getData.js: -------------------------------------------------------------------------------- 1 | import request from './request'; 2 | 3 | async function getData(path) { 4 | switch (path) { 5 | case '/': 6 | let data = {}; 7 | await request.config({ url: '/api/user/getUserInfo' }).then(res => { 8 | data = res; 9 | }); 10 | return data; 11 | default: 12 | return { path }; 13 | } 14 | } 15 | 16 | export default getData; 17 | -------------------------------------------------------------------------------- /client/redux/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import types from './types'; 3 | 4 | function count(state = 0, action) { 5 | switch (action.type) { 6 | case types.ADD_COUNTER: 7 | return state + 1; 8 | case types.DEL_COUNTER: 9 | return state - 1; 10 | default: 11 | return state; 12 | } 13 | } 14 | 15 | export default combineReducers({ 16 | count 17 | }); 18 | -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | My Blog 6 | 7 | 8 | 9 | 10 |
<%- root %>
11 | 12 | 13 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter as Router } from 'react-router-dom'; 4 | import RoutesIndex from './page/index.jsx'; 5 | import { Provider } from 'react-redux'; 6 | import store from './redux/store'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | 12 | 13 | , 14 | document.getElementById('root') 15 | ); 16 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | import Koa from 'koa'; 2 | import json from 'koa-json'; 3 | import bodyParser from 'koa-bodyparser'; 4 | import logger from 'koa-logger'; 5 | import session from 'koa-session'; 6 | import compress from 'koa-compress'; 7 | import convert from 'koa-convert'; 8 | import cors from 'koa2-cors'; 9 | 10 | const app = new Koa(); 11 | app.use(convert(session(app))); 12 | app.use(compress()); 13 | app.use(bodyParser()); 14 | app.use(cors()); 15 | app.use(json()); 16 | app.use(logger()); 17 | 18 | export default app; 19 | -------------------------------------------------------------------------------- /server/controllers/user.js: -------------------------------------------------------------------------------- 1 | import query from './config'; 2 | 3 | const findUserInfo = () => { 4 | const _sql = 'select * from user'; 5 | return query(_sql, []); 6 | }; 7 | 8 | const getUserInfo = async ctx => { 9 | let data = {}; 10 | 11 | await findUserInfo().then(result => { 12 | data = result[0]; 13 | }); 14 | 15 | // data = { 16 | // userId: 1002, 17 | // name: 'xwb007', 18 | // gender: '男', 19 | // age: 24 20 | // }; 21 | 22 | ctx.body = data; 23 | }; 24 | 25 | export default getUserInfo; 26 | -------------------------------------------------------------------------------- /client/components/Button/index.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @param < Button /> 3 | * @time 2019/1/20 4 | */ 5 | 6 | import React from 'react'; 7 | import classnames from 'classnames'; 8 | import styles from './index.less'; 9 | 10 | class Button extends React.Component { 11 | render() { 12 | const { children, className, ...others } = this.props; 13 | return ( 14 | 17 | ); 18 | } 19 | } 20 | 21 | export default Button; 22 | -------------------------------------------------------------------------------- /server/server.prod.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import path from 'path'; 3 | import serve from 'koa-static'; 4 | import views from 'koa-views' 5 | import app from './app.js'; 6 | import router from './routes/index'; 7 | import clientRoute from './middlewares/clientRoute'; 8 | 9 | const port = process.env.port || 3000; 10 | 11 | app.use(views(path.resolve(__dirname, '../dist/views'), { map: { html: 'ejs' } })); 12 | app.use(serve(path.resolve(__dirname, '../dist'))); 13 | app.use(router.routes()); 14 | app.use(router.allowedMethods()); 15 | app.use(clientRoute); 16 | app.listen(port, () => { 17 | console.log(`open up http://localhost:${port}/ in your browser.`); 18 | }); 19 | -------------------------------------------------------------------------------- /client/router/index.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 返回一个基本的App 3 | */ 4 | import React from 'react'; 5 | import { Route, Switch } from 'react-router-dom'; 6 | import Home from '../page/Home/index.jsx'; 7 | import About from '../page/About/index.jsx'; 8 | 9 | const routes = [{ path: '/', component: Home }, { path: '/about', component: About }]; 10 | 11 | class RoutesIndex extends React.Component { 12 | render() { 13 | const { ...props } = this.props; 14 | return ( 15 |
16 | 17 | {routes.map((item, index) => ( 18 | } /> 19 | ))} 20 | 21 |
22 | ); 23 | } 24 | } 25 | 26 | export { RoutesIndex, routes }; 27 | -------------------------------------------------------------------------------- /server/controllers/config.js: -------------------------------------------------------------------------------- 1 | import mysql from 'mysql'; 2 | 3 | const pool = mysql.createPool({ 4 | host: 'localhost', 5 | user: 'root', 6 | password: '123456', 7 | port: '3306', 8 | database: 'test' 9 | }); 10 | 11 | /** 12 | * 13 | * @param sql 接收的sql语句 14 | * @param values 接受的参数: 为数组 15 | */ 16 | const query = function(sql, values) { 17 | return new Promise((resolve, reject) => { 18 | pool.getConnection(function(err, connection) { 19 | if (err) { 20 | resolve(err); 21 | } else { 22 | connection.query(sql, values, (err, rows) => { 23 | if (err) { 24 | reject(err); 25 | } else { 26 | resolve(rows); 27 | } 28 | connection.release(); 29 | }); 30 | } 31 | }); 32 | }); 33 | }; 34 | 35 | export default query; 36 | -------------------------------------------------------------------------------- /client/page/Home/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link, withRouter } from 'react-router-dom'; 3 | 4 | import getData from '../../common/getData'; 5 | import styles from './index.less'; 6 | 7 | class Home extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | user: props.staticContext 12 | }; 13 | } 14 | 15 | async componentDidMount() { 16 | this.setState({ user: await getData(this.props.match.path) }); 17 | } 18 | 19 | render() { 20 | const { user } = this.state; 21 | return ( 22 |
23 | About 24 |

hello koa-react-template

25 |

{user && user.userId}

26 |

{user && user.name}

27 |

{user && user.gender}

28 |

{user && user.age}

29 |
30 | ); 31 | } 32 | } 33 | 34 | export default withRouter(Home); 35 | -------------------------------------------------------------------------------- /server/middlewares/clientRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderToStaticMarkup } from 'react-dom/server'; 3 | import { StaticRouter } from 'react-router-dom'; 4 | import { Provider } from 'react-redux'; 5 | import store from '../../client/redux/store.js'; 6 | 7 | import { RoutesIndex, routes } from '../../client/router/index.jsx'; 8 | import getData from '../../client/common/getData'; 9 | 10 | async function clientRoute(ctx, next) { 11 | for (let item of routes) { 12 | if (item.path == ctx.url) { 13 | const data = await getData(ctx.url); 14 | await ctx.render('index', { 15 | root: renderToStaticMarkup( 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | }); 23 | break; 24 | } 25 | } 26 | await next(); 27 | } 28 | 29 | export default clientRoute; 30 | -------------------------------------------------------------------------------- /client/page/About/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { Link, withRouter } from 'react-router-dom'; 4 | import reduxTypes from '../../redux/types'; 5 | import { Button } from '../../components'; 6 | import styles from './index.less'; 7 | 8 | class About extends Component { 9 | handleAddClick() { 10 | this.props.dispatch({ 11 | type: reduxTypes.ADD_COUNTER 12 | }); 13 | } 14 | handleDelClick() { 15 | this.props.dispatch({ 16 | type: reduxTypes.DEL_COUNTER 17 | }); 18 | } 19 | render() { 20 | const { count } = this.props; 21 | return ( 22 |
23 |

24 | Home 25 |

26 | 29 | {count} 30 | 33 |
34 | ); 35 | } 36 | } 37 | 38 | export default withRouter(About); 39 | -------------------------------------------------------------------------------- /client/common/request.js: -------------------------------------------------------------------------------- 1 | import qs from 'qs'; 2 | import axios from 'axios'; 3 | import isNode from 'isnode'; 4 | 5 | class Request { 6 | constructor() { 7 | this.base = { 8 | type: 'get', 9 | meta: isNode ? 'http://127.0.0.1:3000' : 'http://192.168.100.95:3000' 10 | }; 11 | this.options = { 12 | url: null, 13 | params: null, 14 | data: null 15 | }; 16 | } 17 | config(obj) { 18 | this.options = obj; 19 | return this.run(); 20 | } 21 | setConfig(type) { 22 | return { 23 | method: type || this.base.type, 24 | url: this.base.meta + this.options.url, 25 | params: this.options.params || {}, // Get的参数 26 | data: this.options.data ? (this.options.data.constructor === FormData ? this.options.data : qs.stringify(this.options.data)) : {} // Post的参数 27 | }; 28 | } 29 | run() { 30 | return new Promise((resolve, reject) => { 31 | axios 32 | .request(this.setConfig(this.options.type)) 33 | .then(res => { 34 | resolve(res.data); 35 | }) 36 | .catch(res => { 37 | reject(res); 38 | }); 39 | }); 40 | } 41 | } 42 | export default new Request(); 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Koa-React-mysql SSR 服务端渲染 2 | 3 | ## Technology Stack 4 | 5 | - [Koa2](https://github.com/koajs/koa) 6 | - [React](https://github.com/reactjs/reactjs.org) 7 | - [mysql](https://github.com/mysql/mysql-server) 8 | 9 | ## Installing Koa-React-SSR 10 | 11 | ```shell 12 | 13 | $ npm install 14 | 15 | ``` 16 | 17 | ## Getting Started 18 | 19 | ```shell 20 | 21 | $ npm start //devServer客户端开发环境 http://localhost:8080/ 22 | $ npm run dev //开发环境运行 http://localhost:3000/ 23 | $ npm run build //打包生产环境 24 | $ npm run server //运行生产环境 http://localhost:3000/ 25 | 26 | ``` 27 | 28 | ## pm2 后台永久启动 29 | 30 | - [pm2](https://github.com/Unitech/pm2) 31 | 32 | ```shell 33 | 34 | $ npm run startpm //永久启动服务 35 | $ npm run delpm //杀死全部进程 36 | 37 | ``` 38 | 39 | ## warning 40 | 41 | 如果运行报错,有可能是你的 mysql 数据库未安装或者连接密码错误,请核对... 42 | 43 | 或直接 Koa-React-SSR\server\controllers\user.js 进行注释 44 | 45 | ```js 46 | // import query from './config'; 47 | 48 | // const findUserInfo = () => { 49 | // const _sql = 'select * from user'; 50 | // return query(_sql, []); 51 | // }; 52 | 53 | const getUserInfo = async ctx => { 54 | let data = {}; 55 | 56 | // await findUserInfo().then(result => { 57 | // data = result[0]; 58 | // }); 59 | 60 | data = { 61 | userId: 1001, 62 | name: 'xwb007', 63 | gender: '男', 64 | age: 24 65 | }; 66 | 67 | ctx.body = data; 68 | }; 69 | 70 | export default getUserInfo ; 71 | ``` 72 | -------------------------------------------------------------------------------- /server/server.dev.js: -------------------------------------------------------------------------------- 1 | require('babel-polyfill'); 2 | require('source-map-support').install(); 3 | require('@babel/register')({ 4 | presets: ['@babel/preset-env', '@babel/preset-react'] 5 | }); 6 | require('@babel/core').transform('code', { 7 | plugins: ['@babel/plugin-transform-runtime'] 8 | }); 9 | require('css-modules-require-hook')({ 10 | extensions: ['.less'], 11 | processorOpts: { parser: require('postcss-less').parse }, 12 | camelCase: true, 13 | generateScopedName: '[local]_[hash:base64:10]' 14 | }); 15 | require('asset-require-hook')({ 16 | name: '/[hash].[ext]', 17 | extensions: ['jpg', 'png', 'gif', 'webp'], 18 | limit: 8192 19 | }); 20 | 21 | const fs = require('fs'); 22 | const path = require('path'); 23 | const views = require('koa-views'); 24 | const convert = require('koa-convert'); 25 | const webpack = require('webpack'); 26 | const config = require('../build/webpack.dev.config'); 27 | const compiler = webpack(config); 28 | const devMiddleware = require('koa-webpack-dev-middleware'); 29 | const hotMiddleware = require('koa-webpack-hot-middleware'); 30 | const app = require('./app.js').default; 31 | const router = require('./routes').default; 32 | const clientRoute = require('./middlewares/clientRoute').default; 33 | const port = process.env.port || 3000; 34 | 35 | compiler.plugin('emit', (compilation, callback) => { 36 | const assets = compilation.assets; 37 | let file, data; 38 | Object.keys(assets).forEach(key => { 39 | if (key.match(/\.html$/)) { 40 | file = path.resolve(__dirname, key); 41 | data = assets[key].source(); 42 | fs.writeFileSync(file, data); 43 | } 44 | }); 45 | callback(); 46 | }); 47 | 48 | app.use(views(path.resolve(__dirname, '../views'), { map: { html: 'ejs' } })); 49 | app.use(router.routes()); 50 | app.use(router.allowedMethods()); 51 | app.use(clientRoute); 52 | 53 | app.use(convert(devMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }))); 54 | app.use(convert(hotMiddleware(compiler))); 55 | 56 | app.listen(port, () => { 57 | console.log(`\n==> open up http://localhost:${port}/ in your browser.\n`); 58 | }); 59 | 60 | module.exports = app; 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-webpack", 3 | "scripts": { 4 | "start": "cross-env webpack-dev-server --compress --config build/webpack.config.js", 5 | "dev": "cross-env nodemon server/server.dev.js --watch server", 6 | "build": "rimraf ./dist && cross-env webpack --config ./build/webpack.prod.config.js", 7 | "server": "npm run build && node ./dist/server/server.js", 8 | "startpm": "pm2 start ./server/server.dev.js --name xwb007", 9 | "delpm": "pm2 delete all" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.18.0", 13 | "classnames": "^2.2.6", 14 | "consolidate": "^0.15.1", 15 | "koa": "^2.6.2", 16 | "koa-bodyparser": "^4.2.1", 17 | "koa-compose": "^4.1.0", 18 | "koa-compress": "^3.0.0", 19 | "koa-convert": "^1.2.0", 20 | "koa-json": "^2.0.2", 21 | "koa-logger": "^3.2.0", 22 | "koa-router": "^7.4.0", 23 | "koa-session": "^5.10.1", 24 | "koa-static": "^5.0.0", 25 | "koa-views": "^6.1.5", 26 | "koa2-cors": "^2.0.6", 27 | "mysql": "^2.16.0", 28 | "react": "^16.7.0", 29 | "react-dom": "^16.7.0", 30 | "react-redux": "^6.0.0", 31 | "react-router-dom": "^4.3.1", 32 | "redux": "^4.0.1", 33 | "redux-thunk": "^2.3.0" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.2.2", 37 | "@babel/plugin-transform-runtime": "^7.2.0", 38 | "@babel/preset-env": "^7.2.3", 39 | "@babel/preset-react": "^7.0.0", 40 | "@babel/register": "^7.0.0", 41 | "asset-require-hook": "^1.2.0", 42 | "babel-loader": "^8.0.5", 43 | "babel-polyfill": "^6.26.0", 44 | "cross-env": "^5.2.0", 45 | "css-loader": "^2.1.0", 46 | "css-modules-require-hook": "^4.2.3", 47 | "ejs": "^2.6.1", 48 | "html-loader": "^0.5.5", 49 | "html-webpack-plugin": "^3.2.0", 50 | "isnode": "0.0.1", 51 | "isomorphic-style-loader": "^4.0.0", 52 | "json-loader": "^0.5.7", 53 | "koa-webpack-dev-middleware": "^2.0.2", 54 | "koa-webpack-hot-middleware": "^1.0.3", 55 | "less": "^3.9.0", 56 | "less-loader": "^4.1.0", 57 | "mini-css-extract-plugin": "^0.5.0", 58 | "nodemon": "^1.18.10", 59 | "pm2": "^3.2.9", 60 | "postcss-less": "^3.1.2", 61 | "postcss-loader": "^3.0.0", 62 | "progress-bar-webpack-plugin": "^1.12.1", 63 | "rimraf": "^2.6.3", 64 | "style-loader": "^0.23.1", 65 | "url-loader": "^1.1.2", 66 | "webpack": "^4.29.3", 67 | "webpack-cli": "^3.2.3", 68 | "webpack-dev-server": "^3.1.14" 69 | } 70 | } 71 | --------------------------------------------------------------------------------