├── .babelrc ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config ├── webpack.config.common.js ├── webpack.config.dev.js └── webpack.config.prod.js ├── nodemon.json ├── package.json ├── postcss.config.js ├── server.js ├── src ├── App.js ├── api │ ├── actions.js │ ├── index.js │ └── reducer.js ├── client.js ├── components │ ├── Article │ │ └── index.js │ ├── CssModule │ │ ├── index.js │ │ └── index.scss │ ├── LoginForm │ │ ├── index.js │ │ └── index.scss │ └── NavBar │ │ ├── Nav │ │ ├── index.js │ │ └── index.scss │ │ ├── index.js │ │ └── index.scss ├── containers │ ├── AppLayout.js │ ├── LoginForm │ │ └── index.js │ ├── NavBar │ │ ├── index.js │ │ ├── index.less │ │ └── index.scss │ └── NotFound.js ├── helpers │ ├── combineServer.js │ └── history.js ├── imgs │ └── BiazfanxmamNRoxxVxka.png ├── index.html ├── router.js ├── server │ ├── index.js │ ├── logger.js │ └── middlewares │ │ ├── api.js │ │ ├── development.js │ │ ├── production.js │ │ └── renderHtml.js ├── store │ ├── appStore.js │ └── index.js └── style.scss └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | "es2015", 10 | "stage-0", 11 | "react" 12 | ], 13 | "retainLines": true, 14 | "plugins": [ 15 | "transform-runtime", 16 | "syntax-dynamic-import", 17 | "react-hot-loader/babel", 18 | "react-loadable/babel", 19 | "transform-decorators-legacy", 20 | ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": true }] 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "jest": true, 6 | "node": true 7 | }, 8 | "extends": [ 9 | "fbjs", 10 | "plugin:prettier/recommended" 11 | ], 12 | "parser": "babel-eslint", 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "plugins": [ 17 | "prettier", 18 | "jsx-a11y", 19 | "react" 20 | ], 21 | "rules": { 22 | "prettier/prettier": 0 23 | }, 24 | "settings": { 25 | "import/resolver": { 26 | "webpack": { 27 | "config": "./config/webpack.config.prod.js" 28 | } 29 | }, 30 | "react": { 31 | "version": "16.2" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | package-lock.json -diff 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | build-dev 3 | node_modules 4 | .tags* 5 | .idea 6 | *.log 7 | yarn.lock 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": false, 9 | "jsxBracketSameLine": false, 10 | "arrowParens": "always" 11 | } 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | git: 2 | depth: 3 3 | branches: 4 | except: 5 | - docs 6 | language: node_js 7 | node_js: 8 | - "node" 9 | - "8.9" 10 | script: 11 | - npm run lint 12 | - npm run build 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | # [2.3.0](https://github.com/antonfisher/react-express-webpack/compare/v2.2.0...v2.3.0) (2018-05-10) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * move dev deps to dev deps ([e7a42bc](https://github.com/antonfisher/react-express-webpack/commit/e7a42bc)) 12 | 13 | 14 | ### Features 15 | 16 | * update deps, express@4.16.3, webpack@4.8.1, react@16.3.2 ([b5aa7fa](https://github.com/antonfisher/react-express-webpack/commit/b5aa7fa)) 17 | * upgrade to prettier@1.12.1 ([299ddc3](https://github.com/antonfisher/react-express-webpack/commit/299ddc3)) 18 | 19 | 20 | 21 | 22 | # [2.2.0](https://github.com/antonfisher/react-express-webpack/compare/v2.1.0...v2.2.0) (2018-03-12) 23 | 24 | 25 | ### Features 26 | 27 | * hide package-lock.json from git diff output ([6b63ff2](https://github.com/antonfisher/react-express-webpack/commit/6b63ff2)) 28 | * upgrade to Webpack 4 ([f6224bc](https://github.com/antonfisher/react-express-webpack/commit/f6224bc)) 29 | * use compact build output ([5d5bb55](https://github.com/antonfisher/react-express-webpack/commit/5d5bb55)) 30 | * use last version of uglifyjs ([cd71f35](https://github.com/antonfisher/react-express-webpack/commit/cd71f35)) 31 | * use react-hot-loader v4 ([121bda7](https://github.com/antonfisher/react-express-webpack/commit/121bda7)) 32 | * use webpack merge for merging configs ([d765ec6](https://github.com/antonfisher/react-express-webpack/commit/d765ec6)) 33 | 34 | 35 | 36 | 37 | # [2.1.0](https://github.com/antonfisher/react-express-webpack/compare/v2.0.0...v2.1.0) (2018-02-01) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * always include Intl to main bundle ([7b9a36f](https://github.com/antonfisher/react-express-webpack/commit/7b9a36f)) 43 | 44 | 45 | ### Features 46 | 47 | * update packadge.json for windows support (closes [#3](https://github.com/antonfisher/react-express-webpack/issues/3)) ([2b8f7ac](https://github.com/antonfisher/react-express-webpack/commit/2b8f7ac)) 48 | 49 | 50 | 51 | 52 | # [2.0.0](https://github.com/antonfisher/react-express-webpack/compare/v1.2.0...v2.0.0) (2018-01-27) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * move html-webpack-harddisk-plugin dependency to dev ([9fe7747](https://github.com/antonfisher/react-express-webpack/commit/9fe7747)) 58 | * remove unused dependency 'history' ([75bc02f](https://github.com/antonfisher/react-express-webpack/commit/75bc02f)) 59 | * remove unused gif file-loader rule ([f121d0b](https://github.com/antonfisher/react-express-webpack/commit/f121d0b)) 60 | * use application logger in webpack-dev-middleware ([badfbc0](https://github.com/antonfisher/react-express-webpack/commit/badfbc0)) 61 | 62 | 63 | ### Features 64 | 65 | * remove react-tap-event-plugin dependency ([a6cd1e9](https://github.com/antonfisher/react-express-webpack/commit/a6cd1e9)) 66 | * support for realy old browsers, add intl polyfill (closes [#2](https://github.com/antonfisher/react-express-webpack/issues/2)) ([94f4e95](https://github.com/antonfisher/react-express-webpack/commit/94f4e95)) 67 | * update React to 16.2.0 ([4f9bd1b](https://github.com/antonfisher/react-express-webpack/commit/4f9bd1b)) 68 | * upgrade to react 16 ([1f73233](https://github.com/antonfisher/react-express-webpack/commit/1f73233)) 69 | * use flat componets/containers structure ([e44986b](https://github.com/antonfisher/react-express-webpack/commit/e44986b)) 70 | * use prettier code formatter ([894f014](https://github.com/antonfisher/react-express-webpack/commit/894f014)) 71 | 72 | 73 | 74 | 75 | # [1.2.0](https://github.com/antonfisher/react-express-webpack/compare/v1.1.0...v1.2.0) (2017-09-22) 76 | 77 | 78 | ### Bug Fixes 79 | 80 | * don't sync router with redux store with old way ([70b0d1d](https://github.com/antonfisher/react-express-webpack/commit/70b0d1d)) 81 | 82 | 83 | ### Features 84 | 85 | * use '/' as root page url ([30b75b6](https://github.com/antonfisher/react-express-webpack/commit/30b75b6)) 86 | 87 | 88 | 89 | 90 | # [1.1.0](https://github.com/antonfisher/react-express-webpack/compare/v1.0.3...v1.1.0) (2017-09-22) 91 | 92 | 93 | ### Features 94 | 95 | * add ip to logger output, don't log query starting ([88c589f](https://github.com/antonfisher/react-express-webpack/commit/88c589f)) 96 | * add unhandled errors handling, rename APP_PORT to HTTP_PORT, log app env ([8584748](https://github.com/antonfisher/react-express-webpack/commit/8584748)) 97 | * remove separated app config file ([ce0d66f](https://github.com/antonfisher/react-express-webpack/commit/ce0d66f)) 98 | 99 | 100 | 101 | 102 | ## [1.0.2](https://github.com/antonfisher/react-express-webpack/compare/v1.0.0...v1.0.2) (2017-04-28) 103 | 104 | 105 | ### Bug Fixes 106 | 107 | * always show response status code on error ([fcaf20b](https://github.com/antonfisher/react-express-webpack/commit/fcaf20b)) 108 | * modals don't work properly in production mode ([3e07ec4](https://github.com/antonfisher/react-express-webpack/commit/3e07ec4)) 109 | * remove unused code from venders file ([922ac69](https://github.com/antonfisher/react-express-webpack/commit/922ac69)) 110 | * set valid entries order for react-hot-loader ([5a0191e](https://github.com/antonfisher/react-express-webpack/commit/5a0191e)) 111 | * use both end and close events in logger ([123f5c0](https://github.com/antonfisher/react-express-webpack/commit/123f5c0)) 112 | 113 | 114 | ### Features 115 | 116 | * add url, explanation and status code to api error messages ([95a1613](https://github.com/antonfisher/react-express-webpack/commit/95a1613)) 117 | * migrate to react-router v4 ([3dfecd7](https://github.com/antonfisher/react-express-webpack/commit/3dfecd7)) 118 | * use winston as default logger ([a554ec3](https://github.com/antonfisher/react-express-webpack/commit/a554ec3)) 119 | 120 | 121 | 122 | 123 | ## [1.0.1](https://github.com/antonfisher/react-express-webpack/compare/v1.0.0...v1.0.1) (2017-04-28) 124 | 125 | 126 | ### Bug Fixes 127 | 128 | * always show response status code on error ([fcaf20b](https://github.com/antonfisher/react-express-webpack/commit/fcaf20b)) 129 | * modals don't work properly in production mode ([3e07ec4](https://github.com/antonfisher/react-express-webpack/commit/3e07ec4)) 130 | * remove unused code from venders file ([922ac69](https://github.com/antonfisher/react-express-webpack/commit/922ac69)) 131 | * set valid entries order for react-hot-loader ([5a0191e](https://github.com/antonfisher/react-express-webpack/commit/5a0191e)) 132 | * use both end and close events in logger ([123f5c0](https://github.com/antonfisher/react-express-webpack/commit/123f5c0)) 133 | 134 | 135 | ### Features 136 | 137 | * add url, explanation and status code to api error messages ([95a1613](https://github.com/antonfisher/react-express-webpack/commit/95a1613)) 138 | * migrate to react-router v4 ([3dfecd7](https://github.com/antonfisher/react-express-webpack/commit/3dfecd7)) 139 | * use winston as default logger ([a554ec3](https://github.com/antonfisher/react-express-webpack/commit/a554ec3)) 140 | 141 | 142 | 143 | 144 | # 1.0.0 (2017-04-19) 145 | 146 | 147 | ### Bug Fixes 148 | 149 | * add npm build to travis ([f7b353b](https://github.com/antonfisher/react-express-webpack/commit/f7b353b)) 150 | * lint errors ([17a5f7e](https://github.com/antonfisher/react-express-webpack/commit/17a5f7e)) 151 | * lints errors after upgrade ([cbb0eaa](https://github.com/antonfisher/react-express-webpack/commit/cbb0eaa)) 152 | 153 | 154 | ### Features 155 | 156 | * add travis ci and badges ([db7bc3d](https://github.com/antonfisher/react-express-webpack/commit/db7bc3d)) 157 | * migrate to React 15.5.4 ([dac4421](https://github.com/antonfisher/react-express-webpack/commit/dac4421)) 158 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Anton Fisher 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 | React+Mobx server side render boilerplate with ES2015, Express.js, and Webpack. 2 | 3 | ## 更新 4 | 5 | - Babel7版本请切换分支到babel-7 6 | - 或者访问[https://github.com/Tecode/react-mobx-ssr/tree/babel-7](https://github.com/Tecode/react-mobx-ssr/tree/babel-7) 7 | 8 | ## Technologies 9 | 10 | - React (v16) + Mobx (v4) + React Router (v4) 11 | - Express.js (v4) as production and development server 12 | - Webpack 4.12.0 (production and development configurations) 13 | - SCSS support (+ sanitize.css included) 14 | - ES2015 15 | - Antd v3.6.2 16 | 17 | ## Features 18 | - Server side render(ssr) 19 | - Code splitting 20 | - Preconfigured router 21 | - React Antd UI example theme 22 | - preconfigured eslint and Prettier code formatter 23 | - React Hot Loader 24 | - Linux/MacOS/Windows 25 | 26 | ## Usage 27 | 28 | ### Installation 29 | ```bash 30 | git clone https://github.com/Tecode/react-mobx-ssr.git 31 | cd react-mobx-ssr 32 | npm install Or yarn 33 | 34 | # remove boilerplate git references 35 | rm ./.git 36 | ``` 37 | 38 | ### Scripts 39 | ```bash 40 | # run development mode 41 | npm run dev 42 | 43 | # run production mode 44 | npm run build 45 | npm start 46 | 47 | # run prettier 48 | npm run prettier 49 | 50 | # run lint 51 | npm run lint 52 | 53 | # run on a different port 54 | HTTP_PORT=3001 npm run dev 55 | ``` 56 | 57 | ## License 58 | MIT License. Free use and change. 59 | -------------------------------------------------------------------------------- /config/webpack.config.common.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | const webpack = require('webpack'); 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | 5 | const isDev = process.env.NODE_ENV !== 'production'; 6 | 7 | module.exports = { 8 | target: 'web', 9 | entry: [ 10 | 'babel-polyfill', 11 | './src/client.js' 12 | ], 13 | output: { 14 | publicPath: '/', 15 | path: resolve(__dirname, '..', 'build'), 16 | filename: '[name].bundle.js', 17 | chunkFilename: isDev ? '[name].chunk.js' : '[name].[chunkhash:8].chunk.js', 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.js$/, 23 | use: ['babel-loader'], 24 | exclude: /node_modules/ 25 | }, 26 | { 27 | test: /\.html$/, 28 | loader: 'html-loader' 29 | }, 30 | { 31 | test: /\.css$/, 32 | use: ExtractTextPlugin.extract({ 33 | fallback: { 34 | loader: 'style-loader', 35 | options: { sourceMap: isDev } 36 | }, 37 | use: [ 38 | { 39 | loader: 'css-loader', 40 | options: { 41 | localIdentName: isDev ? '[path]-[name]_[local]' : '[name]_[local]_[hash:5]', // [hash:base64] 42 | modules: true, 43 | sourceMap: isDev, 44 | minimize: !isDev 45 | } 46 | }, 47 | { 48 | loader: 'postcss-loader', 49 | options: { sourceMap: isDev } 50 | } 51 | ] 52 | }) 53 | }, 54 | { 55 | test: /\.s?css$/, 56 | use: ExtractTextPlugin.extract({ 57 | fallback: { 58 | loader: 'style-loader', 59 | options: { sourceMap: isDev } 60 | }, 61 | use: [ 62 | { 63 | loader: 'css-loader', 64 | options: { 65 | localIdentName: isDev ? '[path]-[name]_[local]' : '[name]_[local]_[hash:5]', // [hash:base64] 66 | modules: true, 67 | sourceMap: isDev, 68 | minimize: !isDev 69 | } 70 | }, 71 | { 72 | loader: 'sass-loader', 73 | options: { 74 | sourceMap: isDev 75 | } 76 | }, 77 | { 78 | loader: 'postcss-loader', 79 | options: { sourceMap: isDev } 80 | } 81 | ] 82 | }) 83 | }, 84 | { 85 | test: /\.less$/, 86 | use: ['style-loader', 'css-loader', 'less-loader'] 87 | }, 88 | { 89 | test: /\.(eot|svg|ttf|woff|woff2)$/, 90 | loader: 'file-loader' 91 | }, 92 | { 93 | test: /\.(jpg|png|gif)$/, 94 | use: [ 95 | { 96 | loader: 'file-loader?limit=5000&name=imgs/[name].[hash:6].[ext]' 97 | } 98 | ] 99 | } 100 | ] 101 | }, 102 | plugins: [ 103 | new ExtractTextPlugin({ 104 | filename: '[name].css', 105 | disable: isDev 106 | }), 107 | new webpack.EnvironmentPlugin(['NODE_ENV']) 108 | ], 109 | resolve: { 110 | modules: [resolve(__dirname, '../src/'), 'node_modules'], 111 | extensions: ['.js', '.scss', '.less'] 112 | }, 113 | optimization: { 114 | splitChunks: { 115 | chunks: 'all' 116 | } 117 | }, 118 | stats: { 119 | assetsSort: '!size', 120 | children: false, 121 | chunks: true, 122 | colors: true, 123 | entrypoints: false, 124 | modules: false 125 | } 126 | }; 127 | -------------------------------------------------------------------------------- /config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const {resolve} = require('path'); 2 | const webpack = require('webpack'); 3 | const merge = require('webpack-merge'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin'); 6 | 7 | const commonConfig = require('./webpack.config.common'); 8 | 9 | module.exports = merge(commonConfig, { 10 | mode: 'development', 11 | entry: [`webpack-hot-middleware/client?http://localhost:${process.env.HTTP_PORT}&reload=true`], 12 | output: { 13 | hotUpdateMainFilename: 'hot-update.[hash:6].json', 14 | hotUpdateChunkFilename: 'hot-update.[hash:6].js' 15 | }, 16 | devtool: 'cheap-module-eval-source-map', 17 | plugins: [ 18 | new webpack.HotModuleReplacementPlugin(), 19 | new webpack.NamedModulesPlugin(), 20 | new HtmlWebpackPlugin({ 21 | inject: true, 22 | template: resolve(__dirname, '..', 'src', 'index.html'), 23 | filename: 'client.html', 24 | //favicon: resolve(__dirname, '..', 'src', 'client', 'static', 'favicon.png'), 25 | alwaysWriteToDisk: true 26 | }), 27 | new HtmlWebpackHarddiskPlugin({ 28 | outputPath: resolve(__dirname, '..', 'build-dev') 29 | }) 30 | ] 31 | }); 32 | -------------------------------------------------------------------------------- /config/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const {resolve} = require('path'); 2 | const merge = require('webpack-merge'); 3 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | const commonConfig = require('./webpack.config.common'); 7 | 8 | module.exports = merge(commonConfig, { 9 | mode: 'production', 10 | plugins: [ 11 | new UglifyJsPlugin({ 12 | parallel: true, 13 | extractComments: true 14 | }), 15 | new HtmlWebpackPlugin({ 16 | hash: true, 17 | inject: true, 18 | template: resolve(__dirname, '..', 'src', 'index.html'), 19 | filename: 'server.html', 20 | //favicon: resolve(__dirname, '..', 'src', 'client', 'static', 'favicon.png'), 21 | minify: { 22 | removeComments: true, 23 | collapseWhitespace: true, 24 | removeRedundantAttributes: true, 25 | useShortDoctype: true, 26 | removeEmptyAttributes: true, 27 | removeStyleLinkTypeAttributes: true, 28 | keepClosingSlash: true, 29 | minifyJS: true, 30 | minifyCSS: true, 31 | minifyURLs: true 32 | } 33 | }) 34 | ] 35 | }); 36 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "ignore": ["node_modules"], 4 | "watch": [ 5 | "src/server", 6 | "store/", 7 | "server.js" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.2.0", 3 | "author": { 4 | "name": "aming", 5 | "email": "283731869@qq.com", 6 | "url": "http://www.soscooon.com" 7 | }, 8 | "description": "React boilerplate with ES2015, Express.js, and Webpack4", 9 | "license": "MIT", 10 | "engines": { 11 | "node": ">=7.5.0" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/Tecode/react-mobx-ssr" 16 | }, 17 | "scripts": { 18 | "dev": "cross-env NODE_ENV=development nodemon server.js", 19 | "start": "cross-env NODE_ENV=production node server.js", 20 | "prebuild": "rimraf build", 21 | "build": "npm run build:client", 22 | "build:client": "cross-env NODE_ENV=production webpack -p --progress --config=config/webpack.config.prod.js", 23 | "build:server": "copyfiles -a -u 1 src/server/**/**/* build", 24 | "lint": "eslint --ignore-path .gitignore --ignore-pattern node_modules -- .", 25 | "prettier": "prettier --ignore-path .gitignore --write './**/*.js'", 26 | "test": "exit 1", 27 | "release:patch": "npm run lint && npx standard-version -r patch && git push --follow-tags origin master", 28 | "release:minor": "npm run lint && npx standard-version -r minor && git push --follow-tags origin master", 29 | "release:major": "npm run lint && npx standard-version -r major && git push --follow-tags origin master" 30 | }, 31 | "postcss": {}, 32 | "devDependencies": { 33 | "babel-cli": "6.26.0", 34 | "babel-core": "6.26.3", 35 | "babel-eslint": "8.2.3", 36 | "babel-loader": "7.1.4", 37 | "babel-plugin-dynamic-import-node": "^2.1.0", 38 | "babel-plugin-import-inspector": "^2.0.0", 39 | "babel-polyfill": "6.26.0", 40 | "babel-preset-env": "1.6.1", 41 | "babel-preset-react": "6.24.1", 42 | "babel-preset-stage-0": "6.24.1", 43 | "babel-register": "^6.26.0", 44 | "babel-watch": "2.0.7", 45 | "copyfiles": "2.0.0", 46 | "css-loader": "0.28.11", 47 | "eslint": "4.19.1", 48 | "eslint-config-fbjs": "2.0.1", 49 | "eslint-config-prettier": "2.9.0", 50 | "eslint-import-resolver-webpack": "0.9.0", 51 | "eslint-plugin-babel": "5.1.0", 52 | "eslint-plugin-flowtype": "2.46.3", 53 | "eslint-plugin-import": "2.11.0", 54 | "eslint-plugin-jsx-a11y": "6.0.3", 55 | "eslint-plugin-prettier": "2.6.0", 56 | "eslint-plugin-react": "7.7.0", 57 | "eslint-plugin-relay": "0.0.21", 58 | "extract-text-webpack-plugin": "4.0.0-beta.0", 59 | "file-loader": "^1.1.11", 60 | "html-loader": "0.5.5", 61 | "html-webpack-harddisk-plugin": "0.2.0", 62 | "html-webpack-plugin": "3.2.0", 63 | "import-inspector": "^2.0.0", 64 | "intl": "1.2.5", 65 | "isomorphic-style-loader": "^4.0.0", 66 | "less": "^2.7.3", 67 | "less-loader": "^4.0.5", 68 | "node-sass": "4.9.0", 69 | "postcss-load-config": "1.2.0", 70 | "postcss-loader": "2.1.5", 71 | "prettier": "1.12.1", 72 | "prop-types": "15.6.1", 73 | "react": "16.3.2", 74 | "react-dom": "16.3.2", 75 | "react-hot-loader": "4.1.3", 76 | "react-intl": "2.4.0", 77 | "react-router": "^4.2.0", 78 | "react-router-dom": "^4.3.1", 79 | "rimraf": "2.6.2", 80 | "sanitize.css": "5.0.0", 81 | "sass-loader": "7.0.1", 82 | "style-loader": "0.21.0", 83 | "uglifyjs-webpack-plugin": "1.2.5", 84 | "webpack": "4.12.0", 85 | "webpack-cli": "2.1.3", 86 | "webpack-dev-middleware": "3.1.3", 87 | "webpack-hot-middleware": "2.22.1", 88 | "webpack-isomorphic-tools": "^3.0.6", 89 | "webpack-merge": "4.1.2", 90 | "webpack-node-externals": "^1.7.2", 91 | "whatwg-fetch": "2.0.4" 92 | }, 93 | "dependencies": { 94 | "antd": "^3.6.2", 95 | "babel-plugin-import": "^1.7.0", 96 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 97 | "babel-plugin-transform-decorators-legacy": "^1.3.5", 98 | "babel-plugin-transform-runtime": "^6.23.0", 99 | "babel-runtime": "^6.26.0", 100 | "body-parser": "1.18.2", 101 | "cheerio": "^1.0.0-rc.2", 102 | "compression": "1.7.2", 103 | "cross-env": "5.1.5", 104 | "express": "4.16.3", 105 | "history": "^4.7.2", 106 | "lodash": "^4.17.11", 107 | "mobx": "^4.3.1", 108 | "mobx-react": "^5.2.0", 109 | "nodemon": "^1.17.5", 110 | "react-helmet": "^5.2.0", 111 | "react-loadable": "^5.5.0", 112 | "winston": "2.4.2" 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /* Set your postcss-loader configuration here */ 2 | 3 | module.exports = { 4 | plugins: [require('precss'), require('autoprefixer')] 5 | }; 6 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require.extensions['.scss'] = () => { 2 | return; 3 | }; 4 | require.extensions['.css'] = () => { 5 | return; 6 | }; 7 | require.extensions['.less'] = () => { 8 | return; 9 | }; 10 | require.extensions['.png'] = () => { 11 | return; 12 | }; 13 | require("babel-register")({ 14 | "plugins": [ 15 | "dynamic-import-node" 16 | ] 17 | }); 18 | require('./src/server'); 19 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import {hot} from 'react-hot-loader'; 3 | 4 | import Routers from './router'; 5 | 6 | class App extends Component { 7 | render() { 8 | return ; 9 | } 10 | } 11 | 12 | export default hot(module)(App); 13 | -------------------------------------------------------------------------------- /src/api/actions.js: -------------------------------------------------------------------------------- 1 | import {createAction} from 'redux-actions'; 2 | 3 | import api from 'api/index'; 4 | import {showModal} from 'containers/ModalsLayout/actions'; 5 | import ErrorWindow from 'components/ErrorWindow'; 6 | 7 | function* createGuidGenerator() { 8 | let i = 1; 9 | while (i) { 10 | yield i++; 11 | } 12 | } 13 | 14 | const guidGenerator = createGuidGenerator(); 15 | 16 | export const API_REQUEST_STARTED = 'API_REQUEST_STARTED'; 17 | export const apiRequestStarted = createAction(API_REQUEST_STARTED); 18 | export const API_REQUEST_FINISHED = 'API_REQUEST_FINISHED'; 19 | export const apiRequestFinished = createAction(API_REQUEST_FINISHED); 20 | 21 | export const API_DATA_SERVERS_LOADED = 'API_DATA_SERVERS_LOADED'; 22 | export const apiDataServersLoaded = createAction(API_DATA_SERVERS_LOADED); 23 | 24 | export function apiGetServers(callback) { 25 | return function dispatchApiGetServers(dispatch) { 26 | const requestId = guidGenerator.next().value; 27 | dispatch(apiRequestStarted({requestId})); 28 | return api 29 | .getStats() 30 | .then((data) => { 31 | dispatch(apiDataServersLoaded(data)); 32 | dispatch(apiRequestFinished({requestId})); 33 | if (callback) { 34 | callback(); // get rid of callback here? 35 | } 36 | }) 37 | .catch((error) => { 38 | dispatch(apiRequestFinished({requestId, error})); 39 | dispatch( 40 | showModal({ 41 | key: ErrorWindow.NAME, 42 | props: { 43 | title: error.title, 44 | message: error.message, 45 | explanation: `URL: ${error.url} ${error.statusCode}` 46 | } 47 | }) 48 | ); 49 | }); 50 | }; 51 | } 52 | 53 | export function apiAddServer(data, callback) { 54 | return function dispatchApiAddServer(dispatch) { 55 | const requestId = guidGenerator.next().value; 56 | dispatch(apiRequestStarted({requestId})); 57 | return api 58 | .addServer(data) 59 | .then(() => { 60 | dispatch(apiGetServers()); 61 | dispatch(apiRequestFinished({requestId})); 62 | if (callback) { 63 | callback(); // get rid of callback here? 64 | } 65 | }) 66 | .catch((error) => { 67 | dispatch(apiRequestFinished({requestId, error})); 68 | dispatch( 69 | showModal({ 70 | key: ErrorWindow.NAME, 71 | props: { 72 | title: error.title, 73 | message: error.message, 74 | explanation: `URL: ${error.url} ${error.statusCode}` 75 | } 76 | }) 77 | ); 78 | }); 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | const defaultParams = { 2 | mode: 'none' 3 | }; 4 | 5 | let endpoint = ''; 6 | 7 | function setEndpoint(value) { 8 | endpoint = value; 9 | } 10 | 11 | /** 12 | * @param {String} url 13 | * @param {String} message 14 | * @param {Number} statusCode 15 | * @constructor 16 | */ 17 | function ApiError(url, message, statusCode) { 18 | this.url = url; 19 | this.message = message; 20 | this.statusCode = statusCode || ''; 21 | this.title = 'API Error'; 22 | this.stack = new Error().stack; 23 | } 24 | 25 | /** 26 | * @param {String} url 27 | * @param {Error} error 28 | * @param {Number|undefined} statusCode 29 | * @throws {ApiError} 30 | */ 31 | function throwApiError(url, error, statusCode) { 32 | throw new ApiError(url, error, statusCode); 33 | } 34 | 35 | /** 36 | * @param {Object} params 37 | * @returns {Promise.} 38 | * @private 39 | */ 40 | function _request(params) { 41 | let requestUrl; 42 | let requestParams; 43 | if (typeof params === 'string') { 44 | requestUrl = params; 45 | requestParams = {}; 46 | } else { 47 | const {url, ...restParams} = params; 48 | requestUrl = url; 49 | requestParams = restParams; 50 | } 51 | 52 | let rawResponse; 53 | return fetch(requestUrl, {...defaultParams, ...requestParams}) 54 | .then((response) => { 55 | rawResponse = response; 56 | return response.json(); 57 | }) 58 | .then((json) => { 59 | if (json && json.error) { 60 | return throwApiError(requestUrl, json.error, rawResponse.status); 61 | } 62 | return json; 63 | }) 64 | .catch((error) => throwApiError(requestUrl, error.message)); 65 | } 66 | 67 | // application api 68 | 69 | function getStats() { 70 | return _request(`${endpoint}/stats`); 71 | } 72 | 73 | function addServer(payload) { 74 | return _request({ 75 | url: `${endpoint}/servers`, 76 | method: 'POST', 77 | headers: new Headers({'content-type': 'application/json'}), 78 | body: JSON.stringify(payload), 79 | ...defaultParams 80 | }); 81 | } 82 | 83 | export default { 84 | getStats, 85 | addServer, 86 | setEndpoint 87 | }; 88 | -------------------------------------------------------------------------------- /src/api/reducer.js: -------------------------------------------------------------------------------- 1 | import {List, Map, OrderedMap} from 'immutable'; 2 | 3 | import {API_DATA_SERVERS_LOADED, API_REQUEST_FINISHED, API_REQUEST_STARTED} from 'api/actions'; 4 | 5 | const initialState = Map({ 6 | loading: false, 7 | requests: OrderedMap({}), 8 | errors: Map({ 9 | last: null 10 | }), 11 | lastUpdate: Map({ 12 | servers: null 13 | }), 14 | data: Map({ 15 | servers: List() 16 | }) 17 | }); 18 | 19 | export default function ApiReducer(state = initialState, action) { 20 | switch (action.type) { 21 | case API_REQUEST_STARTED: 22 | return state.setIn(['requests', action.payload.requestId], action.payload).set('loading', true); 23 | 24 | case API_REQUEST_FINISHED: 25 | return state 26 | .removeIn(['requests', action.payload.requestId]) 27 | .set('loading', state.get('requests').size > 1) 28 | .setIn( 29 | ['errors', 'last'], 30 | action.payload.error ? action.payload.error.message : state.getIn(['errors', 'last']) 31 | ); 32 | 33 | case API_DATA_SERVERS_LOADED: 34 | return state 35 | .setIn(['lastUpdate', 'servers'], Date.now()) 36 | .setIn(['data', 'servers'], List(action.payload.servers)); 37 | 38 | default: 39 | return state; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import 'whatwg-fetch'; 3 | import Loadable from 'react-loadable'; 4 | import intl from 'intl'; 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | import { Provider } from 'mobx-react'; 8 | // import api from 'api/index'; 9 | import allStore from './store'; 10 | import { Router } from 'react-router-dom'; 11 | import App from './App'; 12 | import combineServerData from './helpers/combineServer'; 13 | import browserHistory from './helpers/history'; 14 | // global styles 15 | import 'antd/dist/antd.less'; 16 | 17 | // apply polyfill 18 | if (!window.Intl) { 19 | window.Intl = intl; 20 | } 21 | 22 | // api.setEndpoint('/api'); 23 | combineServerData(allStore, window.__INITIAL_STATE__); 24 | const renderApp = () => { 25 | Loadable.preloadReady().then(() => { 26 | ReactDOM.hydrate( 27 | 28 | 29 | 30 | 31 | , 32 | document.getElementById('app') 33 | ) 34 | }); 35 | } 36 | 37 | renderApp(); -------------------------------------------------------------------------------- /src/components/Article/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Article = () => ( 4 |

