├── .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 |
21 |
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 |
--------------------------------------------------------------------------------