├── .babelrc ├── .eslintrc.js ├── .gitignore ├── README.md ├── build ├── config.js ├── utils.js ├── webpack.base.config.js ├── webpack.dev.config.js ├── webpack.local.config.js ├── webpack.production.config.js └── webpack.test.config.js ├── dist ├── index.html ├── index.js └── js │ └── sw.js ├── log ├── error │ └── index.log.-2019-02-27.log └── request │ └── index.log.-2019-02-27.log ├── logs ├── date │ └── index.log.-2019-02-27.log └── render │ └── index.log.-2019-02-27.log ├── nohup.log ├── package.json ├── postcss.config.js ├── server ├── api │ ├── base.js │ └── demo.js ├── app.js ├── config │ └── index.js ├── const │ └── index.js ├── controllers │ ├── api │ │ └── demo.js │ └── demo.js ├── dist │ └── views │ │ └── index.html ├── index.js ├── log │ └── logger.js ├── middlewares │ ├── catchError.js │ └── timeLogger.js ├── nohup.log ├── routes │ ├── api │ │ └── demo.js │ ├── demo.js │ └── index.js ├── templating.js └── utils │ └── time.js └── src ├── api ├── base.js └── demo.js ├── app.js ├── assets └── scss │ ├── base.scss │ ├── base │ ├── normalize.scss │ └── variables.scss │ ├── common.scss │ ├── components │ └── movie.scss │ └── pages │ ├── detail.scss │ └── home.scss ├── common └── index.js ├── components ├── index.js └── movie │ └── Index.jsx ├── containers ├── detail.js ├── devTool.js └── home.js ├── index.html ├── layout └── Index.jsx ├── pages ├── column │ └── Index.jsx ├── detail │ └── Index.jsx └── home │ └── Index.jsx ├── redux ├── actions │ ├── detail.js │ └── home.js ├── constants │ ├── detail.js │ └── home.js ├── middlewares │ └── promiseMiddleware.js ├── reducers │ ├── detail.js │ ├── home.js │ └── index.js └── store │ └── createStore.js └── root └── route.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | }, 8 | "useBuiltIns": "usage" 9 | }], 10 | "@babel/preset-react" 11 | ], 12 | "plugins": [ 13 | 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: true, 11 | jquery: true 12 | }, 13 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 14 | extends: 'standard', 15 | // required to lint *.vue files 16 | plugins: [ 17 | 'html', 18 | 'react' 19 | ], 20 | globals: { 21 | API_ORIGIN: false, 22 | ROUTER_MODE: false, 23 | }, 24 | // add your custom rules here 25 | 'rules': { 26 | 'indent': ['warn', 4, { SwitchCase: 1 }], 27 | 'semi': ['warn', 'always'], 28 | 'camelcase': 0, 29 | 'comma-dangle': ['error', { 30 | 'arrays': 'only-multiline', 31 | 'objects': 'only-multiline', 32 | 'imports': 'only-multiline', 33 | 'exports': 'only-multiline', 34 | 'functions': 'ignore', 35 | }], 36 | 'no-unused-vars': ['warn'], 37 | 'no-undef': 2, 38 | 'arrow-parens': 0, 39 | // allow async-await 40 | // 'generator-star-spacing': 0, 41 | // allow debugger during development 42 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 43 | 'no-callback-literal': 0, 44 | 'no-new': 0, 45 | "eol-last": 0, 46 | "react/jsx-uses-react": "error", 47 | "react/jsx-uses-vars": "error" 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .idea/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

React SSR

3 |

Delightful React Server-Side Rendering

4 |