MN2018-09-19T07:52:12.383Z - info: Application env: development 5 | 2018-09-19T07:52:12.947Z - info: HTTP server is now running on http://localhost:3001 6 | webpack built 2400ef2b021b433f8527 in 5849ms 7 | 2018-09-19T07:52:18.328Z - info: Hash: 2400ef2b021b433f8527 8 | Version: webpack 4.8.1 9 | Time: 5849ms 10 | Built at: 2018-09-19 15:52:18 11 | Asset Size Chunks Chunk Names 12 | imgs/BiazfanxmamNRoxxVxka.fda383.png 51.9 KiB [emitted] 13 | main.js 146 KiB main [emitted] main 14 | vendor.chunk.js 10 MiB vendor [emitted] vendor 15 | client.html 626 bytes [emitted] 16 | Entrypoint main = vendor.chunk.js main.js 17 | 18 | + 830 hidden modules 19 | Child html-webpack-plugin for "client.html": 20 | Asset Size Chunks Chunk Names 21 | client.html 27.9 KiB 0 22 | Entrypoint undefined = client.html 23 | [./node_modules/html-webpack-plugin/lib/loader.js!./src/index.html] 569 bytes {0} [built] 24 | 2018-09-19T07:52:18.329Z - info: Compiled successfully. 25 | 2018-09-19T07:52:18.484Z - info: ::1 - GET /article - 200 - 0.055s 26 | 2018-09-19T07:52:18.531Z - info: ::1 - GET /[object%20Object] - 200 - 0.019s 27 | 2018-09-19T07:52:18.544Z - info: ::1 - GET /vendor.chunk.js - 304 - 0.013s 28 | 2018-09-19T07:52:18.548Z - info: ::1 - GET /main.js - 200 - 0.001s 29 | 2018-09-19T07:52:19.492Z - info: ::1 - GET /imgs/BiazfanxmamNRoxxVxka.fda383.png - 200 - 0.001s 30 | 2018-09-19T07:52:19.676Z - info: ::1 - GET /favicon.ico - 200 - 0.012s 31 | 2018-09-19T07:52:21.560Z - info: ::1 - GET /favicon.ico - 304 - 0.016s 32 |

