├── .eslintrc
├── .gitignore
├── .vscode
└── settings.json
├── README.md
├── babel.config.js
├── config
├── index.js
└── webpack
│ ├── client.base.js
│ ├── client.dev.js
│ ├── client.pro.js
│ ├── server.base.js
│ ├── server.dev.js
│ └── server.pro.js
├── jsconfig.json
├── package.json
├── scripts
└── start.js
├── src
├── app
│ ├── common
│ │ ├── ajax.js
│ │ ├── global-data.js
│ │ └── parse-url.js
│ ├── components
│ │ ├── footer
│ │ │ └── index.js
│ │ ├── head
│ │ │ ├── index.js
│ │ │ └── style.scss
│ │ ├── meta
│ │ │ └── index.js
│ │ ├── posts
│ │ │ └── list
│ │ │ │ ├── index.js
│ │ │ │ └── style.scss
│ │ ├── shell.js
│ │ └── ui
│ │ │ └── loading
│ │ │ ├── images
│ │ │ └── loading.gif
│ │ │ ├── index.js
│ │ │ └── style.scss
│ ├── init-data.js
│ ├── pages
│ │ ├── global.scss
│ │ ├── home
│ │ │ └── index.js
│ │ ├── not-found
│ │ │ └── index.js
│ │ ├── posts-detail
│ │ │ └── index.js
│ │ ├── sign-in
│ │ │ ├── images
│ │ │ │ └── react-icon.png
│ │ │ ├── index.js
│ │ │ └── style.scss
│ │ ├── topics
│ │ │ └── index.js
│ │ └── variables.scss
│ ├── router
│ │ ├── index.js
│ │ └── list.js
│ ├── static
│ │ └── img
│ │ │ └── favicon.png
│ ├── store
│ │ ├── actions
│ │ │ ├── posts.js
│ │ │ ├── scroll.js
│ │ │ └── user.js
│ │ ├── clone.js
│ │ ├── index.js
│ │ └── reducers
│ │ │ ├── index.js
│ │ │ ├── posts.js
│ │ │ ├── scroll.js
│ │ │ └── user.js
│ └── views
│ │ ├── index.html
│ │ └── index_dev.html
├── client
│ ├── index.js
│ └── service-worker.js
└── server
│ ├── cache.js
│ ├── index.js
│ ├── render.js
│ ├── server.js
│ └── sign.js
└── yarn.lock
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "standard",
4 | "eslint:recommended",
5 | "plugin:prettier/recommended",
6 | "plugin:react/recommended",
7 | "plugin:react-hooks/recommended",
8 | "prettier",
9 | "prettier/react",
10 | "prettier/babel",
11 | "prettier/standard"
12 | ],
13 | "globals": {
14 | "describe": true,
15 | "beforeEach": true,
16 | "afterEach": true,
17 | "after": true,
18 | "it": true,
19 | "ArriveFooter": true,
20 | "__CLIENT__": true,
21 | "__SERVER__": true,
22 | "adsbygoogle": true
23 | },
24 | "plugins": ["react-hooks", "react", "babel"],
25 | "parser": "babel-eslint",
26 | "env": {
27 | "es6": true,
28 | "browser": true,
29 | "node": true
30 | },
31 | "rules": {
32 | "no-console": "off",
33 | "no-debugger": "off",
34 | "no-unused-vars": 0,
35 | "react/jsx-uses-react": "error",
36 | "react/jsx-uses-vars": "error",
37 | "react/no-render-return-value": "off",
38 | "react-hooks/rules-of-hooks": "error",
39 | "react-hooks/exhaustive-deps": [
40 | "warn",
41 | {
42 | "additionalHooks": "(useMyCustomHook|useMyOtherCustomHook)"
43 | }
44 | ],
45 | "react/display-name": "off",
46 | "prettier/prettier": [
47 | "error",
48 | {
49 | "jsxSingleQuote": true,
50 | "singleQuote": true,
51 | "parser": "flow",
52 | "semi": false,
53 | "arrowParens": "avoid",
54 | "printWidth": 140,
55 | "trailingComma": "none"
56 | }
57 | ]
58 | },
59 | "parserOptions": {
60 | "ecmaVersion": 7,
61 | "sourceType": "module",
62 | "ecmaFeatures": {
63 | "jsx": true,
64 | "experimentalObjectRestSpread": true
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # Compiled binary addons (http://nodejs.org/api/addons.html)
20 | build/Release
21 |
22 | # Dependency directory
23 | # Deployed apps should consider commenting this line out:
24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
25 | node_modules
26 |
27 | https
28 | dist
29 | *.DS_Store
30 | package-lock.json
31 | yarn.lock
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.tabSize": 2,
3 | "editor.formatOnSave": true,
4 | "editor.defaultFormatter": "esbenp.prettier-vscode",
5 | "editor.codeActionsOnSave": {
6 | // For ESLint
7 | "source.fixAll.eslint": true,
8 | // For TSLint
9 | "source.fixAll.tslint": true,
10 | // For Stylelint
11 | "source.fixAll.stylelint": true
12 | },
13 | "files.associations": {
14 | "*.js": "javascript"
15 | },
16 | "emmet.triggerExpansionOnTab": true,
17 | "eslint.options": {
18 | "extensions": [".js", ".jsx"],
19 | "plugins": ["html"]
20 | },
21 | "javascript.implicitProjectConfig.experimentalDecorators": true,
22 | "javascript.updateImportsOnFileMove.enabled": "always",
23 | "javascript.suggestionActions.enabled": false,
24 | "javascript.validate.enable": false,
25 | "prettier.jsxSingleQuote": true,
26 | "prettier.jsxBracketSameLine": false,
27 | "prettier.disableLanguages": ["react"],
28 | "prettier.printWidth": 140,
29 | "prettier.singleQuote": true,
30 | "prettier.semi": false,
31 | "prettier.arrowParens": "avoid",
32 | "prettier.trailingComma": "none",
33 | "vsicons.dontShowNewVersionMessage": true,
34 | "window.zoomLevel": 1,
35 | "workbench.iconTheme": "vscode-icons"
36 | }
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ⚛️ React 同构脚手架
2 |
3 | Web 前端世界日新月异变化太快,为了让自己跟上节奏不掉队,总结出了自己的一套 React 脚手架,方便日后项目可以基于此快速上手开发。
4 |
5 | ## 特点
6 |
7 | - 🖥 支持首屏服务端渲染,支持 SEO
8 | - ✂️ 按页面将代码分片,然后按需加载
9 | - 🌈 支持 CSS Modules,避免 CSS 全局污染
10 | - ⚙️ 支持流行 UI 框架 Bootstrap 4
11 | - 🔄 开发环境支持热更新
12 | - 🎛 内置登录、退出、页面权限控制、帖子列表获取、帖子详情获取等功能
13 | - 🚧 内置用户访问页面时,301、404 状态相应的处理逻辑
14 |
15 | ## 开始
16 |
17 | **_没有在 windows 机器上测试过,可能会报错_**
18 |
19 | ```
20 | $ git clone git@github.com:54sword/react-starter.git
21 | $ cd react-starter
22 | $ npm install or yarn
23 | $ npm run start or yarn start
24 | ```
25 |
26 | 浏览器打开 [http://localhost:4000](http://localhost:4000)
27 |
28 | ## 相关命令说明
29 |
30 | ### 开发环境
31 |
32 | ```
33 | npm run start org yarn start
34 | ```
35 |
36 | ### 生产环境测试
37 |
38 | ```
39 | npm run pro or yarn pro
40 | ```
41 |
42 | ### 查看包大小
43 |
44 | ```
45 | npm run analyzer or yarn analyzer
46 | ```
47 |
48 | ## 部署到服务器
49 |
50 | 1、打包项目
51 |
52 | ```
53 | npm run dist or yarn dist
54 | ```
55 |
56 | 2、将项目上传至你的服务器
57 | 3、启动服务
58 |
59 | Node 启动服务
60 |
61 | ```
62 | node ./dist/server/server.js
63 | ```
64 |
65 | 或使用 pm2 启动服务
66 |
67 | ```
68 | pm2 start ./dist/server/server.js --name "react-starter" --max-memory-restart 400M
69 | ```
70 |
71 | ## 更新
72 |
73 | ### 2020 年 10 月 13 日
74 |
75 | - 所有页面使用 `hooks` 重构
76 | - 优化 `redux` 写法
77 | - 所有依赖包升级到最新,除 `webpack` 没有升级到 5.0
78 | - `react-loadable` 替换成 `@loadable/component`
79 | - 增加了 `react-hot-loader` react 热更新
80 | - 增加了全局 CSS 变量 `additionalData: '@import "~@/pages/variables.scss";'`,见 webpack 配置文件
81 | - 更多自己 clone 下来和老的对比下。
82 |
83 | ### 2020 年 08 月 31 日
84 |
85 | - `progress-bar-webpack-plugin`替换成`WebpackBar`
86 | - `node-sass`替换成`dart-sass`
87 |
88 | ### 2018 年 12 月 7 日
89 |
90 | - 增加 webpack-bundle-analyzer 查看模块大小
91 | - 增加 postcss 的 autoprefixer 浏览器前缀的插件
92 | - 增加 webpack aliases 别名 @ = 指向 src 目录,Config = 指向 config/index
93 | - 增加 progress-bar-webpack-plugin 打包进度
94 | - 把 actions 和 reducers 放入 store 目录,统一管理
95 | - 升级 react-css-modules 为 babel-plugin-react-css-modules 简化 CSSmodules
96 | - 更新前
97 |
98 | ```
99 | import CSSModules from 'react-css-modules';
100 | import styles from './style.scss';
101 | @CSSModules(styles)
102 | ```
103 |
104 | - 使用方法
105 |
106 | ```
107 |
108 | ```
109 |
110 | - 更新后
111 |
112 | ```
113 | import './style.scss';
114 | ```
115 |
116 | - 使用方法一样
117 |
118 | ### 2018 年 10 月 7 日
119 |
120 | - 升级 webpack 4,以及 webpack 配置优化
121 | - 升级 babel 7
122 | - 升级 React 以及相关依赖到最新版本
123 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | var config = require('./config')
2 |
3 | module.exports = function (api) {
4 | api.cache.forever()
5 |
6 | return {
7 | presets: [
8 | '@babel/preset-env',
9 | '@babel/preset-react',
10 | '@babel/preset-flow'
11 | // '@babel/preset-typescript'
12 | ],
13 | plugins: [
14 | ['@babel/plugin-proposal-decorators', { legacy: true }],
15 | '@babel/plugin-proposal-class-properties',
16 | '@babel/plugin-syntax-dynamic-import',
17 | '@babel/plugin-transform-modules-commonjs',
18 | '@babel/plugin-proposal-object-rest-spread',
19 | '@babel/plugin-proposal-export-namespace-from',
20 | 'react-hot-loader/babel',
21 | '@loadable/babel-plugin',
22 | [
23 | '@dr.pogodin/react-css-modules',
24 | {
25 | exclude: 'node_modules',
26 | generateScopedName: config.class_scoped_name,
27 | webpackHotModuleReloading: true,
28 | filetypes: {
29 | '.scss': {
30 | syntax: 'postcss-scss',
31 | plugins: ['postcss-nested']
32 | }
33 | }
34 | }
35 | ]
36 | ]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
1 | // 生产环境配置
2 | let config = {
3 | // 正式环境
4 | debug: false,
5 |
6 | // 域名
7 | host: 'localhost',
8 |
9 | // 服务端口
10 | port: 6666,
11 |
12 | cookie_prefix: 'auth',
13 |
14 | cache_time: 300000,
15 |
16 | // 登录token,cookie 的名称
17 | auth_cookie_name: 'signin-cookie',
18 |
19 | // https://github.com/css-modules/css-modules
20 | class_scoped_name: '[hash:base64:8]',
21 |
22 | // 前端打包后,静态资源路径前缀
23 | // 生成效果如://localhost:4000/app.bundle.js
24 | public_path: '//localhost:4000',
25 |
26 | name: 'React 同构脚手架', // 网站标题
27 |
28 | favicon: '',
29 |
30 | // 添加内容到模版的head中
31 | head: `
32 |
33 |
34 |
35 | `
36 | }
37 |
38 | config.head += config.favicon
39 |
40 | // 开发环境配置
41 | if (process.env.NODE_ENV == 'development') {
42 | config.debug = true
43 | config.port = 5000
44 | config.class_scoped_name = '[name]_[local]__[hash:base64:5]'
45 | }
46 |
47 | module.exports = config
48 |
--------------------------------------------------------------------------------
/config/webpack/client.base.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack')
2 | const path = require('path')
3 | const chalk = require('chalk')
4 | const HtmlwebpackPlugin = require('html-webpack-plugin')
5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
6 | const TerserPlugin = require('terser-webpack-plugin')
7 | const WebpackBar = require('webpackbar')
8 | const LoadablePlugin = require('@loadable/webpack-plugin')
9 |
10 | const config = require('../index')
11 | const devMode = process.env.NODE_ENV === 'development'
12 |
13 | /**
14 | * 配置 autoprefixer 各浏览器前缀
15 | * postcss-flexbugs-fixes 检查flex错误
16 | * */
17 | const postcssConfig = {
18 | loader: 'postcss-loader',
19 | options: {
20 | postcssOptions: {
21 | plugins: ['postcss-flexbugs-fixes', 'autoprefixer']
22 | }
23 | }
24 | }
25 |
26 | module.exports = {
27 | name: 'client',
28 | target: 'web',
29 |
30 | resolve: {
31 | extensions: ['.ts', '.tsx', '.js'],
32 | alias: {
33 | '@': path.resolve('src/app'),
34 | 'react-dom': '@hot-loader/react-dom',
35 | Config: path.resolve('config/index')
36 | }
37 | },
38 |
39 | entry: {
40 | app: ['@babel/polyfill', './src/client/index']
41 | },
42 |
43 | output: {
44 | path: path.resolve(__dirname, '../../dist/client'),
45 | filename: devMode ? '[name].bundle.js' : '[name].[hash].js',
46 | publicPath: config.public_path + '/'
47 | },
48 |
49 | resolveLoader: {
50 | moduleExtensions: ['-loader']
51 | },
52 |
53 | optimization: {
54 | // namedModules: true,
55 | // noEmitOnErrors: true,
56 | minimize: !devMode,
57 | minimizer: [
58 | new TerserPlugin({
59 | cache: true,
60 | parallel: true,
61 | sourceMap: true,
62 | terserOptions: {
63 | compress: {
64 | // 关键代码
65 | warnings: true,
66 | drop_debugger: true,
67 | drop_console: true
68 | }
69 | }
70 | })
71 | ],
72 | splitChunks: {
73 | cacheGroups: {
74 | styles: {
75 | name: 'styles',
76 | test: /(\.css|\.scss)$/,
77 | chunks: 'all',
78 | enforce: true
79 | },
80 | commons: {
81 | name: 'vendor',
82 | test: /[\\/]node_modules[\\/]/,
83 | chunks: 'all'
84 | }
85 | }
86 | }
87 | },
88 |
89 | module: {
90 | rules: [
91 | // js 文件解析
92 | {
93 | test: /\.js$/i,
94 | exclude: /node_modules/,
95 | loader: 'babel'
96 | },
97 | {
98 | test: /\.scss$/,
99 | // test: /\.(sa|sc|c)ss$/,
100 | use: [
101 | 'css-hot',
102 | {
103 | loader: MiniCssExtractPlugin.loader
104 | },
105 | {
106 | loader: `css`,
107 | options: {
108 | modules: {
109 | localIdentName: config.class_scoped_name
110 | },
111 | sourceMap: true,
112 | importLoaders: 1
113 | }
114 | },
115 | {
116 | loader: `sass`,
117 | options: {
118 | // Prefer `dart-sass`
119 | implementation: require('sass'),
120 | sassOptions: {
121 | fiber: false
122 | },
123 | additionalData: '@import "~@/pages/variables.scss";'
124 | }
125 | },
126 | { ...postcssConfig }
127 | ]
128 | },
129 |
130 | // css 解析
131 | {
132 | test: /\.css$/,
133 | use: [
134 | 'css-hot',
135 | {
136 | loader: MiniCssExtractPlugin.loader
137 | },
138 | {
139 | loader: `css`
140 | },
141 | { ...postcssConfig }
142 | ]
143 | },
144 |
145 | // 小于8K的图片,转 base64
146 | {
147 | test: /\.(png|jpe?g|gif|bmp|svg)$/,
148 | use: [
149 | {
150 | loader: 'url',
151 | options: {
152 | // 配置图片编译路径
153 | limit: 8192, // 小于8k将图片转换成base64
154 | name: '[name].[hash:8].[ext]',
155 | outputPath: 'images/'
156 | }
157 | }
158 | ]
159 | },
160 | {
161 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
162 | loader: 'url',
163 | options: {
164 | limit: 8192,
165 | name: 'fonts/[name].[hash:8].[ext]'
166 | }
167 | }
168 | ]
169 | },
170 |
171 | plugins: [
172 | new webpack.DefinePlugin({
173 | __SERVER__: 'false',
174 | __CLIENT__: 'true'
175 | }),
176 |
177 | // 提取css插件
178 | new MiniCssExtractPlugin({
179 | filename: devMode ? '[name].css' : '[name].[hash].css',
180 | allChunks: true,
181 | ignoreOrder: true
182 | }),
183 |
184 | new WebpackBar(),
185 | new LoadablePlugin(),
186 |
187 | // 创建视图模版文件,给server使用
188 | // 主要是打包后的添加的css、js静态文件路径添加到模版中
189 | new HtmlwebpackPlugin({
190 | filename: path.resolve(__dirname, '../../dist/server/index.ejs'),
191 | template: `src/app/views/index${devMode ? '_dev' : ''}.html`,
192 | metaDom: '<%- meta %>',
193 | htmlDom: '<%- html %>',
194 | reduxState: '<%- reduxState %>',
195 | head: config.head
196 | // inject: false
197 | })
198 | ]
199 | }
200 |
--------------------------------------------------------------------------------
/config/webpack/client.dev.js:
--------------------------------------------------------------------------------
1 | const baseConfig = require('./client.base')
2 | const webpack = require('webpack')
3 | const WriteFileWebpackPlugin = require('write-file-webpack-plugin')
4 |
5 | const config = {
6 | mode: 'development',
7 | ...baseConfig,
8 | plugins: [new WriteFileWebpackPlugin(), new webpack.HotModuleReplacementPlugin(), ...baseConfig.plugins],
9 | devtool: 'cheap-module-inline-source-map'
10 | }
11 |
12 | module.exports = config
13 |
--------------------------------------------------------------------------------
/config/webpack/client.pro.js:
--------------------------------------------------------------------------------
1 | const baseConfig = require('./client.base')
2 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
3 | const OfflinePlugin = require('offline-plugin')
4 | const CopyPlugin = require('copy-webpack-plugin')
5 | const { CleanWebpackPlugin } = require('clean-webpack-plugin')
6 |
7 | const webpackConfig = {
8 | mode: 'production',
9 | ...baseConfig,
10 | plugins: [
11 | // 清空打包目录
12 | new CleanWebpackPlugin(),
13 | new CopyPlugin({
14 | patterns: [{ from: 'src/static/img/favicon.png', to: 'favicon.png' }]
15 | }),
16 |
17 | new OptimizeCSSAssetsPlugin({
18 | assetNameRegExp: /\.css\.*(?!.*map)/g, // 注意不要写成 /\.css$/g
19 | cssProcessor: require('cssnano'),
20 | cssProcessorOptions: {
21 | discardComments: {
22 | removeAll: true
23 | },
24 | // 避免 cssnano 重新计算 z-index
25 | safe: true,
26 | // cssnano 集成了autoprefixer的功能
27 | // 会使用到autoprefixer进行无关前缀的清理
28 | // 关闭autoprefixer功能
29 | // 使用postcss的autoprefixer功能
30 | autoprefixer: false
31 | },
32 | canPrint: true
33 | }),
34 | new OfflinePlugin({
35 | autoUpdate: 1000 * 60 * 5,
36 | ServiceWorker: {
37 | publicPath: '/sw.js'
38 | },
39 | // 排除不需要缓存的文件
40 | excludes: ['../server/index.ejs']
41 | }),
42 | ...baseConfig.plugins
43 | ]
44 | }
45 |
46 | module.exports = webpackConfig
47 |
--------------------------------------------------------------------------------
/config/webpack/server.base.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack')
2 | const path = require('path')
3 | const chalk = require('chalk')
4 | const nodeExternals = require('webpack-node-externals')
5 | const WebpackBar = require('webpackbar')
6 | const TerserPlugin = require('terser-webpack-plugin')
7 | const LoadablePlugin = require('@loadable/webpack-plugin')
8 |
9 | const config = require('../index')
10 | const devMode = process.env.NODE_ENV === 'development'
11 |
12 | module.exports = {
13 | name: 'server',
14 | target: 'node',
15 |
16 | resolve: {
17 | extensions: ['.ts', '.tsx', '.js'],
18 | alias: {
19 | '@': path.resolve('src/app'),
20 | 'react-dom': '@hot-loader/react-dom',
21 | Config: path.resolve('config/index')
22 | }
23 | },
24 |
25 | entry: {
26 | app: ['./src/server/index']
27 | },
28 |
29 | externals: [
30 | nodeExternals({
31 | // we still want imported css from external files to be bundled otherwise 3rd party packages
32 | // which require us to include their own css would not work properly
33 | allowlist: /\.css$/
34 | })
35 | ],
36 |
37 | output: {
38 | path: path.resolve(__dirname, '../../dist/server'),
39 | filename: 'server.js',
40 | publicPath: config.public_path + '/'
41 | },
42 |
43 | resolveLoader: {
44 | moduleExtensions: ['-loader']
45 | },
46 |
47 | optimization: {
48 | minimize: !devMode,
49 | minimizer: [
50 | new TerserPlugin({
51 | cache: true,
52 | parallel: true,
53 | sourceMap: true,
54 | terserOptions: {
55 | compress: {
56 | // 关键代码
57 | warnings: true,
58 | drop_debugger: true,
59 | drop_console: true
60 | }
61 | }
62 | })
63 | ]
64 | },
65 |
66 | module: {
67 | rules: [
68 | // js 文件解析
69 | {
70 | test: /\.js$/i,
71 | exclude: /node_modules/,
72 | loader: 'babel'
73 | },
74 |
75 | // scss 文件解析
76 | {
77 | test: /\.scss$/,
78 | // test: /\.(sa|sc|c)ss$/,
79 | use: [
80 | {
81 | loader: `css`,
82 | options: {
83 | modules: {
84 | localIdentName: config.class_scoped_name,
85 | exportOnlyLocals: true
86 | }
87 | // onlyLocals: true // 只映射,不打包CSS
88 | }
89 | },
90 | {
91 | loader: `sass`,
92 | options: {
93 | // Prefer `dart-sass`
94 | implementation: require('sass'),
95 | sassOptions: {
96 | fiber: false
97 | },
98 | additionalData: '@import "~@/pages/variables.scss";'
99 | }
100 | }
101 | ]
102 | },
103 |
104 | // css 文件解析
105 | {
106 | test: /\.css$/,
107 | use: [
108 | {
109 | loader: `css`,
110 | options: {
111 | modules: {
112 | exportOnlyLocals: true // 只映射,不打包CSS
113 | }
114 | }
115 | }
116 | ]
117 | }
118 | ]
119 | },
120 |
121 | plugins: [
122 | new webpack.DefinePlugin({
123 | __SERVER__: 'true',
124 | __CLIENT__: 'false'
125 | }),
126 | new WebpackBar(),
127 | new LoadablePlugin()
128 | ]
129 | }
130 |
--------------------------------------------------------------------------------
/config/webpack/server.dev.js:
--------------------------------------------------------------------------------
1 | const baseConfig = require('./server.base')
2 | const webpack = require('webpack')
3 | const WriteFileWebpackPlugin = require('write-file-webpack-plugin')
4 |
5 | const config = {
6 | mode: 'development',
7 | ...baseConfig,
8 | plugins: [new WriteFileWebpackPlugin(), ...baseConfig.plugins, new webpack.HotModuleReplacementPlugin()]
9 | }
10 |
11 | module.exports = config
12 |
--------------------------------------------------------------------------------
/config/webpack/server.pro.js:
--------------------------------------------------------------------------------
1 | const baseConfig = require('./server.base')
2 | const { CleanWebpackPlugin } = require('clean-webpack-plugin')
3 |
4 | const config = {
5 | mode: 'production',
6 | ...baseConfig,
7 | plugins: [
8 | ...baseConfig.plugins,
9 | // 清空打包目录
10 | new CleanWebpackPlugin()
11 | ]
12 | }
13 |
14 | module.exports = config
15 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "experimentalDecorators": true,
5 | "paths": {
6 | "@/*": ["src/app/*"],
7 | "Config": ["./config/index"]
8 | }
9 | },
10 | "exclude": ["node_modules"],
11 | "include": ["src", "Config"]
12 | }
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-starter",
3 | "version": "2.1.0",
4 | "description": "React 同构脚手架",
5 | "scripts": {
6 | "start": "cross-env NODE_ENV=development node ./scripts/start.js",
7 | "dev": "cross-env NODE_ENV=development webpack --config config/webpack/server.dev.js && NODE_ENV=development webpack --config config/webpack/client.dev.js && node ./dist/server/server.js",
8 | "pro": "cross-env NODE_ENV=production webpack --config config/webpack/server.pro.js && NODE_ENV=production webpack --config config/webpack/client.pro.js && node ./dist/server/server.js",
9 | "dist": "cross-env NODE_ENV=production webpack --config config/webpack/server.pro.js && NODE_ENV=production webpack --config config/webpack/client.pro.js",
10 | "analyzer": "npm run dist",
11 | "server": "cross-env node ./dist/server/server.js"
12 | },
13 | "engines": {
14 | "node": ">=8.6.0"
15 | },
16 | "author": "吴世剑 <54sword@163.com>",
17 | "license": "MIT",
18 | "dependencies": {
19 | "@hot-loader/react-dom": "^17.0.0-rc.2",
20 | "@loadable/component": "^5.13.2",
21 | "@loadable/server": "^5.13.2",
22 | "axios": "^0.20.0",
23 | "body-parser": "^1.19.0",
24 | "bootstrap": "^4.5.2",
25 | "compression": "^1.7.4",
26 | "cookie-parser": "^1.4.5",
27 | "ejs": "^3.1.5",
28 | "express": "^4.17.1",
29 | "helmet": "^4.1.0",
30 | "jquery": "^3.5.1",
31 | "lru-cache": "^6.0.0",
32 | "offline-plugin": "^5.0.7",
33 | "popper.js": "^1.16.1",
34 | "react": "^16.13.1",
35 | "react-dom": "^16.13.1",
36 | "react-loadable": "^5.5.0",
37 | "react-meta-tags": "^0.7.4",
38 | "react-redux": "^7.2.1",
39 | "react-router": "^5.2.0",
40 | "react-router-dom": "^5.2.0",
41 | "redux": "^4.0.5",
42 | "redux-logger": "^3.0.6",
43 | "redux-thunk": "^2.3.0"
44 | },
45 | "devDependencies": {
46 | "@babel/core": "^7.11.6",
47 | "@babel/plugin-proposal-class-properties": "^7.10.4",
48 | "@babel/plugin-proposal-decorators": "^7.10.5",
49 | "@babel/plugin-proposal-export-namespace-from": "^7.10.4",
50 | "@babel/plugin-syntax-dynamic-import": "^7.8.3",
51 | "@babel/polyfill": "^7.11.5",
52 | "@babel/preset-env": "^7.11.5",
53 | "@babel/preset-flow": "^7.10.4",
54 | "@babel/preset-react": "^7.10.4",
55 | "@babel/preset-typescript": "^7.10.4",
56 | "@dr.pogodin/babel-plugin-react-css-modules": "^6.0.6",
57 | "@loadable/babel-plugin": "^5.13.2",
58 | "@loadable/webpack-plugin": "^5.13.0",
59 | "autoprefixer": "^10.0.1",
60 | "babel-eslint": "^10.1.0",
61 | "babel-loader": "^8.1.0",
62 | "babel-plugin-react-css-modules": "^5.2.6",
63 | "clean-webpack-plugin": "^3.0.0",
64 | "copy-webpack-plugin": "^6.2.1",
65 | "cross-env": "^7.0.2",
66 | "css-hot-loader": "^1.4.4",
67 | "css-loader": "^4.3.0",
68 | "eslint-config-prettier": "^6.12.0",
69 | "eslint-config-standard": "^14.1.1",
70 | "eslint-plugin-babel": "^5.3.1",
71 | "eslint-plugin-html": "^6.1.0",
72 | "eslint-plugin-import": "^2.22.1",
73 | "eslint-plugin-node": "^11.1.0",
74 | "eslint-plugin-prettier": "^3.1.4",
75 | "eslint-plugin-promise": "^4.2.1",
76 | "eslint-plugin-react": "^7.21.4",
77 | "eslint-plugin-react-hooks": "^4.1.2",
78 | "eslint-plugin-standard": "^4.0.1",
79 | "fibers": "^5.0.0",
80 | "file-loader": "^6.1.1",
81 | "html-webpack-plugin": "^4.5.0",
82 | "mini-css-extract-plugin": "^1.0.0",
83 | "nodemon": "^2.0.4",
84 | "optimize-css-assets-webpack-plugin": "^5.0.4",
85 | "postcss": "^8.1.1",
86 | "postcss-flexbugs-fixes": "^4.2.1",
87 | "postcss-loader": "^4.0.4",
88 | "postcss-nested": "^5.0.1",
89 | "postcss-scss": "^3.0.2",
90 | "prettier-eslint": "^11.0.0",
91 | "react-hot-loader": "^4.13.0",
92 | "redux-logger": "^3.0.6",
93 | "rimraf": "^3.0.2",
94 | "sass": "^1.27.0",
95 | "sass-loader": "^10.0.3",
96 | "terser-webpack-plugin": "^4.2.3",
97 | "url-loader": "^4.1.1",
98 | "webpack": "^4.44.2",
99 | "webpack-bundle-analyzer": "^3.9.0",
100 | "webpack-cli": "^3.3.12",
101 | "webpack-dev-server": "^3.11.0",
102 | "webpack-hot-middleware": "^2.25.0",
103 | "webpack-node-externals": "^2.5.2",
104 | "webpackbar": "^4.0.0",
105 | "write-file-webpack-plugin": "^4.5.1"
106 | },
107 | "browserslist": [
108 | "> 1%",
109 | "last 2 versions",
110 | "not ie <= 10"
111 | ]
112 | }
113 |
--------------------------------------------------------------------------------
/scripts/start.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack')
2 | const express = require('express')
3 | const nodemon = require('nodemon')
4 | const rimraf = require('rimraf')
5 | const webpackDevMiddleware = require('webpack-dev-middleware')
6 | const webpackHotMiddleware = require('webpack-hot-middleware')
7 |
8 | const clientConfig = require('../config/webpack/client.dev')
9 | const serverConfig = require('../config/webpack/server.dev')
10 |
11 | const config = require('../config')
12 |
13 | const compilerPromise = compiler => {
14 | return new Promise((resolve, reject) => {
15 | compiler.plugin('done', stats => {
16 | if (!stats.hasErrors()) {
17 | return resolve()
18 | }
19 | // eslint-disable-next-line prefer-promise-reject-errors
20 | return reject('Compilation failed')
21 | })
22 | }).catch(function (reason) {
23 | console.log('errcatch:', reason)
24 | })
25 | }
26 |
27 | const app = express()
28 | const WEBPACK_PORT = config.port + 1
29 |
30 | const start = async () => {
31 | rimraf.sync('./dist')
32 |
33 | let publicPath = config.public_path.split(':')
34 |
35 | publicPath.pop()
36 |
37 | publicPath = publicPath.join(':')
38 |
39 | clientConfig.entry.app.unshift(`webpack-hot-middleware/client?path=${publicPath}:${WEBPACK_PORT}/__webpack_hmr`)
40 |
41 | clientConfig.output.hotUpdateMainFilename = `[hash].hot-update.json`
42 | clientConfig.output.hotUpdateChunkFilename = `[id].[hash].hot-update.js`
43 |
44 | clientConfig.output.publicPath = `${publicPath}:${WEBPACK_PORT}/`
45 | serverConfig.output.publicPath = `${publicPath}:${WEBPACK_PORT}/`
46 |
47 | const clientCompiler = webpack([clientConfig, serverConfig])
48 |
49 | const _clientCompiler = clientCompiler.compilers[0]
50 | const _serverCompiler = clientCompiler.compilers[1]
51 |
52 | const clientPromise = compilerPromise(_clientCompiler)
53 | const serverPromise = compilerPromise(_serverCompiler)
54 |
55 | app.use((req, res, next) => {
56 | res.header('Access-Control-Allow-Origin', '*')
57 | return next()
58 | })
59 |
60 | app.use(
61 | webpackDevMiddleware(_clientCompiler, {
62 | publicPath: clientConfig.output.publicPath
63 | })
64 | )
65 |
66 | // 客户端热更新
67 | app.use(webpackHotMiddleware(_clientCompiler))
68 |
69 | app.use(express.static('../dist/client'))
70 |
71 | app.listen(WEBPACK_PORT)
72 |
73 | // 服务端代码更新监听
74 | _serverCompiler.watch({ ignored: /node_modules/ }, (error, stats) => {
75 | if (!error && !stats.hasErrors()) {
76 | console.log('------error----' + stats.toString(serverConfig.stats))
77 | return
78 | }
79 | if (error) {
80 | console.log(error, '-------error-------')
81 | }
82 | })
83 |
84 | process.on('unhandledRejection', (reason, p) => {
85 | console.log('Unhandled Rejection at: Promise', p, 'reason:', reason)
86 | // application specific logging, throwing an error, or other logic here
87 | })
88 |
89 | await serverPromise
90 | await clientPromise
91 |
92 | const script = nodemon({
93 | script: `./dist/server/server.js`,
94 | ignore: ['src', 'scripts', 'config', './*.*', 'build/client']
95 | })
96 |
97 | script.on('restart', () => {
98 | console.log('Server side app has been restarted.')
99 | })
100 |
101 | script.on('quit', () => {
102 | console.log('Process ended')
103 | process.exit()
104 | })
105 |
106 | script.on('error', () => {
107 | console.log('An error occured. Exiting')
108 | process.exit(1)
109 | })
110 | }
111 |
112 | start()
113 |
--------------------------------------------------------------------------------
/src/app/common/ajax.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | const AJAX = ({ url = '', method = 'get', data = {}, headers = {} }) => {
4 | let option = { url, method, headers }
5 |
6 | if (method == 'get') {
7 | data._t = new Date().getTime()
8 | option.params = data
9 | } else if (method == 'post') {
10 | option.data = data
11 | }
12 |
13 | return axios(option)
14 | .then(resp => {
15 | if (resp && resp.data) {
16 | let res = resp.data
17 | return [null, res]
18 | } else {
19 | return ['return none']
20 | }
21 | })
22 | .catch(function(error) {
23 | if (error && error.response && error.response.data) {
24 | return [error.response.data]
25 | } else {
26 | return ['return error']
27 | }
28 | })
29 | }
30 |
31 | export default AJAX
32 |
--------------------------------------------------------------------------------
/src/app/common/global-data.js:
--------------------------------------------------------------------------------
1 | const globalData = {}
2 |
3 | export function set(key, val) {
4 | globalData[key] = val
5 | }
6 |
7 | export function get(key) {
8 | return globalData[key]
9 | }
10 |
11 | export function remove(key) {
12 | delete globalData[key]
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/common/parse-url.js:
--------------------------------------------------------------------------------
1 |
2 | const parseUrl = (search) => {
3 | const paramPart = search.substr(1).split('&');
4 | return paramPart.reduce(function(res, item) {
5 | if (item) {
6 | let parts = item.split('=');
7 | res[parts[0]] = parts[1] || '';
8 | }
9 | return res;
10 | }, {});
11 | }
12 |
13 | export default parseUrl;
14 |
--------------------------------------------------------------------------------
/src/app/components/footer/index.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent, Fragment } from 'react'
2 |
3 | export default () => {
4 | return (
5 | <>
6 |
7 | >
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/components/head/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link, NavLink } from 'react-router-dom'
3 | import { useStore, useSelector } from 'react-redux'
4 |
5 | import { signOut } from '@/store/actions/user'
6 | import { getUserInfo } from '@/store/reducers/user'
7 |
8 | import './style.scss'
9 |
10 | export default () => {
11 | const store = useStore()
12 | const userinfo = useSelector(state => getUserInfo(state))
13 |
14 | const onSignOut = async () => {
15 | const _signOut = () => signOut()(store.dispatch, store.getState)
16 | const [, success] = await _signOut()
17 | if (success) {
18 | // 退出成功
19 | window.location.reload()
20 | } else {
21 | alert('退出失败')
22 | }
23 | }
24 |
25 | return (
26 |
27 |
76 |
77 | )
78 | }
79 |
--------------------------------------------------------------------------------
/src/app/components/head/style.scss:
--------------------------------------------------------------------------------
1 | .test{
2 | width: 100%;
3 | background-color: #333 !important;
4 | display: flex;
5 | box-sizing: border-box;
6 | transform: translate(0,0);
7 | transition: all .2s linear;
8 | }
9 |
--------------------------------------------------------------------------------
/src/app/components/meta/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import MetaTags, { ReactTitle } from 'react-meta-tags'
4 |
5 | import { name } from 'Config'
6 |
7 | export default function Meta({ title, children }) {
8 | let _title = ''
9 |
10 | _title += title || name
11 | if (title) _title += ` - ${name}`
12 |
13 | return (
14 | <>
15 |
16 | {children ? {children} : null}
17 | >
18 | )
19 | }
20 |
21 | Meta.propTypes = {
22 | title: PropTypes.string,
23 | children: PropTypes.any
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/components/posts/list/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Link } from 'react-router-dom'
4 | import { useStore, useSelector } from 'react-redux'
5 | import { loadPostsList } from '@/store/actions/posts'
6 | import { getPostsListByListId } from '@/store/reducers/posts'
7 |
8 | import './style.scss'
9 |
10 | const PostsList = ({ id, filter }) => {
11 | const store = useStore()
12 | const list = useSelector(state => getPostsListByListId(state, id))
13 |
14 | const { loading, data } = list
15 |
16 | useEffect(() => {
17 | const getData = args => loadPostsList(args)(store.dispatch, store.getState)
18 | if (!data) {
19 | getData({
20 | id,
21 | filter
22 | })
23 | }
24 | }, [data, store.dispatch, store.getState])
25 |
26 | return (
27 |
28 | {loading ?
loading...
: null}
29 |
30 | {data &&
31 | data.map((item, index) => (
32 |
37 |
38 |
39 | {item.user_id.nickname}
40 |
41 |
42 |
{item.title}
43 | {item.topic_id.name}
44 |
45 |
{item.content_summary}
46 |
{item.comment_count > 0 ? `有${item.comment_count}人评论` : null}
47 |
48 | ))}
49 |
50 |
51 | )
52 | }
53 |
54 | PostsList.propTypes = {
55 | // 要获取的列表的id
56 | id: PropTypes.string.isRequired,
57 | // 筛选条件
58 | filter: PropTypes.object.isRequired
59 | }
60 |
61 | PostsList.loadDataOnServer = async ({ store, match, res, req, user }) => {
62 | await loadPostsList({
63 | id: 'home',
64 | filter: {
65 | sort_by: 'create_at',
66 | deleted: false,
67 | weaken: false
68 | }
69 | })(store.dispatch, store.getState)
70 | }
71 |
72 | export default PostsList
73 |
--------------------------------------------------------------------------------
/src/app/components/posts/list/style.scss:
--------------------------------------------------------------------------------
1 | .avatar{
2 | width: 30px;
3 | height: 30px;
4 | border-radius: 15px;
5 | margin-right:10px;
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/components/shell.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | // redux
5 | import { useStore } from 'react-redux'
6 | import { saveScrollPosition, setScrollPosition } from '@/store/actions/scroll'
7 |
8 | // tools
9 | import parseUrl from '@/common/parse-url'
10 |
11 | export default function (Component) {
12 | function Shell({ history, location, match, staticContext }) {
13 | const [notFound, setNotFound] = useState('')
14 |
15 | const store = useStore()
16 |
17 | const { pathname, search } = location
18 |
19 | location.params = search ? parseUrl(search) : {}
20 |
21 | useEffect(() => {
22 | setScrollPosition(pathname + search)(store.dispatch, store.getState)
23 | return () => {
24 | saveScrollPosition(pathname + search)(store.dispatch, store.getState)
25 | }
26 | }, [pathname, search, store.dispatch, store.getState])
27 |
28 | if (notFound) {
29 | return {notFound}
30 | }
31 |
32 | return
33 | }
34 |
35 | Shell.loadDataOnServer =
36 | Component.loadDataOnServer ||
37 | function () {
38 | return { code: 200 }
39 | }
40 |
41 | Shell.propTypes = {
42 | history: PropTypes.object,
43 | location: PropTypes.object,
44 | match: PropTypes.object,
45 | staticContext: PropTypes.object
46 | }
47 |
48 | return Shell
49 | }
50 |
--------------------------------------------------------------------------------
/src/app/components/ui/loading/images/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/54sword/react-starter/a5df88130664b2eb40006c2ad86f1ae8899b5fba/src/app/components/ui/loading/images/loading.gif
--------------------------------------------------------------------------------
/src/app/components/ui/loading/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import './style.scss'
5 |
6 | const Loading = ({ text = '正在加载中...' }) => {
7 | return (
8 |
9 |
10 | {text}
11 |
12 | )
13 | }
14 |
15 | Loading.propTypes = {
16 | text: PropTypes.string
17 | }
18 |
19 | export default Loading
20 |
--------------------------------------------------------------------------------
/src/app/components/ui/loading/style.scss:
--------------------------------------------------------------------------------
1 |
2 | .loading{
3 | text-align: center;
4 | padding:15px 0 15px 0;
5 | font-size: 12px;
6 |
7 | span{
8 | margin-right:5px;
9 | margin-bottom: -3px;
10 | display: inline-block;
11 | width: 16px;
12 | height: 16px;
13 | background-image: url(./images/loading.gif);
14 | background-size: cover;
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/init-data.js:
--------------------------------------------------------------------------------
1 | import { saveAccessToken, saveUserInfo, loadUserInfo } from '@/store/actions/user'
2 | // 初始化数据
3 | // redux 中的数据清理、以及准备一些经常不变的数据
4 | export default async (store, accessToken) => {
5 | // 一些经常通用数据,不会经常更新的数据,在服务器获取并储存在store中
6 | if (accessToken) {
7 | // 储存用户信息
8 | // 储存access token
9 | const [err, user] = await loadUserInfo({ accessToken })(store.dispatch, store.getState)
10 | const obj = { id: '001', nickname: user }
11 | store.dispatch(saveAccessToken({ accessToken }))
12 | store.dispatch(saveUserInfo({ userinfo: obj }))
13 | return [err, obj]
14 | } else {
15 | return []
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/pages/global.scss:
--------------------------------------------------------------------------------
1 | body{
2 | padding-top: 56px;
3 | }
4 |
5 |
--------------------------------------------------------------------------------
/src/app/pages/home/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | // 壳组件
4 | import Shell from '@/components/shell'
5 | import Meta from '@/components/meta'
6 | import PostsList from '@/components/posts/list'
7 |
8 | const Home = () => {
9 | return (
10 |
22 | )
23 | }
24 |
25 | Home.loadDataOnServer = async ({ store, match, res, req, user }) => {
26 | if (user) return { code: 200 }
27 | await PostsList.loadDataOnServer({ store, match, res, req, user })
28 | return { code: 200 }
29 | }
30 |
31 | export default Shell(Home)
32 |
--------------------------------------------------------------------------------
/src/app/pages/not-found/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Shell from '@/components/shell'
4 | import Meta from '@/components/meta'
5 |
6 | const NotFound = () => {
7 | return (
8 | <>
9 |
10 | 404,无法找到该页面
11 | >
12 | )
13 | }
14 |
15 | NotFound.loadDataOnServer = async ({ store, match, res, req, user }) => {
16 | return { code: 404 }
17 | }
18 |
19 | export default Shell(NotFound)
20 |
--------------------------------------------------------------------------------
/src/app/pages/posts-detail/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { useRouteMatch, useLocation } from 'react-router-dom'
3 | import { useStore, useSelector } from 'react-redux'
4 |
5 | import { loadPostsList } from '@/store/actions/posts'
6 | import { getPostsListByListId } from '@/store/reducers/posts'
7 |
8 | import Shell from '@/components/shell'
9 | import Meta from '@/components/meta'
10 | import Loading from '@/components/ui/loading'
11 |
12 | const PostsDetail = () => {
13 | const location = useLocation()
14 | const {
15 | params: { id }
16 | } = useRouteMatch()
17 | const store = useStore()
18 | const list = useSelector(state => getPostsListByListId(state, id))
19 |
20 | const { loading, data } = list || {}
21 | const posts = data && data[0] ? data[0] : null
22 |
23 | useEffect(() => {
24 | const getData = args => loadPostsList(args)(store.dispatch, store.getState)
25 | console.log(list, 'list')
26 | if (!posts) {
27 | getData({
28 | id,
29 | filter: {
30 | _id: id
31 | }
32 | })
33 | }
34 | }, [list.data, store.dispatch, store.getState])
35 |
36 | return (
37 |
38 | {loading ?
: null}
39 |
40 |
41 |
42 | {posts ? (
43 |
44 |
{posts.title}
45 |
{posts.topic_id.name}
46 |
47 | {posts.content_html ?
: null}
48 |
49 | ) : null}
50 |
51 | )
52 | }
53 |
54 | PostsDetail.loadDataOnServer = async ({ store, match, res, req, user }) => {
55 | if (user) return { code: 200 }
56 | const { id } = match.params
57 | const [err, data] = await loadPostsList({
58 | id: id,
59 | filter: {
60 | _id: id,
61 | deleted: false,
62 | weaken: false
63 | }
64 | })(store.dispatch, store.getState)
65 |
66 | // 没有找到帖子,设置页面 http code 为404
67 | if (err || data.length == 0) {
68 | return { code: 404 }
69 | } else {
70 | return { code: 200 }
71 | }
72 | }
73 |
74 | export default Shell(PostsDetail)
75 |
--------------------------------------------------------------------------------
/src/app/pages/sign-in/images/react-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/54sword/react-starter/a5df88130664b2eb40006c2ad86f1ae8899b5fba/src/app/pages/sign-in/images/react-icon.png
--------------------------------------------------------------------------------
/src/app/pages/sign-in/index.js:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import { useStore } from 'react-redux'
5 | import { signIn } from '@/store/actions/user'
6 |
7 | import './style.scss'
8 |
9 | import Shell from '@/components/shell'
10 | import Meta from '@/components/meta'
11 |
12 | export default Shell(() => {
13 | const store = useStore()
14 | const nickname = useRef()
15 |
16 | const onSignIn = async event => {
17 | event.preventDefault()
18 |
19 | const name = nickname.current
20 |
21 | if (!name.value) {
22 | name.focus()
23 | return false
24 | }
25 |
26 | const _signIn = args => signIn(args)(store.dispatch, store.getState)
27 | let [err, success] = await _signIn({ nickname: name.value })
28 | if (success) {
29 | window.location.href = '/'
30 | }
31 | return false
32 | }
33 |
34 | return (
35 |
46 | )
47 | })
48 |
--------------------------------------------------------------------------------
/src/app/pages/sign-in/style.scss:
--------------------------------------------------------------------------------
1 |
2 | .container{
3 | position: absolute;
4 | top:0px;
5 | left:0px;
6 | width: 100%;
7 | height: 100%;
8 | background-color: #343a40;
9 |
10 | > form{
11 | margin-top: 10%;
12 | color:#fff;
13 | }
14 |
15 | .icon{
16 | background-image: url(./images/react-icon.png);
17 | height: 120px;
18 | background-size: contain;
19 | background-position: center;
20 | background-repeat: no-repeat;
21 | margin:0 0 20px 0;
22 | }
23 | }
24 |
25 | :global{
26 |
27 | .form-signin {
28 | width: 100%;
29 | max-width: 330px;
30 | padding: 15px;
31 | margin: 0 auto;
32 | }
33 | .form-signin .checkbox {
34 | font-weight: 400;
35 | }
36 | .form-signin .form-control {
37 | position: relative;
38 | box-sizing: border-box;
39 | height: auto;
40 | padding: 10px;
41 | font-size: 16px;
42 | }
43 | .form-signin .form-control:focus {
44 | z-index: 2;
45 | }
46 | .form-signin input[type="email"] {
47 | margin-bottom: -1px;
48 | border-bottom-right-radius: 0;
49 | border-bottom-left-radius: 0;
50 | }
51 | .form-signin input[type="password"] {
52 | margin-bottom: 10px;
53 | border-top-left-radius: 0;
54 | border-top-right-radius: 0;
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/src/app/pages/topics/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Shell from '@/components/shell'
4 | import Meta from '@/components/meta'
5 |
6 | export default Shell(() => {
7 | return (
8 |
9 |
10 |
Topics
11 |
12 | )
13 | })
14 |
--------------------------------------------------------------------------------
/src/app/pages/variables.scss:
--------------------------------------------------------------------------------
1 | $default-color: #1985ff;
--------------------------------------------------------------------------------
/src/app/router/index.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react'
2 | import { Route, Switch, Redirect } from 'react-router-dom'
3 |
4 | import list from './list'
5 |
6 | /**
7 | * 创建路由
8 | * @param {Object} userinfo 用户信息,以此判断用户是否是登录状态,并控制页面访问权限
9 | * @return {[type]}
10 | */
11 | export default ({ user = {}, enterEvent = () => {} }) => {
12 | // 进入路由的权限控制
13 | const enter = (role, Layout, props, route) => {
14 | enterEvent()
15 |
16 | switch (role) {
17 | // 任何人
18 | case 'everybody':
19 | return
20 | // 游客
21 | case 'tourists':
22 | if (user.id) {
23 | return
24 | } else {
25 | return
26 | }
27 | // 注册用户
28 | case 'member':
29 | if (!user.id) {
30 | return
31 | } else {
32 | return
33 | }
34 | }
35 | }
36 |
37 | const dom = () => (
38 |
39 |
40 | {list.map((route, index) => (
41 |
42 | ))}
43 |
44 |
45 | {list.map((route, index) => {
46 | if (!route.body) return
47 | return enter(route.enter, route.body, props, route)} />
48 | })}
49 |
50 |
51 | {list.map((route, index) => (
52 |
53 | ))}
54 |
55 |
56 | )
57 |
58 | return {
59 | dom,
60 | list
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/app/router/list.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import loadable from '@loadable/component'
3 |
4 | import Head from '@/components/head'
5 | import Footer from '@/components/footer'
6 | import Loading from '@/components/ui/loading'
7 |
8 | const exact = true
9 | const base = { exact, head: Head, footer: Footer }
10 |
11 | export default [
12 | {
13 | path: '/',
14 | ...base,
15 | body: loadable(() => import('@/pages/home'), {
16 | fallback:
17 | }),
18 | enter: 'everybody'
19 | },
20 |
21 | {
22 | path: '/posts/:id',
23 | ...base,
24 | body: loadable(() => import('@/pages/posts-detail'), {
25 | fallback:
26 | }),
27 | enter: 'everybody'
28 | },
29 |
30 | {
31 | path: '/topics',
32 | ...base,
33 | body: loadable(() => import('@/pages/topics'), {
34 | fallback:
35 | }),
36 | enter: 'member'
37 | },
38 |
39 | {
40 | path: '/sign-in',
41 | ...base,
42 | body: loadable(() => import('@/pages/sign-in'), {
43 | fallback:
44 | }),
45 | enter: 'everybody'
46 | },
47 |
48 | {
49 | path: '**',
50 | head: Head,
51 | footer: Footer,
52 | body: loadable(() => import('@/pages/not-found'), {
53 | fallback:
54 | }),
55 | enter: 'everybody'
56 | }
57 | ]
58 |
--------------------------------------------------------------------------------
/src/app/static/img/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/54sword/react-starter/a5df88130664b2eb40006c2ad86f1ae8899b5fba/src/app/static/img/favicon.png
--------------------------------------------------------------------------------
/src/app/store/actions/posts.js:
--------------------------------------------------------------------------------
1 | import Ajax from '@/common/ajax'
2 | import { getPostsListByListId } from '../reducers/posts'
3 |
4 | export function loadPostsList({ id, filter = {} }) {
5 | return (dispatch, getState) => {
6 | return new Promise(async (resolve, reject) => {
7 | let list = getPostsListByListId(getState(), id)
8 |
9 | list.loading = true
10 | list.filter = filter
11 | if (!list.data) list.data = []
12 |
13 | let variables = convertFilrerFormat(filter)
14 |
15 | if (!variables) {
16 | variables = ''
17 | } else {
18 | variables = `(${variables})`
19 | }
20 |
21 | // 储存 cookie
22 | let [err, data] = await Ajax({
23 | url: 'http://admin.xiaoduyu.com/graphql',
24 | method: 'post',
25 | data: {
26 | operationName: null,
27 | variables: {},
28 | query: `{
29 | posts${variables}{
30 | _id
31 | comment_count
32 | content_html
33 | title
34 | topic_id{
35 | _id
36 | name
37 | }
38 | type
39 | user_id{
40 | _id
41 | nickname
42 | brief
43 | avatar_url
44 | }
45 | }
46 | }`
47 | }
48 | })
49 |
50 | if (data && data.data) {
51 | list.loading = false
52 |
53 | let postsData = data.data[Reflect.ownKeys(data.data)[0]]
54 |
55 | if (postsData && postsData.length > 0) {
56 | list.data = list.data.concat(modifyList(postsData))
57 | }
58 |
59 | dispatch({ type: 'SET_POSTS_LIST_BY_NAME', id, data: list })
60 | resolve([null, list.data])
61 | } else {
62 | resolve(['loadPostList load failed'])
63 | }
64 | })
65 | }
66 | }
67 |
68 | const modifyList = data => {
69 | let arr = []
70 | data.map(item => {
71 | let text = item.content_html.replace(/<[^>]+>/g, '')
72 | if (text.length > 140) text = text.slice(0, 140) + '...'
73 | item.content_summary = text
74 |
75 | arr.push(item)
76 | })
77 |
78 | return arr
79 | }
80 |
81 | // 将参数对象转换成,GraphQL提交参数的格式
82 | const convertFilrerFormat = params => {
83 | let arr = []
84 | for (let i in params) {
85 | let v = ''
86 | switch (typeof params[i]) {
87 | case 'string':
88 | v = '"' + params[i] + '"'
89 | break
90 | case 'number':
91 | v = params[i]
92 | break
93 | default:
94 | v = params[i]
95 | }
96 | arr.push(i + ':' + v)
97 | }
98 | return arr.join(',')
99 | }
100 |
--------------------------------------------------------------------------------
/src/app/store/actions/scroll.js:
--------------------------------------------------------------------------------
1 | export function setScrollPosition(name) {
2 | return (dispatch, getState) => {
3 | dispatch({ type: 'SET_SCROLL_POSITION', name })
4 | }
5 | }
6 |
7 | export function saveScrollPosition(name) {
8 | return (dispatch, getState) => {
9 | dispatch({ type: 'SAVE_SCROLL_POSITION', name })
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/store/actions/user.js:
--------------------------------------------------------------------------------
1 | import Ajax from '@/common/ajax'
2 |
3 | // 储存accessToken到redux
4 | export function saveAccessToken({ accessToken }) {
5 | return dispatch => {
6 | dispatch({ type: 'SAVE_ACCESS_TOKEN', accessToken })
7 | }
8 | }
9 |
10 | export function saveUserInfo({ userinfo }) {
11 | return dispatch => {
12 | dispatch({ type: 'SAVE_USERINFO', userinfo })
13 | }
14 | }
15 |
16 | export function loadUserInfo({ accessToken }) {
17 | console.log(accessToken, '---')
18 | return async (dispatch, getState) => {
19 | dispatch({ type: 'SAVE_USER_INFO', data: accessToken })
20 | return [null, accessToken]
21 | }
22 | }
23 |
24 | export function signIn({ nickname }) {
25 | return async () => {
26 | // 这里写你的登录请求,登录成功以后,将token储存到cookie,使用httpOnly(比较安全)
27 | // your code ...
28 | // 储存 cookie
29 | let [err, data] = await Ajax({
30 | url: window.location.origin + '/sign/in',
31 | method: 'post',
32 | data: {
33 | nickname: nickname
34 | }
35 | })
36 |
37 | if (data && data.success) {
38 | return [null, true]
39 | } else {
40 | return ['sign error']
41 | }
42 | }
43 | }
44 |
45 | export function signOut() {
46 | return async () => {
47 | const [err, data] = await Ajax({
48 | url: window.location.origin + '/sign/out',
49 | method: 'post'
50 | })
51 |
52 | if (data && data.success) {
53 | return [null, true]
54 | } else {
55 | return ['signOut error']
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/app/store/clone.js:
--------------------------------------------------------------------------------
1 | export default function (obj) {
2 | return JSON.parse(JSON.stringify(obj))
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose, combineReducers } from 'redux'
2 | import thunk from 'redux-thunk'
3 | import rootReducer from './reducers'
4 | import { createLogger } from 'redux-logger'
5 |
6 | const middleware = [thunk]
7 |
8 | // 如果是在客户端环境,并且是开发模式,那么打印redux日志
9 | if (process.env.NODE_ENV === 'development' && __CLIENT__) {
10 | middleware.push(createLogger())
11 | }
12 |
13 | export default function configureStore(initialState = {}) {
14 | const store = createStore(combineReducers(rootReducer), initialState, compose(applyMiddleware(...middleware)))
15 |
16 | return store
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/store/reducers/index.js:
--------------------------------------------------------------------------------
1 | import user from './user'
2 | import posts from './posts'
3 | export default {
4 | user,
5 | posts
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/store/reducers/posts.js:
--------------------------------------------------------------------------------
1 | import cloneObj from '../clone'
2 |
3 | const initialState = {}
4 |
5 | export default (state = initialState, action = {}) => {
6 | switch (action.type) {
7 | case 'SET_POSTS_LIST_BY_NAME':
8 | const { id, data } = action
9 | state[id] = data
10 | default:
11 | return state
12 | }
13 | return cloneObj(state)
14 | }
15 |
16 | export const getPostsListByListId = (state, id) => {
17 | return state.posts[id] ? state.posts[id] : {}
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/store/reducers/scroll.js:
--------------------------------------------------------------------------------
1 | import cloneObj from '../clone'
2 | const initialState = {}
3 |
4 | export default (state = cloneObj(initialState), action = {}) => {
5 | switch (action.type) {
6 | case 'SAVE_SCROLL_POSITION':
7 | if (action.name) state[action.name] = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0
8 | break
9 |
10 | case 'SET_SCROLL_POSITION':
11 | // 1、先设置置顶
12 | // window.scrollTo(0, action.name ? state[action.name] : 0)
13 |
14 | // 2、浏览器会执行自带滚动条的位置记录
15 |
16 | // 3、如果存在位置,则覆盖条浏览器的滚动条位置
17 | // if (typeof state[action.name] !== 'undefined') {
18 | // 延迟一点点,覆盖掉浏览器自带的滚动条位置记录
19 | // setTimeout(() => {
20 | window.scrollTo(0, action.name && state[action.name] ? state[action.name] : 0)
21 | // })
22 | // }
23 | break
24 |
25 | case 'CLEAN':
26 | state = {}
27 | break
28 | }
29 | return cloneObj(state)
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/store/reducers/user.js:
--------------------------------------------------------------------------------
1 | import cloneObj from '../clone'
2 |
3 | const initialState = {}
4 |
5 | export default (state = initialState, action = {}) => {
6 | switch (action.type) {
7 | case 'SAVE_ACCESS_TOKEN':
8 | state.accessToken = action.accessToken
9 | return state
10 |
11 | case 'SAVE_USERINFO':
12 | state.userinfo = action.userinfo
13 | return state
14 |
15 | default:
16 | return state
17 | }
18 |
19 | return cloneObj(state)
20 | }
21 |
22 | // 获取 access token
23 | export const getAccessToken = state => state.user.accessToken
24 |
25 | // 获取用户信息
26 | export const getUserInfo = state => state.user.userinfo || {}
27 |
--------------------------------------------------------------------------------
/src/app/views/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= htmlWebpackPlugin.options.head %>
8 |
9 |
10 | <%= htmlWebpackPlugin.options.metaDom %>
11 |
12 |
13 |
14 | <%= htmlWebpackPlugin.options.htmlDom %>
15 |
16 |
17 |
20 |
21 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/app/views/index_dev.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | <%= htmlWebpackPlugin.options.head %> <%= htmlWebpackPlugin.options.metaDom %>
6 |
7 |
8 | <%= htmlWebpackPlugin.options.htmlDom %>
9 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/client/index.js:
--------------------------------------------------------------------------------
1 | // 引入 bootstrap
2 | import 'bootstrap/dist/css/bootstrap.min.css'
3 | import 'jquery'
4 | import 'popper.js'
5 | import 'bootstrap/dist/js/bootstrap.min.js'
6 |
7 | import { hot, setConfig } from 'react-hot-loader'
8 | import React from 'react'
9 | import ReactDOM from 'react-dom'
10 | import { BrowserRouter } from 'react-router-dom'
11 | import { Provider } from 'react-redux'
12 | import { matchPath } from 'react-router'
13 | import { loadableReady } from '@loadable/component'
14 |
15 | import './service-worker'
16 |
17 | // 引入全局样式
18 | import '../app/pages/global.scss'
19 |
20 | import configureStore from '@/store'
21 | import createRouter from '@/router'
22 | import { getUserInfo } from '@/store/reducers/user'
23 |
24 | import { domain, debug } from 'Config'
25 |
26 | setConfig({ logLevel: 'debug', reloadHooks: false })
27 |
28 | if (!debug) {
29 | const { origin, pathname } = window.location
30 | // 打开的不是目标网站跳转到目标网站
31 | if (origin !== domain) {
32 | window.location.href = domain + pathname
33 | }
34 | // 禁止被iframe
35 | if (window.top !== window.self) {
36 | window.top.location = window.location
37 | }
38 | }
39 |
40 | ;(async function () {
41 | // 从页面中获取服务端生产redux数据,作为客户端redux初始值
42 | const store = configureStore(window.__initState__)
43 | let userinfo = getUserInfo(store.getState())
44 | if (!userinfo || !userinfo.id) userinfo = {}
45 | let enterEvent = () => {}
46 | const { href, pathname } = window.location
47 |
48 | const router = createRouter({ user: userinfo, enterEvent })
49 | const Page = hot(module)(router.dom)
50 |
51 | let _route = null
52 |
53 | router.list.some(route => {
54 | const match = matchPath(pathname, route)
55 | if (match && match.path) {
56 | _route = route
57 | return true
58 | }
59 | })
60 |
61 | // 预先加载首屏的js(否则会出现,loading 一闪的情况)
62 | await _route.body.preload()
63 |
64 | const renderMethod = module.hot ? ReactDOM.render : ReactDOM.hydrate
65 | loadableReady(() => {
66 | renderMethod(
67 |
68 |
69 |
70 |
71 | ,
72 | document.getElementById('app')
73 | )
74 | })
75 |
76 | if (debug) {
77 | if (module.hot) {
78 | module.hot.accept()
79 | }
80 | }
81 |
82 | // 解决在 ios safari iframe 上touchMove 滚动后,外部的点击事件会无效的问题
83 | document.addEventListener('touchmove', function (e) {
84 | e.preventDefault()
85 | })
86 | })()
87 |
--------------------------------------------------------------------------------
/src/client/service-worker.js:
--------------------------------------------------------------------------------
1 | import * as OfflinePluginRuntime from 'offline-plugin/runtime'
2 | import * as globalData from '@/common/global-data'
3 |
4 | const ServiceWorker = {
5 | get: function () {
6 | return new Promise(resolve => {
7 | if ('serviceWorker' in navigator) {
8 | navigator.serviceWorker.getRegistrations().then(registrations => {
9 | resolve(registrations)
10 | })
11 | } else {
12 | resolve(null)
13 | }
14 | })
15 | },
16 |
17 | install: function () {
18 | return new Promise(resolve => {
19 | if (process.env.NODE_ENV !== 'development') {
20 | OfflinePluginRuntime.install()
21 | }
22 | resolve()
23 | })
24 | },
25 |
26 | uninstall: function () {
27 | // eslint-disable-next-line no-async-promise-executor
28 | return new Promise(resolve => {
29 | if ('serviceWorker' in navigator) {
30 | navigator.serviceWorker
31 | .getRegistrations()
32 | .then(registrations => {
33 | for (const registration of registrations) {
34 | registration.unregister()
35 | }
36 | resolve()
37 | })
38 | // eslint-disable-next-line handle-callback-err
39 | .catch(err => {
40 | resolve()
41 | })
42 | } else {
43 | resolve()
44 | }
45 | })
46 | }
47 | }
48 |
49 | export default ServiceWorker
50 |
51 | globalData.set('service-worker', ServiceWorker)
52 | ServiceWorker.install()
53 |
--------------------------------------------------------------------------------
/src/server/cache.js:
--------------------------------------------------------------------------------
1 | import { CACHA_TIME } from 'Config'
2 |
3 | var LRU = require('lru-cache')
4 | var options = { max: 100, maxAge: CACHA_TIME }
5 | var cache = new LRU(options)
6 |
7 | if (!CACHA_TIME) {
8 | cache = {
9 | get: () => '',
10 | set: () => ''
11 | }
12 | }
13 |
14 | export default cache
15 |
--------------------------------------------------------------------------------
/src/server/index.js:
--------------------------------------------------------------------------------
1 | require('@babel/polyfill')
2 | require('./server')
3 |
--------------------------------------------------------------------------------
/src/server/render.js:
--------------------------------------------------------------------------------
1 | // 服务端渲染依赖
2 | import path from 'path'
3 | import React from 'react'
4 | import ReactDOMServer from 'react-dom/server'
5 | import { StaticRouter, matchPath } from 'react-router'
6 | import { Provider } from 'react-redux'
7 | import MetaTagsServer from 'react-meta-tags/server'
8 | import { MetaTagsContext } from 'react-meta-tags'
9 | import { ChunkExtractor } from '@loadable/server'
10 |
11 | // 创建redux store
12 | import createStore from '@/store'
13 | // 路由组件
14 | import createRouter from '@/router'
15 | // 加载初始数据
16 | import initData from '@/init-data'
17 |
18 | import { auth_cookie_name, debug } from 'Config'
19 |
20 | export default async (req, res) => {
21 | const params = {
22 | code: 200,
23 | redirect: '',
24 | html: '',
25 | meta: '',
26 | reduxState: '{}',
27 | user: null
28 | }
29 |
30 | // 创建新的store
31 | let store = createStore()
32 |
33 | const accessToken = req.cookies[auth_cookie_name] || ''
34 | const [err, user] = await initData(store, accessToken)
35 |
36 | params.user = user
37 |
38 | let router = createRouter({ user })
39 |
40 | let route = null
41 | let match = null
42 |
43 | router.list.some(_route => {
44 | const _match = matchPath(req.path, _route)
45 | if (_match) {
46 | _match.search = req._parsedOriginalUrl.search || ''
47 | route = _route
48 | match = _match
49 | }
50 | return _match
51 | })
52 |
53 | if (route.enter === 'tourists' && user) {
54 | // 游客
55 | params.code = 403
56 | params.redirect = '/'
57 | return params
58 | } else if (route.enter === 'member' && !user) {
59 | // 注册会员
60 | params.code = 403
61 | params.redirect = '/'
62 | return params
63 | }
64 |
65 | // if (route.loadData) {
66 | // // 服务端加载数据,并返回页面的状态
67 | // const { code, redirect } = await route.loadData({ store, match, res, req, user })
68 | // params.code = code
69 | // params.redirect = redirect
70 | // }
71 |
72 | const nodeStats = path.resolve('./dist/server/loadable-stats.json')
73 | const webStats = path.resolve('./dist/client/loadable-stats.json')
74 |
75 | const nodeExtractor = new ChunkExtractor({ statsFile: nodeStats, entrypoints: ['app'] })
76 | const webExtractor = new ChunkExtractor({ statsFile: webStats, entrypoints: ['app'] })
77 |
78 | // 获取路由dom
79 | const Page = router.dom
80 |
81 | await route.body.preload()
82 | const p = await route.body.load()
83 | if (p.default.loadDataOnServer) {
84 | // console.log('xxxxxxxxxx')
85 | const { code, redirect } = await p.default.loadDataOnServer({
86 | store,
87 | match,
88 | res,
89 | req,
90 | user
91 | })
92 | params.code = code
93 | params.redirect = redirect
94 | }
95 |
96 | const metaTagsInstance = MetaTagsServer()
97 |
98 | // console.log(webExtractor.getScriptTags(), 'webExtractor', nodeExtractor.getLinkTags(), 'nodeExtractor', webExtractor.getStyleTags())
99 |
100 | // html
101 | params.html = ReactDOMServer.renderToString(
102 | webExtractor.collectChunks(
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | )
111 | )
112 |
113 | // 获取页面的meta,嵌套到模版中
114 | params.meta = metaTagsInstance.renderToString()
115 |
116 | // console.log(metaTagsInstance.renderToString(), 'metaTagsInstance.renderToString()')
117 |
118 | params.debug = debug
119 |
120 | // redux
121 | params.reduxState = JSON.stringify(store.getState()).replace(/ {
6 | const router = express.Router()
7 |
8 | router.post('/in', (req, res) => {
9 | const nickname = req.body.nickname
10 |
11 | res.cookie(auth_cookie_name, nickname, { path: '/', httpOnly: true, maxAge: 1000 * 60 * 60 * 24 * 30 })
12 | res.send({ success: true })
13 | })
14 |
15 | router.post('/out', (req, res) => {
16 | res.clearCookie(auth_cookie_name)
17 | res.send({ success: true })
18 | })
19 |
20 | return router
21 | }
22 |
--------------------------------------------------------------------------------