5 | 6 | # 线上DEMO演示 7 | 基于这个demo扩展的做了个人博客。可以参考下 8 | http://www.shuxia123.com/ 9 | 10 | # React SSR 11 | 这是基于`React`、`React-Router`、`Redux`、`Koa2.0`实现的React服务端渲染方案。为了更好的演示,已实现一个简单的电影首页、电影详情页。 12 | 数据资源是豆瓣上的资源。样式也参考了豆瓣的样式 13 | 14 | # 快速上手 15 | ``` 16 | // 安装依赖 17 | npm install 18 | 19 | // 为了构建一个模板(dist/index.html)给koa模板引擎使用 20 | npm run build:local 21 | 22 | // 启动本地node服务器,这里设置的端口是4322 23 | npm run server:dev 24 | 25 | // 启动前端服务,这里的服务端口号是8088 26 | npm start 27 | 28 | // 访问 http://localhost:4322进行开发 29 | ``` 30 | 31 | # 功能预览 32 | 目前已实现首页、详情页的交互和相关接口 33 | ``` 34 | 首页:(直接访问时,会走服务端渲染出首页dom,浏览器右键可以查看源码) 35 | http://localhost:4322 36 | 37 | 详情页:(直接访问时,会走服务端渲染出详情页dom,浏览器右键可以查看源码) 38 | http://localhost:4322/detail/30163509 39 | 40 | api接口: 41 | http://localhost:4322/api/home 42 | http://localhost:4322/api/movie/30163509 43 | 44 | ``` 45 | 46 | # 实现原理 47 | 48 |
49 | 50 |
51 | 如果你想查看更多原理内容,可以看我的知乎。 52 | 53 | React SSR 实现原理 54 | 55 | 56 | # webpack依赖目录 与 nodejs依赖目录 57 | webpack中配置resolve.alias依赖目录,方便import 58 | 59 | ``` 60 | // build/webpack.base.config.js文件 61 | alias: { 62 | '@scss': resolve('src/assets/scss'), 63 | '@api': resolve('src/api'), 64 | '@containers': resolve('src/containers'), 65 | '@components': resolve('src/components') 66 | }, 67 | ``` 68 | 因此node下也需要配置一致的目录,否则会提示"@components 目录找不到"。这个可以通过 module-alias 进行配置。 69 | ``` 70 | // package.json 配置 71 | "_moduleAliases": { 72 | "@scss": "src/assets/scss", 73 | "@api": "src/api", 74 | "@containers": "src/containers", 75 | "@components": "src/components" 76 | } 77 | ``` 78 | 79 | ## 首页预览 和 浏览器源码 80 | 81 | 82 | 访问 http://localhost:4322 83 | 84 |
85 | 86 |
87 | 88 | 89 | ## 详情页预览 和浏览器源码 90 |
91 | 92 |
-------------------------------------------------------------------------------- /build/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 配置文件 3 | * 生产环境 production 4 | * 测试环境 test 5 | * 开发环境 development 6 | */ 7 | 8 | module.exports = { 9 | // 生产环境 10 | production: { 11 | env: 'production', // 环境 12 | api: '', // api 接口地址 13 | publicPath: '', // 静态资源地址 14 | imagePath: '', // 图片资源地址 15 | devtool: 'false', // devtool 16 | noHash: false, 17 | }, 18 | // 测试环境 19 | test: { 20 | env: 'test', // 环境 21 | api: '', // api 接口地址 22 | publicPath: '', // 静态资源地址 23 | imagePath: '', // 图片资源地址 24 | devtool: 'false', // devtool 25 | noHash: false, 26 | }, 27 | // 开发环境构建,用于做ssr 28 | local: { 29 | env: 'local', // 环境 30 | api: 'https://www.shuxia123.com/', // api 接口地址 31 | publicPath: '/dev/', // 静态资源地址 32 | imagePath: '', // 图片资源地址 33 | devtool: 'false', // devtool, 34 | noHash: true, 35 | }, 36 | // 开发环境 37 | development: { 38 | env: 'development', // 环境 39 | api: 'https://www.shuxia123.com/', // api 接口地址 40 | publicPath: '/dev/', // 静态资源地址 41 | imagePath: '', // 图片资源地址 42 | port: '8088', // 开发端口 43 | devtool: 'source-map', // devtool 44 | noHash: true, 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | // 工具类 2 | const path = require('path'); 3 | 4 | exports.resolve = function resolve (...args) { 5 | return path.join(__dirname, '..', ...args); 6 | }; 7 | -------------------------------------------------------------------------------- /build/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const cleanWebpackPlugin = require('clean-webpack-plugin'); 3 | const miniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | const htmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | const {resolve} = require('./utils'); 7 | const _config = require('./config'); 8 | 9 | module.exports = function (mode) { 10 | const configMode = _config[mode]; 11 | const IS_DEVELOPMENT = mode === 'development'; 12 | const IS_LOCAL = mode === 'local'; 13 | 14 | let webpackConfig = { 15 | cache: true, 16 | entry: { 17 | index: resolve('src', 'app.js') 18 | }, 19 | output: { 20 | path: resolve('dist'), 21 | publicPath: configMode.publicPath, 22 | filename: configMode.noHash ? '[name].js' : '[name].[chunkhash].js', 23 | chunkFilename: configMode.noHash ? '[name].js' : '[name].[chunkhash].js' 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.jsx?$/, 29 | include: resolve('src'), 30 | loader: 'babel-loader', 31 | options: IS_DEVELOPMENT ? { 32 | cacheDirectory: true, 33 | plugins: ['react-hot-loader/babel'], 34 | } : {} 35 | }, 36 | // 代码格式检查 37 | { 38 | test: /\.js$/, 39 | enforce: 'pre', 40 | include: resolve('src'), 41 | loader: 'eslint-loader', 42 | options: { 43 | formatter: require('eslint-friendly-formatter') 44 | } 45 | }, 46 | { 47 | test: /\.scss$/, 48 | include: resolve('src'), 49 | use: [ 50 | (IS_DEVELOPMENT || IS_LOCAL) ? 'style-loader' : miniCssExtractPlugin.loader, 51 | 'css-loader', 52 | 'postcss-loader', 53 | 'sass-loader' 54 | ] 55 | }, 56 | { 57 | test: /\.(png|jpg|gif|svg)$/, 58 | loader: `url-loader?limit=1&name=${configMode.imagePath}${configMode.noHash ? '[name].[ext]' : '[name].[hash:8]'}.[ext]` 59 | }, 60 | { 61 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 62 | loader: `file-loader?name=${configMode.publicPath}fonts/${configMode.noHash ? '[name].[ext]' : '[name].[hash:8]'}.[ext]` 63 | }, 64 | { 65 | test: /\.html$/, 66 | include: resolve('src'), 67 | loader: 'html-loader' 68 | } 69 | ] 70 | }, 71 | plugins: [ 72 | // html 模板配置 73 | new htmlWebpackPlugin({ 74 | filename: 'index.html', 75 | template: './src/index.html', 76 | hash: false, 77 | inject: 'body', 78 | xhtml: false, 79 | minify: { 80 | removeComments: true, 81 | } 82 | }), 83 | // 注入环境变量,在代码内可以引用 84 | new webpack.DefinePlugin({ 85 | 'NODE_ENV': configMode.env, 86 | 'process.env.API': JSON.stringify(configMode.api), 87 | }) 88 | ], 89 | resolve: { 90 | // 依赖 91 | alias: { 92 | '@scss': resolve('src/assets/scss'), 93 | '@api': resolve('src/api'), 94 | '@containers': resolve('src/containers'), 95 | '@components': resolve('src/components') 96 | }, 97 | // 文件后缀自动补全 98 | extensions: ['.js', '.jsx'], 99 | }, 100 | // 第三方依赖,可以写在这里,不打包 101 | externals: {} 102 | }; 103 | 104 | 105 | if (IS_DEVELOPMENT) { 106 | // 开发环境 107 | // 开启热更新 108 | 109 | // 热更新 110 | webpackConfig.plugins.push( 111 | new webpack.NamedModulesPlugin()) 112 | ; 113 | webpackConfig.plugins.push( 114 | new webpack.HotModuleReplacementPlugin() 115 | ); 116 | } else if (IS_LOCAL) { 117 | webpackConfig.plugins.push( 118 | new webpack.NamedModulesPlugin()) 119 | ; 120 | webpackConfig.plugins.push( 121 | new webpack.HotModuleReplacementPlugin() 122 | ); 123 | } else { 124 | // 生产环境、测试环境 125 | // 采用增加更新的形式,出于更好的利用CDN缓存,采用的hash格式说明如下: 126 | // css: 采用contenthash;内容更新才会更新hash,这样只有样式内容改变了,才会改变对应的hash 127 | // js:采用chunkhash;这样每次修改代码只会更新manifest(映射文件),对应的更新文件输出的包 128 | 129 | // 清空构建目录 130 | webpackConfig.plugins.push( 131 | new cleanWebpackPlugin(['./dist']) 132 | ); 133 | 134 | // 抽离css,命名采用contenthash 135 | webpackConfig.plugins.push( 136 | new miniCssExtractPlugin({ 137 | filename: 'css/[name].[contenthash:8].css' 138 | }) 139 | ); 140 | 141 | // 公共代码 142 | webpackConfig.optimization = { 143 | splitChunks: { 144 | chunks: 'initial', 145 | minSize: 0, 146 | maxAsyncRequests: 5, 147 | maxInitialRequests: 3, 148 | automaticNameDelimiter: '~', 149 | name: true, 150 | cacheGroups: { 151 | common: { 152 | test: /[\\/]src\/common[\\/]/, 153 | chunks: 'all', 154 | name: 'common', 155 | minChunks: 1, 156 | priority: 10 157 | }, 158 | vendor: { 159 | test: /[\\/]node_modules[\\/]/, 160 | chunks: 'all', 161 | name: 'vendor', 162 | minChunks: 1, 163 | priority: 10 164 | } 165 | } 166 | }, 167 | runtimeChunk: { 168 | name: 'manifest', 169 | } 170 | }; 171 | } 172 | 173 | return webpackConfig; 174 | }; 175 | -------------------------------------------------------------------------------- /build/webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 开发环境webpack配置文件 3 | */ 4 | const merge = require('webpack-merge'); 5 | const baseConfig = require('./webpack.base.config'); 6 | const config = require('./config'); 7 | const {resolve} = require('./utils'); 8 | const mode = config.development; 9 | 10 | module.exports = merge(baseConfig(mode.env), { 11 | devtool: mode.devtool, 12 | mode: mode.env, 13 | devServer: { 14 | contentBase: resolve('dist'), 15 | hot: true, 16 | disableHostCheck: true, 17 | port: mode.port 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /build/webpack.local.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 开发环境webpack配置文件 3 | */ 4 | const merge = require('webpack-merge'); 5 | const baseConfig = require('./webpack.base.config'); 6 | const config = require('./config'); 7 | const {resolve} = require('./utils'); 8 | const mode = config.local; 9 | 10 | module.exports = merge(baseConfig(mode.env), { 11 | devtool: mode.devtool, 12 | mode: mode.env, 13 | devServer: { 14 | contentBase: resolve('dist'), 15 | hot: true, 16 | port: mode.port 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /build/webpack.production.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 测试环境webpack配置文件 3 | */ 4 | const merge = require('webpack-merge'); 5 | const baseConfig = require('./webpack.base.config'); 6 | const config = require('./config'); 7 | const mode = config.production; 8 | 9 | module.exports = merge(baseConfig(mode.env), { 10 | devtool: mode.devtool, 11 | mode: mode.env 12 | }); 13 | -------------------------------------------------------------------------------- /build/webpack.test.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 测试环境webpack配置文件 3 | */ 4 | const merge = require('webpack-merge'); 5 | const baseConfig = require('./webpack.base.config'); 6 | const config = require('./config'); 7 | const mode = config.test; 8 | 9 | module.exports = merge(baseConfig(mode.env), { 10 | devtool: mode.devtool, 11 | mode: mode.env 12 | }); 13 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{ title }} 14 | 15 | 16 | 17 | 18 | {{ layout | safe }} 19 | 22 | 23 | 24 | 36 | -------------------------------------------------------------------------------- /dist/js/sw.js: -------------------------------------------------------------------------------- 1 | const cacheName = 'shuxia123_index'; 2 | const RUNTIME = 'shuxia123_runtime'; 3 | self.addEventListener('install', function (event) { 4 | event.waitUntil( 5 | caches.open(cacheName).then(cache => cache.addAll([ 6 | '/dev/index.js' 7 | ])) 8 | ); 9 | }); 10 | 11 | self.addEventListener('fetch', event => { 12 | // Skip cross-origin requests, like those for Google Analytics. 13 | if (event.request.url.startsWith(self.location.origin)) { 14 | event.respondWith( 15 | caches.open(RUNTIME).then(cache => { 16 | return fetch(event.request).then(response => { 17 | return cache.put(event.request, response.clone()).then(() => { 18 | return response; 19 | }); 20 | }).catch((error) => { 21 | return caches.match(event.request).then(cachedResponse => { 22 | if (cachedResponse) { 23 | return cachedResponse; 24 | } 25 | }); 26 | }); 27 | }) 28 | ); 29 | } 30 | }); 31 | 32 | self.addEventListener('activate', event => { 33 | console.log('active'); 34 | }); 35 | -------------------------------------------------------------------------------- /log/error/index.log.-2019-02-27.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coocssweb/react-ssr/9a5f43c04166d55f699b9e093e5537f7ee7d3918/log/error/index.log.-2019-02-27.log -------------------------------------------------------------------------------- /log/request/index.log.-2019-02-27.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coocssweb/react-ssr/9a5f43c04166d55f699b9e093e5537f7ee7d3918/log/request/index.log.-2019-02-27.log -------------------------------------------------------------------------------- /logs/date/index.log.-2019-02-27.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coocssweb/react-ssr/9a5f43c04166d55f699b9e093e5537f7ee7d3918/logs/date/index.log.-2019-02-27.log -------------------------------------------------------------------------------- /logs/render/index.log.-2019-02-27.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coocssweb/react-ssr/9a5f43c04166d55f699b9e093e5537f7ee7d3918/logs/render/index.log.-2019-02-27.log -------------------------------------------------------------------------------- /nohup.log: -------------------------------------------------------------------------------- 1 | module.js:529 2 | throw err; 3 | ^ 4 | 5 | Error: Cannot find module 'browserslist' 6 | at Function.Module._resolveFilename (module.js:527:15) 7 | at Function.Module._load (module.js:476:23) 8 | at Module.require (module.js:568:17) 9 | at require (internal/module.js:11:18) 10 | at _browserslist (/coocss/react-ssr/node_modules/_@babel_preset-env@7.3.4@@babel/preset-env/lib/normalize-options.js:20:39) 11 | at Object. (/coocss/react-ssr/node_modules/_@babel_preset-env@7.3.4@@babel/preset-env/lib/normalize-options.js:80:50) 12 | at Module._compile (module.js:624:30) 13 | at Module._compile (/coocss/react-ssr/node_modules/_pirates@4.0.1@pirates/lib/index.js:99:24) 14 | at Module._extensions..js (module.js:635:10) 15 | at Object.newLoader [as .js] (/coocss/react-ssr/node_modules/_pirates@4.0.1@pirates/lib/index.js:104:7) 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "src", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --config build/webpack.dev.config.js --open --hot --https --watch-poll", 8 | "server:dev": "export NODE_ENV=development && nodemon ./server/index.js", 9 | "server": "export NODE_ENV=production && nohup node ./server/index.js > nohup.log 2>&1 &", 10 | "build:local": "webpack --mode production --config build/webpack.local.config.js --progress --hide-modules", 11 | "build:test": "webpack --mode production --config build/webpack.test.config.js --progress --hide-modules", 12 | "build": "webpack --mode production --config build/webpack.production.config.js --progress --hide-modules" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "@babel/core": "^7.3.4", 18 | "@babel/polyfill": "^7.2.5", 19 | "@babel/preset-env": "^7.2.0", 20 | "@babel/preset-react": "^7.0.0", 21 | "@babel/preset-stage-0": "^7.0.0", 22 | "@babel/register": "^7.0.0", 23 | "@babel/runtime": "^7.2.0", 24 | "autoprefixer": "^9.4.2", 25 | "axios": "^0.18.0", 26 | "babel-eslint": "^10.0.1", 27 | "babel-loader": "^8.0.4", 28 | "browserslist": "^4.4.2", 29 | "clean-webpack-plugin": "^1.0.1", 30 | "cross-env": "^5.1.4", 31 | "css-loader": "^2.0.0", 32 | "eslint": "^5.10.0", 33 | "eslint-config-standard": "^12.0.0", 34 | "eslint-friendly-formatter": "^4.0.1", 35 | "eslint-loader": "^2.1.1", 36 | "eslint-plugin-html": "^5.0.0", 37 | "eslint-plugin-import": "^2.14.0", 38 | "eslint-plugin-node": "^8.0.0", 39 | "eslint-plugin-promise": "^4.0.1", 40 | "eslint-plugin-react": "^7.11.1", 41 | "eslint-plugin-standard": "^4.0.0", 42 | "file-loader": "^2.0.0", 43 | "glob": "^7.1.3", 44 | "html-loader": "^0.5.5", 45 | "html-webpack-plugin": "^3.2.0", 46 | "install": "^0.12.2", 47 | "koa": "^2.4.1", 48 | "koa-bodyparser": "^4.2.0", 49 | "koa-convert": "^1.2.0", 50 | "koa-cors": "0.0.16", 51 | "koa-logger": "^3.2.0", 52 | "koa-router": "^7.4.0", 53 | "koa-static": "^5.0.0", 54 | "log4js": "^3.0.5", 55 | "mini-css-extract-plugin": "^0.5.0", 56 | "module-alias": "^2.2.0", 57 | "node-sass": "^4.11.0", 58 | "nunjucks": "^3.1.3", 59 | "path": "^0.12.7", 60 | "postcss-loader": "^3.0.0", 61 | "postcss-px2rem": "^0.3.0", 62 | "qs": "^6.5.2", 63 | "querystring": "^0.2.0", 64 | "react-hot-loader": "^4.7.1", 65 | "sass-loader": "^7.1.0", 66 | "style-loader": "^0.23.1", 67 | "to-fast-properties": "^2.0.0", 68 | "url-loader": "^1.1.2", 69 | "webpack": "^4.29.5", 70 | "webpack-cli": "^3.1.2", 71 | "webpack-dev-server": "^3.1.10", 72 | "webpack-merge": "^4.2.1", 73 | "webpack-node-externals": "^1.7.2" 74 | }, 75 | "dependencies": { 76 | "classnames": "^2.2.6", 77 | "immutable": "^4.0.0-rc.12", 78 | "prop-types": "^15.7.2", 79 | "react": "^16.8.3", 80 | "react-dom": "^16.8.3", 81 | "react-onclickoutside": "^6.7.1", 82 | "react-redux": "^6.0.1", 83 | "react-router": "^4.3.1", 84 | "react-router-dom": "^4.3.1", 85 | "react-transition-group": "^2.5.0", 86 | "redux": "^4.0.1", 87 | "redux-immutablejs": "^0.0.8", 88 | "redux-logger": "^3.0.6", 89 | "redux-router": "^2.1.2", 90 | "whatwg-fetch": "^2.0.4" 91 | }, 92 | "_moduleAliases": { 93 | "@scss": "src/assets/scss", 94 | "@api": "src/api", 95 | "@containers": "src/containers", 96 | "@components": "src/components" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | // const autoprefixer = require('autoprefixer'); 3 | // const px2rem = require('postcss-px2rem'); 4 | module.exports = { 5 | plugins: { 6 | 'autoprefixer': true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /server/api/base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * api 处理基础类 3 | * Created by 王佳欣 on 2018/7/5. 4 | */ 5 | import Axios from 'axios'; 6 | import Config from '../config'; 7 | import querystring from 'querystring'; 8 | import log4js from 'log4js'; 9 | const loggerError = log4js.getLogger('errorFile'); 10 | const loggerData = log4js.getLogger('dateFile'); 11 | const loggerMail = log4js.getLogger('mail'); 12 | 13 | export default class Base { 14 | async request({ 15 | host = Config.api, 16 | path, 17 | data = {}, 18 | method = 'get', 19 | responseType = 'json', 20 | contentType = 'application/x-www-form-urlencoded', 21 | accessToken 22 | }) { 23 | let requestData; 24 | let params; 25 | if (method === 'get') { 26 | params = data; 27 | } else if (method === 'post') { 28 | requestData = querystring.stringify(Object.assign({}, data)); 29 | } 30 | 31 | let timestamp1 = (new Date()).valueOf(); 32 | 33 | let result = await Axios({ 34 | url: `${host}${path}`, 35 | method, 36 | data: requestData || {}, 37 | params: params || {}, 38 | timeout: 5000, 39 | headers: { 40 | 'X-Access-Token': accessToken || '', 41 | 'Content-Type': contentType 42 | } 43 | }).then((response) => { 44 | return response ? response.data : {}; 45 | }).catch(error => { 46 | // 日志记录,错误日志 47 | loggerError.error(error); 48 | Config.logger && loggerMail.error(error); 49 | return error.response ? error.response.data : {meta: {code: 500, msg: `catch path: ${path}`}, response: {}}; 50 | }); 51 | 52 | // 日志记录,接口访问时长记录 53 | loggerData.trace(`${(new Date()).valueOf() - timestamp1}ms: ${path}`); 54 | 55 | return result; 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /server/api/demo.js: -------------------------------------------------------------------------------- 1 | import Base from './base'; 2 | // 影片信息 3 | const movies = [ 4 | { 5 | id: '1652592', 6 | title: '阿丽塔:战斗天使', 7 | photo: 'https://coocssweb.github.io/photos/react-ssr/movie-01.jpg', 8 | score: 7.6, 9 | meta: '122分钟 / 动作 / 科幻 / 冒险 / 罗伯特·罗德里格兹(导演) / 罗莎·萨拉查 / 克里斯托弗·沃尔兹 / 基恩·约翰逊 / 2019-02-22(中国大陆) 上映', 10 | desc: '故事发生在遥远的26世纪,外科医生依德(克里斯托弗·瓦尔兹 Christoph Waltz 饰)在垃圾场里捡到了只剩下头部的机械少女将她带回家中,给她装上了本来为自己已故的女儿所准备的义体,并取名阿丽塔(罗莎·萨拉扎尔 Rosa Salazar 饰)。', 11 | }, 12 | { 13 | id: '30163509', 14 | title: '飞驰人生6', 15 | photo: 'https://coocssweb.github.io/photos/react-ssr/movie-02.jpg', 16 | score: 7.0, 17 | meta: '98分钟 / 喜剧 / 韩寒(导演) / 沈腾 / 黄景瑜 / 尹正 / 2019-02-05(中国大陆) 上映', 18 | desc: '曾经在赛车界叱咤风云、如今却只能经营炒饭大排档的赛车手张驰(沈腾饰)决定重返车坛挑战年轻一代的天才。然而没钱没车没队友,甚至驾照都得重新考,这场笑料百出不断被打脸的复出之路,还有更多哭笑不得的窘境在等待着这位过气车神……', 19 | }, 20 | { 21 | id: '4840388', 22 | title: '新喜剧之王', 23 | photo: 'https://coocssweb.github.io/photos/react-ssr/movie-03.jpg', 24 | score: 5.88, 25 | meta: '91分钟 / 剧情 / 喜剧 / 周星驰(导演) / 王宝强 / 鄂靖文 / 张全蛋 / 2019-02-05(中国大陆) 上映', 26 | desc: '一直有个明星梦的小镇大龄女青年如梦跑龙套多年未果。她和父亲关系紧张,亲友都劝她放弃,只有男友查理还支持她。在剧组,如梦遇见了年少时启发她演戏的男演员马可。但此时过气多年的马可却因内心自卑而性情狂躁,对如梦百般折磨。如梦仍乐观坚持演戏,然而一次比一次沉重的打击却接踵而来,最后她决定放弃梦想,回到父母身边找了份稳定工作,但却得知自己入围了知名导演新片的大型选角。如梦陷入艰难抉择...', 27 | } 28 | ]; 29 | 30 | // 电影类型 31 | const types = [ 32 | { 33 | id: 11, 34 | name: '动作' 35 | }, 36 | { 37 | id: 12, 38 | name: '搞笑' 39 | }, 40 | { 41 | id: 13, 42 | name: '漫威' 43 | }, 44 | { 45 | id: 14, 46 | name: '科幻' 47 | } 48 | ]; 49 | 50 | class Demo extends Base { 51 | fetchHome () { 52 | return new Promise((resolve, reject) => { 53 | resolve({ 54 | seo: { 55 | title: 'SSR首页', 56 | keywords: 'SSR首页 Keywords', 57 | description: 'SSR首页 Description' 58 | }, 59 | data: { 60 | movies, 61 | banner: 'https://coocssweb.github.io/photos/react-ssr/banner.jpg' 62 | } 63 | }); 64 | }); 65 | } 66 | 67 | fetchColumn () { 68 | return new Promise((resolve, reject) => { 69 | resolve({ 70 | seo: { 71 | title: 'SSR栏目页', 72 | keywords: 'SSR栏目页 Keywords', 73 | description: 'SSR栏目页 Description' 74 | }, 75 | data: { 76 | types, 77 | movies, 78 | banner: 'https://coocssweb.github.io/photos/react-ssr/column-banner.jpeg' 79 | } 80 | }); 81 | }); 82 | } 83 | 84 | fetchOneMovie (id) { 85 | const movie = movies.filter((item) => { 86 | if (item.id === id) { 87 | return item; 88 | } 89 | })[0]; 90 | 91 | return new Promise((resolve, reject) => { 92 | resolve({ 93 | seo: { 94 | title: movie.title, 95 | keywords: movie.title, 96 | description: movie.desc 97 | }, 98 | data: { 99 | movie 100 | } 101 | }); 102 | }); 103 | } 104 | } 105 | 106 | export default new Demo(); 107 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import Koa from 'koa'; 3 | import Cors from 'koa-cors'; 4 | import BodyParser from 'koa-bodyparser'; 5 | import serve from 'koa-static'; 6 | import Convert from 'koa-convert'; 7 | import routes from './routes'; 8 | import Config from './config'; 9 | import templating from './templating'; 10 | import timeLogger from './middlewares/timeLogger'; 11 | import catchError from './middlewares/catchError'; 12 | 13 | const app = new Koa(); 14 | 15 | // 中间件处理 16 | app.use(Convert(Cors())); 17 | app.use(BodyParser()); 18 | app.use(catchError()); 19 | app.use(timeLogger()); 20 | app.use(serve(`${path.join(__dirname, '..', 'dist/js')}`)); 21 | console.log(`${path.join(__dirname, '..', 'dist/js')}`); 22 | // 模板目录 23 | app.use(templating('dist/', { 24 | noCache: Config.noCache, 25 | watch: Config.watch 26 | })); 27 | app.use(routes.routes(), routes.allowedMethods()); 28 | 29 | app.listen(4322); 30 | -------------------------------------------------------------------------------- /server/config/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // 正式环境 3 | production: { 4 | api: 'http://mallapi.m.com/', 5 | host: 'mall.meitu.com', 6 | watch: false, 7 | noCache: false 8 | }, 9 | // 测试环境 10 | test: { 11 | api: 'http://testmallapi.meitu.com/', 12 | host: 'testmall.meitu.com', 13 | watch: true, 14 | noCache: true 15 | }, 16 | // 开发环境 17 | development: { 18 | api: 'http://testmallapi.meitu.com/', 19 | host: 'localmall.meitu.com', 20 | watch: true, 21 | noCache: true 22 | } 23 | }[process.env.NODE_ENV]; 24 | -------------------------------------------------------------------------------- /server/const/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coocssweb/react-ssr/9a5f43c04166d55f699b9e093e5537f7ee7d3918/server/const/index.js -------------------------------------------------------------------------------- /server/controllers/api/demo.js: -------------------------------------------------------------------------------- 1 | import demoApi from '../../api/demo.js'; 2 | 3 | // 获取主页相关 4 | let fetchHome = async (ctx, next) => { 5 | let { id } = ctx.params; 6 | let result = await demoApi.fetchHome(); 7 | ctx.body = JSON.stringify(result); 8 | await next(); 9 | }; 10 | 11 | // 获取栏目信息 12 | let fetchColumn = async (ctx, next) => { 13 | let { id } = ctx.params; 14 | let result = await demoApi.fetchColumn(); 15 | ctx.body = JSON.stringify(result); 16 | await next(); 17 | }; 18 | 19 | // 获取一部电影 20 | let fetchOneMovie = async (ctx, next) => { 21 | let { id } = ctx.params; 22 | let result = await demoApi.fetchOneMovie(id); 23 | ctx.body = JSON.stringify(result); 24 | await next(); 25 | }; 26 | 27 | export default { 28 | fetchHome, 29 | fetchColumn, 30 | fetchOneMovie 31 | }; 32 | -------------------------------------------------------------------------------- /server/controllers/demo.js: -------------------------------------------------------------------------------- 1 | import demoApi from '../api/demo'; 2 | 3 | // 首页 4 | let home = async function (ctx, next) { 5 | let result = await demoApi.fetchHome(); 6 | await ctx.render('index.html', { 7 | seo: result.seo, 8 | data: { 9 | home: result.data 10 | } 11 | }); 12 | 13 | await next(); 14 | }; 15 | 16 | // 栏目 17 | let column = async function (ctx, next) { 18 | let result = await demoApi.fetchColumn(); 19 | await ctx.render('index.html', { 20 | seo: result.seo, 21 | data: { 22 | column: result.data 23 | } 24 | }); 25 | 26 | await next(); 27 | }; 28 | 29 | // 详细 30 | let detail = async function (ctx, next) { 31 | const { id } = ctx.params; 32 | let result = await demoApi.fetchOneMovie(id); 33 | await ctx.render('index.html', { 34 | seo: result.seo, 35 | data: { 36 | detail: result.data 37 | } 38 | }); 39 | 40 | await next(); 41 | }; 42 | 43 | module.exports = { 44 | home, 45 | column, 46 | detail 47 | }; 48 | -------------------------------------------------------------------------------- /server/dist/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | require('@babel/register')({ 2 | presets: [ '@babel/preset-env' ] 3 | }); 4 | require('@babel/polyfill'); 5 | require('./app.js'); 6 | -------------------------------------------------------------------------------- /server/log/logger.js: -------------------------------------------------------------------------------- 1 | import log4js from 'log4js'; 2 | import path from 'path'; 3 | log4js.configure( 4 | { 5 | appenders: { 6 | templateFile: { 7 | type: 'dateFile', 8 | filename: `${path.join(__dirname, '../../', 'logs/render/index.log')}`, 9 | pattern: '-yyyy-MM-dd.log', 10 | alwaysIncludePattern: true, 11 | compress: false, 12 | encoding: 'utf-8', 13 | category : 'templateFile' 14 | }, 15 | dateFile: { 16 | type: 'dateFile', 17 | filename: `${path.join(__dirname, '../../', 'logs/date/index.log')}`, 18 | pattern: '-yyyy-MM-dd.log', 19 | alwaysIncludePattern: true, 20 | compress: false, 21 | encoding: 'utf-8', 22 | category : 'dateFile' 23 | }, 24 | errorFile: { 25 | type: 'dateFile', 26 | filename: `${path.join(__dirname, '../../', 'log/error/index.log')}`, 27 | pattern: '-yyyy-MM-dd.log', 28 | alwaysIncludePattern: true, 29 | compress: false, 30 | encoding: 'utf-8', 31 | category : 'errorFile' 32 | }, 33 | timeFile: { 34 | type: 'dateFile', 35 | filename: `${path.join(__dirname, '../../', 'log/request/index.log')}`, 36 | pattern: '-yyyy-MM-dd.log', 37 | alwaysIncludePattern: true, 38 | compress: false, 39 | encoding: 'utf-8', 40 | category : 'timeFile' 41 | }, 42 | mail: { 43 | type: '@log4js-node/smtp', 44 | recipients: 'wjx2@meitu.com,wlw1@meitu.com,srb@meitu.com,lyx4@meitu.com', 45 | sendInterval: 600, 46 | transport: 'SMTP', 47 | subject: '错误报警-PC_MAIL', 48 | sender: '1974740999@qq.com', 49 | SMTP: { 50 | host: 'smtp.qq.com', 51 | secureConnection: true, 52 | port: 465, 53 | auth: { 54 | user: '1974740999@qq.com', 55 | pass: 'dqwkmmcfjfdxbcig' 56 | }, 57 | debug: true 58 | } 59 | } 60 | }, 61 | categories: { 62 | templateFile: { appenders: ['templateFile'], level: 'trace' }, 63 | dateFile: { appenders: ['dateFile'], level: 'trace' }, 64 | timeFile: { appenders: ['timeFile'], level: 'trace' }, 65 | errorFile: { appenders: ['errorFile'], level: 'error' }, 66 | mail: { appenders: ['mail'], level: 'error' }, 67 | default: { appenders: ['dateFile', 'errorFile'], level: 'trace' } 68 | } 69 | } 70 | ); 71 | -------------------------------------------------------------------------------- /server/middlewares/catchError.js: -------------------------------------------------------------------------------- 1 | import Config from '../config'; 2 | export default () => { 3 | return async (ctx, next) => { 4 | try { 5 | await next(); 6 | if (ctx.status === 404) { 7 | ctx.throw(404); 8 | } 9 | } catch (err) { 10 | const status = err.status || 500; 11 | ctx.status = status; 12 | if (status === 404) { 13 | // ctx.response.redirect(`${ctx.request.protocol}://${Config.host}/h5/404.html`); 14 | } else if (status === 500) { 15 | // ctx.response.redirect(`${ctx.request.protocol}://${Config.host}/h5/500.html`); 16 | } 17 | } 18 | }; 19 | }; -------------------------------------------------------------------------------- /server/middlewares/timeLogger.js: -------------------------------------------------------------------------------- 1 | import log4js from 'log4js'; 2 | const loggerTime = log4js.getLogger('timeFile'); 3 | export default () => { 4 | return async (ctx, next) => { 5 | let url = ctx.request.url; 6 | if (url.indexOf('/api/') !== -1) { 7 | await next(); 8 | } else { 9 | let timestamp1 = (new Date()).valueOf(); 10 | let index_sq = `request: ${timestamp1}`; 11 | loggerTime.trace(`${index_sq} start ${url}`); 12 | await next(); 13 | loggerTime.trace(`${index_sq},${(new Date()).valueOf() - timestamp1}ms,${url}`); 14 | } 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /server/nohup.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coocssweb/react-ssr/9a5f43c04166d55f699b9e093e5537f7ee7d3918/server/nohup.log -------------------------------------------------------------------------------- /server/routes/api/demo.js: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router'; 2 | import ApiControl from '../../controllers/api/demo'; 3 | 4 | let router = new Router({ 5 | prefix: '/api' 6 | }); 7 | router.get('/home', ApiControl.fetchHome); 8 | router.get('/column', ApiControl.fetchColumn); 9 | router.get('/movie/:id', ApiControl.fetchOneMovie); 10 | 11 | export default router; -------------------------------------------------------------------------------- /server/routes/demo.js: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router'; 2 | import demoControl from '../controllers/demo'; 3 | 4 | let router = new Router({ 5 | prefix: '/' 6 | }); 7 | 8 | router.get('/', demoControl.home); 9 | router.get('column', demoControl.column); 10 | router.get('detail/:id', demoControl.detail); 11 | 12 | export default router; 13 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router'; 2 | import demo from './demo'; 3 | import demoApi from './api/demo'; 4 | 5 | let router = Router(); 6 | router.use(demo.routes(), demo.allowedMethods()); 7 | router.use(demoApi.routes(), demoApi.allowedMethods()); 8 | export default router; 9 | -------------------------------------------------------------------------------- /server/templating.js: -------------------------------------------------------------------------------- 1 | import nunjucks from 'nunjucks'; 2 | // react rely on 3 | import React from 'react'; 4 | import { renderToString } from 'react-dom/server'; 5 | import { StaticRouter } from 'react-router' 6 | import { Provider } from "react-redux"; 7 | import Layout from '../src/layout'; 8 | import createStore from '../src/redux/store/createStore'; 9 | 10 | 11 | function createEnv(path, opts) { 12 | let autoescape = opts.autoescape === undefined ? true : opts.autoescape; 13 | let noCache = opts.noCache || false; 14 | let watch = opts.watch || false; 15 | let throwOnUndefined = opts.throwOnUndefined || false; 16 | let env = new nunjucks.Environment( 17 | new nunjucks.FileSystemLoader(path, { 18 | noCache: noCache, 19 | watch: watch, 20 | }), { 21 | autoescape: autoescape, 22 | throwOnUndefined: throwOnUndefined 23 | }); 24 | if (opts.filters) { 25 | for (let filter in opts.filters) { 26 | env.addFilter(filter, opts.filters[filter]); 27 | } 28 | } 29 | return env; 30 | } 31 | 32 | function template(path, opts) { 33 | let ENV = createEnv(path, opts); 34 | return async (ctx, next) => { 35 | ctx.render = function (view, model) { 36 | try { 37 | const store = createStore(model.data); 38 | const reactDomStr = renderToString( 39 | 40 | 41 | 42 | 43 | 44 | ); 45 | 46 | ctx.response.body = ENV.render(view, Object.assign( 47 | // layout组件节点渲染结果 48 | { layout: reactDomStr }, 49 | // seo相关 50 | model.seo || {}, 51 | // 服务端渲染的数据 52 | { data: model.data || { home: {} } } 53 | ) 54 | ); 55 | } catch (err) { 56 | ctx.response.body = ENV.render(view, {meta: err}); 57 | } 58 | ctx.response.type = 'text/html'; 59 | }; 60 | await next(); 61 | }; 62 | } 63 | 64 | export default template; 65 | -------------------------------------------------------------------------------- /server/utils/time.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by cooky on 2018/8/7. 3 | */ 4 | export default (timestamp, fmt = 'yyyy-MM-dd hh:mm:ss') => { 5 | let date = new Date(timestamp); 6 | let o = { 7 | 'M+': date.getMonth() + 1, // 月份 8 | 'd+': date.getDate(), // 日 9 | 'h+': date.getHours(), // 小时 10 | 'm+': date.getMinutes(), // 分 11 | 's+': date.getSeconds(), // 秒 12 | 'S': date.getMilliseconds() // 毫秒 13 | }; 14 | if (/(y+)/.test(fmt)) { 15 | fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length)); 16 | } 17 | 18 | for (let k in o) { 19 | if (new RegExp(`(${k})`).test(fmt)) { 20 | fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (('00' + o[k]).substr(('' + o[k]).length))); 21 | } 22 | } 23 | return fmt; 24 | }; -------------------------------------------------------------------------------- /src/api/base.js: -------------------------------------------------------------------------------- 1 | import 'whatwg-fetch'; 2 | class Base { 3 | request ({ path, data = {}, method = 'GET', requireLogin }) { 4 | const headers = { 5 | 'Accept': 'application/json', 6 | 'Content-Type': 'application/json' 7 | }; 8 | 9 | let settings = { 10 | method, 11 | headers, 12 | mode: 'cors' 13 | }; 14 | 15 | let requestUrl = `${process.env.API}${path}`; 16 | 17 | return new Promise((resolve, reject) => { 18 | fetch(requestUrl, settings).then((response) => { 19 | return response.json(); 20 | }).then((response) => { 21 | resolve(response.data); 22 | }).catch((error) => { 23 | reject(error); 24 | }); 25 | }); 26 | } 27 | } 28 | 29 | export default Base; -------------------------------------------------------------------------------- /src/api/demo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by coocss on 2019/2/26. 3 | */ 4 | import Base from './base'; 5 | 6 | class Demo extends Base { 7 | fetchHome () { 8 | return this.request({ path: 'api/home' }); 9 | } 10 | fetchDetail (id) { 11 | return this.request({ path: `api/movie/${id}` }); 12 | } 13 | } 14 | 15 | export default new Demo(); 16 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import '@scss/base.scss'; 2 | import ReactDOM from 'react-dom'; 3 | import React from 'react'; 4 | import { Provider } from 'react-redux'; 5 | import createStore from './redux/store/createStore'; 6 | import Root from './root/route'; 7 | import './common'; 8 | const store = createStore(window['defaultRenderData']); 9 | 10 | ReactDOM.hydrate( 11 | 12 | 13 | , 14 | document.getElementById('app') 15 | ); -------------------------------------------------------------------------------- /src/assets/scss/base.scss: -------------------------------------------------------------------------------- 1 | @import "base/normalize"; 2 | @import "base/variables"; 3 | -------------------------------------------------------------------------------- /src/assets/scss/base/normalize.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in 9 | * IE on Windows Phone and in iOS. 10 | */ 11 | 12 | html { 13 | line-height: 1.15; /* 1 */ 14 | -ms-text-size-adjust: 100%; /* 2 */ 15 | -webkit-text-size-adjust: 100%; /* 2 */ 16 | } 17 | 18 | /* Sections 19 | ========================================================================== */ 20 | 21 | /** 22 | * Remove the margin in all browsers (opinionated). 23 | */ 24 | 25 | body { 26 | margin: 0; 27 | } 28 | 29 | /** 30 | * Add the correct display in IE 9-. 31 | */ 32 | 33 | article, 34 | aside, 35 | footer, 36 | header, 37 | nav, 38 | section { 39 | display: block; 40 | } 41 | 42 | /** 43 | * Correct the font size and margin on `h1` elements within `section` and 44 | * `article` contexts in Chrome, Firefox, and Safari. 45 | */ 46 | 47 | h1 { 48 | font-size: 2em; 49 | margin: 0.67em 0; 50 | } 51 | 52 | /* Grouping content 53 | ========================================================================== */ 54 | 55 | /** 56 | * Add the correct display in IE 9-. 57 | * 1. Add the correct display in IE. 58 | */ 59 | 60 | figcaption, 61 | figure, 62 | main { /* 1 */ 63 | display: block; 64 | } 65 | 66 | /** 67 | * Add the correct margin in IE 8. 68 | */ 69 | 70 | figure { 71 | margin: 1em 40px; 72 | } 73 | 74 | /** 75 | * 1. Add the correct box sizing in Firefox. 76 | * 2. Show the overflow in Edge and IE. 77 | */ 78 | 79 | hr { 80 | box-sizing: content-box; /* 1 */ 81 | height: 0; /* 1 */ 82 | overflow: visible; /* 2 */ 83 | } 84 | 85 | /** 86 | * 1. Correct the inheritance and scaling of font size in all browsers. 87 | * 2. Correct the odd `em` font sizing in all browsers. 88 | */ 89 | 90 | pre { 91 | font-family: monospace, monospace; /* 1 */ 92 | font-size: 1em; /* 2 */ 93 | } 94 | 95 | /* Text-level semantics 96 | ========================================================================== */ 97 | 98 | /** 99 | * 1. Remove the gray background on active links in IE 10. 100 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 101 | */ 102 | 103 | a { 104 | background-color: transparent; /* 1 */ 105 | -webkit-text-decoration-skip: objects; /* 2 */ 106 | } 107 | 108 | /** 109 | * 1. Remove the bottom border in Chrome 57- and Firefox 39-. 110 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 111 | */ 112 | 113 | abbr[title] { 114 | border-bottom: none; /* 1 */ 115 | text-decoration: underline; /* 2 */ 116 | text-decoration: underline dotted; /* 2 */ 117 | } 118 | 119 | /** 120 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 121 | */ 122 | 123 | b, 124 | strong { 125 | font-weight: inherit; 126 | } 127 | 128 | /** 129 | * Add the correct font weight in Chrome, Edge, and Safari. 130 | */ 131 | 132 | b, 133 | strong { 134 | font-weight: bolder; 135 | } 136 | 137 | /** 138 | * 1. Correct the inheritance and scaling of font size in all browsers. 139 | * 2. Correct the odd `em` font sizing in all browsers. 140 | */ 141 | 142 | code, 143 | kbd, 144 | samp { 145 | font-family: monospace, monospace; /* 1 */ 146 | font-size: 1em; /* 2 */ 147 | } 148 | 149 | /** 150 | * Add the correct font style in Android 4.3-. 151 | */ 152 | 153 | dfn { 154 | font-style: italic; 155 | } 156 | 157 | /** 158 | * Add the correct background and color in IE 9-. 159 | */ 160 | 161 | mark { 162 | background-color: #ff0; 163 | color: #000; 164 | } 165 | 166 | /** 167 | * Add the correct font size in all browsers. 168 | */ 169 | 170 | small { 171 | font-size: 80%; 172 | } 173 | 174 | /** 175 | * Prevent `sub` and `sup` elements from affecting the line height in 176 | * all browsers. 177 | */ 178 | 179 | sub, 180 | sup { 181 | font-size: 75%; 182 | line-height: 0; 183 | position: relative; 184 | vertical-align: baseline; 185 | } 186 | 187 | sub { 188 | bottom: -0.25em; 189 | } 190 | 191 | sup { 192 | top: -0.5em; 193 | } 194 | 195 | /* Embedded content 196 | ========================================================================== */ 197 | 198 | /** 199 | * Add the correct display in IE 9-. 200 | */ 201 | 202 | audio, 203 | video { 204 | display: inline-block; 205 | } 206 | 207 | /** 208 | * Add the correct display in iOS 4-7. 209 | */ 210 | 211 | audio:not([controls]) { 212 | display: none; 213 | height: 0; 214 | } 215 | 216 | /** 217 | * Remove the border on images inside links in IE 10-. 218 | */ 219 | 220 | img { 221 | border-style: none; 222 | } 223 | 224 | /** 225 | * Hide the overflow in IE. 226 | */ 227 | 228 | svg:not(:root) { 229 | overflow: hidden; 230 | } 231 | 232 | /* Forms 233 | ========================================================================== */ 234 | 235 | /** 236 | * 1. Change the font styles in all browsers (opinionated). 237 | * 2. Remove the margin in Firefox and Safari. 238 | */ 239 | 240 | button, 241 | input, 242 | optgroup, 243 | select, 244 | textarea { 245 | font-family: sans-serif; /* 1 */ 246 | font-size: 100%; /* 1 */ 247 | line-height: 1.15; /* 1 */ 248 | margin: 0; /* 2 */ 249 | } 250 | 251 | /** 252 | * Show the overflow in IE. 253 | * 1. Show the overflow in Edge. 254 | */ 255 | 256 | button, 257 | input { /* 1 */ 258 | overflow: visible; 259 | } 260 | 261 | /** 262 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 263 | * 1. Remove the inheritance of text transform in Firefox. 264 | */ 265 | 266 | button, 267 | select { /* 1 */ 268 | text-transform: none; 269 | } 270 | 271 | /** 272 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 273 | * controls in Android 4. 274 | * 2. Correct the inability to style clickable types in iOS and Safari. 275 | */ 276 | 277 | button, 278 | html [type="button"], /* 1 */ 279 | [type="reset"], 280 | [type="submit"] { 281 | -webkit-appearance: button; /* 2 */ 282 | } 283 | 284 | /** 285 | * Remove the inner border and padding in Firefox. 286 | */ 287 | 288 | button::-moz-focus-inner, 289 | [type="button"]::-moz-focus-inner, 290 | [type="reset"]::-moz-focus-inner, 291 | [type="submit"]::-moz-focus-inner { 292 | border-style: none; 293 | padding: 0; 294 | } 295 | 296 | /** 297 | * Restore the focus styles unset by the previous rule. 298 | */ 299 | 300 | button:-moz-focusring, 301 | [type="button"]:-moz-focusring, 302 | [type="reset"]:-moz-focusring, 303 | [type="submit"]:-moz-focusring { 304 | outline: 1px dotted ButtonText; 305 | } 306 | 307 | /** 308 | * Correct the padding in Firefox. 309 | */ 310 | 311 | fieldset { 312 | padding: 0.35em 0.75em 0.625em; 313 | } 314 | 315 | /** 316 | * 1. Correct the text wrapping in Edge and IE. 317 | * 2. Correct the color inheritance from `fieldset` elements in IE. 318 | * 3. Remove the padding so developers are not caught out when they zero out 319 | * `fieldset` elements in all browsers. 320 | */ 321 | 322 | legend { 323 | box-sizing: border-box; /* 1 */ 324 | color: inherit; /* 2 */ 325 | display: table; /* 1 */ 326 | max-width: 100%; /* 1 */ 327 | padding: 0; /* 3 */ 328 | white-space: normal; /* 1 */ 329 | } 330 | 331 | /** 332 | * 1. Add the correct display in IE 9-. 333 | * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. 334 | */ 335 | 336 | progress { 337 | display: inline-block; /* 1 */ 338 | vertical-align: baseline; /* 2 */ 339 | } 340 | 341 | /** 342 | * Remove the default vertical scrollbar in IE. 343 | */ 344 | 345 | textarea { 346 | overflow: auto; 347 | } 348 | 349 | /** 350 | * 1. Add the correct box sizing in IE 10-. 351 | * 2. Remove the padding in IE 10-. 352 | */ 353 | 354 | [type="checkbox"], 355 | [type="radio"] { 356 | box-sizing: border-box; /* 1 */ 357 | padding: 0; /* 2 */ 358 | } 359 | 360 | /** 361 | * Correct the cursor style of increment and decrement buttons in Chrome. 362 | */ 363 | 364 | [type="number"]::-webkit-inner-spin-button, 365 | [type="number"]::-webkit-outer-spin-button { 366 | height: auto; 367 | } 368 | 369 | /** 370 | * 1. Correct the odd appearance in Chrome and Safari. 371 | * 2. Correct the outline style in Safari. 372 | */ 373 | 374 | [type="search"] { 375 | -webkit-appearance: textfield; /* 1 */ 376 | outline-offset: -2px; /* 2 */ 377 | } 378 | 379 | /** 380 | * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. 381 | */ 382 | 383 | [type="search"]::-webkit-search-cancel-button, 384 | [type="search"]::-webkit-search-decoration { 385 | -webkit-appearance: none; 386 | } 387 | 388 | /** 389 | * 1. Correct the inability to style clickable types in iOS and Safari. 390 | * 2. Change font properties to `inherit` in Safari. 391 | */ 392 | 393 | ::-webkit-file-upload-button { 394 | -webkit-appearance: button; /* 1 */ 395 | font: inherit; /* 2 */ 396 | } 397 | 398 | /* Interactive 399 | ========================================================================== */ 400 | 401 | /* 402 | * Add the correct display in IE 9-. 403 | * 1. Add the correct display in Edge, IE, and Firefox. 404 | */ 405 | 406 | details, /* 1 */ 407 | menu { 408 | display: block; 409 | } 410 | 411 | /* 412 | * Add the correct display in all browsers. 413 | */ 414 | 415 | summary { 416 | display: list-item; 417 | } 418 | 419 | /* Scripting 420 | ========================================================================== */ 421 | 422 | /** 423 | * Add the correct display in IE 9-. 424 | */ 425 | 426 | canvas { 427 | display: inline-block; 428 | } 429 | 430 | /** 431 | * Add the correct display in IE. 432 | */ 433 | 434 | template { 435 | display: none; 436 | } 437 | 438 | /* Hidden 439 | ========================================================================== */ 440 | 441 | /** 442 | * Add the correct display in IE 10-. 443 | */ 444 | 445 | [hidden] { 446 | display: none; 447 | } 448 | -------------------------------------------------------------------------------- /src/assets/scss/base/variables.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 基础样式变量 3 | * 用于定于全局基础颜色、基础size 4 | */ 5 | // 全局宽度 6 | $globalWidth: 750px; 7 | $globalBgColor: #f6f6f6; 8 | 9 | // 颜色 10 | $colorWhite: #fff; 11 | $colorBlack: #000; 12 | $colorBlackLite: #333; 13 | $colorGray: #999; 14 | $colorGrayLite: #ccc; 15 | $colorGreen: #42bd56; 16 | $colorGreenLite: #effaf0; 17 | $colorOrange: #ffb712; 18 | 19 | // 大小 20 | $sizeXS: 12px; 21 | $sizeS: 14px; 22 | $sizeM: 16px; 23 | $sizeL: 24px; 24 | $sizeXL: 32px; -------------------------------------------------------------------------------- /src/assets/scss/common.scss: -------------------------------------------------------------------------------- 1 | @import "base/variables"; 2 | body{ 3 | background-color: $globalBgColor; 4 | font-family: "Helvetica Neue",Helvetica,Roboto,Arial,sans-serif; 5 | font-size: 12PX; 6 | } 7 | h1, h2, h3, h4, h5, div, p{ 8 | padding: 0px; 9 | margin: 0px; 10 | } 11 | .layout{ 12 | max-width: $globalWidth; 13 | margin: 0px auto; 14 | } 15 | #app{ 16 | 17 | } 18 | .clearfix{ 19 | &:after{ 20 | content: ''; 21 | display: block; 22 | height: 0px; 23 | clear: both; 24 | } 25 | } 26 | 27 | @import "pages/home"; 28 | @import "pages/detail"; 29 | 30 | @import "components/movie"; -------------------------------------------------------------------------------- /src/assets/scss/components/movie.scss: -------------------------------------------------------------------------------- 1 | .movie{ 2 | display: block; 3 | text-decoration: none; 4 | width: 45%; 5 | margin-bottom: 20px; 6 | color: #000; 7 | &-photo{ 8 | display: block; 9 | width: 100%; 10 | padding-bottom: 145%; 11 | background-size: cover; 12 | background-position: center; 13 | } 14 | &-info{ 15 | 16 | } 17 | &-title{ 18 | overflow: hidden; 19 | white-space: nowrap; 20 | text-overflow: ellipsis; 21 | font-size: 16px; 22 | margin-top: 10px; 23 | font-weight: normal; 24 | } 25 | &-rating{ 26 | margin-top: 10px; 27 | color: #999; 28 | } 29 | } -------------------------------------------------------------------------------- /src/assets/scss/pages/detail.scss: -------------------------------------------------------------------------------- 1 | .detail{ 2 | background-color: #ffffff; 3 | padding: 20px; 4 | &-title{ 5 | font-size: 24px; 6 | line-height: 24px; 7 | word-break: break-all; 8 | } 9 | &-info{ 10 | margin-top: 20px; 11 | } 12 | &-left{ 13 | margin-right: 120px; 14 | line-height: 1.5; 15 | } 16 | &-rating{ 17 | color: $colorGray; 18 | } 19 | &-meta{ 20 | margin-top: 10px; 21 | } 22 | &-right{ 23 | float: right; 24 | } 25 | &-photo{ 26 | display: block; 27 | width: 100px; 28 | } 29 | &-btns{ 30 | margin-top: 30px; 31 | display: flex; 32 | justify-content: center; 33 | } 34 | &-btn{ 35 | display: block; 36 | height: 30px; 37 | line-height: 30px; 38 | border: 1px solid $colorOrange; 39 | border-radius: 3px; 40 | color: $colorOrange; 41 | font-size: 15px; 42 | text-align: center; 43 | background-color: transparent; 44 | flex: 1; 45 | outline: none; 46 | &:first-child{ 47 | margin-right: 20px; 48 | } 49 | } 50 | &-desc{ 51 | margin-top: 30px; 52 | line-height: 1.5; 53 | color: $colorGray; 54 | } 55 | &-back{ 56 | display: block; 57 | height: 40px; 58 | line-height: 40px; 59 | width: 100%; 60 | border-radius: 3px; 61 | color: $colorWhite; 62 | background-color: $colorGreen; 63 | font-size: 15px; 64 | text-align: center; 65 | outline: none; 66 | border:none; 67 | margin-top: 40px; 68 | } 69 | } -------------------------------------------------------------------------------- /src/assets/scss/pages/home.scss: -------------------------------------------------------------------------------- 1 | .home{ 2 | &Banner{ 3 | &-photo{ 4 | display: block; 5 | width: 100%; 6 | } 7 | } 8 | 9 | &Nav{ 10 | display: flex; 11 | align-content: space-around; 12 | background-color: #fff; 13 | &-link{ 14 | display: block; 15 | width: 25%; 16 | text-align: center; 17 | padding: 10px 0px; 18 | text-decoration: none; 19 | } 20 | &-icon{ 21 | width: 44px; 22 | height: 44px; 23 | box-sizing: border-box; 24 | background: no-repeat 50%; 25 | background-size: contain; 26 | display: inline-block; 27 | border-radius: 50%; 28 | &--home{ 29 | background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDQiIGhlaWdodD0iNDQiIHZpZXdCb3g9IjAgMCA0NCA0NCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZGVmcz48bGluZWFyR3JhZGllbnQgeDE9IjAlIiB5MT0iMCUiIHkyPSIxMDAlIiBpZD0iYSI+PHN0b3Agc3RvcC1jb2xvcj0iI0ZEOEZBNCIgb2Zmc2V0PSIwJSIvPjxzdG9wIHN0b3AtY29sb3I9IiNGMTYwNzAiIG9mZnNldD0iMTAwJSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxnIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbD0ibm9uZSI+PGNpcmNsZSBmaWxsPSJ1cmwoI2EpIiBjeD0iMjIiIGN5PSIyMiIgcj0iMjIiLz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMiAxMSkiIGZpbGw9IiNGRkYiPjxyZWN0IHg9IjMiIHk9IjkiIHdpZHRoPSIyIiBoZWlnaHQ9IjgiIHJ4PSIuNSIvPjxyZWN0IHg9IjciIHk9IjkiIHdpZHRoPSIyIiBoZWlnaHQ9IjgiIHJ4PSIuNSIvPjxyZWN0IHg9IjExIiB5PSI5IiB3aWR0aD0iMiIgaGVpZ2h0PSI4IiByeD0iLjUiLz48cmVjdCB4PSIxNSIgeT0iOSIgd2lkdGg9IjIiIGhlaWdodD0iOCIgcng9Ii41Ii8+PHBhdGggZD0iTTExLjE3Ni44NTZsNy4zMzcgNS4zMzVBMSAxIDAgMCAxIDE3LjkyNSA4SDIuMDc1YTEgMSAwIDAgMS0uNTg4LTEuODA5TDguODI0Ljg1NmEyIDIgMCAwIDEgMi4zNTIgMHoiLz48cmVjdCB5PSIxOSIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIiIHJ4PSIxIi8+PHJlY3QgeD0iMiIgeT0iMTgiIHdpZHRoPSIxNiIgaGVpZ2h0PSIyIiByeD0iMSIvPjwvZz48L2c+PC9zdmc+Cg==); 30 | } 31 | &--column{ 32 | background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSIgdmlld0JveD0iMCAwIDQ1IDQ1Ij48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImEiIHgxPSIxNS4wMjQlIiB4Mj0iODEuNzIlIiB5MT0iMTcuNDgxJSIgeTI9Ijg1LjI3NSUiPjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiNGREJFNDEiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNGQ0E0MUQiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48ZyBmaWxsPSJub25lIj48Y2lyY2xlIGN4PSIyMzUiIGN5PSI0MiIgcj0iMjIiIGZpbGw9InVybCgjYSkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMTIgLTE5KSIvPjxnIGZpbGw9IiNGRkYiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE0IDE0KSI+PHJlY3Qgd2lkdGg9IjgiIGhlaWdodD0iOCIgcng9IjIiLz48cmVjdCB3aWR0aD0iOCIgaGVpZ2h0PSI4IiB5PSIxMCIgcng9IjIiLz48cmVjdCB3aWR0aD0iOCIgaGVpZ2h0PSI4IiB4PSIxMCIgcng9IjIiLz48cmVjdCB3aWR0aD0iOCIgaGVpZ2h0PSI4IiB4PSI5LjY1NyIgeT0iOS42NTciIHJ4PSIyIiB0cmFuc2Zvcm09InJvdGF0ZSg0NSAxMy42NTcgMTMuNjU3KSIvPjwvZz48L2c+PC9zdmc+Cg==); 33 | } 34 | &--find{ 35 | background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NSIgaGVpZ2h0PSI0NSIgdmlld0JveD0iMCAwIDQ1IDQ1Ij48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImEiIHgxPSIxNS43ODQlIiB4Mj0iODMuNDI1JSIgeTE9IjE1LjE1OCUiIHkyPSI4NS41MjYlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjNzM5REY4Ii8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjNTc3Q0Q2Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48Y2lyY2xlIGN4PSIxNDIiIGN5PSI0MiIgcj0iMjIiIGZpbGw9InVybCgjYSkiIGZpbGwtcnVsZT0ibm9uemVybyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTExOSAtMTkpIi8+PGNpcmNsZSBjeD0iMTQyIiBjeT0iNDIiIHI9IjExIiBmaWxsPSIjRkZGIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTE5IC0xOSkiLz48cGF0aCBmaWxsPSIjNUY4NURGIiBzdHJva2U9IiM1Rjg1REYiIGQ9Ik0yMS4zMjYgMjAuODM2bDYuMTczLTIuODgxYS40LjQgMCAwIDEgLjUzMi41MzFMMjUuMTUgMjQuNjZhMSAxIDAgMCAxLS40ODMuNDgzbC02LjE3NCAyLjg4MWEuNC40IDAgMCAxLS41MzItLjUzMWwyLjg4MS02LjE3NGExIDEgMCAwIDEgLjQ4NC0uNDgzeiIvPjxjaXJjbGUgY3g9IjE0MiIgY3k9IjQyIiByPSIxIiBmaWxsPSIjRkZGIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTE5IC0xOSkiLz48L2c+PC9zdmc+Cg==); 36 | } 37 | &--self{ 38 | background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDQiIGhlaWdodD0iNDQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxkZWZzPjxsaW5lYXJHcmFkaWVudCB4MT0iMTYuNTYzJSIgeTE9IjE0LjgzMiUiIHgyPSI4Mi4zODclIiB5Mj0iODYuNCUiIGlkPSJhIj48c3RvcCBzdG9wLWNvbG9yPSIjNjdDQzVGIiBvZmZzZXQ9IjAlIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzUxQUY0NiIgb2Zmc2V0PSIxMDAlIi8+PC9saW5lYXJHcmFkaWVudD48cGF0aCBkPSJNMjIgMjNhNSA1IDAgMSAxIDAtMTAgNSA1IDAgMCAxIDAgMTB6bS0zLjM2IDJoNi43MkE0LjgyNiA0LjgyNiAwIDAgMSAzMCAyOC41bC4yNzIuOTVBMiAyIDAgMCAxIDI4LjM0OSAzMkgxNS42NWEyIDIgMCAwIDEtMS45MjMtMi41NUwxNCAyOC41YTQuODI2IDQuODI2IDAgMCAxIDQuNjQtMy41eiIgaWQ9ImIiLz48L2RlZnM+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48Y2lyY2xlIGZpbGw9InVybCgjYSkiIGZpbGwtcnVsZT0ibm9uemVybyIgY3g9IjIyIiBjeT0iMjIiIHI9IjIyIi8+PHVzZSBmaWxsPSIjRkZGIiB4bGluazpocmVmPSIjYiIvPjwvZz48L3N2Zz4K); 39 | } 40 | } 41 | &-name{ 42 | margin-top: 6px; 43 | text-align: center; 44 | font-size: 13px; 45 | line-height: 16px; 46 | color: #818181; 47 | } 48 | } 49 | 50 | &Hot{ 51 | background-color: #fff; 52 | margin-top: 10px; 53 | &-title{ 54 | margin: 0px; 55 | padding: 20px; 56 | font-size: 19px; 57 | line-height: 19px; 58 | 59 | } 60 | &-content{ 61 | display: flex; 62 | justify-content: space-between; 63 | flex-wrap: wrap; 64 | margin: 0px 20px; 65 | 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/common/index.js: -------------------------------------------------------------------------------- 1 | import '@scss/common.scss'; 2 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import movie from './movie'; 2 | 3 | export const Movie = movie; 4 | -------------------------------------------------------------------------------- /src/components/movie/Index.jsx: -------------------------------------------------------------------------------- 1 | // import './index.scss'; 2 | import React, {Component} from 'react'; 3 | import propTypes from 'prop-types'; 4 | import className from 'classnames'; 5 | import { NavLink } from 'react-router-dom'; 6 | 7 | class Index extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = {}; 11 | } 12 | 13 | render() { 14 | const { props } = this; 15 | return ( 16 | 17 |
18 |
19 |

{ props.title }

20 |
{ props.score } 分
21 |
22 | 23 | ); 24 | } 25 | } 26 | 27 | export default Index; 28 | -------------------------------------------------------------------------------- /src/containers/detail.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Detail from '../pages/detail'; 3 | import * as detailActions from '../redux/actions/detail'; 4 | 5 | function mapStateToProps (state) { 6 | let data = state['detail']; 7 | return { 8 | ...data 9 | }; 10 | } 11 | 12 | function mapDispatchToProps (dispatch) { 13 | return { 14 | fetchOne: (id) => dispatch(detailActions.fetchOne(id)), 15 | }; 16 | } 17 | 18 | export default connect(mapStateToProps, mapDispatchToProps)(Detail); 19 | -------------------------------------------------------------------------------- /src/containers/devTool.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevTools } from 'redux-devtools'; 3 | import LogMonitor from 'redux-devtools-log-monitor'; 4 | import DockMonitor from 'redux-devtools-dock-monitor'; 5 | 6 | export default createDevTools( 7 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /src/containers/home.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Home from '../pages/home'; 3 | import * as homeActions from '../redux/actions/home'; 4 | 5 | function mapStateToProps (state) { 6 | let data = state['home']; 7 | return { 8 | ...data 9 | }; 10 | } 11 | 12 | function mapDispatchToProps (dispatch) { 13 | return { 14 | fetchHome: () => dispatch(homeActions.fetchHome()) 15 | }; 16 | } 17 | 18 | export default connect(mapStateToProps, mapDispatchToProps)(Home); 19 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{ title }} 14 | 15 | 16 | 17 | 18 | {{ layout | safe }} 19 | 22 | 23 | 34 | -------------------------------------------------------------------------------- /src/layout/Index.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import propTypes from 'prop-types'; 3 | import className from 'classnames'; 4 | import { Route, Switch } from 'react-router-dom'; 5 | import Home from '../containers/home'; 6 | import Detail from '../containers/detail'; 7 | 8 | class Index extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = {}; 12 | } 13 | 14 | render() { 15 | return ( 16 |
17 | 18 | 19 | 20 | 21 |
22 | ); 23 | } 24 | } 25 | 26 | export default Index; 27 | -------------------------------------------------------------------------------- /src/pages/column/Index.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import propTypes from 'prop-types'; 3 | import className from 'classnames'; 4 | import { NavLink } from 'react-router-dom'; 5 | 6 | class Index extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = {}; 10 | } 11 | 12 | render() { 13 | return ( 14 |
15 |
16 | 17 |
18 |
19 | 动作 20 |
21 |
22 | 23 |
24 |
25 | ); 26 | } 27 | } 28 | 29 | export default Index; 30 | -------------------------------------------------------------------------------- /src/pages/detail/Index.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import propTypes from 'prop-types'; 3 | import className from 'classnames'; 4 | 5 | class Index extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | movie: props.movie 10 | }; 11 | } 12 | 13 | static getDerivedStateFromProps (nextProps, prevState) { 14 | return { 15 | movie: nextProps.movie 16 | }; 17 | } 18 | 19 | shouldComponentUpdate (nextProps, prevState ) { 20 | return this.state.movie.id !== prevState.movie.id 21 | } 22 | 23 | componentDidMount () { 24 | const { id } = this.props.match.params; 25 | if (id && this.state.movie.id !== id) { 26 | this.props.fetchOne(id); 27 | } 28 | } 29 | 30 | handleTodoClick () { 31 | alert('想看'); 32 | } 33 | 34 | handleDoneClick () { 35 | alert('看过'); 36 | } 37 | 38 | handleBackClick () { 39 | const { history } = this.props; 40 | if (history.length > 1) { 41 | history.goBack(); 42 | } else { 43 | history.replace(''); 44 | } 45 | } 46 | 47 | render() { 48 | let { movie } = this.state; 49 | return ( 50 |
51 |

{ movie.title }

52 |
53 |
54 | 55 |
56 |
57 |
58 | { movie.score } 59 |
60 |

61 | { movie.meta } 62 |

63 |
64 |
65 |
66 | 68 | 70 |
71 |
72 | { movie.desc } 73 |
74 | 75 | 76 |
77 | ); 78 | } 79 | } 80 | 81 | export default Index; 82 | -------------------------------------------------------------------------------- /src/pages/home/Index.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import propTypes from 'prop-types'; 3 | import className from 'classnames'; 4 | import { NavLink } from 'react-router-dom'; 5 | import { Movie } from '../../components'; 6 | 7 | class Index extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | banner: props.banner, 12 | movies: props.movies 13 | }; 14 | } 15 | 16 | static getDerivedStateFromProps (nextProps, prevState) { 17 | return { 18 | banner: nextProps.banner, 19 | movies: nextProps.movies 20 | } 21 | } 22 | 23 | componentDidMount () { 24 | if (this.state.movies.length === 0) { 25 | this.props.fetchHome(); 26 | } 27 | } 28 | 29 | render() { 30 | const { state } = this; 31 | let banner = state.banner; 32 | let movies = state.movies; 33 | return ( 34 |
35 |
36 | 37 |
38 |
39 | 40 |
41 |
影院首页
42 |
43 | 44 |
45 |
精彩栏目
46 |
47 | 48 |
49 |
精彩发现
50 |
51 | 52 |
53 |
我关心的
54 |
55 |
56 |
57 |

影院热映

58 |
59 | { 60 | movies.map((movie) => { 61 | return () 62 | }) 63 | } 64 |
65 |
66 |
67 | ); 68 | } 69 | } 70 | 71 | export default Index; 72 | -------------------------------------------------------------------------------- /src/redux/actions/detail.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by coocss on 2019/2/28. 3 | */ 4 | import * as detailActionTypes from '../constants/detail'; 5 | import demoApi from '../../api/demo'; 6 | 7 | export const fetchOne = (id) => { 8 | return { 9 | types: [detailActionTypes.DETAIL_FETCH_REQUEST, detailActionTypes.DETAIL_FETCH_SUCCESS, detailActionTypes.DETAIL_FETCH_ERROR], 10 | promise: () => { 11 | return demoApi.fetchDetail(id); 12 | } 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /src/redux/actions/home.js: -------------------------------------------------------------------------------- 1 | import * as homeActionTypes from '../constants/home'; 2 | import demoApi from '../../api/demo'; 3 | 4 | export const fetchHome = () => { 5 | return { 6 | types: [homeActionTypes.HOME_FETCH_REQUEST, homeActionTypes.HOME_FETCH_SUCCESS, homeActionTypes.HOME_FETCH_ERROR], 7 | promise: () => { 8 | return demoApi.fetchHome(); 9 | } 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/redux/constants/detail.js: -------------------------------------------------------------------------------- 1 | // 获取详情页 2 | export const DETAIL_FETCH_REQUEST = 'DETAIL_FETCH_REQUEST'; 3 | export const DETAIL_FETCH_SUCCESS = 'DETAIL_FETCH_SUCCESS'; 4 | export const DETAIL_FETCH_ERROR = 'DETAIL_FETCH_ERROR'; 5 | -------------------------------------------------------------------------------- /src/redux/constants/home.js: -------------------------------------------------------------------------------- 1 | // 获取首页 2 | export const HOME_FETCH_REQUEST = 'HOME_FETCH_REQUEST'; 3 | export const HOME_FETCH_SUCCESS = 'HOME_FETCH_SUCCESS'; 4 | export const HOME_FETCH_ERROR = 'HOME_FETCH_ERROR'; 5 | -------------------------------------------------------------------------------- /src/redux/middlewares/promiseMiddleware.js: -------------------------------------------------------------------------------- 1 | export default ({ dispatch, getState }) => { 2 | return (next) => (action) => { 3 | const { promise, types, ...rest } = action; 4 | 5 | if (!promise) { 6 | return next(action); 7 | } 8 | 9 | const [REQUEST, SUCCESS, FAILURE] = types; 10 | 11 | next({ ...rest, type: REQUEST }); 12 | 13 | return promise().then( 14 | (result) => { 15 | next({ ...rest, result, type: SUCCESS }); 16 | }, 17 | (error) => { 18 | next({ ...rest, error, type: FAILURE }); 19 | } 20 | ); 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /src/redux/reducers/detail.js: -------------------------------------------------------------------------------- 1 | import * as detailActionTypes from '../constants/detail'; 2 | // import Immutable from 'immutable'; 3 | 4 | const initialState = { 5 | loading: false, 6 | movie: {} 7 | }; 8 | 9 | export default (state = initialState, action) => { 10 | switch (action.type) { 11 | case detailActionTypes.DETAIL_FETCH_REQUEST: 12 | return Object.assign({}, state, { loading: true }); 13 | case detailActionTypes.DETAIL_FETCH_SUCCESS: 14 | return Object.assign({}, state, { loading: false }, { movie: action.result.movie }); 15 | case detailActionTypes.DETAIL_FETCH_ERROR: 16 | return Object.assign({}, state, { loading: false }); 17 | default: 18 | return state; 19 | } 20 | }; -------------------------------------------------------------------------------- /src/redux/reducers/home.js: -------------------------------------------------------------------------------- 1 | import * as homeActionTypes from '../constants/home'; 2 | // import Immutable from 'immutable'; 3 | 4 | const initialState = { 5 | loading: false, 6 | banner: '', 7 | movies: [] 8 | }; 9 | 10 | export default (state = initialState, action) => { 11 | switch (action.type) { 12 | case homeActionTypes.HOME_FETCH_REQUEST: 13 | return Object.assign({}, state, { loading: true }); 14 | case homeActionTypes.HOME_FETCH_SUCCESS: 15 | const { movies, banner } = action.result; 16 | return Object.assign({}, state, { loading: false }, { movies, banner }); 17 | case homeActionTypes.HOME_FETCH_ERROR: 18 | return Object.assign({}, state, { loading: false }); 19 | default: 20 | return state; 21 | } 22 | }; -------------------------------------------------------------------------------- /src/redux/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import home from './home'; 3 | import detail from './detail'; 4 | 5 | export default combineReducers({ 6 | home, 7 | detail 8 | }); 9 | -------------------------------------------------------------------------------- /src/redux/store/createStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import promiseMiddleware from '../middlewares/promiseMiddleware'; 3 | import RootReducer from '../reducers'; 4 | 5 | export default (data) => { 6 | let enhancer; 7 | 8 | if (process.env.NODE_ENV === 'development') { 9 | enhancer = compose( 10 | applyMiddleware( 11 | promiseMiddleware 12 | ), 13 | // window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), 14 | // DevTool.instrument() 15 | ); 16 | } else { 17 | enhancer = compose( 18 | applyMiddleware( 19 | promiseMiddleware 20 | ) 21 | ); 22 | } 23 | 24 | let finalCreateStore = enhancer(createStore); 25 | 26 | return finalCreateStore(RootReducer, data); 27 | }; 28 | -------------------------------------------------------------------------------- /src/root/route.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 3 | import Layout from '../layout'; 4 | 5 | export default function () { 6 | return ( 7 | 8 | 9 | 10 | 11 | ); 12 | }; 13 | --------------------------------------------------------------------------------