33 | ); 34 | 35 | export default Article; -------------------------------------------------------------------------------- /src/components/CssModule/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import styles from './index.scss' 4 | 5 | @inject('appStore') 6 | @observer 7 | export default class CssModule extends Component { 8 | render() { 9 | return ( 10 |
11 | MIKOLC 12 | Hello world! 13 | Store: {this.props.appStore.name} 14 |
15 | ); 16 | } 17 | } -------------------------------------------------------------------------------- /src/components/CssModule/index.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | background: red; 3 | } 4 | .name { 5 | font-size: 2em; 6 | font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif 7 | } 8 | .store { 9 | margin-left: .5em; 10 | font-weight: bold; 11 | } -------------------------------------------------------------------------------- /src/components/LoginForm/index.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import Form from 'antd/lib/form'; 3 | import Icon from 'antd/lib/icon'; 4 | import Input from 'antd/lib/input'; 5 | import Button from 'antd/lib/button'; 6 | import Checkbox from 'antd/lib/checkbox'; 7 | const FormItem = Form.Item; 8 | import styles from './index.scss'; 9 | 10 | class LoginForm extends Component { 11 | handleSubmit = (e) => { 12 | e.preventDefault(); 13 | this.props.form.validateFields((err, values) => { 14 | if (!err) { 15 | console.log('Received values of form: ', values); 16 | } 17 | }); 18 | }; 19 | render() { 20 | const {getFieldDecorator} = this.props.form; 21 | return ( 22 |
23 |
24 | 25 | {getFieldDecorator('userName', { 26 | rules: [{required: true, message: 'Please input your username!'}] 27 | })(} placeholder="Username" />)} 28 | 29 | 30 | {getFieldDecorator('password', { 31 | rules: [{required: true, message: 'Please input your Password!'}] 32 | })( 33 | } 35 | type="password" 36 | placeholder="Password" 37 | /> 38 | )} 39 | 40 | 41 | {getFieldDecorator('remember', { 42 | valuePropName: 'checked', 43 | initialValue: true 44 | })(Remember me)} 45 | 46 | Forgot password 47 | 48 | 51 | Or register now! 52 | 53 |
54 |
55 | ); 56 | } 57 | } 58 | 59 | export default Form.create()(LoginForm); 60 | -------------------------------------------------------------------------------- /src/components/LoginForm/index.scss: -------------------------------------------------------------------------------- 1 | .form_box { 2 | width: 300px; 3 | margin: 10% auto 0 auto; 4 | .login_form_button { 5 | width: 100%; 6 | border-radius: 0; 7 | } 8 | input[class='ant-input'] { 9 | border-radius: 0; 10 | } 11 | } -------------------------------------------------------------------------------- /src/components/NavBar/Nav/index.js: -------------------------------------------------------------------------------- 1 | // Dead simple component for the hello world (hi mom!) 2 | 3 | import React from 'react'; 4 | import styles from './index.scss'; 5 | import {observer, inject} from 'mobx-react'; 6 | import PropTypes from 'prop-types'; 7 | import Menu from 'antd/lib/menu'; 8 | import Dropdown from 'antd/lib/dropdown'; 9 | import Icon from 'antd/lib/icon'; 10 | import img from '../../../imgs/BiazfanxmamNRoxxVxka.png'; 11 | 12 | function Nav() { 13 | const menu = ( 14 | 15 | 16 | 17 | 个人中心 18 | 19 | 20 | 21 | 系统设置 22 | 23 | 24 | 25 | 退出登录 26 | 27 | 28 | ); 29 | return ( 30 | 42 | ); 43 | } 44 | Nav.propTypes = { 45 | homeStore: PropTypes.object, 46 | appStore: PropTypes.object 47 | }; 48 | export default inject('homeStore', 'appStore')(observer(Nav)); 49 | -------------------------------------------------------------------------------- /src/components/NavBar/Nav/index.scss: -------------------------------------------------------------------------------- 1 | .nav_box { 2 | float: right; 3 | list-style: none; 4 | padding-right: 12px; 5 | li { 6 | padding: 0 12px; 7 | &:hover { 8 | cursor: pointer; 9 | background-color: #e6f7ff 10 | } 11 | } 12 | } 13 | .img_box { 14 | width: 26px; 15 | height: 26px; 16 | border-radius: 30px; 17 | display: inline-block; 18 | margin: 0 10px 0 0; 19 | img { 20 | width: 100%; 21 | height: 100%; 22 | } 23 | } 24 | .text { 25 | padding-left: 10px; 26 | } -------------------------------------------------------------------------------- /src/components/NavBar/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { PropTypes } from 'prop-types'; 3 | import { Link } from 'react-router-dom'; 4 | import Layout from 'antd/lib/layout'; 5 | import Menu from 'antd/lib/menu'; 6 | import Icon from 'antd/lib/icon'; 7 | import Nav from './Nav'; 8 | import 'antd/lib/layout/style'; 9 | import styles from './index.scss'; 10 | const { Header, Sider, Content } = Layout; 11 | 12 | export default class NavBar extends Component { 13 | static propTypes = { 14 | children: PropTypes.any 15 | }; 16 | state = { 17 | collapsed: false 18 | }; 19 | toggle = () => { 20 | this.setState({ 21 | collapsed: !this.state.collapsed 22 | }); 23 | }; 24 | render() { 25 | return ( 26 | 27 | 28 |
Logo
29 | 30 | 31 | 32 | 33 | 新增文章 34 | 35 | 36 | 37 | 38 | 39 | 首页 40 | 41 | 42 | 43 |
44 | 45 |
46 | 51 |
53 | 54 | {this.props.children} 55 | 56 |
57 |
58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/NavBar/index.scss: -------------------------------------------------------------------------------- 1 | .trigger { 2 | font-size: 18px; 3 | line-height: 64px; 4 | padding: 0 24px; 5 | cursor: pointer; 6 | transition: color .3s; 7 | &:hover { 8 | color: #1890ff; 9 | } 10 | } 11 | 12 | .logo { 13 | height: 32px; 14 | background: #fafafa; 15 | margin: 16px; 16 | } 17 | .layout { 18 | height: 100%; 19 | border-right: 1px solid #fafafa; 20 | } -------------------------------------------------------------------------------- /src/containers/AppLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Loadable from 'react-loadable'; 3 | import { Route, Switch } from 'react-router-dom'; 4 | import NotFound from '../containers/NotFound'; 5 | import NavBar from '../components/NavBar'; 6 | // import Article from '../components/Article'; 7 | 8 | const Home = Loadable({ 9 | loader: () => import('../components/CssModule' /* webpackChunkName: 'Home' */), 10 | loading: () =>

加载中...

11 | }); 12 | 13 | const Article = Loadable({ 14 | loader: () => import('../components/Article' /* webpackChunkName: 'Article' */), 15 | loading: () =>

加载中...

16 | }); 17 | 18 | export class AppLayout extends React.Component { 19 | // static propTypes = { 20 | // loading: PropTypes.bool.isRequired 21 | // }; 22 | render() { 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | } 33 | } 34 | 35 | export default AppLayout; 36 | -------------------------------------------------------------------------------- /src/containers/LoginForm/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import LoginFormBody from '../../components/LoginForm'; 4 | 5 | export default class LoginForm extends React.Component { 6 | static propTypes = { 7 | children: PropTypes.node 8 | }; 9 | 10 | static defaultProps = { 11 | children: '' 12 | }; 13 | 14 | render() { 15 | return ; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/containers/NavBar/index.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {Helmet} from 'react-helmet'; 4 | import {observer, inject} from 'mobx-react'; 5 | import styles from './index.scss'; 6 | 7 | @inject('appStore') 8 | @observer 9 | export default class NavBar extends Component { 10 | static propTypes = { 11 | appStore: PropTypes.object 12 | }; 13 | render() { 14 | return ( 15 |
16 | 17 |

文章相关56{this.props.appStore.name}

18 |

文章相关

19 |
20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/containers/NavBar/index.less: -------------------------------------------------------------------------------- 1 | .title { 2 | font-size: 32px; 3 | } -------------------------------------------------------------------------------- /src/containers/NavBar/index.scss: -------------------------------------------------------------------------------- 1 | .title { 2 | font-size: 32px; 3 | } -------------------------------------------------------------------------------- /src/containers/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function NotFound() { 4 | return ( 5 |
6 |

Page not found.

7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/helpers/combineServer.js: -------------------------------------------------------------------------------- 1 | export default function combineServerData(allStore, data) { 2 | const keyArr = Object.keys(allStore); 3 | keyArr.map((key) => { 4 | if (allStore[key].combineServerData) { 5 | allStore[key].combineServerData(data[key]); 6 | } 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/helpers/history.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory, createMemoryHistory } from 'history'; 2 | // use this way to fix bug(Invariant Violation: Browser history needs a DOM) 3 | export default (typeof window !== 'undefined' 4 | ? createBrowserHistory() 5 | : createMemoryHistory()); 6 | -------------------------------------------------------------------------------- /src/imgs/BiazfanxmamNRoxxVxka.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/react-mobx-ssr/e3c6d266a33742b3e81113886c755c92132946c7/src/imgs/BiazfanxmamNRoxxVxka.png -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | react16+mobx+ssr 11 | 12 | 17 | 18 |
19 | 20 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Route, Switch } from 'react-router-dom'; 3 | 4 | import AppLayout from './containers/AppLayout'; 5 | import LoginForm from './containers/LoginForm'; 6 | 7 | export default class Routers extends Component { 8 | render() { 9 | return ( 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import bodyParser from 'body-parser'; 3 | import Loadable from 'react-loadable'; 4 | import setupApiRoutes from './middlewares/api'; 5 | import logger from './logger'; 6 | import development from './middlewares/development'; 7 | import production from './middlewares/production'; 8 | 9 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 10 | const HTTP_PORT = process.env.HTTP_PORT || 3001; 11 | 12 | function onUnhandledError(err) { 13 | try { 14 | logger.error(err); 15 | } catch (e) { 16 | console.log('LOGGER ERROR:', e); //eslint-disable-line no-console 17 | console.log('APPLICATION ERROR:', err); //eslint-disable-line no-console 18 | } 19 | process.exit(1); 20 | } 21 | 22 | process.on('unhandledRejection', onUnhandledError); 23 | process.on('uncaughtException', onUnhandledError); 24 | 25 | const setupAppRoutes = process.env.NODE_ENV === 'development' ? development : production; 26 | 27 | const app = express(); 28 | 29 | app.set('env', process.env.NODE_ENV); 30 | logger.info(`Application env: ${process.env.NODE_ENV}`); 31 | 32 | app.use(logger.expressMiddleware); 33 | app.use(bodyParser.json()); 34 | 35 | // application routes 36 | setupApiRoutes(app); 37 | setupAppRoutes(app); 38 | 39 | if (HTTP_PORT) { 40 | Loadable.preloadAll().then(() => { 41 | app.listen(HTTP_PORT, function() { 42 | logger.info(`HTTP server is now running on http://localhost:${HTTP_PORT}`); 43 | }); 44 | }); 45 | } else { 46 | global.console.error( 47 | '==> 😭 OMG!!! No PORT environment variable has been specified' 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/server/logger.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const {homedir} = require('os'); 3 | const { 4 | Logger, 5 | transports: {Console, File} 6 | } = require('winston'); 7 | 8 | const LOG_FILE_NAME = '.application.log'; 9 | const LOG_FILE_PATH = 10 | process.env.NODE_ENV === 'production' 11 | ? path.join(homedir(), LOG_FILE_NAME) 12 | : path.join(__dirname, '..', '..', LOG_FILE_NAME); 13 | const LOG_LEVEL = process.env.LOG_LEVEL || (process.env.NODE_ENV === 'production' ? 'verbose' : 'debug'); 14 | 15 | const logger = new Logger({ 16 | transports: [ 17 | new Console({ 18 | level: LOG_LEVEL, 19 | colorize: true, 20 | timestamp: true, 21 | prettyPrint: true 22 | }), 23 | new File({ 24 | level: LOG_LEVEL, 25 | filename: LOG_FILE_PATH, 26 | handleExceptions: true, 27 | humanReadableUnhandledException: true, 28 | prettyPrint: true, 29 | tailable: true, 30 | maxsize: 10 * 1024 * 1024, 31 | maxFiles: 10, 32 | json: false 33 | }) 34 | ] 35 | }); 36 | 37 | logger.expressMiddleware = function expressMiddleware(req, res, next) { 38 | if (req.url.includes('__webpack') && process.env.NODE_ENV === 'development') { 39 | return next(); 40 | } 41 | 42 | const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; 43 | const defaultMessage = `${ip} - ${req.method} ${req.url}`; 44 | const startTimestemp = Date.now(); 45 | const waitingTimePrintInterval = 5000; 46 | 47 | let waitingTime = 0; 48 | const intervalId = setInterval(() => { 49 | waitingTime += waitingTimePrintInterval; 50 | logger.verbose(`${defaultMessage} - wait for ${waitingTime / 1000}s...`); 51 | }, waitingTimePrintInterval); 52 | 53 | const printExecutionTime = (statusCode = '') => { 54 | const message = `${defaultMessage} - ${statusCode} - ${(Date.now() - startTimestemp) / 1000}s`; 55 | if (res.statusCode < 400) { 56 | logger.info(message); 57 | } else { 58 | logger.warn(message); 59 | } 60 | clearInterval(intervalId); 61 | }; 62 | 63 | req.on('end', () => printExecutionTime(res.statusCode)); 64 | req.on('close', () => printExecutionTime('[closed by user]')); 65 | 66 | return next(); 67 | }; 68 | 69 | logger.info(`Application logs file: ${LOG_FILE_PATH}`); 70 | 71 | module.exports = logger; 72 | -------------------------------------------------------------------------------- /src/server/middlewares/api.js: -------------------------------------------------------------------------------- 1 | // API 2 | 3 | const servers = [{id: 1, name: 'a'}, {id: 2, name: 'b'}, {id: 3, name: 'c'}]; 4 | 5 | export default function(app) { 6 | app.get('/api/stats', (req, res) => { 7 | setTimeout(() => { 8 | res.json({ 9 | // error: 'server error message', 10 | status: 'online', 11 | servers 12 | }); 13 | }, 3000); 14 | }); 15 | 16 | app.post('/api/servers', (req, res) => { 17 | if (!req.body.name) { 18 | return res.json({ 19 | error: 'cannot add server with empty name' 20 | }); 21 | } 22 | return setTimeout(() => { 23 | servers.push({ 24 | id: servers[servers.length - 1].id + 1, 25 | name: req.body.name 26 | }); 27 | res.json({ 28 | success: true 29 | }); 30 | }, 3000); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/server/middlewares/development.js: -------------------------------------------------------------------------------- 1 | import {resolve} from 'path'; 2 | import webpack from 'webpack'; 3 | import webpackDevMiddleware from 'webpack-dev-middleware'; 4 | import webpackHotMiddleware from 'webpack-hot-middleware'; 5 | import logger from '../logger'; 6 | import webpackConfig from '../../../config/webpack.config.dev'; 7 | import renderHtml from './renderHtml'; 8 | 9 | const compiler = webpack(webpackConfig); 10 | 11 | export default function(app) { 12 | app.use( 13 | webpackDevMiddleware(compiler, { 14 | logger, 15 | publicPath: webpackConfig.output.publicPath, 16 | stats: { 17 | colors: true 18 | } 19 | }) 20 | ); 21 | 22 | app.use(webpackHotMiddleware(compiler)); 23 | 24 | // all other requests be handled by UI itself 25 | app.get('*', (req, res) => { 26 | res.status('200'); 27 | res.send(renderHtml(resolve(__dirname, '..', '..', '..', 'build-dev', 'client.html'), req)); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/server/middlewares/production.js: -------------------------------------------------------------------------------- 1 | import {resolve} from 'path'; 2 | import express from 'express'; 3 | import compression from 'compression'; 4 | import renderHtml from './renderHtml'; 5 | 6 | const clientBuildPath = resolve(__dirname, '..', '..', '..', 'build'); 7 | 8 | export default function(app) { 9 | app.use(compression()); 10 | app.use('/', express.static(clientBuildPath)); 11 | 12 | // all other requests be handled by UI itself 13 | app.get('*', (req, res) => { 14 | res.status('200'); 15 | res.send(renderHtml(resolve(clientBuildPath, 'server.html'), req)); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /src/server/middlewares/renderHtml.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import fs from 'fs'; 3 | import { Helmet } from 'react-helmet'; 4 | import cheerio from 'cheerio'; 5 | import { renderToString } from 'react-dom/server'; 6 | import { StaticRouter } from 'react-router'; 7 | import { Provider } from 'mobx-react'; 8 | import App from '../../router'; 9 | import allStore from '../../store'; 10 | import { toJS } from 'mobx'; 11 | 12 | export default function (path, req) { 13 | allStore.miniStore = { name: 'PPPPP' }; 14 | const context = {}; 15 | const componentHTML = ( 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | const prepareStore = (store) => { 23 | const keyArr = Object.keys(allStore); 24 | const output = {}; 25 | keyArr.forEach((key) => { 26 | output[key] = toJS(store[key]); 27 | }); 28 | return output; 29 | }; 30 | const helmet = Helmet.renderStatic(); 31 | const HTML_TEMPLATE = fs.readFileSync(path).toString(); 32 | const $template = cheerio.load(HTML_TEMPLATE, { decodeEntities: false }); 33 | $template('head').append(helmet.title.toString() + helmet.meta.toString() + helmet.link.toString()); 34 | // console.log(renderToString(), '----------------------------appString') 35 | $template('#app').html(`${renderToString(componentHTML)}`); 36 | $template('#app').after(``); 37 | return $template.html(); 38 | } 39 | -------------------------------------------------------------------------------- /src/store/appStore.js: -------------------------------------------------------------------------------- 1 | import {observable, action} from 'mobx'; 2 | 3 | class AppSore { 4 | @observable name = 'solo8969'; 5 | @observable day = '2096'; 6 | @action.bound 7 | log() { 8 | console.log('mobx'); 9 | } 10 | } 11 | 12 | export default new AppSore(); 13 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import appStore from './appStore'; 2 | 3 | const allStore = () => ({ 4 | homeStore: {name: 'bob', age: '12'}, 5 | appStore 6 | }); 7 | 8 | export default allStore(); 9 | -------------------------------------------------------------------------------- /src/style.scss: -------------------------------------------------------------------------------- 1 | html, body { 2 | background-color: white; 3 | min-width: 700px; 4 | font-family: 'Comfortaa'; 5 | } 6 | --------------------------------------------------------------------------------