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