├── .eslintrc.js ├── .gitignore ├── .idea ├── misc.xml ├── modules.xml ├── react_ultimate_framework.iml └── vcs.xml ├── README.md ├── client ├── assets │ └── favicon.ico ├── config │ ├── constants.js │ └── server.js ├── constant │ └── config.js ├── css │ ├── index.css │ ├── reboot.css │ ├── reset.css │ ├── utilities.css │ └── variables.css ├── default.hbs ├── layout │ ├── MainLayout.js │ └── mainLayout.css ├── page │ ├── components │ │ ├── LoadingEffect.css │ │ ├── LoadingEffect.js │ │ └── TopicList.js │ ├── index.css │ ├── index.js │ ├── indexSPA │ │ ├── TopicListConnect.js │ │ ├── actions │ │ │ └── topic.js │ │ ├── configureStore.js │ │ ├── index.js │ │ └── reducers │ │ │ ├── index.js │ │ │ └── topic.js │ └── manager │ │ ├── indexView.js │ │ └── sagas.js ├── store │ └── configureStore.js └── utils │ ├── common.js │ ├── fetchUtil.js │ └── profile.js ├── config ├── babel.config.js ├── webpack.common.config.js ├── webpack_client_dev.config.js ├── webpack_client_production.config.js ├── webpack_server_dev.config.js └── webpack_server_production.config.js ├── dev ├── index.js └── restartServer.js ├── doc └── flowImg.png ├── ecosystem.config.js ├── package.json ├── server ├── index.js ├── routes │ └── index.js └── utils │ └── serverRender.js ├── stats.generated.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "extends": ["eslint:recommended", "plugin:react/recommended"], 9 | "globals": { 10 | "__DEV__": true, 11 | "__CLIENT__": true, 12 | __SERVER__: true, 13 | __PRODUCTION__: true, 14 | }, 15 | "parser": "babel-eslint", 16 | "parserOptions": { 17 | "ecmaFeatures": { 18 | "experimentalObjectRestSpread": true, 19 | "jsx": true 20 | }, 21 | "sourceType": "module" 22 | }, 23 | "plugins": [ 24 | "react" 25 | ], 26 | "rules": { 27 | "no-unused-vars":"off", 28 | "no-console":"off", 29 | "react/prop-types": "off", 30 | "react/display-name": "off" 31 | } 32 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | server_dist 4 | release 5 | logs 6 | .idea 7 | .idea/workspace.xml 8 | ecosystem.config.js 9 | stats.generated.json -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/react_ultimate_framework.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 用react开发了不少项目,大多数是客户端渲染。 3 | 当涉及到资讯类,官网类的网站时,为了优化seo,必须使用react服务器渲染。 4 | 于是查阅不少资料,总结一套自己觉得还不错的框架。 5 | 说是框架,更像是一堆配置的集合。 6 | 7 | 8 | ## 特点 9 | - 前后端分离,nodejs做中间层(这里的后端一般指提供api接口的后端,比如java后端) 10 | - 支持webpack多页面多入口配置 11 | - 支持react 服务器渲染,包含集成了redux的SPA页面服务器渲染 12 | - live reload。修改客户端代码,浏览器自动刷新;修改服务器代码,自动reload。如果页面用到服务器渲染,修改前后端公共代码,将同时起作用 13 | - 使用postCss,同时可支持服务器渲染+css modules 14 | - 集成了ant-design UI,可以选择不用 15 | - 所有的依赖均已经升级到最新版本(😅尴尬,这里webpack是3的版本,最新已经到4) 16 | 17 | ## 运行 18 | ``` shell 19 | yarn install 20 | yarn start 21 | ``` 22 | 打开 http://localhost:8087 23 | 或者 http://localhost:8087/indexSPA (单页面redux服务器渲染) 24 | 25 | ## 目录结构 26 | ``` 27 | ├── client 客户端react代码 28 | │   ├── assets 图片或字体资源文件夹 29 | │   ├── component 30 | │   ├── config 31 | │   ├── constant 32 | │   ├── css 33 | │   ├── decorator 34 | │   ├── default.hbs 项目中使用handlebars,来渲染模版,实际上只用了非常少的功能 35 | │   ├── layout 36 | │   ├── page 客户端多页面入口 37 | │   └── utils 38 | ├── config webpack配置文件夹 39 | │   ├── babel.config.js webpack的babel-loader配置提取到这个文件 40 | │   ├── webpack.common.config.js webpack的入口和生成html文件的配置提取到这个文件 41 | │   ├── webpack_client_dev.config.js 开发环境,客户端webpack配置 42 | │   ├── webpack_client_production.config.js 生产环境,客户端webpack配置 43 | │   ├── webpack_server_dev.config.js 开发环境,node后端webpack配置 44 | │   └── webpack_server_production.config.js 生产环境,node后端webpack配置 45 | ├── dev 46 | │   ├── index.js       开发环境关键的启动脚本,npm start的入口 47 | │   └── restartServer.js   实现对node server重启的功能,参考how to shutdown nodejs server 48 | ├── server 49 | │   ├── index.js        服务器入口文件 50 | │   ├── routes          路由 51 | │   └── utils 52 | ├── ecosystem.config.js 发布部署的pm2配置,暂未使用 53 | ├── package.json 54 | ├── stats.generated.json assets-webpack-plugin插件生成的webpack资源列表文件 55 | └── yarn.lock 推荐使用yarn 56 | ``` 57 | 58 | 59 | ## 客户端配置 60 | 客户端配置很常见,主要是webpack配置,一个用于开发环境`webpack_client_dev.config.js`,一个用于生产环境`webpack_client_production.config.js`,支持webpack多页面配置, 61 | 把入口entry和生成html文件的html-webpack-plugin配置提取到`webpack.common.config.js`。 62 | ``` javascript 63 | const entry = { 64 | vendor: ['react', 'react-dom'], 65 | home: ['./client/page/index.js'], 66 | homeSPA: ['./client/page/indexSPA/index.js'], 67 | }; 68 | ``` 69 | 这里也把babel-loader的option配置都集中到`babel.config.js`,因为服务器端也需要用到,下面会讲到。 70 | 71 | ## 服务器端配置(关键) 72 | web服务器采用了koa2框架。 73 | 74 | 如果不需要服务器渲染,node中间层,只用作webpack打包,一般使用两个中间件`webpack-dev-middleware` 75 | 和`webpack-hot-middleware`,一个用于在服务器端打包,一个支持hot reload特性 76 | 77 | 配置如下 78 | ``` javascript 79 | var webpack = require('webpack'), 80 | webpackDevMiddleware = require('webpack-dev-middleware'), 81 | webpackHotMiddleware = require('webpack-hot-middleware'), 82 | webpackDevConfig = require('./webpack.config.js'); 83 | 84 | var compiler = webpack(webpackDevConfig); 85 | 86 | // attach to the compiler & the server 87 | app.use(webpackDevMiddleware(compiler, { 88 | 89 | // public path should be the same with webpack config 90 | publicPath: webpackDevConfig.output.publicPath, 91 | noInfo: true, 92 | stats: { 93 | colors: true 94 | } 95 | })); 96 | app.use(webpackHotMiddleware(compiler)); 97 | ``` 98 | 99 | ### 服务器端渲染遇到的一些问题 100 | 101 | #### 服务器同样需要引用客户端的组件,调用`react-dom/server`的`renderToString`才能将组件渲染成html。node端无法理解前端代码中require的css文件和图片。   102 | 103 | **解决方法:** 104 | 使用webpack对服务器端代码进行打包。 105 | 对于css文件webpack配置为`ignore-loader`,忽略css。 106 | 对于图片文件,同样使用`url-loader`,配置成和客户端一样(经常会配置成小于多少k图片转换成base64)。 107 | 108 | 需要注意的一些地方: 109 | 1. 我们只对自己写的代码打包,node_modules里的安装模块不打包。但是如果引用的是前端组件库的代码,必须同样打包。 这里使用`webpack-node-externals`,防止webpack打包node_modules的代码。 110 | ``` javascript 111 | target: 'node', // in order to ignore built-in modules like path, fs, etc. 112 | externals: [nodeExternals({whitelist:[/^antd/]})], // in order to ignore all modules in node_modules folder, 113 | ``` 114 | 115 | #### 对服务端代码进行了webpack打包,使其可以正常require css文件和图片,但开发过程中怎么样才能继续保留webpack实时打包,热刷新机制? 116 | **解决方法:** 117 | 118 | **客户端** 119 | 120 | 开发过程中实时打包,我们依旧使用`webpack-dev-midddleware`和`webpack-hot-midddleware`,但要注意保证这两个对象不会因为服务器重启而被销毁。 121 | 122 | **服务端(重点)** 123 | 124 | 当后端代码改变,我们同样需要重启后端,由于我们使用了webpack对后端代码进行打包,需要自己实现重启后端的工作。 125 | 126 | **实现node服务器重启:** 127 | 128 | (核心)webpack的compiler对象提供了watch模式,同时暴露出了打包过程中的事件钩子([详见文档](https://doc.webpack-china.org/api/compiler/))。 129 | 130 | 于是,我们监听后端webpack对compiler对象的重新打包事件和打包完成事件,分别销毁服务器和重启服务器,自己实现了后端的修改热刷新。 131 | 这里使用到到两个hook事件: 132 | ``` javascript 133 | // webpack监听到代码改变,开始重新打包时,销毁现有的server对象 134 | serverCompiler.plugin("compile", stats => { 135 | destroyServer(serverCompiler); 136 | console.log(chalk.yellow("server compiling.... ")); 137 | }); 138 | 139 | // 打包完成,重新启动服务器 140 | serverCompiler.plugin('done', stats => { 141 | console.log(chalk.blue("server compile done! ")); 142 | restartServer(serverCompiler, devMidware, hotMidware) 143 | }); 144 | ``` 145 | 如何关闭http server,这里参考了stackoverflow上的一个答案 [shut down http server](https://stackoverflow.com/questions/14626636/how-do-i-shutdown-a-node-js-https-server-immediately) 146 | 147 | 关键的一点,我们在这个项目中启动了两次webpack打包,一个对客户端的`clientCompiler`,一个对后端的`serverCompiler`。客户端至关重要的两个对象,`webpackDevMiddle`和`webpackHotMiddle`对象无论如何都不能销毁。 148 | 149 | 我们将后端webpack打包时,指定了`libriaryTarget`为`commonjs`,这里写个hook脚本,手动启动或者销毁服务器对象,在hook脚本中始终保存着`webpackDevMiddle`和`webpackHotMiddle`对象。 150 | 151 | 试想一下,不保留这两个middleware,那么修改node端代码的每一次重启都会导致客户端的重新打包,这是非常慢的过程。 152 | 153 | 工程中,这个只使用在开发环境的hook脚本在`dev`文件夹,也是`npm start`的入口。 154 | 155 | ![](https://github.com/wuyanwuyan/react_ultimate_framework/raw/master/doc/flowImg.png) 156 | 157 | ## 服务器渲染的示例 158 | 服务器渲染的流程: 159 | 1. node端获取数据,作为服务器渲染 组件的props。 160 | 2. React服务端渲染HTML,放在和客户端渲染一样的标签位置 161 | ``` javascript 162 | import {renderToString} from 'react-dom/server'; 163 | renderToString() 164 | ``` 165 | ``` handlebars 166 | 167 |
{{{renderContent}}}
168 | 169 | ``` 170 | 3. 脱水。服务端交给浏览器的不光要有HTML,还需要有“脱水数据”,也就是在服务端渲染过程中给React组件的输入数据。 171 | “脱水数据”传递至浏览器的方式: 172 | ``` handlebars 173 | 174 |
{{{content}}}
175 | 176 | 177 | ``` 178 | 4. 注水。当浏览器渲染时可以直接根据“脱水数据”来渲染React组件,这个过程叫做“注水”。使用“脱水数据”就是为了保证两端数据一致,同时避免不必要的服务器请求。 179 | ``` javascript 180 | if (__CLIENT__) { 181 | let initState = window.__INITIAL_STATE__ || {}; 182 | ReactDOM.hydrate(, document.getElementById("react-container")); 183 | } 184 | ``` 185 | 186 | 工程例子里面使用到了一个cnode的api,[get /topics 主题首页](https://cnodejs.org/api),实现服务器端渲染cnode端首页,以及包含使用了redux端单页面应用,如何实现react服务器渲染。 187 | -------------------------------------------------------------------------------- /client/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyanwuyan/react_ultimate_framework/5d67ce140ed555256bdffa150495cd1669860cfd/client/assets/favicon.ico -------------------------------------------------------------------------------- /client/config/constants.js: -------------------------------------------------------------------------------- 1 | export const Base_color = '#00529c'; //'rgb(0,82,156)'; 2 | export const Dark_color = '#646464';//'rgb(100,100,100)'; 3 | export const Black_color = '#323232'; //'rgb(50,50,50)'; 4 | export const Split_color = '#d9d9d9'; //'rgb(217,217,217)'; 5 | export const Hightlight_color = '#F4F4F4'; 6 | 7 | 8 | 9 | 10 | 11 | // 通用正则 12 | export const Phone_reg = /(^(13\d|15[^4\D]|17[13678]|18\d)\d{8}|170[^346\D]\d{7})$/; 13 | 14 | -------------------------------------------------------------------------------- /client/config/server.js: -------------------------------------------------------------------------------- 1 | const TestServerIp_Port = '115.159.47.29:8080'; 2 | const ProServerIp_Port = '123.206.178.83:8080'; 3 | 4 | export default { 5 | backend: __DEV__ ? `http://${TestServerIp_Port}/v1` : 6 | `http://${ProServerIp_Port}/v1`, 7 | Ip_Port: __DEV__ ? TestServerIp_Port : ProServerIp_Port, 8 | } -------------------------------------------------------------------------------- /client/constant/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | }; 3 | 4 | export const topic = { 5 | '': '全部', 6 | good: '精华', 7 | share: '分享', 8 | ask: '问答', 9 | job: '招聘', 10 | } 11 | 12 | export default config; 13 | 14 | -------------------------------------------------------------------------------- /client/css/index.css: -------------------------------------------------------------------------------- 1 | @import "./reset.css"; 2 | @import "./utilities.css"; -------------------------------------------------------------------------------- /client/css/reboot.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 62.5%; 3 | } 4 | 5 | body{ 6 | background-color: #e1e1e1; 7 | } 8 | 9 | a:active,a:hover,a:focus,a:focus-within{ 10 | text-decoration: none; 11 | outline:0; 12 | } -------------------------------------------------------------------------------- /client/css/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section, main { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } -------------------------------------------------------------------------------- /client/css/utilities.css: -------------------------------------------------------------------------------- 1 | @import "./variables.css"; 2 | 3 | /*Margin*/ 4 | 5 | .margin { 6 | margin: var(--boxsize) 7 | } 8 | 9 | .margin-md { 10 | margin: var(--boxsize-md) 11 | } 12 | 13 | .margin-xs { 14 | margin: var(--boxsize-xs) 15 | } 16 | 17 | .margin-left { 18 | margin-left: var(--boxsize) 19 | } 20 | 21 | .margin-left-lg { 22 | margin-left: var(--boxsize-lg) 23 | } 24 | 25 | .margin-left-sm { 26 | margin-left: var(--boxsize-sm) 27 | } 28 | 29 | .margin-left-md { 30 | margin-left: var(--boxsize-md) 31 | } 32 | 33 | .margin-left-bg { 34 | margin-left: var(--boxsize-bg) 35 | } 36 | 37 | .margin-left-xs { 38 | margin-left: var(--boxsize-xs) 39 | } 40 | 41 | .margin-right { 42 | margin-right: var(--boxsize) 43 | } 44 | 45 | .margin-right-lg { 46 | margin-right: var(--boxsize-lg) 47 | } 48 | 49 | .margin-right-sm { 50 | margin-right: var(--boxsize-sm) 51 | } 52 | 53 | .margin-right-md { 54 | margin-right: var(--boxsize-md) 55 | } 56 | 57 | .margin-right-bg { 58 | margin-right: var(--boxsize-bg) 59 | } 60 | 61 | .margin-right-xs { 62 | margin-right: var(--boxsize-xs) 63 | } 64 | 65 | .margin-top { 66 | margin-top: var(--boxsize) 67 | } 68 | 69 | .margin-top-lg { 70 | margin-top: var(--boxsize-lg) 71 | } 72 | 73 | .margin-top-sm { 74 | margin-top: var(--boxsize-sm) 75 | } 76 | 77 | .margin-top-md { 78 | margin-top: var(--boxsize-md) 79 | } 80 | 81 | .margin-top-bg { 82 | margin-right: var(--boxsize-bg) 83 | } 84 | 85 | .margin-top-xs { 86 | margin-top: var(--boxsize-xs) 87 | } 88 | 89 | .margin-bottom { 90 | margin-bottom: var(--boxsize) 91 | } 92 | 93 | .margin-bottom-lg { 94 | margin-bottom: var(--boxsize-lg) 95 | } 96 | 97 | .margin-bottom-sm { 98 | margin-bottom: var(--boxsize-sm) 99 | } 100 | 101 | .margin-bottom-md { 102 | margin-bottom: var(--boxsize-md) 103 | } 104 | 105 | .margin-bottom-xs { 106 | margin-bottom: var(--boxsize-xs) 107 | } 108 | 109 | .margin-vertical { 110 | margin-bottom: var(--boxsize); 111 | margin-top: var(--boxsize) 112 | } 113 | 114 | .margin-vertical-lg { 115 | margin-bottom: var(--boxsize-lg); 116 | margin-top: var(--boxsize-lg) 117 | } 118 | 119 | .margin-vertical-sm { 120 | margin-bottom: var(--boxsize-sm); 121 | margin-top: var(--boxsize-sm) 122 | } 123 | 124 | .margin-vertical-md { 125 | margin-bottom: var(--boxsize-md); 126 | margin-top: var(--boxsize-md) 127 | } 128 | 129 | .margin-vertical-xs { 130 | margin-bottom: var(--boxsize-xs); 131 | margin-top: var(--boxsize-xs) 132 | } 133 | 134 | /* Padding */ 135 | .padding { 136 | padding: var(--boxsize) 137 | } 138 | 139 | .padding-lg { 140 | padding: var(--boxsize-lg) 141 | } 142 | 143 | .padding-sm { 144 | padding: var(--boxsize-sm) 145 | } 146 | 147 | .padding-md { 148 | padding: var(--boxsize-md) 149 | } 150 | 151 | .padding-left { 152 | padding-left: var(--boxsize) 153 | } 154 | 155 | .padding-left-lg { 156 | padding-left: var(--boxsize-lg) 157 | } 158 | 159 | .padding-left-bg { 160 | padding-left: var(--boxsize-bg) 161 | } 162 | 163 | .padding-left-sm { 164 | padding-left: var(--boxsize-sm) 165 | } 166 | 167 | .padding-left-md { 168 | padding-left: var(--boxsize-md) 169 | } 170 | 171 | .padding-right { 172 | padding-right: var(--boxsize) 173 | } 174 | 175 | .padding-right-lg { 176 | padding-right: var(--boxsize-lg) 177 | } 178 | 179 | .padding-right-sm { 180 | padding-right: var(--boxsize-sm) 181 | } 182 | 183 | .padding-right-md { 184 | padding-right: var(--boxsize-md) 185 | } 186 | 187 | .padding-top { 188 | padding-top: var(--boxsize) 189 | } 190 | 191 | .padding-top-lg { 192 | padding-top: var(--boxsize-lg) 193 | } 194 | 195 | .padding-top-sm { 196 | padding-top: var(--boxsize-sm) 197 | } 198 | 199 | .padding-top-md { 200 | padding-top: var(--boxsize-md) 201 | } 202 | 203 | .padding-bottom { 204 | padding-bottom: var(--boxsize) 205 | } 206 | 207 | .padding-bottom-xs { 208 | padding-bottom: var(--boxsize-xs) 209 | } 210 | 211 | .padding-bottom-lg { 212 | padding-bottom: var(--boxsize-lg) 213 | } 214 | 215 | .padding-bottom-sm { 216 | padding-bottom: var(--boxsize-sm) 217 | } 218 | 219 | .padding-bottom-md { 220 | padding-bottom: var(--boxsize-md) 221 | } 222 | 223 | .padding-vertical { 224 | padding-bottom: var(--boxsize); 225 | padding-top: var(--boxsize) 226 | } 227 | 228 | .padding-vertical-lg { 229 | padding-bottom: var(--boxsize-lg); 230 | padding-top: var(--boxsize-lg) 231 | } 232 | 233 | .padding-vertical-sm { 234 | padding-bottom: var(--boxsize-sm); 235 | padding-top: var(--boxsize-sm) 236 | } 237 | 238 | .padding-vertical-md { 239 | padding-bottom: var(--boxsize-md); 240 | padding-top: var(--boxsize-md) 241 | } 242 | 243 | .clearfix:after { 244 | visibility: hidden; 245 | display: block; 246 | font-size: 0; 247 | content: " "; 248 | clear: both; 249 | height: 0; 250 | } 251 | 252 | .text-truncate { 253 | overflow: hidden; 254 | text-overflow: ellipsis; 255 | white-space: nowrap; 256 | } 257 | 258 | .flex { 259 | display: flex; 260 | } 261 | 262 | .flex_center_v { 263 | display: flex; 264 | align-items: center; 265 | } 266 | 267 | .flex_center_h { 268 | display: flex; 269 | justify-content: center; 270 | } 271 | 272 | .flex_center_vh { 273 | display: flex; 274 | justify-content: center; 275 | align-items: center; 276 | } 277 | 278 | .flex1 { 279 | flex: 1; 280 | } 281 | 282 | .flex-column { 283 | display: flex; 284 | flex-direction: column; 285 | } 286 | 287 | .margin_left_auto { 288 | margin-left: auto; 289 | } 290 | 291 | .border-bottom-line { 292 | border-bottom: 1px gray solid; 293 | } 294 | 295 | .flex-space-between { 296 | display: flex; 297 | justify-content: space-between; 298 | } 299 | 300 | .overflowAuto { 301 | overflow: auto; 302 | } 303 | 304 | 305 | -------------------------------------------------------------------------------- /client/css/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --font-color: white; 3 | 4 | /*Margin & Padding */ 5 | --boxsize-lg: 50px; 6 | --boxsize: 30px; 7 | --boxsize-md: 20px; 8 | --boxsize-bg: 15px; 9 | --boxsize-sm: 10px; 10 | --boxsize-xs: 5px; 11 | 12 | } -------------------------------------------------------------------------------- /client/default.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {{title}} 13 | {{{scriptInline}}} 14 | 15 | 16 |
{{{content}}}
17 | 18 | 19 | -------------------------------------------------------------------------------- /client/layout/MainLayout.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./mainLayout.css"; 3 | import {NavLink} from "react-router-dom"; 4 | import {Input} from "antd"; 5 | 6 | const Search = Input.Search; 7 | 8 | 9 | export default class MainLayout extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | } 13 | 14 | render() { 15 | return ( 16 |
17 |
18 |
19 | 20 | 21 | 22 | console.log(value)} 24 | style={{ width: 200}} 25 | /> 26 | 27 | 48 |
49 | 50 | 51 |
52 |
53 | {this.props.children} 54 |
55 |
56 | ) 57 | } 58 | } 59 | 60 | 61 | -------------------------------------------------------------------------------- /client/layout/mainLayout.css: -------------------------------------------------------------------------------- 1 | header { 2 | background-color: #444; 3 | 4 | & > div{ 5 | font-size: 13px; 6 | padding: 1rem; 7 | width: 90%; 8 | margin: 0 auto; 9 | } 10 | 11 | } 12 | 13 | main{ 14 | width: 90%; 15 | margin: 15px auto; 16 | } 17 | 18 | .brand { 19 | 20 | img { 21 | width: 12rem; 22 | } 23 | } 24 | 25 | .nav { 26 | margin-bottom: 0; 27 | 28 | a { 29 | font-size: 1.3rem; 30 | padding: 1rem 1.5rem; 31 | color: #ccc 32 | } 33 | } -------------------------------------------------------------------------------- /client/page/components/LoadingEffect.css: -------------------------------------------------------------------------------- 1 | .load-container { 2 | font-size: 0; 3 | width: 33px; 4 | height: 30px; 5 | position: relative; 6 | overflow: hidden; 7 | box-sizing: content-box; 8 | [ class |= load ] { 9 | margin-left: 3px; 10 | width: 3px; 11 | height: 100%; 12 | display: inline-block; 13 | animation: stretchdelay 1.2s infinite ease-in-out; 14 | background: deepskyblue; 15 | } 16 | .load-2 { 17 | animation-delay: -1.1s; 18 | } 19 | .load-3 { 20 | animation-delay: -1s; 21 | } 22 | .load-4 { 23 | animation-delay: -.9s; 24 | } 25 | .load-5 { 26 | animation-delay: -.8s; 27 | } 28 | 29 | } 30 | 31 | @keyframes stretchdelay { 32 | 0%, 100%, 40% { 33 | transform: scaleY(.4); 34 | } 35 | 36 | 20% { 37 | transform: scaleY(1); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /client/page/components/LoadingEffect.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './LoadingEffect.css'; 3 | 4 | function Loader(props) { 5 | return ( 6 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | ) 19 | } 20 | 21 | export default Loader; 22 | -------------------------------------------------------------------------------- /client/page/components/TopicList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Pagination} from "antd"; 3 | import {topic} from '../../constant/config'; 4 | import LoadingEffect from './LoadingEffect'; 5 | 6 | export default class TopicList extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | _onChangePage = (page) => { 12 | this.props.onChangePage(page); 13 | } 14 | 15 | _renderTab = (value) => { 16 | if (value.top) return 置顶; 17 | if(value.good) return 精华; 18 | 19 | return {topic[value.tab]} 20 | } 21 | 22 | render() { 23 | const props = this.props; 24 | if(!props.topic_list){ 25 | return ; 26 | } 27 | return ( 28 |
29 |
30 | 31 | {props.topic_list.data.map(value => { 32 | return ( 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | {value.reply_count} 42 | 43 | / 44 | 45 | {value.visit_count} 46 | 47 | 48 | { 49 | this._renderTab(value) 50 | } 51 | 53 | {value.title} 54 | 55 | 56 |
57 | ) 58 | })} 59 |
60 | 61 |
62 | 64 |
65 |
66 | ) 67 | } 68 | } -------------------------------------------------------------------------------- /client/page/index.css: -------------------------------------------------------------------------------- 1 | @import "../css/reboot.css"; 2 | 3 | .topic-header{ 4 | padding: 1rem; 5 | background-color: #f6f6f6; 6 | border-radius: 3px 3px 0 0; 7 | } 8 | 9 | .topic-tab{ 10 | margin: 0 1rem; 11 | color: #80bd01; 12 | 13 | &.current-tab{ 14 | background-color: #80bd01; 15 | color: #fff; 16 | padding: 3px 4px; 17 | border-radius: 3px; 18 | } 19 | } 20 | 21 | .cell { 22 | font-size: 1.4rem; 23 | padding: 10px; 24 | background: #fff; 25 | border-top: 1px solid #f0f0f0; 26 | 27 | &:nth-child(1){ 28 | border-top: none; 29 | 30 | } 31 | } 32 | 33 | .user_avatar { 34 | img{ 35 | width: 30px; 36 | height: 30px; 37 | border-radius: 3px; 38 | } 39 | } 40 | 41 | .reply_count { 42 | width: 70px; 43 | display: inline-block; 44 | text-align: center; 45 | color:#9e78c0; 46 | } 47 | 48 | reply_count{ 49 | } 50 | 51 | .count_seperator { 52 | font-size: 1.0rem; 53 | } 54 | 55 | .count_of_visits { 56 | font-size: 1.0rem; 57 | color: #b4b4b4; 58 | } 59 | 60 | .user_small_avatar{ 61 | height: 18px; 62 | width: 18px; 63 | vertical-align: middle; 64 | margin-right: .5em; 65 | border-radius: 3px; 66 | } 67 | 68 | .topiclist-tab { 69 | background-color: #e5e5e5; 70 | color: #999; 71 | padding: 2px 4px; 72 | border-radius: 3px; 73 | font-size: 1.2rem; 74 | margin-right: 2px; 75 | } 76 | 77 | .put-top{ 78 | background: #80bd01; 79 | padding: 2px 4px; 80 | border-radius: 3px; 81 | color: #fff; 82 | font-size: 1.2rem; 83 | margin-right: 2px; 84 | 85 | } -------------------------------------------------------------------------------- /client/page/index.js: -------------------------------------------------------------------------------- 1 | import "../css/index.css"; 2 | 3 | import React from "react"; 4 | import ReactDOM from "react-dom"; 5 | import {LocaleProvider, Pagination} from "antd"; 6 | // 由于 antd 组件的默认文案是英文,所以需要修改为中文 7 | import zhCN from "antd/lib/locale-provider/zh_CN"; 8 | import moment from "moment"; 9 | import "moment/locale/zh-cn"; 10 | import {BrowserRouter, Route, StaticRouter, NavLink, Switch} from "react-router-dom"; 11 | 12 | import MainLayout from "../layout/MainLayout"; 13 | import TopicList from './components/TopicList'; 14 | import {topic} from '../constant/config'; 15 | import './index.css'; 16 | 17 | moment.locale('zh-cn'); 18 | 19 | const Router = __CLIENT__ ? BrowserRouter : StaticRouter; 20 | 21 | class Home extends React.Component { 22 | constructor(props) { 23 | super(props); 24 | } 25 | 26 | _onChangePage = (page) => { 27 | location.href = `${location.pathname}?page=${page}`; 28 | } 29 | 30 | render() { 31 | const props = this.props; 32 | 33 | return ( 34 | 35 | 36 | 37 |
38 |
39 | { 40 | Object.keys(topic).map((value) => 41 | 47 | {topic[value]} 48 | ) 49 | } 50 |
51 | 52 |
53 |
54 |
55 |
56 | ); 57 | } 58 | } 59 | 60 | if (__CLIENT__) { 61 | let initState = window.__INITIAL_STATE__ || {}; 62 | ReactDOM.hydrate(, document.getElementById("react-container")); 63 | } 64 | 65 | export default Home; -------------------------------------------------------------------------------- /client/page/indexSPA/TopicListConnect.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import TopicList from '../components/TopicList'; 4 | import {fetchTopicList} from './actions/topic'; 5 | 6 | class TopicListWrapper extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | componentDidMount() { 12 | // this.props.dispatch(fetchTopicList(null,1)); 13 | } 14 | 15 | componentWillReceiveProps(nextProps){ 16 | if(nextProps.match.params.topic !== this.props.match.params.topic){ 17 | this.props.dispatch(fetchTopicList(nextProps.match.params.topic,1)); 18 | } 19 | } 20 | 21 | _onChangePage = (page) => { 22 | this.props.dispatch(fetchTopicList(this.props.match.params.topic,page)); 23 | } 24 | 25 | render() { 26 | const props = this.props; 27 | return 32 | } 33 | } 34 | 35 | function mapStateToProps(state) { 36 | return { 37 | topic_list: state.topic.topic_list, 38 | page: state.topic.page, 39 | } 40 | } 41 | 42 | function mapDispathToProps() { 43 | 44 | } 45 | 46 | export default connect(mapStateToProps)(TopicListWrapper); -------------------------------------------------------------------------------- /client/page/indexSPA/actions/topic.js: -------------------------------------------------------------------------------- 1 | import {fetchGet} from "../../../utils/fetchUtil"; 2 | 3 | 4 | export const REQUEST_TOPIC_LIST = 'REQUEST_TOPIC_LIST'; 5 | export const RECEIVE_TOPIC_LIST = 'RECEIVE_TOPIC_LIST'; 6 | 7 | 8 | export const fetchTopicList = (tab, page) => (dispatch, getState) => { 9 | dispatch({ 10 | type: REQUEST_TOPIC_LIST, 11 | tab, 12 | page 13 | }); 14 | 15 | 16 | fetchGet('https://cnodejs.org/api/v1/topics', {tab, page}).then(data => { 17 | dispatch({ 18 | type: RECEIVE_TOPIC_LIST, 19 | data, 20 | }) 21 | }); 22 | 23 | }; -------------------------------------------------------------------------------- /client/page/indexSPA/configureStore.js: -------------------------------------------------------------------------------- 1 | import {createStore, applyMiddleware, compose} from 'redux'; 2 | import ReduxThunk from 'redux-thunk'; 3 | import rootReducer from './reducers'; 4 | 5 | export default function configureStore(initialState = {}) { 6 | const middlewares = [ReduxThunk]; 7 | 8 | let composeEnhancers = compose; 9 | if (__DEV__ && __CLIENT__) { 10 | middlewares.push(require('redux-immutable-state-invariant').default(), require('redux-logger').logger); 11 | 12 | if (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) { 13 | composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__; 14 | } 15 | } 16 | 17 | const storeEnhancers = composeEnhancers( 18 | applyMiddleware(...middlewares) 19 | ); 20 | 21 | let store = createStore(rootReducer, initialState, storeEnhancers); 22 | return store; 23 | } -------------------------------------------------------------------------------- /client/page/indexSPA/index.js: -------------------------------------------------------------------------------- 1 | import "../../css/index.css"; 2 | 3 | import React from "react"; 4 | import ReactDOM from "react-dom"; 5 | import {Provider} from 'react-redux'; 6 | import {LocaleProvider} from "antd"; 7 | // 由于 antd 组件的默认文案是英文,所以需要修改为中文 8 | import zhCN from "antd/lib/locale-provider/zh_CN"; 9 | import moment from "moment"; 10 | import "moment/locale/zh-cn"; 11 | import {BrowserRouter, Route, StaticRouter, NavLink, Switch} from "react-router-dom"; 12 | 13 | import MainLayout from "../../layout/MainLayout"; 14 | import TopicListConnect from './TopicListConnect'; 15 | import {topic} from '../../constant/config'; 16 | import configureStore from './configureStore'; 17 | import '../index.css'; 18 | 19 | moment.locale('zh-cn'); 20 | 21 | const Router = __CLIENT__ ? BrowserRouter : StaticRouter; 22 | 23 | class HomeSPA extends React.Component { 24 | constructor(props) { 25 | super(props); 26 | } 27 | 28 | render() { 29 | const props = this.props; 30 | return ( 31 | 32 | 33 | 34 |
35 | { 36 | Object.keys(topic).map((value) => 37 | 42 | {topic[value]} 43 | ) 44 | } 45 |
46 | 47 | 48 | 49 | 50 |
51 |
52 |
53 | ); 54 | } 55 | } 56 | 57 | if (__CLIENT__) { 58 | let initState = window.__INITIAL_STATE__ || {}; 59 | const store = configureStore(initState); 60 | ReactDOM.hydrate( 61 | 62 | 63 | , 64 | document.getElementById("react-container")); 65 | } 66 | 67 | export default HomeSPA; -------------------------------------------------------------------------------- /client/page/indexSPA/reducers/index.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux'; 2 | 3 | import {topic} from './topic'; 4 | 5 | const rootReducers = combineReducers({ 6 | topic, 7 | }); 8 | 9 | export default rootReducers; -------------------------------------------------------------------------------- /client/page/indexSPA/reducers/topic.js: -------------------------------------------------------------------------------- 1 | import {REQUEST_TOPIC_LIST, RECEIVE_TOPIC_LIST} from "../actions/topic"; 2 | 3 | const defaultState = { 4 | topic_list: null, 5 | page: 0, 6 | tab: null, 7 | } 8 | 9 | export function topic(state = defaultState, action) { 10 | switch (action.type) { 11 | case REQUEST_TOPIC_LIST: 12 | return { 13 | topic_list:null, 14 | page:action.page, 15 | tab:action.tab, 16 | }; 17 | case RECEIVE_TOPIC_LIST: 18 | return { 19 | ...state, 20 | topic_list:action.data 21 | }; 22 | default: 23 | return state; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /client/page/manager/indexView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class Manager extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | } 7 | 8 | render() { 9 | return ( 10 |
11 | 12 |
13 | ) 14 | } 15 | } -------------------------------------------------------------------------------- /client/page/manager/sagas.js: -------------------------------------------------------------------------------- 1 | export function* helloSaga() { 2 | 3 | } -------------------------------------------------------------------------------- /client/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import {createStore, applyMiddleware, compose} from 'redux'; 2 | import createSagaMiddleware from 'redux-saga'; 3 | 4 | 5 | export default function configureStore(rootReducer, initialState = {}) { 6 | 7 | const sagaMiddleware = createSagaMiddleware(); 8 | const middlewares = [sagaMiddleware]; 9 | if (process.env.NODE_ENV !== 'production') { 10 | middlewares.push(require('redux-immutable-state-invariant').default()); 11 | } 12 | const storeEnhancers = compose( 13 | applyMiddleware(...middlewares), 14 | (window && window.devToolsExtension) ? window.devToolsExtension() : f => f, 15 | ); 16 | 17 | let store = createStore(rootReducer, initialState, storeEnhancers); 18 | store.runSaga = sagaMiddleware.run; 19 | return store; 20 | } -------------------------------------------------------------------------------- /client/utils/common.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyanwuyan/react_ultimate_framework/5d67ce140ed555256bdffa150495cd1669860cfd/client/utils/common.js -------------------------------------------------------------------------------- /client/utils/fetchUtil.js: -------------------------------------------------------------------------------- 1 | export function fetchGet(url, query = {}, option = {}) { 2 | let isOk; 3 | let serializeQuery = serialize(query); 4 | let finalUrl = `${url}` + (serializeQuery ? `?${serializeQuery}` : ''); 5 | 6 | __DEV__ && console.log('%c start fetchGet: ' + finalUrl, 'color: green'); 7 | 8 | let headers = { 9 | // 'Content-Type': 'application/json;charset=utf-8' 10 | }; 11 | 12 | 13 | return new Promise((resolve, reject) => { 14 | fetch(finalUrl, { 15 | headers, 16 | }) 17 | .then((response) => { 18 | // token过期 ,没权限 19 | // if (response.status === 401) { 20 | // 21 | // } 22 | 23 | isOk = !!response.ok; 24 | const contentType = response.headers.get("content-type"); 25 | if (contentType && contentType.indexOf("application/json") !== -1) { 26 | return response.json(); 27 | } 28 | else { 29 | return response.text() 30 | } 31 | }) 32 | .then((responseData) => { 33 | if (isOk) { 34 | resolve(responseData); 35 | } else { 36 | reject(responseData); 37 | } 38 | }) 39 | .catch((error) => { 40 | reject(error); 41 | }); 42 | }); 43 | } 44 | 45 | 46 | export function fetchPost(url, data = {}, type = 'json') { 47 | let isOk; 48 | 49 | let headers = {}; 50 | 51 | 52 | if (type === 'json') { 53 | // headers.Accept = 'application/json'; 54 | headers['Content-Type'] = 'application/json'; 55 | } 56 | 57 | let finalUrl = `${url}`; 58 | 59 | __DEV__ && console.log('%c start fetchPost: ' + finalUrl, ' data: ', data, 'color: green'); 60 | return new Promise((resolve, reject) => { 61 | fetch(finalUrl, { 62 | method: 'POST', 63 | headers, 64 | body: JSON.stringify(data), 65 | }) 66 | .then((response) => { 67 | // token过期 ,没权限 68 | // if (response.status === 401) { 69 | // 70 | // } 71 | 72 | isOk = !!response.ok; 73 | const contentType = response.headers.get("content-type"); 74 | if (contentType && contentType.indexOf("application/json") !== -1) { 75 | return response.json(); 76 | } 77 | else { 78 | return response.text() 79 | } 80 | }) 81 | .then((responseData) => { 82 | if (isOk) { 83 | resolve(responseData); 84 | } else { 85 | reject(responseData); 86 | } 87 | }) 88 | .catch((error) => { 89 | reject(error); 90 | }); 91 | }); 92 | } 93 | 94 | export function fetchPut(url) { 95 | 96 | } 97 | 98 | export function fetchDelete(url, query = {}, option = {}) { 99 | } 100 | 101 | export function serialize(obj) { 102 | var str = []; 103 | for (var p in obj) 104 | if (obj.hasOwnProperty(p) && obj[p] !== undefined && obj[p] !== null) { 105 | str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); 106 | } 107 | return str.join("&"); 108 | } -------------------------------------------------------------------------------- /client/utils/profile.js: -------------------------------------------------------------------------------- 1 | // 保存用户的基本信息 2 | 3 | let userInfo = null; 4 | 5 | function get() { 6 | return userInfo; 7 | } 8 | 9 | function login(data) { 10 | userInfo = data; 11 | } 12 | 13 | function logout() { 14 | userInfo = null; 15 | } 16 | 17 | export default { 18 | get, login, logout 19 | } -------------------------------------------------------------------------------- /config/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dev_client: { 3 | // presets: ["es2015", "stage-0", "react", "react-hmre"], 4 | presets: ["es2015", "stage-0", "react"], 5 | "plugins": [ 6 | ["transform-decorators-legacy"], 7 | ["transform-runtime"], 8 | ["import", { 9 | "libraryName": "antd", 10 | "style": "css" 11 | }] 12 | ], 13 | // cacheDirectory: true, 14 | babelrc: false 15 | }, 16 | 17 | pro_client: { 18 | presets: ["es2015", "stage-0", "react"], 19 | "plugins": [ 20 | ["transform-decorators-legacy"], 21 | ["transform-runtime"], 22 | ["import", { 23 | "libraryName": "antd", 24 | "style": "css" 25 | }] 26 | ], 27 | babelrc: false 28 | }, 29 | dev_server: { 30 | presets: [ 31 | ["env", { 32 | "targets": {"node": "current"} 33 | }], 34 | ["stage-0"], 35 | ["react"] 36 | ], 37 | "plugins": [ 38 | ["transform-decorators-legacy"], 39 | ["transform-runtime"], 40 | ["import", { 41 | "libraryName": "antd" 42 | }] 43 | ], 44 | // cacheDirectory: true, 45 | babelrc: false 46 | }, 47 | pro_server: { 48 | presets: [ 49 | ["env", { 50 | "targets": {"node": "current"} 51 | }], 52 | ["stage-0"], 53 | ["react"] 54 | ], 55 | "plugins": [ 56 | ["transform-decorators-legacy"], 57 | ["transform-runtime"], 58 | ["import", { 59 | "libraryName": "antd" 60 | }] 61 | ], 62 | babelrc: false 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /config/webpack.common.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 提取webpack 开发和生成环境下公有的配置 3 | * 包括 handlebars生成配置 , 多页面entey入口配置 4 | * @type {{hbs_html_config: [*]}} 5 | */ 6 | const hotMiddlewareScript = 'webpack-hot-middleware/client?timeout=2000&reload=true&name=desktop'; // webpack-hot-middleware热更新需要添加到入口文件 7 | const entry = { 8 | vendor: ['react', 'react-dom'], 9 | home: ['./client/page/index.js'], 10 | homeSPA: ['./client/page/indexSPA/index.js'], 11 | }; 12 | 13 | let entry_dev = {}; 14 | Object.keys(entry).forEach(key => { 15 | if (key !== 'vendor') { 16 | entry_dev[key] = [hotMiddlewareScript].concat(entry[key]); 17 | } else { 18 | entry_dev[key] = entry[key]; 19 | } 20 | }) 21 | 22 | /** 23 | * webpack entry有几个,这里就有几个handlebars的配置 24 | * @type {{hbs_html_config: [*], entry_dev: {}, entry: {vendor: [*], home: [*], login: [*]}}} 25 | */ 26 | module.exports = { 27 | hbs_html_config: [ 28 | { 29 | template: './client/default.hbs', 30 | filename: 'home.hbs', 31 | chunks: ['manifest', 'vendor', 'home'] 32 | }, 33 | { 34 | template: './client/default.hbs', 35 | filename: 'homeSPA.hbs', 36 | chunks: ['manifest', 'vendor', 'homeSPA'] 37 | }, 38 | // { 39 | // template: './client/default.hbs', 40 | // filename: 'manager.hbs', 41 | // chunks: ['manifest', 'vendor', 'manager'] 42 | // }, 43 | // { 44 | // template: './client/default.hbs', 45 | // filename: 'bitChart.hbs', 46 | // chunks: ['manifest', 'vendor', 'bitChart'] 47 | // }, 48 | // { 49 | // template: './client/default.hbs', 50 | // filename: 'download.hbs', 51 | // chunks: ['manifest', 'vendor', 'download'] 52 | // } 53 | ], 54 | entry_dev, 55 | entry 56 | }; 57 | -------------------------------------------------------------------------------- /config/webpack_client_dev.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const ExtractTextPlugin = require("extract-text-webpack-plugin"); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const AssetsPlugin = require('assets-webpack-plugin'); 6 | const webpackCommon = require('./webpack.common.config'); 7 | const babelConfig = require("./babel.config").dev_client; 8 | const ROOT_PATH = process.cwd(); 9 | 10 | const extractCssPlugin = new ExtractTextPlugin({ 11 | filename: "css/[name].css", 12 | // disable:true 13 | }); 14 | 15 | var htmlPlugins = webpackCommon.hbs_html_config.map(v => 16 | new HtmlWebpackPlugin({ 17 | favicon: './client/assets/favicon.ico', //favicon路径 18 | inject: true, 19 | template: v.template, 20 | filename: v.filename, 21 | chunks: v.chunks 22 | } 23 | )); 24 | 25 | var plugins = [ 26 | new webpack.DefinePlugin({ 27 | __CLIENT__: true, 28 | __SERVER__: false, 29 | __PRODUCTION__: false, 30 | __DEV__: true, 31 | "process.env": { 32 | NODE_ENV: '"development"' 33 | }, 34 | }), 35 | new AssetsPlugin({filename: 'stats.generated.json', path: ROOT_PATH, prettyPrint: true}), 36 | extractCssPlugin, 37 | new webpack.optimize.CommonsChunkPlugin({ 38 | name: ['vendor', 'manifest'], 39 | filename: 'js/[name].js', 40 | minChunks: Infinity, 41 | }), 42 | new webpack.HotModuleReplacementPlugin(), 43 | new webpack.NoEmitOnErrorsPlugin(), 44 | ]; 45 | 46 | plugins = plugins.concat(htmlPlugins); 47 | 48 | module.exports = { 49 | name: 'desktop', 50 | entry: webpackCommon.entry_dev, 51 | output: { 52 | path: path.resolve(ROOT_PATH, './dist'), 53 | filename: 'js/[name].js', 54 | publicPath: "/" 55 | }, 56 | 57 | module: { 58 | rules: [ 59 | { 60 | test: /\.(js|jsx)$/, 61 | loader: 'babel-loader', 62 | exclude: /node_modules/, 63 | options: babelConfig 64 | }, 65 | { 66 | test: /\.(css|pcss)$/, 67 | use: extractCssPlugin.extract({ 68 | fallback: "style-loader", 69 | use: [ 70 | { 71 | loader: "css-loader", 72 | options: { 73 | sourceMap: true, 74 | importLoaders: 1, 75 | } 76 | }, { 77 | loader: 'postcss-loader', 78 | options: { 79 | sourceMap: true, 80 | plugins: () => [ 81 | require('postcss-import'), 82 | require('postcss-cssnext'), 83 | require('precss') 84 | ] 85 | } 86 | } 87 | ] 88 | }) 89 | }, 90 | { 91 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 92 | loader: 'url-loader', 93 | options: { 94 | limit: 7186, 95 | name: 'static/images/[name].[ext]' 96 | } 97 | }, 98 | { 99 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 100 | loader: 'url-loader', 101 | options: { 102 | limit: Infinity, 103 | name: 'static/fonts/[name].[ext]' 104 | } 105 | } 106 | ] 107 | }, 108 | 109 | plugins: plugins, 110 | devtool: 'source-map' 111 | } -------------------------------------------------------------------------------- /config/webpack_client_production.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const ExtractTextPlugin = require("extract-text-webpack-plugin"); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | const webpackCommon = require('./webpack.common.config'); 7 | const babelConfig = require("./babel.config").pro_client; 8 | 9 | const ROOT_PATH = process.cwd(); 10 | 11 | const extractCssPlugin = new ExtractTextPlugin({ 12 | filename: "css/[name].[contenthash].css" 13 | }); 14 | 15 | var htmlPlugins = webpackCommon.hbs_html_config.map(v => 16 | new HtmlWebpackPlugin({ 17 | favicon: './client/assets/favicon.ico', //favicon路径 18 | inject: true, 19 | template: v.template, 20 | filename: v.filename, 21 | chunks: v.chunks, 22 | minify: { //压缩HTML文件 23 | removeComments: true, //移除HTML中的注释 24 | collapseWhitespace: true //删除空白符与换行符 25 | } 26 | } 27 | )) 28 | 29 | var plugins = [ 30 | new webpack.DefinePlugin({ 31 | __CLIENT__: true, 32 | __SERVER__: false, 33 | __PRODUCTION__: true, 34 | __DEV__: false, 35 | 'process.env': { 36 | NODE_ENV: '"production"' 37 | } 38 | }), 39 | extractCssPlugin, 40 | new webpack.HashedModuleIdsPlugin(), 41 | new webpack.optimize.CommonsChunkPlugin({ 42 | name: ['vendor', 'manifest'], 43 | filename: 'js/[name].[chunkhash].js', 44 | minChunks: Infinity, 45 | }), 46 | new webpack.optimize.UglifyJsPlugin( 47 | { 48 | compress: {warnings: false, drop_console: true, collapse_vars: true,}, 49 | output: {comments: false}, 50 | beautify: false, 51 | comments: false 52 | } 53 | ) 54 | ]; 55 | 56 | plugins = plugins.concat(htmlPlugins); 57 | 58 | module.exports = { 59 | entry: webpackCommon.entry, 60 | output: { 61 | path: path.resolve(ROOT_PATH, './release/client'), 62 | filename: 'js/[name].[chunkhash].js', 63 | publicPath: "/" 64 | }, 65 | 66 | module: { 67 | rules: [ 68 | { 69 | test: /\.(js|jsx)$/, 70 | loader: 'babel-loader', 71 | exclude: /node_modules/, 72 | options: babelConfig 73 | }, 74 | { 75 | test: /\.(css|pcss)$/, 76 | use: extractCssPlugin.extract({ 77 | fallback: "style-loader", 78 | use: [{ 79 | loader: "css-loader", 80 | options: { 81 | minimize: true 82 | } 83 | }, { 84 | loader: 'postcss-loader', 85 | options: { 86 | plugins: () => [ 87 | require('postcss-import'), 88 | require('postcss-cssnext'), 89 | require('precss') 90 | ] 91 | } 92 | }] 93 | }) 94 | }, 95 | { 96 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 97 | loader: 'url-loader', 98 | options: { 99 | limit: 7186, 100 | name: 'static/images/[name].[hash].[ext]' 101 | } 102 | }, 103 | { 104 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 105 | loader: 'url-loader', 106 | options: { 107 | limit: Infinity, 108 | name: 'static/fonts/[name].[hash].[ext]' 109 | } 110 | } 111 | ] 112 | }, 113 | 114 | plugins: plugins 115 | } -------------------------------------------------------------------------------- /config/webpack_server_dev.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const nodeExternals = require('webpack-node-externals'); 5 | const babelConfig = require("./babel.config").dev_server; 6 | const ROOT_PATH = process.cwd(); 7 | 8 | module.exports = { 9 | entry: './server/index.js', 10 | output: { 11 | path: path.resolve(ROOT_PATH, './server_dist'), 12 | filename: 'server.js', 13 | publicPath: "/", 14 | libraryTarget: 'commonjs2' //导出成commonjs2规范的包 15 | }, 16 | 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.(js|jsx)$/, 21 | loader: 'babel-loader', 22 | options: babelConfig 23 | }, 24 | { 25 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 26 | loader: 'url-loader', 27 | options: { 28 | emitFile:false, 29 | limit: 7186, 30 | name: 'static/images/[name].[ext]' 31 | } 32 | }, 33 | { 34 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 35 | loader: 'url-loader', 36 | options: { 37 | emitFile:false, 38 | limit: Infinity, 39 | name: 'static/fonts/[name].[ext]' 40 | } 41 | }, 42 | { 43 | test: /\.(css|pcss|less)$/, 44 | loader: 'ignored-loader', 45 | } 46 | ] 47 | }, 48 | 49 | plugins: [ 50 | new webpack.DefinePlugin({ 51 | __CLIENT__: false, 52 | __SERVER__: true, 53 | __PRODUCTION__: false, 54 | __DEV__: true, 55 | "process.env": { 56 | NODE_ENV: '"development"', 57 | PORT: 8087 58 | }, 59 | }) 60 | ], 61 | target: 'node', // in order to ignore built-in modules like path, fs, etc. 62 | externals: [nodeExternals({whitelist:[/^antd/]})], // in order to ignore all modules in node_modules folder, 63 | node: { 64 | __filename: false, 65 | __dirname: false 66 | }, 67 | devtool: 'source-map' 68 | } -------------------------------------------------------------------------------- /config/webpack_server_production.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | var nodeExternals = require('webpack-node-externals'); 4 | var babelConfig = require("./babel.config").pro_server; 5 | const ROOT_PATH = process.cwd(); 6 | 7 | module.exports = { 8 | entry: './server/index.js', 9 | output: { 10 | path: path.resolve(ROOT_PATH, './release'), 11 | filename: 'server.js', 12 | publicPath: "/" 13 | }, 14 | 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.(js|jsx)$/, 19 | loader: 'babel-loader', 20 | options: babelConfig 21 | }, 22 | { 23 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 24 | loader: 'url-loader', 25 | options: { 26 | emitFile:false, 27 | limit: 7186, 28 | name: 'static/images/[name].[hash].[ext]' 29 | } 30 | }, 31 | { 32 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 33 | loader: 'url-loader', 34 | options: { 35 | emitFile:false, 36 | limit: Infinity, 37 | name: 'static/fonts/[name].[hash].[ext]' 38 | } 39 | }, 40 | { 41 | test: /\.(css|pcss|less)$/, 42 | loader: 'ignored-loader', 43 | } 44 | ] 45 | }, 46 | 47 | plugins: [ 48 | new webpack.DefinePlugin({ 49 | __CLIENT__: false, 50 | __SERVER__: true, 51 | __PRODUCTION__: true, 52 | __DEV__: false, 53 | "process.env": { 54 | NODE_ENV: '"production"', 55 | PORT: 8010 56 | }, 57 | }) 58 | ], 59 | target: 'node', // in order to ignore built-in modules like path, fs, etc. 60 | externals: [nodeExternals({whitelist:[/^antd/]})],//nodeModules, // in order to ignore all modules in node_modules folder, 61 | context: ROOT_PATH, 62 | node: { 63 | __filename: false, 64 | __dirname: false 65 | } 66 | } -------------------------------------------------------------------------------- /dev/index.js: -------------------------------------------------------------------------------- 1 | require('source-map-support').install({environment: 'node', entryOnly: false}); // 让node支持source-map 2 | const chalk = require('chalk'); 3 | 4 | const webpack = require('webpack'); 5 | const webpackServerDevConfig = require('../config/webpack_server_dev.config.js'); 6 | const webpackClientDevConfig = require('../config/webpack_client_dev.config.js'); 7 | const expressDevMiddleware = require('webpack-dev-middleware'); 8 | const expressHotMiddleware = require('webpack-hot-middleware'); 9 | const {destroyServer, restartServer} = require('./restartServer'); 10 | 11 | let clientCompiler = webpack(webpackClientDevConfig); 12 | 13 | clientCompiler.plugin("compile", stats => { 14 | console.log(chalk.yellow("client compiling.... ")); 15 | }); 16 | 17 | clientCompiler.plugin('done', stats => { 18 | console.log(chalk.green("client compile done!")); 19 | }); 20 | 21 | let webpackDevOptions = { 22 | // logLevel: 'warn', 23 | publicPath: webpackClientDevConfig.output.publicPath, 24 | stats: 'minimal', 25 | watchOptions: { 26 | aggregateTimeout: 400, // client 重新编译要晚于server,这个数值要略大点 27 | poll: 1000, 28 | ignored: /node_modules/ 29 | }, 30 | }; 31 | 32 | const devMidware = expressDevMiddleware(clientCompiler, webpackDevOptions); 33 | 34 | const hotMidware = expressHotMiddleware(clientCompiler); 35 | 36 | let serverCompiler = webpack(webpackServerDevConfig); 37 | 38 | /* 使用compiler.watch 启动时也会compile ,不需要serverCompiler.run */ 39 | serverCompiler.watch({ 40 | aggregateTimeout: 300, 41 | poll: 1000, 42 | ignored: /node_modules/ 43 | }, (err, stats) => { 44 | }); 45 | 46 | serverCompiler.plugin("compile", stats => { 47 | destroyServer(serverCompiler); 48 | console.log(chalk.yellow("server compiling.... ")); 49 | }); 50 | 51 | serverCompiler.plugin('done', stats => { 52 | console.log(chalk.blue("server compile done! ")); 53 | restartServer(serverCompiler, devMidware, hotMidware) 54 | }); 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /dev/restartServer.js: -------------------------------------------------------------------------------- 1 | // 这个模块只完成一件事,销毁server,重启server 2 | const chalk = require('chalk'); 3 | const path = require('path'); 4 | const c2k = require('koa2-connect'); // express middleware to koa2 5 | 6 | let pipePromise = Promise.resolve(); 7 | 8 | // Maintain a hash of all connected sockets 9 | let server = null, sockets = {}, nextSocketId = 0; 10 | 11 | function destroyServerAsync(serverCompiler) { 12 | return new Promise((resolve) => { 13 | // Make sure our newly built server bundles aren't in the module cache. 14 | Object.keys(require.cache).forEach((modulePath, index) => { 15 | if (modulePath.indexOf(serverCompiler.options.output.path) !== -1) { 16 | delete require.cache[modulePath]; 17 | } 18 | }); 19 | 20 | if (server) { 21 | // Destroy all open sockets 22 | for (var socketId in sockets) { 23 | sockets[socketId].destroy(); 24 | } 25 | server.close(function () { 26 | console.log('Server destroyed!!!'); 27 | server = null; 28 | resolve(); 29 | }); 30 | } else { 31 | resolve(); 32 | } 33 | }) 34 | } 35 | 36 | exports.destroyServer = function (serverCompiler) { 37 | pipePromise = pipePromise.then(() => { 38 | return destroyServerAsync(serverCompiler); 39 | }); 40 | } 41 | 42 | exports.restartServer = function (serverCompiler, devMidware, hotMidware) { 43 | pipePromise.then(() => { 44 | 45 | var bundlePath = path.join(serverCompiler.options.output.path, serverCompiler.options.output.filename); 46 | 47 | console.log("try require server entry ?"); 48 | 49 | let serverEntry; 50 | 51 | try { 52 | serverEntry = require(bundlePath).default; 53 | server = serverEntry(c2k(devMidware), c2k(hotMidware), devMidware); 54 | console.log("require server entry done!"); 55 | } catch (e) { 56 | console.log(chalk.red('require server entry error: ')); 57 | console.error(e); 58 | } 59 | 60 | //参考 shut down http server https://stackoverflow.com/questions/14626636/how-do-i-shutdown-a-node-js-https-server-immediately 61 | sockets = {}, nextSocketId = 0; 62 | server && server.on('connection', (socket) => { 63 | // Add a newly connected socket 64 | var socketId = nextSocketId++; 65 | sockets[socketId] = socket; 66 | 67 | // Remove the socket when it closes 68 | socket.on('close', () => { 69 | delete sockets[socketId]; 70 | }); 71 | 72 | }); 73 | }); 74 | } -------------------------------------------------------------------------------- /doc/flowImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyanwuyan/react_ultimate_framework/5d67ce140ed555256bdffa150495cd1669860cfd/doc/flowImg.png -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * Application configuration section 4 | * http://pm2.keymetrics.io/docs/usage/application-declaration/ 5 | */ 6 | apps: [ 7 | // First application 8 | { 9 | name: 'react_ultimate_framework', 10 | script: 'release/server.js', 11 | instances: 2, 12 | error_file: "./logs/app-err.log",//错误输出日志 13 | out_file: "./logs/app-out.log", //日志 14 | env: { 15 | COMMON_VARIABLE: 'true' 16 | }, 17 | env_production: { 18 | NODE_ENV: 'production' 19 | } 20 | } 21 | ], 22 | 23 | /** 24 | * Deployment section 25 | * http://pm2.keymetrics.io/docs/usage/deployment/ 26 | */ 27 | deploy: { 28 | production: { 29 | user: 'claude', 30 | host: [ 31 | { 32 | "host": "localhost", 33 | "port": "22" 34 | } 35 | ], 36 | ref: 'origin/master', 37 | repo: 'git@github.com:wuyanwuyan/react_ultimate_framework.git', 38 | path: '/Users/claude/Desktop/react_ultimate_framework', 39 | "post-setup": "ls -la", 40 | 'post-deploy': 'yarn install --production=false && npm run build && pm2 startOrRestart ecosystem.config.js --env production', 41 | env: { 42 | "NODE_ENV": "production" 43 | } 44 | }, 45 | dev: { 46 | user: 'claude', 47 | host: [ 48 | { 49 | "host": "localhost", 50 | "port": "22" 51 | } 52 | ], 53 | ref: 'origin/master', 54 | repo: 'git@github.com:wuyanwuyan/react_ultimate_framework.git', 55 | path: '/var/www/dev/react_ultimate_framework', 56 | 'post-deploy': 'yarn install --production=false && npm run build && pm2 reload ecosystem.config.js --env dev', 57 | env: { 58 | NODE_ENV: 'dev' 59 | } 60 | } 61 | } 62 | }; 63 | 64 | // 第一次部署,要 pm2 deploy ecosystem.config.js production setup 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react_ultimate_framework", 3 | "version": "1.0.0", 4 | "description": "react ultimate framework for fast start", 5 | "main": "index.js", 6 | "repository": "git@github.com:wuyanwuyan/react_ultimate_framework.git", 7 | "author": "claude ", 8 | "license": "MIT", 9 | "scripts": { 10 | "start": "node dev/index.js", 11 | "clean": "rm -rf ./dist && rm -rf ./server_dist && rm -rf ./release", 12 | "build": "npm run build_client_pro && npm run build_server_pro", 13 | "build_client_dev": "webpack --config=./config/webpack_client_dev.config.js && webpack --config=./config/webpack_client_dev_mobile.config.js", 14 | "build_server_dev": "webpack --display-error-details --config=./config/webpack_server_dev.config.js", 15 | "build_client_pro": "webpack --config=./config/webpack_client_production.config.js && webpack --config=./config/webpack_client_production_mobile.config.js", 16 | "build_server_pro": "webpack --config=./config/webpack_server_production.config.js", 17 | "eslint-checkAll": "eslint ./client --ext js && eslint ./server --ext js", 18 | "precommit": "lint-staged" 19 | }, 20 | "lint-staged": { 21 | "client/**/*.js": "eslint", 22 | "server/**/*.js": "eslint" 23 | }, 24 | "dependencies": { 25 | "antd": "^3.1.6", 26 | "classnames": "^2.2.5", 27 | "handlebars": "^4.0.10", 28 | "ignored-loader": "^0.0.1", 29 | "isomorphic-fetch": "^2.2.1", 30 | "koa": "^2.3.0", 31 | "koa-bodyparser": "^4.2.0", 32 | "koa-router": "^7.4.0", 33 | "koa-static": "^4.0.1", 34 | "koa2-connect": "^1.0.2", 35 | "moment": "^2.18.1", 36 | "react": "^16.2.0", 37 | "react-dom": "^16.2.0", 38 | "react-redux": "^5.0.6", 39 | "react-router": "^4.2.0", 40 | "react-router-dom": "^4.2.2", 41 | "redux": "^3.7.2", 42 | "redux-saga": "^0.16.0", 43 | "redux-thunk": "^2.2.0" 44 | }, 45 | "devDependencies": { 46 | "assets-webpack-plugin": "^3.5.1", 47 | "babel-cli": "^6.26.0", 48 | "babel-core": "^6.26.0", 49 | "babel-eslint": "^8.2.2", 50 | "babel-loader": "^7.1.2", 51 | "babel-plugin-import": "^1.6.3", 52 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 53 | "babel-plugin-transform-runtime": "^6.23.0", 54 | "babel-preset-env": "^1.6.0", 55 | "babel-preset-es2015": "^6.24.1", 56 | "babel-preset-react": "^6.24.1", 57 | "babel-preset-react-hmre": "^1.1.1", 58 | "babel-preset-stage-0": "^6.24.1", 59 | "chalk": "^2.1.0", 60 | "css-loader": "^0.28.5", 61 | "eslint": "^4.19.1", 62 | "eslint-plugin-react": "^7.7.0", 63 | "extract-text-webpack-plugin": "^3.0.0", 64 | "file-loader": "^1.1.6", 65 | "html-webpack-plugin": "^2.30.1", 66 | "husky": "^0.14.3", 67 | "ignore-loader": "^0.1.2", 68 | "lint-staged": "^7.0.3", 69 | "postcss-cssnext": "^3.0.2", 70 | "postcss-import": "^11.0.0", 71 | "postcss-loader": "^2.0.6", 72 | "precss": "^3.1.0", 73 | "redux-immutable-state-invariant": "^2.1.0", 74 | "redux-logger": "^3.0.6", 75 | "source-map-support": "^0.5.3", 76 | "style-loader": "^0.20.1", 77 | "url-loader": "^0.6.2", 78 | "webpack": "^3.5.5", 79 | "webpack-dev-middleware": "^2.0.4", 80 | "webpack-hot-middleware": "^2.18.2", 81 | "webpack-node-externals": "^1.6.0" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | import 'isomorphic-fetch'; 2 | import Koa from "koa"; 3 | import bodyParser from "koa-bodyparser"; 4 | import indexRoute from "./routes/index"; 5 | import http from "http"; 6 | 7 | export default function serverEntry(devMiddleware, hotMiddleware, devMidware) { 8 | const app = new Koa(); 9 | 10 | app.use(async function (ctx, next) { // error handle 11 | try { 12 | await next(); 13 | } catch (err) { 14 | ctx.status = err.status || 500; 15 | ctx.body = err.message; 16 | ctx.app.emit('error', err, ctx); 17 | } 18 | }); 19 | 20 | if (process.env.NODE_ENV !== "production") { 21 | app.use(devMiddleware); 22 | hotMiddleware && app.use(hotMiddleware); 23 | devMidware && require('./utils/serverRender').setCompiler(devMidware); 24 | } 25 | 26 | if (process.env.NODE_ENV === "production") { 27 | app.use(require('koa-static')(__dirname + '/client')); 28 | } 29 | 30 | app.use(bodyParser()); 31 | 32 | app.use(indexRoute.routes()); 33 | 34 | app.use(async (ctx) => { 35 | ctx.body = 404; 36 | }); 37 | 38 | const port = process.env.PORT || 8087; 39 | 40 | var server = http.createServer(app.callback()); 41 | 42 | server.listen(port, function (err) { 43 | if (err) { 44 | console.error(err); 45 | return; 46 | } 47 | console.log('✅ Server start success! Listening at http://localhost:%s \n------------------------------------------------------------', port); 48 | }); 49 | 50 | return server; 51 | } 52 | 53 | if (process.env.NODE_ENV === "production") { 54 | serverEntry(); 55 | } 56 | 57 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import koaRouter from "koa-router"; 3 | import {createStore} from "redux"; 4 | import {Provider} from "react-redux"; 5 | import {renderToString} from 'react-dom/server'; 6 | import {renderHbs} from "../utils/serverRender"; 7 | import Home from "../../client/page/index"; 8 | import HomeSPA from "../../client/page/indexSPA"; 9 | import {fetchGet} from '../../client/utils/fetchUtil'; 10 | import rootReducer from '../../client/page/indexSPA/reducers'; 11 | 12 | let router = new koaRouter(); 13 | 14 | router.get(['/indexSPA', '/indexSPA/:topic'], async (ctx) => { 15 | 16 | let page = ctx.query.page || 1; 17 | 18 | const topic_list = await fetchGet('https://cnodejs.org/api/v1/topics', {tab: ctx.params.topic, page}); 19 | 20 | let state = { 21 | topic:{ 22 | topic_list, 23 | tab: ctx.params.topic, 24 | page, 25 | } 26 | }; 27 | 28 | const store = createStore(rootReducer, state) 29 | 30 | ctx.body = await renderHbs('homeSPA.hbs', { 31 | content: renderToString( 32 | 33 | 34 | ), 35 | initialState: JSON.stringify(state), 36 | }) 37 | }); 38 | 39 | router.get(['/', '/:topic'], async (ctx) => { 40 | 41 | console.log(ctx.params.topic); 42 | let page = ctx.query.page || 1; 43 | 44 | const topic_list = await fetchGet('https://cnodejs.org/api/v1/topics', {tab: ctx.params.topic, page}); 45 | 46 | let state = { 47 | location: ctx.url, 48 | context: {}, 49 | topic_list, 50 | page 51 | }; 52 | ctx.body = await renderHbs('home.hbs', { 53 | content: renderToString(), 54 | initialState: JSON.stringify(state), 55 | }) 56 | }); 57 | 58 | 59 | export default router; -------------------------------------------------------------------------------- /server/utils/serverRender.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import React from 'react'; 4 | import {renderToString} from 'react-dom/server'; 5 | import Handlebars from 'handlebars'; 6 | 7 | let devMidware = null; 8 | let fsystem = fs; 9 | 10 | const readFileThunk = function (hbsName) { 11 | let filename; 12 | if (devMidware) { 13 | filename = devMidware.getFilenameFromUrl(`/${hbsName}`); 14 | } else { 15 | filename = path.join(__dirname, `/client/${hbsName}`) 16 | } 17 | 18 | return new Promise((resolve, reject) => { 19 | fsystem.readFile(filename, 'utf8', function (err, data) { 20 | if (err) { 21 | return reject(err); 22 | } 23 | resolve(data); 24 | }); 25 | }); 26 | } 27 | 28 | export function setCompiler(midware) { 29 | devMidware = midware; 30 | fsystem = devMidware.fileSystem; 31 | } 32 | 33 | let DEFAULT_STATE = { 34 | description: "服务器渲染", 35 | keywords: "react server side render,服务器渲染", 36 | title: "react server side render", 37 | content: "", 38 | initialState: "null", 39 | } 40 | 41 | export async function renderHbs(hbsName, data) { 42 | data = {...DEFAULT_STATE, ...data}; 43 | var source = await readFileThunk(hbsName); 44 | var template = Handlebars.compile(source); 45 | var result = template(data); 46 | return result; 47 | } -------------------------------------------------------------------------------- /stats.generated.json: -------------------------------------------------------------------------------- 1 | { 2 | "homeSPA": { 3 | "js": "/js/homeSPA.js", 4 | "css": "/css/homeSPA.css" 5 | }, 6 | "home": { 7 | "js": "/js/home.js", 8 | "css": "/css/home.css" 9 | }, 10 | "vendor": { 11 | "js": "/js/vendor.js" 12 | }, 13 | "manifest": { 14 | "js": "/js/manifest.js" 15 | } 16 | } --------------------------------------------------------------------------------