├── .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 |
11 | 12 | 13 | 21 |
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 |
36 | 37 |
onSignIn(e)}> 38 |
39 |

React同构脚手架

40 | 41 | 44 | 45 |
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 | --------------------------------------------------------------------------------