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