├── .babelrc ├── .gitignore ├── README.md ├── config └── config.js ├── docs ├── react技术栈项目结构探究.md ├── redux-saga初体验.md └── 关于项目中的webpack使用.md ├── frontWeb ├── components │ ├── city │ │ ├── actionTypes.js │ │ ├── actions.js │ │ ├── citySaga.js │ │ ├── index.js │ │ ├── reducer.js │ │ └── views │ │ │ ├── City.js │ │ │ ├── CityHeader.js │ │ │ ├── back.png │ │ │ └── style.css │ ├── common │ │ ├── AdCell.js │ │ ├── AdHeader.js │ │ ├── ListCell.js │ │ ├── LoadingMore.js │ │ ├── right-arrow.png │ │ └── style.css │ ├── detail │ │ ├── actionTypes.js │ │ ├── actions.js │ │ ├── detailSaga.js │ │ ├── index.js │ │ ├── reducer.js │ │ └── views │ │ │ ├── Detail.js │ │ │ ├── ShouldKnow.js │ │ │ ├── arrow_down.png │ │ │ ├── broad.png │ │ │ ├── gou.png │ │ │ ├── style.css │ │ │ └── time.png │ ├── home │ │ ├── actionTypes.js │ │ ├── actions.js │ │ ├── homeSaga.js │ │ ├── index.js │ │ ├── reducer.js │ │ └── views │ │ │ ├── Home.js │ │ │ ├── ad │ │ │ ├── Ad.js │ │ │ └── style.css │ │ │ ├── category │ │ │ ├── Category.js │ │ │ └── style.css │ │ │ ├── cheapOrReducers │ │ │ ├── CheapOrReducers.js │ │ │ └── style.css │ │ │ ├── guessULike │ │ │ ├── GuessULike.js │ │ │ └── style.css │ │ │ ├── homeHeader │ │ │ ├── HomeHeader.js │ │ │ ├── arrow_down.png │ │ │ ├── homeHeader.css │ │ │ ├── search.png │ │ │ └── usered.png │ │ │ └── style.css │ ├── notFound │ │ ├── actionTypes.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── reducer.js │ │ └── views │ │ │ └── NotFound.js │ ├── search │ │ ├── actionTypes.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── reducer.js │ │ └── views │ │ │ └── Search.js │ ├── user │ │ ├── actionTypes.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── reducer.js │ │ └── views │ │ │ └── User.js │ └── wrap │ │ ├── actionTypes.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── reducer.js │ │ ├── views │ │ ├── DianpingApp.js │ │ ├── reset.css │ │ └── style.css │ │ └── wrapSaga.js ├── fetchApi │ ├── get.js │ └── post.js ├── index.js ├── reducer.js ├── rootSaga.js └── store.js ├── hotReloadServer.js ├── package.json ├── postcss.config.js ├── record.patch ├── record ├── city.jpg ├── detail_1.jpg ├── detail_2.jpg ├── framework.png ├── home_1.jpg ├── home_2.jpg ├── loading.jpg ├── loading.png ├── play.gif ├── saga.png └── state_tree.gif ├── serverData └── resData.js ├── util └── Util.js ├── webpack.config.dev.js ├── webpack.config.prod.js ├── webpack_chunk.patch └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015","react","stage-0","env"], 3 | "plugins": [ 4 | "react-hot-loader/babel" 5 | ], 6 | "env": { 7 | "production":{ 8 | "preset":["react-optimize"] 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React技术栈实现XXX电商App-Demo 2 | 3 | > 项目地址:https://github.com/Nealyang/React-Fullstack-Dianping-Demo 4 | 5 | > 技术栈:react、react-router4.x 、 react-redux 、 webpack3.x、 redux-saga 、 css-module 、 ES6 、babel... 6 | 7 | >在慕课网看到相关视频,但是我等屌丝码农真心买不起这个价位的视频。有幸看到源代码,但是看到代码的我。。。也不是很苟同上面代码中react技术栈这一套使用方式。遂自己写了一个demo。 8 | 9 | --- 10 | 11 | 一同学习react、node的同学欢迎加入: 12 | 13 | Node.js技术交流群:209530601 14 | 15 | React技术栈:398240621 16 | 17 | --- 18 | 19 | ### 微信公众号 20 | 21 | ![img](https://github.com/Nealyang/PersonalBlog/blob/master/img/wx.jpg) 22 | 23 | > 扫码关注微信公众号,获取第一手文章推送 24 | 25 | ## 项目截图 26 | 27 | * 加载 28 | 29 | 30 | ![loading](./record/loading.jpg) 31 | 32 | * 首页 33 | 34 | ![首页](./record/home_1.jpg) 35 | 36 | 37 | 38 | ![首页](./record/home_2.jpg) 39 | 40 | * 详情页 41 | 42 | 43 | ![detail](./record/detail_1.jpg) 44 | 45 | 46 | ![detail](./record/detail_2.jpg) 47 | 48 | * 城市选择 49 | 50 | ![city](./record/city.jpg) 51 | 52 | ## 项目运行展示(gif) 53 | > 流量党慎入 54 | 55 | ![app运行展示](https://github.com/Nealyang/React-Fullstack-Dianping-Demo/blob/master/record/play.gif) 56 | 57 | ![state树变化](https://github.com/Nealyang/React-Fullstack-Dianping-Demo/blob/master/record/state_tree.gif) 58 | 59 | ***项目内容不多,就涉及到三个页面,主要是为了学习新的知识。项目中用的redux-saga也是前天才学习的。项目的架构也是最近在各种探讨研究。还求大神多指点~*** 60 | 61 | ## 项目技术总结 62 | 63 | - [x] [react技术栈项目结构探究](./docs/react技术栈项目结构探究.md) 64 | - [x] [redux-saga初体验](./docs/redux-saga初体验.md) 65 | - [x] [关于项目中webpack的配置说明](./docs/关于项目中的webpack使用.md) 66 | 67 | ## 项目简单说明 68 | 69 | * 开发react-redux这一套,我个人的理解是 ***Redux体现的是代码分层、职责分离的编程思想,逻辑与视图严格区分。*** 而某网上的这一套代码,逻辑都写到了view组件层,组件需要关心如何获取数据,如何处理数据,这样的组件层是不容易复用的,Redux的使用也是残缺的。甚至这种情况,你不用Redux,直接定义一个全局的state变量,所有组件都来直接操作它好了。 70 | 71 | * 项目还有许多需要完善的地方,redux-saga的使用方式、项目结构、包括webpack3.x配合react的代码优化以及react的Universal渲染甚至后端Node的代码编写。欢迎各路大神前来指教~ 72 | 73 | ## 项目实现 74 | 75 | - [x] react热更新 76 | - [x] css-module使用 77 | - [x] react-redux异步处理 78 | - [x] react-router 浏览器传参、获取 79 | - [x] redux-saga辅助 80 | - [x] 上拉加载更多 81 | - [x] 全局监控Loading 82 | - [x] ...... 83 | 84 | - [ ] Universal渲染 [可参考我另一个项目webpack1.x](https://github.com/Nealyang/neal-teach-website/blob/master/record/framework.md) 85 | 86 | ## 安装步骤 87 | 88 | # clone this demo 89 | git clone ... 90 | 91 | # install dependencies 92 | npm i (or yarn) 93 | 94 | # serve with hot reload at localhost:8000 95 | npm start 96 | 97 | 98 | 99 | ## more 100 | 101 | 后续会继续改进技术,如果有时间会写一个[全栈的demo](https://github.com/Nealyang/React-Express-Blog-Demo),着重后端Node和mongo的使用。前端依旧使用react技术栈完成。 102 | 103 | (^_^)/~~ 104 | -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | host: process.env.HOST || 'localhost', 3 | port:process.env.PORT || (process.env.NODE_ENV === 'production'?8080:3000), 4 | apiHost:process.env.APIHOST || 'localhost', 5 | apiPort:process.env.APIPORT || '3030', 6 | hotReloadHost:'localhost', 7 | hotReloadPort:'8000', 8 | }; -------------------------------------------------------------------------------- /docs/react技术栈项目结构探究.md: -------------------------------------------------------------------------------- 1 | # React技术栈实现大众点评Demo-项目结构探究 2 | 3 | > [参考链接1](http://www.jianshu.com/p/7de6ccb7b76d?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io) 4 | > [参考链接2](https://segmentfault.com/a/1190000010951171) 5 | > [参考链接3](https://marmelab.com/blog/2015/12/17/react-directory-structure.html) 6 | 7 | >[项目地址](https://github.com/Nealyang/React-Fullstack-Dianping-Demo) 8 | 9 | 10 | ## 项目截图 11 | ![首页](https://github.com/Nealyang/React-Fullstack-Dianping-Demo/blob/master/record/home_1.jpg) 12 | 13 | ## React+Redux项目结构探索 14 | 整理学习react技术栈相关知识,在写了一个[电商AppDemo](https://github.com/Nealyang/React-Fullstack-Dianping-Demo)后,开始思考起该如何高效的组织react项目的项目结构。 15 | 16 | ### 按照类型划分(redux官方实例采用的方式) 17 | 目录结构如下: 18 | 19 | app/ 20 | actions/ 21 | a.js 22 | b.js 23 | components/ 24 | a.js 25 | b.js 26 | containers/ 27 | a.js 28 | b.js 29 | reducers/ 30 | a.js 31 | b.js 32 | index.js 33 | 34 | 这是官网demo中的示例写法,在刚开始学习的时候,我的[很多学习demo](很多学习demo)也是按照这种方式去组织的代码结构 35 | 这种结构最直观的就是,*看起来*非常的简单明了。当然,这也可能就是官网为了给我最直接的引导所采用的项目结构。但是在慢慢后面的使用中你会发现很多的弊端。 36 | 37 | 其中最主要的就是在每次增加一个新的功能的开发一个功能模块的时候,你要各种目录下操作。container里写容器,component里写该功能模块的组件。action、reducer...一系列都得改动。 38 | 39 | 所以如此这般的频繁的切换路径,修改不同的文件。当项目比较大的时候,这种目录结构是非常不方便的 40 | 41 | ### 按照功能划分 42 | 按照功能模块划分其实就是目前我这个项目所用到的。当然,此项目目录结构真的不咋滴。乱七八糟。 43 | 44 | ![项目结构](https://github.com/Nealyang/React-Fullstack-Dianping-Demo/blob/master/record/framework.png) 45 | 46 | feature1/ 47 | components/ 48 | actions.js 49 | container.js 50 | index.js 51 | reducer.js 52 | feature2/ 53 | components/ 54 | actions.js 55 | container.js 56 | index.js 57 | reducer.js 58 | index.js 59 | rootReducer.js 60 | 61 | 在《深入浅出React和Redux》一本书中,推荐的就是这种方式,真正的做到组件化,划分到组件、状态和行为都在同一个文件夹里。方便开发,也易于扩展。组件中只要一个index.js去暴露出接口就行。 62 | 63 | 但是这种结构存在一个问题,就是随着应用扩大(即使我这个应用没有几个页面,但是依旧存在了问题)。 64 | 65 | 因为redux会将整个应用状态作为一个store来管理,不同的模块之间可以共享state中的任何一个部分,这种情况下,可能feature1中的dispatch会影响到feature2中的reducer,正如这个demo中出现的。 66 | 67 | 这种情况下,不同模块间的功能被耦合到了一起。 68 | 69 | ### [Ducks](https://github.com/erikras/ducks-modular-redux) 70 | 71 | github上这么介绍的:A proposal for bundling reducers, action types and actions when using Redux. 72 | 73 | 所以这里,我们翻一下。 74 | 75 | 在创建redux应用时,按照功能性划分,每次会都添加`{actionTypes, actions, reducer}`这样的组合。我之前会把它们分成不同的文件,甚至分到不同的文件夹,但是95%的情况下,只有一对 reducer/actions 会用到对应的 actions。 76 | 对我来说,把这些相关的代码放在一个独立的文件中更方便,这样做还可以很容易的打包到软件库/包中。 77 | 78 | // widgets.js 79 | 80 | // Actions 81 | const LOAD = 'my-app/widgets/LOAD'; 82 | const CREATE = 'my-app/widgets/CREATE'; 83 | const UPDATE = 'my-app/widgets/UPDATE'; 84 | const REMOVE = 'my-app/widgets/REMOVE'; 85 | 86 | // Reducer 87 | export default function reducer(state = {}, action = {}) { 88 | switch (action.type) { 89 | // do reducer stuff 90 | default: return state; 91 | } 92 | } 93 | 94 | // Action Creators 95 | export function loadWidgets() { 96 | return { type: LOAD }; 97 | } 98 | 99 | export function createWidget(widget) { 100 | return { type: CREATE, widget }; 101 | } 102 | 103 | export function updateWidget(widget) { 104 | return { type: UPDATE, widget }; 105 | } 106 | 107 | export function removeWidget(widget) { 108 | return { type: REMOVE, widget }; 109 | } 110 | 111 | #### 规则 112 | 113 | 一个模块 ... 114 | * 必须 export default 函数名为 reducer() 的 reducer 115 | * 必须 作为函数 export 它的 action creators 116 | * 必须 把 action types 定义成形为 npm-module-or-app/reducer/ACTION_TYPE 的字符串 117 | * 如果有外部的reducer需要监听这个action type,或者作为可重用的库发布时, 可以 用 UPPER_SNAKE_CASE 形式 export 它的 action types。 118 | 119 | 上述规则也推荐用在可重用的redux 库中用来组织{actionType, action, reducer} 120 | 121 | 本质上是以应用的状态作为模块的划分依据,而不是以界面功能作为划分模块的依据。这样,管理相同状态的依赖都在同一个文件中,不管哪个容器组件需要使用这部分状态,只需要在这个组件中引入这个状态对应的文件即可。 122 | 123 | 整体的目录结构如下: 124 | 125 | components/ (应用级别的通用组件) 126 | containers/ 127 | feature1/ 128 | components/ (功能拆分出的专用组件) 129 | feature1.js (容器组件) 130 | index.js (feature1对外暴露的接口) 131 | redux/ 132 | index.js (combineReducers) 133 | module1.js (reducer, action types, actions creators) 134 | module2.js (reducer, action types, actions creators) 135 | index.js 136 | 137 | 在前两种项目结构中,当container需要使用actions时,可以通过import * as actions from 'path/to/actions.js'方式,一次性把一个action文件中的所有action creators都引入进来。 138 | 但在使用Ducks结构时,action creators和reducer定义在同一个文件中,import *的导入方式会把reducer也导入进来(如果action types也被export,那么还会导入action types)。 139 | 我们可以把action creators和action types定义到一个命名空间中,解决这个问题。修改如下: 140 | 141 | // widget.js 142 | 143 | // Actions 144 | export const types = { 145 | const LOAD : 'widget/LOAD', 146 | const CREATE : 'widget/CREATE', 147 | const UPDATE : 'widget/UPDATE', 148 | const REMOVE : 'widget/REMOVE' 149 | } 150 | 151 | const initialState = { 152 | widget: null, 153 | isLoading: false, 154 | } 155 | 156 | // Reducer 157 | export default function reducer(state = initialState, action = {}) { 158 | switch (action.type) { 159 | types.LOAD: 160 | //... 161 | types.CREATE: 162 | //... 163 | types.UPDATE: 164 | //... 165 | types.REMOVE: 166 | //... 167 | default: return state; 168 | } 169 | } 170 | 171 | // Action Creators 172 | export actions = { 173 | loadWidget: function() { 174 | return { type: types.LOAD }; 175 | }, 176 | createWidget: createWidget(widget) { 177 | return { type: types.CREATE, widget }; 178 | }, 179 | updateWidget: function(widget) { 180 | return { type: types.UPDATE, widget }; 181 | }, 182 | removeWidget: function(widget) { 183 | return { type: types.REMOVE, widget }; 184 | } 185 | } 186 | 187 | 这样,我们在container中使用actions时,可以通过import { actions } from 'path/to/module.js'引入, 188 | 避免了引入额外的对象,也避免了import时把所有action都列出来的繁琐。 189 | 190 | **[下一个项目]((https://github.com/Nealyang/React-Express-Blog-Demo))将会采用第三种方式去组织代码** 191 | 192 | ## 交流 193 | 194 | Node.js技术交流群:209530601 195 | 196 | React技术栈:398240621 197 | 198 | -------------------------------------------------------------------------------- /docs/redux-saga初体验.md: -------------------------------------------------------------------------------- 1 | # React技术栈实现大众点评Demo-初次使用redux-saga 2 | 3 | >[项目地址](https://github.com/Nealyang/React-Fullstack-Dianping-Demo) 4 | 5 | 6 | ## 项目截图 7 | ![首页](https://github.com/Nealyang/React-Fullstack-Dianping-Demo/blob/master/record/home_1.jpg) 8 | 9 | ## redux-saga介绍 10 | 众所周知,react仅仅是作用在View层的前端框架,redux作为前端的“数据库”,完美!但是依旧残留着前端一直以来的诟病=>异步。 11 | 12 | 所以就少不了有很多的中间件(middleware)来处理这些数据,而redux-saga就是其中之一。 13 | 14 | 不要把redux-saga(下面统称为saga)想的多么牛逼,其实他就是一个***辅助函数***,但是荣耀里辅助拿MVP也不少哈~。 15 | 16 | Saga最大的特点就是它可以让你用同步的方式写异步的代码!想象下,如果它能够用来监听你的异步action,然后又用同步的方式去处理。那么,你的react-redux是不是就轻松了很多! 17 | 18 | 官方介绍,请移步[redux-saga](https://github.com/redux-saga/redux-saga) 19 | 20 | saga相当于在redux原有的数据流中多了一层监控,捕获监听到的action,进行处理后,put一个新的action给相应的reducer去处理。 21 | 22 | ## 基本用法 23 | 1、 使用createSagaMiddleware方法创建saga 的Middleware,然后在创建的redux的store时,使用applyMiddleware函数将创建的saga Middleware实例绑定到store上,最后可以调用saga Middleware的run函数来执行某个或者某些Middleware。 24 | 2、 在saga的Middleware中,可以使用takeEvery或者takeLatest等API来监听某个action,当某个action触发后,saga可以使用call、fetch等api发起异步操作,操作完成后使用put函数触发action,同步更新state,从而完成整个State的更新。 25 | 26 | ## saga的优点 27 | 28 | >下面介绍saga的API,boring~~~所以先来点动力吧 29 | 30 | * 流程拆分更细,应用的逻辑和view更加的清晰,分工明确。异步的action和复杂逻辑的action都可以放到saga中去处理。模块更加的干净 31 | * 因为使用了 Generator,redux-saga让你可以用同步的方式写异步代码 32 | * 能容易地测试 Generator 里所有的业务逻辑 33 | * 可以通过监听Action 来进行前端的打点日志记录,减少侵入式打点对代码的侵入程度 34 | * 。。。 35 | 36 | ## 走马观花API(安装啥的步骤直接略过) 37 | 38 | ### takeEvery 39 | 用来监听action,每个action都触发一次,如果其对应是异步操作的话,每次都发起异步请求,而不论上次的请求是否返回 40 | 41 | import { takeEvery } from 'redux-saga/effects' 42 | 43 | function* watchFetchData() { 44 | yield takeEvery('FETCH_REQUESTED', fetchData) 45 | } 46 | ### takeLatest 47 | 作用同takeEvery一样,唯一的区别是它只关注最后,也就是最近一次发起的异步请求,如果上次请求还未返回,则会被取消。 48 | 49 | function* watchFetchData() { 50 | yield takeLatest('FETCH_REQUESTED', fetchData) 51 | } 52 | 53 | ### redux Effects 54 | 在saga的世界里,***所有的任务都通用 yield Effect 来完成***,Effect暂时就理解为一个任务单元吧,其实就是一个JavaScript的对象,可以通过sagaMiddleWare进行执行。 55 | 56 | 重点说明下,在redux-saga的应用中,所有的Effect都必须被yield后才可以被执行。 57 | 58 | import {fork,call} from 'redux-saga/effects' 59 | 60 | import {getAdDataFlow,getULikeDataFlow} from './components/home/homeSaga' 61 | import {getLocatioFlow} from './components/wrap/wrapSaga' 62 | import {getDetailFolw} from './components/detail/detailSaga' 63 | import {getCitiesFlow} from './components/city/citySaga' 64 | 65 | export default function* rootSaga () { 66 | yield fork(getLocatioFlow); 67 | yield fork(getAdDataFlow); 68 | yield fork(getULikeDataFlow); 69 | yield fork(getDetailFolw); 70 | yield fork(getCitiesFlow); 71 | } 72 | ### call 73 | call用来调用异步函数,将异步函数和函数参数作为call函数的参数传入,返回一个js对象。saga引入他的主要作用是方便测试,同时也能让我们的代码更加规范化。 74 | 75 | 同js原生的call一样,call函数也可以指定this对象,只要把this对象当第一个参数传入call方法就好了 76 | 77 | saga同样提供apply函数,作用同call一样,参数形式同js原生apply方法。 78 | 79 | 80 | export function* getAdData(url) { 81 | yield put({type:wrapActionTypes.START_FETCH}); 82 | yield delay(delayTime);//故意的 83 | try { 84 | return yield call(get,url); 85 | } catch (error) { 86 | yield put({type:wrapActionTypes.FETCH_ERROR}) 87 | }finally { 88 | yield put({type:wrapActionTypes.FETCH_END}) 89 | } 90 | } 91 | 92 | export function* getAdDataFlow() { 93 | while (true){ 94 | let request = yield take(homeActionTypes.GET_AD); 95 | let response = yield call(getAdData,request.url); 96 | yield put({type:homeActionTypes.GET_AD_RESULT_DATA,data:response.data}) 97 | } 98 | } 99 | 100 | ### take 101 | 等待 reactjs dispatch 一个匹配的action。take的表现同takeEvery一样,都是监听某个action,但与takeEvery不同的是,他不是每次action触发的时候都相应,而只是在执行顺序执行到take语句时才会相应action。 102 | 103 | 当在genetator中使用take语句等待action时,generator被阻塞,等待action被分发,然后继续往下执行。 104 | 105 | takeEvery只是监听每个action,然后执行处理函数。对于何时响应action和 如何响应action,takeEvery并没有控制权。 106 | 107 | 而take则不一样,我们可以在generator函数中决定何时响应一个action,以及一个action被触发后做什么操作。 108 | 109 | 最大区别:take只有在执行流达到时才会响应对应的action,而takeEvery则一经注册,都会响应action。 110 | 111 | export function* getAdDataFlow() { 112 | while (true){ 113 | let request = yield take(homeActionTypes.GET_AD); 114 | let response = yield call(getAdData,request.url); 115 | yield put({type:homeActionTypes.GET_AD_RESULT_DATA,data:response.data}) 116 | } 117 | } 118 | 119 | ### put 120 | 触发某一个action,类似于react中的dispatch 121 | 122 | 实例如上 123 | 124 | ### select 125 | 126 | 作用和 redux thunk 中的 getState 相同。通常会与reselect库配合使用 127 | 128 | ### fork 129 | 非阻塞任务调用机制:上面我们介绍过call可以用来发起异步操作,但是相对于generator函数来说,call操作是阻塞的,只有等promise回来后才能继续执行,而fork是非阻塞的 ,当调用fork启动一个任务时,该任务在后台继续执行,从而使得我们的执行流能继续往下执行而不必一定要等待返回。 130 | 131 | ### cancel 132 | cancel的作用是用来取消一个还未返回的fork任务。防止fork的任务等待时间太长或者其他逻辑错误。 133 | 134 | ### all 135 | all提供了一种并行执行异步请求的方式。之前介绍过执行异步请求的api中,大都是阻塞执行,只有当一个call操作放回后,才能执行下一个call操作, call提供了一种类似Promise中的all操作,可以将多个异步操作作为参数参入all函数中, 136 | 如果有一个call操作失败或者所有call操作都成功返回,则本次all操作执行完毕。 137 | 138 | import { all, call } from 'redux-saga/effects' 139 | 140 | // correct, effects will get executed in parallel 141 | const [users, repos] = yield all([ 142 | call(fetch, '/users'), 143 | call(fetch, '/repos') 144 | ]) 145 | 146 | ### race 147 | 有时候当我们并行的发起多个异步操作时,我们并不一定需要等待所有操作完成,而只需要有一个操作完成就可以继续执行流。这就是race的用处。 148 | 他可以并行的启动多个异步请求,只要有一个 请求返回(resolved或者reject),race操作接受正常返回的请求,并且将剩余的请求取消。 149 | 150 | import { race, take, put } from 'redux-saga/effects' 151 | 152 | function* backgroundTask() { 153 | while (true) { ... } 154 | } 155 | 156 | function* watchStartBackgroundTask() { 157 | while (true) { 158 | yield take('START_BACKGROUND_TASK') 159 | yield race({ 160 | task: call(backgroundTask), 161 | cancel: take('CANCEL_TASK') 162 | }) 163 | } 164 | } 165 | 166 | ### actionChannel   167 | 在之前的操作中,所有的action分发是顺序的,但是对action的响应是由异步任务来完成,也即是说对action的处理是无序的。 168 | 169 | 如果需要对action的有序处理的话,可以使用actionChannel函数来创建一个action的缓存队列,但一个action的任务流程处理完成后,才可是执行下一个任务流。 170 | 171 | import { take, actionChannel, call, ... } from 'redux-saga/effects' 172 | 173 | function* watchRequests() { 174 | // 1- Create a channel for request actions 175 | const requestChan = yield actionChannel('REQUEST') 176 | while (true) { 177 | // 2- take from the channel 178 | const {payload} = yield take(requestChan) 179 | // 3- Note that we're using a blocking call 180 | yield call(handleRequest, payload) 181 | } 182 | } 183 | 184 | function* handleRequest(payload) { ... } 185 | 186 | >从我写的这个项目可以看到,其实我很多API都是没有用到,常用的基本也就这么些了 187 | 188 | ## 从代码中去记忆API 189 | 这里我放两个实际项目中代码实例,大家可以看看熟悉下上面说到的API 190 | 191 | rootSaga.js 192 | 193 | // This file contains the sagas used for async actions in our app. It's divided into 194 | // "effects" that the sagas call (`authorize` and `logout`) and the actual sagas themselves, 195 | // which listen for actions. 196 | 197 | // Sagas help us gather all our side effects (network requests in this case) in one place 198 | 199 | import {hashSync} from 'bcryptjs' 200 | import genSalt from '../auth/salt' 201 | import {browserHistory} from 'react-router' 202 | import {take, call, put, fork, race} from 'redux-saga/effects' 203 | import auth from '../auth' 204 | 205 | import { 206 | SENDING_REQUEST, 207 | LOGIN_REQUEST, 208 | REGISTER_REQUEST, 209 | SET_AUTH, 210 | LOGOUT, 211 | CHANGE_FORM, 212 | REQUEST_ERROR 213 | } from '../actions/constants' 214 | 215 | /** 216 | * Effect to handle authorization 217 | * @param {string} username The username of the user 218 | * @param {string} password The password of the user 219 | * @param {object} options Options 220 | * @param {boolean} options.isRegistering Is this a register request? 221 | */ 222 | export function * authorize ({username, password, isRegistering}) { 223 | // We send an action that tells Redux we're sending a request 224 | yield put({type: SENDING_REQUEST, sending: true}) 225 | 226 | // We then try to register or log in the user, depending on the request 227 | try { 228 | let salt = genSalt(username) 229 | let hash = hashSync(password, salt) 230 | let response 231 | 232 | // For either log in or registering, we call the proper function in the `auth` 233 | // module, which is asynchronous. Because we're using generators, we can work 234 | // as if it's synchronous because we pause execution until the call is done 235 | // with `yield`! 236 | if (isRegistering) { 237 | response = yield call(auth.register, username, hash) 238 | } else { 239 | response = yield call(auth.login, username, hash) 240 | } 241 | 242 | return response 243 | } catch (error) { 244 | console.log('hi') 245 | // If we get an error we send Redux the appropiate action and return 246 | yield put({type: REQUEST_ERROR, error: error.message}) 247 | 248 | return false 249 | } finally { 250 | // When done, we tell Redux we're not in the middle of a request any more 251 | yield put({type: SENDING_REQUEST, sending: false}) 252 | } 253 | } 254 | 255 | /** 256 | * Effect to handle logging out 257 | */ 258 | export function * logout () { 259 | // We tell Redux we're in the middle of a request 260 | yield put({type: SENDING_REQUEST, sending: true}) 261 | 262 | // Similar to above, we try to log out by calling the `logout` function in the 263 | // `auth` module. If we get an error, we send an appropiate action. If we don't, 264 | // we return the response. 265 | try { 266 | let response = yield call(auth.logout) 267 | yield put({type: SENDING_REQUEST, sending: false}) 268 | 269 | return response 270 | } catch (error) { 271 | yield put({type: REQUEST_ERROR, error: error.message}) 272 | } 273 | } 274 | 275 | /** 276 | * Log in saga 277 | */ 278 | export function * loginFlow () { 279 | // Because sagas are generators, doing `while (true)` doesn't block our program 280 | // Basically here we say "this saga is always listening for actions" 281 | while (true) { 282 | // And we're listening for `LOGIN_REQUEST` actions and destructuring its payload 283 | let request = yield take(LOGIN_REQUEST) 284 | let {username, password} = request.data 285 | 286 | // A `LOGOUT` action may happen while the `authorize` effect is going on, which may 287 | // lead to a race condition. This is unlikely, but just in case, we call `race` which 288 | // returns the "winner", i.e. the one that finished first 289 | let winner = yield race({ 290 | auth: call(authorize, {username, password, isRegistering: false}), 291 | logout: take(LOGOUT) 292 | }) 293 | 294 | // If `authorize` was the winner... 295 | if (winner.auth) { 296 | // ...we send Redux appropiate actions 297 | yield put({type: SET_AUTH, newAuthState: true}) // User is logged in (authorized) 298 | yield put({type: CHANGE_FORM, newFormState: {username: '', password: ''}}) // Clear form 299 | forwardTo('/dashboard') // Go to dashboard page 300 | } 301 | } 302 | } 303 | 304 | /** 305 | * Log out saga 306 | * This is basically the same as the `if (winner.logout)` of above, just written 307 | * as a saga that is always listening to `LOGOUT` actions 308 | */ 309 | export function * logoutFlow () { 310 | while (true) { 311 | yield take(LOGOUT) 312 | yield put({type: SET_AUTH, newAuthState: false}) 313 | 314 | yield call(logout) 315 | forwardTo('/') 316 | } 317 | } 318 | 319 | /** 320 | * Register saga 321 | * Very similar to log in saga! 322 | */ 323 | export function * registerFlow () { 324 | while (true) { 325 | // We always listen to `REGISTER_REQUEST` actions 326 | let request = yield take(REGISTER_REQUEST) 327 | let {username, password} = request.data 328 | 329 | // We call the `authorize` task with the data, telling it that we are registering a user 330 | // This returns `true` if the registering was successful, `false` if not 331 | let wasSuccessful = yield call(authorize, {username, password, isRegistering: true}) 332 | 333 | // If we could register a user, we send the appropiate actions 334 | if (wasSuccessful) { 335 | yield put({type: SET_AUTH, newAuthState: true}) // User is logged in (authorized) after being registered 336 | yield put({type: CHANGE_FORM, newFormState: {username: '', password: ''}}) // Clear form 337 | forwardTo('/dashboard') // Go to dashboard page 338 | } 339 | } 340 | } 341 | 342 | // The root saga is what we actually send to Redux's middleware. In here we fork 343 | // each saga so that they are all "active" and listening. 344 | // Sagas are fired once at the start of an app and can be thought of as processes running 345 | // in the background, watching actions dispatched to the store. 346 | export default function * root () { 347 | yield fork(loginFlow) 348 | yield fork(logoutFlow) 349 | yield fork(registerFlow) 350 | } 351 | 352 | // Little helper function to abstract going to different pages 353 | function forwardTo (location) { 354 | browserHistory.push(location) 355 | } 356 | 357 | 另一个demo saga也跟我一样,拆分了下 358 | 359 | ![saga](../record/saga.png) 360 | 361 | 简单看两个demo就好 362 | 363 | index.js 364 | 365 | import { takeLatest } from 'redux-saga'; 366 | import { fork } from 'redux-saga/effects'; 367 | import {loadUser} from './loadUser'; 368 | import {loadDashboardSequenced} from './loadDashboardSequenced'; 369 | import {loadDashboardNonSequenced} from './loadDashboardNonSequenced'; 370 | import {loadDashboardNonSequencedNonBlocking, isolatedForecast, isolatedFlight } from './loadDashboardNonSequencedNonBlocking'; 371 | 372 | function* rootSaga() { 373 | /*The saga is waiting for a action called LOAD_DASHBOARD to be activated */ 374 | yield [ 375 | fork(loadUser), 376 | takeLatest('LOAD_DASHBOARD', loadDashboardSequenced), 377 | takeLatest('LOAD_DASHBOARD_NON_SEQUENCED', loadDashboardNonSequenced), 378 | takeLatest('LOAD_DASHBOARD_NON_SEQUENCED_NON_BLOCKING', loadDashboardNonSequencedNonBlocking), 379 | fork(isolatedForecast), 380 | fork(isolatedFlight) 381 | ]; 382 | } 383 | 384 | export default rootSaga; 385 | 386 | loadDashboardNonSequencedNonBlocking.js 387 | 388 | import { call, put, select , take} from 'redux-saga/effects'; 389 | import {loadDeparture, loadFlight, loadForecast } from './apiCalls'; 390 | 391 | export const getUserFromState = (state) => state.user; 392 | 393 | export function* loadDashboardNonSequencedNonBlocking() { 394 | try { 395 | //Wait for the user to be loaded 396 | yield take('FETCH_USER_SUCCESS'); 397 | 398 | //Take the user info from the store 399 | const user = yield select(getUserFromState); 400 | 401 | //Get Departure information 402 | const departure = yield call(loadDeparture, user); 403 | 404 | //Update the UI 405 | yield put({type: 'FETCH_DASHBOARD3_SUCCESS', payload: {departure}}); 406 | 407 | //trigger actions for Forecast and Flight to start... 408 | //We can pass and object into the put statement 409 | yield put({type: 'FETCH_DEPARTURE3_SUCCESS', departure}); 410 | 411 | } catch(error) { 412 | yield put({type: 'FETCH_FAILED', error: error.message}); 413 | } 414 | } 415 | 416 | export function* isolatedFlight() { 417 | try { 418 | /* departure will take the value of the object passed by the put*/ 419 | const departure = yield take('FETCH_DEPARTURE3_SUCCESS'); 420 | 421 | //Flight can be called unsequenced /* BUT NON BLOCKING VS FORECAST*/ 422 | const flight = yield call(loadFlight, departure.flightID); 423 | //Tell the store we are ready to be displayed 424 | yield put({type: 'FETCH_DASHBOARD3_SUCCESS', payload: {flight}}); 425 | 426 | } catch (error) { 427 | yield put({type: 'FETCH_FAILED', error: error.message}); 428 | } 429 | } 430 | 431 | export function* isolatedForecast() { 432 | try { 433 | /* departure will take the value of the object passed by the put*/ 434 | const departure = yield take('FETCH_DEPARTURE3_SUCCESS'); 435 | 436 | const forecast = yield call(loadForecast, departure.date); 437 | yield put({type: 'FETCH_DASHBOARD3_SUCCESS', payload: { forecast }}); 438 | 439 | } catch(error) { 440 | yield put({type: 'FETCH_FAILED', error: error.message}); 441 | } 442 | } 443 | 444 | 445 | ## 交流 446 | 447 | Node.js技术交流群:209530601 448 | 449 | React技术栈:398240621 -------------------------------------------------------------------------------- /docs/关于项目中的webpack使用.md: -------------------------------------------------------------------------------- 1 | # React技术栈实现大众点评Demo-项目配置说明 2 | 3 | >[项目地址](https://github.com/Nealyang/React-Fullstack-Dianping-Demo) 4 | 5 | >关于什么是webpack什么是babel这里就不做过多的介绍了。直接解释下项目中的配置项,安装插件的作用吧 6 | 7 | 8 | ## 项目截图 9 | ![首页](https://github.com/Nealyang/React-Fullstack-Dianping-Demo/blob/master/record/home_1.jpg) 10 | 11 | 12 | 13 | ##项目中babel配置说明 14 | 15 | .babelrc 16 | 17 | { 18 | "presets": ["es2015","react","stage-0","env"], 19 | "plugins": [ 20 | "react-hot-loader/babel" 21 | ], 22 | "env": { 23 | "production":{ 24 | "preset":["react-optimize"] 25 | } 26 | } 27 | } 28 | 29 | 虽然这部分可以放到webpack中配置,但是我依旧喜欢把.babelrc文件单独的拿出来 30 | 31 | ### 安装的插件列表 32 | 33 | "babel-core": 如果某些代码需要调用Babel的API进行转码,就要使用babel-core模块 34 | "babel-loader": webpack打包时候调用, 35 | "babel-plugin-react-transform": 转换react 组件, 36 | "babel-plugin-transform-remove-console": 打包时候删除调试用的console语句, 37 | "babel-polyfill": Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API ,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign)都不会转码.所以babel-polyfill就要用上了。该项目主要是因为saga的使用 38 | "babel-preset-env": 可以根据配置的目标运行环境(environment)自动启用需要的 babel 插件, 39 | "babel-preset-es2015": 转换es6,因为浏览器兼容性的问题, 40 | "babel-preset-react": react转码规则, 41 | "babel-preset-react-hmre": react热更新, 42 | "babel-react-optimize":自动去除propTypes 43 | "babel-preset-stage-0": ES7不同阶段语法提案的转码规则(共有4个阶段),选装一个, 44 | "babel-register": 模块改写require命令,为它加上一个钩子。此后,每当使用require加载.js、.jsx、.es和.es6后缀名的文件,就会先用Babel进行转码。, 45 | "babel-runtime": 是为了减少重复代码而生的。 babel生成的代码,可能会用到一些_extend(), classCallCheck() 之类的工具函数,默认情况下,这些工具函数的代码会包含在编译后的文件中。如果存在多个文件,那每个文件都有可能含有一份重复的代码。, 46 | 47 | 48 | ##webpack的配置说明 49 | 50 | const pathLib = require('path'); 51 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 52 | const webpack = require('webpack'); 53 | const OpenBrowser = require('open-browser-webpack-plugin'); 54 | const ExtractText = require('extract-text-webpack-plugin'); 55 | const CleanPlugin = require('clean-webpack-plugin'); 56 | const ProgressBarPlugin = require('progress-bar-webpack-plugin'); 57 | const config = require('./config/config'); 58 | module.exports = { 59 | entry: { 60 | index: [ 61 | 'babel-polyfill', 62 | 'react-hot-loader/patch', 63 | 'webpack-hot-middleware/client?path=http://localhost:8000/__webpack_hmr', 64 | pathLib.resolve(__dirname,'frontWeb', 'index.js') 65 | ], 66 | vendor: ['react','react-dom','react-router-dom','redux','react-redux','redux-saga','swipe-js-iso','react-swipe','react-addons-pure-render-mixin'] 67 | }, 68 | output: { 69 | path: pathLib.resolve(__dirname, 'build'), 70 | publicPath: "/", 71 | filename: '[name].[hash:8].js' 72 | }, 73 | devtool:'eval-source-map', 74 | module: { 75 | rules: [ 76 | { 77 | test: /\.jsx?$/, 78 | exclude: /node_modules/, 79 | use: ['babel-loader'] 80 | }, 81 | { 82 | test: /\.css$/, 83 | exclude: /node_modules/, 84 | use:ExtractText.extract({ 85 | fallback: "style-loader", 86 | use: [ 87 | { 88 | loader: 'css-loader', 89 | options: { 90 | modules: true, 91 | localIdentName: '[path][name]-[local]-[hash:base64:5]', 92 | importLoaders: 1 93 | } 94 | }, 95 | 'postcss-loader' 96 | ] 97 | }) 98 | }, 99 | { 100 | test:/\.(png|jpg|gif|JPG|GIF|PNG|BMP|bmp|JPEG|jpeg)$/, 101 | exclude:/node_modules/, 102 | use:[ 103 | { 104 | loader:'url-loader', 105 | options: { 106 | limit:8192 107 | } 108 | } 109 | ] 110 | }, 111 | { 112 | test: /\.(eot|woff|ttf|woff2|svg)$/, 113 | use: 'url-loader' 114 | } 115 | ] 116 | }, 117 | plugins: [ 118 | new CleanPlugin(['build']), 119 | new ProgressBarPlugin(), 120 | new webpack.optimize.AggressiveMergingPlugin(), 121 | new webpack.DefinePlugin({ 122 | "progress.env.NODE_ENV":JSON.stringify('development') 123 | }), 124 | new HtmlWebpackPlugin({ 125 | title: "My app", 126 | showErrors: true, 127 | }), 128 | new webpack.HotModuleReplacementPlugin(), 129 | new webpack.NoEmitOnErrorsPlugin(),//保证出错时页面不阻塞,且会在编译结束后报错 130 | new ExtractText({ 131 | filename:'bundle.[contenthash].css', 132 | disable:false, 133 | allChunks:true 134 | }), 135 | new OpenBrowser({url:`http://${config.hotReloadHost}:${config.hotReloadPort}`}), 136 | new webpack.HashedModuleIdsPlugin(), 137 | new webpack.optimize.CommonsChunkPlugin({ 138 | name: 'vendor', 139 | minChunks: function (module) { 140 | return module.context && module.context.indexOf('node_modules') !== -1; 141 | } 142 | }), 143 | new webpack.optimize.CommonsChunkPlugin({ 144 | name: "manifest" 145 | }) 146 | ], 147 | resolve: { 148 | extensions: ['.js', '.json', '.sass', '.scss', '.less', 'jsx'] 149 | } 150 | }; 151 | 152 | 都是一些比较常规的配置,其中需要说明的是,热更新这里需要配置一个服务器,也就是这个项目中的hotReloadServer.js 153 | 154 | 其中,打包的时候,output当存在热更新的时候,输出文件名不能使用chunkhash -------------------------------------------------------------------------------- /frontWeb/components/city/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const GET_CITY_DATA = 'GET_CITY_DATA'; 2 | export const RESOLVE_CITY_DATA = 'RESOLVE_CITY_DATA'; 3 | -------------------------------------------------------------------------------- /frontWeb/components/city/actions.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from './actionTypes' 2 | 3 | export function getCity(url) { 4 | return{ 5 | type: actionTypes.GET_CITY_DATA, 6 | url 7 | } 8 | } -------------------------------------------------------------------------------- /frontWeb/components/city/citySaga.js: -------------------------------------------------------------------------------- 1 | import {call,put ,take} from 'redux-saga/effects' 2 | import * as wrapActionTypes from '../wrap/actionTypes' 3 | import * as cityActionTypes from './actionTypes' 4 | import {get} from '../../fetchApi/get' 5 | 6 | 7 | export function* getCities (url) { 8 | yield put({type:wrapActionTypes.START_FETCH}); 9 | try { 10 | return yield call(get,url) 11 | }catch (err){ 12 | yield put({type:wrapActionTypes.FETCH_ERROR}); 13 | }finally { 14 | yield put({type:wrapActionTypes.FETCH_END}); 15 | } 16 | } 17 | 18 | export function* getCitiesFlow() { 19 | while(true){ 20 | let req = yield take(cityActionTypes.GET_CITY_DATA); 21 | let res = yield call(getCities,req.url); 22 | yield put({type:cityActionTypes.RESOLVE_CITY_DATA,data:res.data}) 23 | } 24 | } -------------------------------------------------------------------------------- /frontWeb/components/city/index.js: -------------------------------------------------------------------------------- 1 | import view from './views/City' 2 | import reducer from './reducer' 3 | import * as actions from './actions' 4 | 5 | export {view,reducer,actions} -------------------------------------------------------------------------------- /frontWeb/components/city/reducer.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from './actionTypes' 2 | 3 | export default function cities(state=[],action) { 4 | switch (action.type){ 5 | case actionTypes.RESOLVE_CITY_DATA: 6 | return action.data; 7 | default : 8 | return state; 9 | } 10 | } -------------------------------------------------------------------------------- /frontWeb/components/city/views/City.js: -------------------------------------------------------------------------------- 1 | import React,{Component,PropTypes} from 'react' 2 | import {getCity} from '../actions' 3 | import {bindActionCreators} from 'redux' 4 | import {connect} from 'react-redux' 5 | import {CityHeader} from "./CityHeader"; 6 | import {actions as wrapActions} from '../../wrap' 7 | import style from './style.css' 8 | 9 | class City extends Component{ 10 | constructor(props){ 11 | super(props); 12 | this.choiceCity = this.choiceCity.bind(this) 13 | } 14 | 15 | render(){ 16 | const {history,cities} = this.props; 17 | return( 18 |
19 | 20 |
21 | 北京 22 | GPS定位 23 |
24 |

热门城市

25 |
26 | {cities.map((item,index)=>{ 27 | return ( 28 |
this.choiceCity(item.name)}> 29 | {item.name} 30 |
31 | ) 32 | })} 33 |
34 |
35 | ) 36 | } 37 | 38 | componentDidMount() { 39 | this.props.getCities('/api/cities'); 40 | } 41 | 42 | choiceCity(cityName){ 43 | this.props.choiceCity({ 44 | cityName 45 | }); 46 | this.props.history.goBack() 47 | } 48 | 49 | } 50 | 51 | City.defaultProps = { 52 | cities:[] 53 | }; 54 | 55 | City.propTypes = { 56 | cities:PropTypes.arrayOf(PropTypes.object.isRequired) 57 | }; 58 | 59 | function mapStateToProps(state) { 60 | return{ 61 | cities:state.cities 62 | } 63 | } 64 | 65 | function mapDispatchToProps(dispatch) { 66 | return{ 67 | getCities:bindActionCreators(getCity,dispatch), 68 | choiceCity:bindActionCreators(wrapActions.updateUserInfo,dispatch) 69 | } 70 | } 71 | 72 | export default connect( 73 | mapStateToProps, 74 | mapDispatchToProps 75 | ) (City); -------------------------------------------------------------------------------- /frontWeb/components/city/views/CityHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import style from './style.css' 3 | import BackIcon from './back.png' 4 | 5 | export const CityHeader = (props) => ( 6 |
props.history.goBack()}> 7 | 8 | 9 | 同步action操作->选择城市 10 | 11 |
12 | ); -------------------------------------------------------------------------------- /frontWeb/components/city/views/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/frontWeb/components/city/views/back.png -------------------------------------------------------------------------------- /frontWeb/components/city/views/style.css: -------------------------------------------------------------------------------- 1 | .container{ 2 | background: #f2f2f2; 3 | } 4 | .headerContainer{ 5 | height: 132px; 6 | background: #fff; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | font-size: 2.5rem; 11 | } 12 | .headerContainer img{ 13 | width: 60px; 14 | position: absolute; 15 | left:20px; 16 | } 17 | 18 | .gpsCity{ 19 | margin: 20px 0; 20 | height: 130px; 21 | display: flex; 22 | align-items: center; 23 | font-size: 2.6rem; 24 | background: #fff; 25 | padding-left: 20px; 26 | color: #bbb; 27 | } 28 | 29 | .gpsCity>span{ 30 | margin-right: 20px; 31 | color: #202020; 32 | } 33 | .hotCity{ 34 | font-size: 2.3rem; 35 | margin-left: 20px; 36 | font-weight: bold; 37 | margin-bottom: 10px; 38 | 39 | } 40 | .hotCityContainer{ 41 | background: #fff; 42 | display: flex; 43 | flex-wrap: wrap; 44 | flex-direction: row; 45 | border-bottom: solid thin #dcdbdb; 46 | } 47 | .hotCityName{ 48 | width: 33%; 49 | display: flex; 50 | align-items: center; 51 | justify-content: center; 52 | height: 130px; 53 | border-top: solid thin #dcdbdb; 54 | border-left: solid thin #dcdbdb; 55 | font-size: 2.4rem; 56 | color: #202020; 57 | } -------------------------------------------------------------------------------- /frontWeb/components/common/AdCell.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import style from './style.css' 3 | 4 | export const AdCell = (props) => ( 5 |
6 | 7 |

8 | {props.data.name} 9 |

10 | {props.data.price} 11 | { 12 | props.type===1&&props.data.subtract?{props.data.subtract} 13 | :props.type===2&&props.data.tag?{props.data.tag}:null 14 | } 15 | 16 |
17 | ); -------------------------------------------------------------------------------- /frontWeb/components/common/AdHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import style from './style.css' 3 | import rightArrow from './right-arrow.png' 4 | 5 | export const AdHeader = (props) => ( 6 |
7 | {props.title} 8 |
9 | 更多优惠 10 | 11 |
12 |
13 | ); 14 | 15 | 16 | -------------------------------------------------------------------------------- /frontWeb/components/common/ListCell.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import style from './style.css' 3 | 4 | export const ListCell = (props) => ( 5 |
props.skipToDetail(new Date().getMilliseconds())}> 6 |
7 | 8 |
9 |
10 |

{props.item.name}

11 |

{props.item.desc}

12 |
13 |
14 | {props.item.price} 15 | {props.item.exPrice} 16 |
17 | 已售 {props.item.cell} 18 |
19 |
20 |
21 | ); -------------------------------------------------------------------------------- /frontWeb/components/common/LoadingMore.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import style from './style.css' 3 | 4 | export default class LoadMore extends Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | render() { 10 | const {loadMore} = this.props; 11 | return ( 12 |
loadMore()}> 13 | 点击加载更多 14 |
15 | ) 16 | } 17 | 18 | componentDidMount() { 19 | let timeoutId ; 20 | let that = this; 21 | function callBack() { 22 | let tag = that.refs.loadMore; 23 | if(tag){ 24 | let top = tag.getBoundingClientRect().top; 25 | let height = document.documentElement.clientHeight; 26 | if(top { 25 | this.setState({ 26 | currentIndex:i 27 | }) 28 | } 29 | }; 30 | return ( 31 |
32 | {Header(this.props.history)} 33 |
34 | 35 | {this.renderSwipe(imgs)} 36 | 37 |
38 | {this.state.currentIndex+1}/{imgs.length} 39 |
40 |
41 |

没名儿生煎

42 |

仅售19.9元!最高价值44元的清爽夏日生煎套餐,建议2人使用。

43 |
44 |
45 |
46 |
47 |
48 | 19.9 49 | 44 50 |
51 |
52 | 卖光了 53 |
54 |
55 |
56 | 57 | 58 | 随时可退 59 | 60 | 61 | 62 | 过期自动退 63 | 64 |
65 |
66 |
67 |

68 | 变更通知 69 |

70 |

71 | 【7月11日更新】【分店暂停接待】没名儿生煎(五道口店)店有效期内无法接待团购用户,您可前往其他分店消费。给您带来不便,深表歉意。 72 |

73 |
74 | 75 |
76 |
77 | 卖光了 78 |
79 |
80 |
81 |

看了此团购的人也看了

82 | { 83 | recommends.map((item,index)=>{ 84 | return( 85 |
86 | 87 |
88 |
{item.name}
89 |

90 | {item.price} 91 | {item.exPrice} 92 |

93 |
94 |
95 | ) 96 | }) 97 | } 98 |
99 | 100 |
101 |
102 | 103 | APP下单返积分抵现金 104 | 105 |
106 |
107 |
108 | ) 109 | } 110 | 111 | componentDidMount() { 112 | this.props.getDetail('/api/orderDetail', this.props.match.params) 113 | } 114 | 115 | renderSwipe(arr) { 116 | return arr.map((item, index) => { 117 | return ( 118 |
119 | 120 |
121 | ) 122 | }) 123 | } 124 | } 125 | 126 | const Header = (props) => ( 127 |
props.goBack()}> 128 | 129 | 130 | 返回 131 | 132 | 团购详情 133 |
134 | ); 135 | 136 | Detail.defaultProps = { 137 | imgs: [], 138 | orderDetail: {}, 139 | recommends: [] 140 | }; 141 | 142 | Detail.propTypes = { 143 | imgs: PropTypes.arrayOf(PropTypes.string.isRequired), 144 | orderDetail: PropTypes.object.isRequired, 145 | recommends: PropTypes.arrayOf(PropTypes.object.isRequired) 146 | }; 147 | 148 | 149 | function mapSateToProps(state) { 150 | return { 151 | imgs: state.orderDetail.imgs, 152 | orderDetail: state.orderDetail.orderDetail, 153 | recommends: state.orderDetail.recommends 154 | } 155 | } 156 | 157 | function mapDispatchToProps(dispatch) { 158 | return { 159 | getDetail: bindActionCreators(getDetail, dispatch) 160 | } 161 | } 162 | 163 | 164 | export default connect( 165 | mapSateToProps, 166 | mapDispatchToProps 167 | )(Detail); -------------------------------------------------------------------------------- /frontWeb/components/detail/views/ShouldKnow.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import style from './style.css' 3 | import Broad from './broad.png' 4 | 5 | export const ShouldKnow = (props) => ( 6 |
7 |

8 | 购买须知 9 | 10 |

11 | {renderCell(props.orderDetail)} 12 |
13 | ); 14 | 15 | const renderCell = (obj) => { 16 | let resultJSX = []; 17 | for (let key in obj) { 18 | let template = ''; 19 | let title = ''; 20 | if (key === 'youxiaoqi') { 21 | title = '有效期'; 22 | }else if(key === 'chuwairiqi'){ 23 | title='除外日期' 24 | }else if(key === 'shiyongshijian'){ 25 | title='使用时间' 26 | }else if(key === 'yuyuetixing'){ 27 | title='预约提醒' 28 | }else if(key === 'guizetixing'){ 29 | title='规则提醒' 30 | }else if(key === 'baojian'){ 31 | title='包间' 32 | }else if(key === 'tangshiwaidai'){ 33 | title='堂食外带' 34 | }else if(key === 'wenxintishi'){ 35 | title='温馨提示' 36 | } 37 | template = 38 |
39 |

{title}

40 | { 41 | obj[key].map((item, index) => { 42 | return ( 43 |
{item}
44 | ) 45 | }) 46 | } 47 |
48 | resultJSX.push(template) 49 | } 50 | return resultJSX 51 | }; 52 | 53 | -------------------------------------------------------------------------------- /frontWeb/components/detail/views/arrow_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/frontWeb/components/detail/views/arrow_down.png -------------------------------------------------------------------------------- /frontWeb/components/detail/views/broad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/frontWeb/components/detail/views/broad.png -------------------------------------------------------------------------------- /frontWeb/components/detail/views/gou.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/frontWeb/components/detail/views/gou.png -------------------------------------------------------------------------------- /frontWeb/components/detail/views/style.css: -------------------------------------------------------------------------------- 1 | .container{ 2 | background: #f0f0f0; 3 | padding-bottom: 200px; 4 | } 5 | .header{ 6 | height: 132px; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | font-size: 2.6rem; 11 | background: #f0f0f0; 12 | } 13 | .backSpan{ 14 | position: absolute; 15 | left: 5px; 16 | display: flex; 17 | align-items: center; 18 | color: #646464; 19 | } 20 | .backSpan img{ 21 | width: 70px; 22 | } 23 | .swipeImg{ 24 | width: 100%; 25 | } 26 | .indexDiv{ 27 | font-size: 1.5rem; 28 | position: absolute; 29 | right: 15px; 30 | top: 50px; 31 | background-color: rgba(0,0,0,.4); 32 | border-radius: 19px; 33 | line-height: 9px; 34 | color: #666; 35 | padding: 14px 18px 14px; 36 | text-align: center; 37 | } 38 | .indexDiv>span{ 39 | color: #fff; 40 | } 41 | .imgIntroduce{ 42 | position: absolute; 43 | z-index: 999; 44 | bottom: 0; 45 | left: 0; 46 | padding: 10px 15px; 47 | width: 100%; 48 | box-sizing: border-box; 49 | background: -webkit-gradient(linear,0 0,0 100%,from(rgba(0,0,0,0)),to(rgba(0,0,0,.6))); 50 | color: #fff; 51 | } 52 | .meiminger{ 53 | font-size: 2.5rem; 54 | } 55 | .int{ 56 | font-size: 1.9rem; 57 | } 58 | .proceDivCon{ 59 | padding-left: 40px; 60 | background: #fff; 61 | } 62 | .priceDivTop,.priceDivBottom{ 63 | display: flex; 64 | justify-content: space-between; 65 | align-items: center; 66 | height: 120px; 67 | border-bottom: solid thin #bbb; 68 | } 69 | .priceDivTop span::before{ 70 | content:'¥' 71 | } 72 | .priceDivTop span:first-child{ 73 | font-size: 4rem; 74 | color: #f5734b; 75 | text-decoration: none; 76 | margin-right: 20px; 77 | } 78 | .priceDivTop span{ 79 | color: #bbb; 80 | font-size: 2rem; 81 | text-decoration: line-through; 82 | } 83 | .priceDivTop div:last-child{ 84 | background: #ccc; 85 | width: 26%; 86 | padding: 15px 0; 87 | margin-right: 20px; 88 | border-radius: 16px; 89 | text-align: center; 90 | font-size: 2.3rem; 91 | color: #fff; 92 | } 93 | .priceDivBottom{ 94 | justify-content: flex-start; 95 | border-bottom: none; 96 | font-size: 1.8rem; 97 | color: #2a2a2a; 98 | } 99 | .priceDivBottom img{ 100 | width: 46px; 101 | margin-right: 13px; 102 | } 103 | .priceDivBottom span:first-child{ 104 | margin-right: 30%; 105 | } 106 | .notifyDiv{ 107 | padding-left: 40px; 108 | margin: 20px 0; 109 | background: #fff; 110 | font-size: 2.1rem; 111 | } 112 | .notifyDiv p:first-child{ 113 | color: #999; 114 | padding: 25px 0; 115 | border-bottom: solid thin #999; 116 | } 117 | .notifyDiv p:last-child{ 118 | color: #808080; 119 | padding:30px 0; 120 | } 121 | .shouldKnow{ 122 | background: #fff; 123 | padding-left: 40px; 124 | font-size: 2.1rem; 125 | } 126 | .boradKnow{ 127 | color: #4a4a4a; 128 | padding: 25px 0; 129 | border-bottom: solid thin #999; 130 | } 131 | .knowCellContainer p{ 132 | color: #999; 133 | padding: 25px 0; 134 | } 135 | .boradKnow img{ 136 | margin-left: 10px; 137 | } 138 | .knowCellContainer div{ 139 | color: #494949; 140 | font-size: 2.1rem; 141 | padding-bottom: 20px; 142 | } 143 | 144 | .knowCellContainer div:last-child{ 145 | border-bottom: solid thin #999; 146 | } 147 | .getButton,.bottomButton{ 148 | background: #fff; 149 | margin:10px 0; 150 | padding:20px 0; 151 | } 152 | .getButton div,.bottomButton div{ 153 | background: #ccc; 154 | width: 90%; 155 | padding: 23px 0; 156 | margin-right: 20px; 157 | border-radius: 16px; 158 | text-align: center; 159 | font-size: 2.3rem; 160 | color: #fff; 161 | margin-left: 5%; 162 | } 163 | .recommendsContainer{ 164 | padding-left: 40px; 165 | font-size: 2.1rem; 166 | background: #fff; 167 | } 168 | .recommendsContainer p:first-child{ 169 | color: #383838; 170 | padding: 25px 0; 171 | border-bottom: solid thin #999; 172 | } 173 | .recommendsCell{ 174 | display: flex; 175 | flex-direction: row; 176 | padding: 20px 0; 177 | border-bottom: solid thin #999; 178 | } 179 | .recommendsCell img{ 180 | width: 30%; 181 | height:30%; 182 | } 183 | .recommendsCell>div{ 184 | margin-left: 25px; 185 | display: flex; 186 | justify-content: space-around; 187 | flex-direction: column; 188 | } 189 | .recommendsName{ 190 | font-size: 2.5rem; 191 | border:none; 192 | color: #333; 193 | } 194 | .recommendsExPrice{ 195 | color: #666; 196 | text-decoration: line-through; 197 | } 198 | .recommendsPrice{ 199 | color: #ff7658; 200 | font-size: 2.9rem; 201 | margin-right: 20px; 202 | } 203 | .recommendsCell span::before{ 204 | content: '¥'; 205 | } 206 | 207 | .bottomButton div{ 208 | background: #f63; 209 | } 210 | .bottomButton{ 211 | position: fixed; 212 | bottom:0; 213 | width: 100%; 214 | margin:0; 215 | } 216 | -------------------------------------------------------------------------------- /frontWeb/components/detail/views/time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/frontWeb/components/detail/views/time.png -------------------------------------------------------------------------------- /frontWeb/components/home/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const GET_AD = 'GET_AD'; 2 | export const GET_AD_RESULT_DATA = 'GET_AD_RESULT_DATA'; 3 | export const GET_U_LIKE = 'GET_U_LIKE'; 4 | export const GET_U_LIKE_RESULT_DATA = 'GET_U_LIKE_RESULT_DATA'; -------------------------------------------------------------------------------- /frontWeb/components/home/actions.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from './actionTypes' 2 | 3 | export function getAd(url) { 4 | return { 5 | type:actionTypes.GET_AD, 6 | url 7 | } 8 | } 9 | 10 | export function getLike(url) { 11 | return{ 12 | type:actionTypes.GET_U_LIKE, 13 | url 14 | } 15 | } -------------------------------------------------------------------------------- /frontWeb/components/home/homeSaga.js: -------------------------------------------------------------------------------- 1 | import {put,take,call,fork} from 'redux-saga/effects' 2 | import {delay} from 'redux-saga' 3 | import {get} from '../../fetchApi/get' 4 | import * as homeActionTypes from './actionTypes' 5 | import * as wrapActionTypes from '../wrap/actionTypes' 6 | 7 | //故意的 8 | const delayTime = 0; 9 | 10 | 11 | export function* getAdData(url) { 12 | yield put({type:wrapActionTypes.START_FETCH}); 13 | yield delay(delayTime);//故意的 14 | try { 15 | return yield call(get,url); 16 | } catch (error) { 17 | yield put({type:wrapActionTypes.FETCH_ERROR}) 18 | }finally { 19 | yield put({type:wrapActionTypes.FETCH_END}) 20 | } 21 | } 22 | 23 | export function* getULikeData(url) { 24 | yield put({type:wrapActionTypes.START_FETCH}); 25 | yield delay(delayTime);//故意的 26 | try { 27 | return yield call(get,url); 28 | }catch (err) { 29 | yield put({type:wrapActionTypes.FETCH_ERROR}) 30 | }finally { 31 | yield put({type:wrapActionTypes.FETCH_END}) 32 | } 33 | } 34 | 35 | export function* getAdDataFlow() { 36 | while (true){ 37 | let request = yield take(homeActionTypes.GET_AD); 38 | let response = yield call(getAdData,request.url); 39 | yield put({type:homeActionTypes.GET_AD_RESULT_DATA,data:response.data}) 40 | } 41 | } 42 | 43 | export function* getULikeDataFlow() { 44 | while(true){ 45 | let request = yield take(homeActionTypes.GET_U_LIKE); 46 | let response = yield call(getULikeData,request.url); 47 | yield put({type:homeActionTypes.GET_U_LIKE_RESULT_DATA,data:response.data}) 48 | } 49 | } -------------------------------------------------------------------------------- /frontWeb/components/home/index.js: -------------------------------------------------------------------------------- 1 | import view from './views/Home' 2 | import reducer from './reducer' 3 | import * as actions from './actions' 4 | 5 | export {view,reducer,actions} -------------------------------------------------------------------------------- /frontWeb/components/home/reducer.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from './actionTypes' 2 | import {combineReducers} from 'redux' 3 | 4 | function resolveADData(state=[],action) { 5 | switch (action.type){ 6 | case actionTypes.GET_AD_RESULT_DATA: 7 | return action.data; 8 | default : 9 | return state 10 | } 11 | } 12 | function resolveULikeData(state=[],action) { 13 | switch (action.type){ 14 | case actionTypes.GET_U_LIKE_RESULT_DATA: 15 | return [...state,...action.data]; 16 | default : 17 | return state 18 | } 19 | } 20 | 21 | const rootReducer = combineReducers({ 22 | adData:resolveADData, 23 | guessULike:resolveULikeData 24 | }); 25 | 26 | export default rootReducer -------------------------------------------------------------------------------- /frontWeb/components/home/views/Home.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import HomeHeader from "./homeHeader/HomeHeader"; 3 | import {connect} from 'react-redux' 4 | import Category from "./category/Category"; 5 | import style from './style.css' 6 | import {getAd,getLike} from '../actions' 7 | import {bindActionCreators} from 'redux' 8 | import Ad from "./ad/Ad"; 9 | import CheapOrReducers from "./cheapOrReducers/CheapOrReducers"; 10 | import GuessULike from "./guessULike/GuessULike"; 11 | 12 | class Home extends Component { 13 | constructor(props) { 14 | super(props); 15 | this.loadMore = this.loadMore.bind(this); 16 | this.skipToLocation = this.skipToLocation.bind(this) 17 | } 18 | 19 | render() { 20 | const {userInfo,ad,cheap,reduces,guessULike,isLoading,history} = this.props; 21 | return ( 22 |
23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 |
31 | ) 32 | } 33 | 34 | componentDidMount() { 35 | this.props.getAdData('/api/getAdData'); 36 | this.props.getULikeData('/api/getULikeData'); 37 | } 38 | skipToLocation(id){ 39 | this.props.history.push(`/detail/${id}`); 40 | } 41 | loadMore(){ 42 | this.props.getULikeData('/api/getULikeData'); 43 | } 44 | } 45 | Home.defaultProps={ 46 | userInfo: {}, 47 | ad:[], 48 | cheap:[], 49 | reduces:[], 50 | guessULike:[], 51 | isLoading:false 52 | }; 53 | 54 | Home.propTypes = { 55 | userInfo: PropTypes.object.isRequired, 56 | ad:PropTypes.arrayOf(PropTypes.object.isRequired), 57 | cheap:PropTypes.arrayOf(PropTypes.object.isRequired), 58 | reduces:PropTypes.arrayOf(PropTypes.object.isRequired), 59 | guessULike:PropTypes.arrayOf(PropTypes.object.isRequired), 60 | isLoading:PropTypes.bool.isRequired 61 | }; 62 | 63 | function mapStateToProps(state) { 64 | return { 65 | userInfo: state.wrap.userInfo, 66 | ad:state.home.adData.ad, 67 | cheap:state.home.adData.cheap, 68 | reduces:state.home.adData.reduces, 69 | guessULike:state.home.guessULike, 70 | isLoading:state.wrap.fetchState==='start' 71 | } 72 | } 73 | 74 | function mapDispatchToProps(dispatch) { 75 | return { 76 | getAdData: bindActionCreators(getAd,dispatch), 77 | getULikeData:bindActionCreators(getLike,dispatch) 78 | } 79 | } 80 | 81 | export default connect( 82 | mapStateToProps, 83 | mapDispatchToProps, 84 | )(Home); -------------------------------------------------------------------------------- /frontWeb/components/home/views/ad/Ad.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import PureRenderMixin from 'react-addons-pure-render-mixin' 3 | import style from './style.css' 4 | 5 | export default class Ad extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 9 | this.renderComponent = this.renderComponent.bind(this) 10 | } 11 | 12 | render() { 13 | return ( 14 |
15 | {this.props.ads && this.renderComponent(this.props.ads)} 16 |
17 | ) 18 | } 19 | 20 | renderComponent(ads) { 21 | if (ads.length > 0) { 22 | return ads.map((item, index) => { 23 | return ( 24 |
25 |
26 |

1?{color:'#fff'}:{color:'#ff7658'}}>{item.name}

27 |

1?{color:'#fff'}:null}>{item.subName}

28 |
29 |
30 | 31 |
32 |
33 | ) 34 | }) 35 | } 36 | } 37 | } 38 | Ad.propTypes = { 39 | ads: PropTypes.arrayOf(PropTypes.object) 40 | }; -------------------------------------------------------------------------------- /frontWeb/components/home/views/ad/style.css: -------------------------------------------------------------------------------- 1 | .container{ 2 | display: flex; 3 | flex-wrap: wrap; 4 | } 5 | 6 | .singleCell{ 7 | width: 50%; 8 | display: flex; 9 | flex-direction: row; 10 | margin-bottom:20px; 11 | padding:20px 0; 12 | background: #fff; 13 | cursor: pointer; 14 | } 15 | .leftDiv{ 16 | flex:2; 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | flex-direction: column; 21 | } 22 | .name{ 23 | font-size: 2.4rem; 24 | color: #ff7658; 25 | } 26 | .subName{ 27 | font-size: 2rem; 28 | color: #c5c2c2; 29 | } 30 | .imgStyle{ 31 | 32 | } 33 | .rightDiv{ 34 | flex:1; 35 | } 36 | 37 | -------------------------------------------------------------------------------- /frontWeb/components/home/views/category/Category.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import PureRenderMixin from 'react-addons-pure-render-mixin' 3 | import style from './style.css' 4 | import ReactSwipe from 'react-swipe'; 5 | 6 | const dataSource = [ 7 | [ 8 | {name: '猫眼电影', src: 'https://www.dpfile.com/sc/eleconfig/20170223152109dp_wx_maoyan_icon.png'}, 9 | {name: '酒店', src: 'https://www.dpfile.com/sc/eleconfig/20160126203337jiudian.png'}, 10 | {name: '休闲娱乐', src: 'https://www.dpfile.com/sc/eleconfig/20160126202841xiuxianyule.png'}, 11 | {name: '外卖', src: 'https://www.dpfile.com/sc/eleconfig/20160126203251waimai.png'}, 12 | {name: '火锅', src: 'https://www.dpfile.com/sc/eleconfig/20160204172927huoguo.png'}, 13 | {name: '美食', src: 'https://www.dpfile.com/sc/eleconfig/20160126194705meishi.png'}, 14 | {name: '丽人', src: 'https://www.dpfile.com/sc/eleconfig/20160126202946liren.png'}, 15 | {name: '休闲娱乐', src: 'https://www.dpfile.com/sc/eleconfig/20160126203542ktv.png'}, 16 | {name: 'KTV', src: 'https://www.dpfile.com/sc/eleconfig/20160126203440zhoubianyou.png'}, 17 | {name: '婚纱摄影', src: 'https://www.dpfile.com/sc/eleconfig/20160126203830jiehun.png'} 18 | ], 19 | [ 20 | {name: '生活服务', src: 'https://www.dpfile.com/sc/eleconfig/20170308125500community_new.png'}, 21 | {name: '景点', src: 'https://www.dpfile.com/sc/eleconfig/20160126205135jingguan.png'}, 22 | {name: '爱车', src: 'https://www.dpfile.com/sc/eleconfig/20160126203742aiche.png'}, 23 | {name: '运动健身', src: 'https://www.dpfile.com/sc/eleconfig/20160126203617jianshen.png'}, 24 | {name: '购物', src: 'https://www.dpfile.com/sc/eleconfig/20160314121215icongouwu135.png'}, 25 | {name: '亲子', src: 'https://www.dpfile.com/sc/eleconfig/20160126203905qinzi.png'}, 26 | {name: '到家', src: 'https://www.dpfile.com/sc/eleconfig/20160126203812daojia.png'}, 27 | {name: '家装', src: 'https://www.dpfile.com/sc/eleconfig/20161213162031zhuangxiu.png'}, 28 | {name: '学习培训', src: 'https://www.dpfile.com/gp/cms/1455525720807.png'}, 29 | {name: '医疗健康', src: 'https://www.dpfile.com/sc/eleconfig/20160126204327yiliao.png'} 30 | ], 31 | [ 32 | {name: '小吃快餐', src: 'https://www.dpfile.com/sc/eleconfig/20160204173331xiaochikuaican.png'}, 33 | {name: '自助餐', src: 'https://www.dpfile.com/sc/eleconfig/20160204173511zizhucan.png'}, 34 | {name: '日本菜', src: 'https://www.dpfile.com/sc/eleconfig/20160415121719rihanliaoli.png'}, 35 | {name: '美发', src: 'https://www.dpfile.com/sc/eleconfig/20160316142804meifa.png'}, 36 | {name: '美甲美瞳', src: 'https://www.dpfile.com/sc/eleconfig/20160316143047meijia.png'}, 37 | {name: '美容SPA', src: 'https://www.dpfile.com/sc/eleconfig/20160316143239meirong.png'}, 38 | {name: '瘦身', src: 'https://www.dpfile.com/sc/eleconfig/20160316143316shoushen.png'}, 39 | {name: '亲子摄影', src: 'https://www.dpfile.com/sc/eleconfig/20160316143612qinzisheying.png'}, 40 | {name: '亲子娱乐', src: 'https://www.dpfile.com/sc/eleconfig/20160316143656qinziyoule.png'}, 41 | {name: '全部分类', src: 'https://www.dpfile.com/sc/eleconfig/20160125182200more.png'} 42 | ] 43 | ]; 44 | 45 | export default class Category extends Component { 46 | constructor(props) { 47 | super(props); 48 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 49 | this.state={ 50 | currentIndex:0 51 | } 52 | } 53 | 54 | render() { 55 | const config = { 56 | auto: 2000, 57 | speed:600, 58 | callback:index =>{ 59 | this.setState({ 60 | currentIndex:index 61 | }) 62 | } 63 | }; 64 | return ( 65 |
66 | 67 |
68 | {dataSource[0].map((item,index)=> 69 |
70 |
71 | 72 |
73 | {item.name} 74 |
75 | )} 76 |
77 |
78 | {dataSource[1].map((item,index)=> 79 |
80 |
81 | 82 |
83 | {item.name} 84 |
85 | )} 86 |
87 |
88 | {dataSource[2].map((item,index)=> 89 |
90 |
91 | 92 |
93 | {item.name} 94 |
95 | )} 96 |
97 |
98 |
99 | 100 | 101 | 102 |
103 |
104 | ) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /frontWeb/components/home/views/category/style.css: -------------------------------------------------------------------------------- 1 | .container{ 2 | height: 390px; 3 | } 4 | .category{ 5 | background: #fff; 6 | } 7 | .singleCategory{ 8 | font-size: 4rem; 9 | display: flex; 10 | flex-direction: row; 11 | flex-wrap: wrap; 12 | } 13 | .cell{ 14 | width: 20%; 15 | background: #fff; 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | flex-direction: column; 20 | margin-top: 40px; 21 | } 22 | .img{ 23 | width: 6rem; 24 | } 25 | .name{ 26 | font-size: 2.2rem; 27 | color: #7a7a79; 28 | margin-top: 5px; 29 | } 30 | .spanContainer{ 31 | display: flex; 32 | align-items: center; 33 | justify-content: center; 34 | margin-top: 10px; 35 | height: 75px; 36 | } 37 | .spanOn{ 38 | display: inline-block; 39 | background: #f5734b; 40 | width: 20px; 41 | height: 20px; 42 | border-radius: 100%; 43 | margin:0 10px; 44 | } 45 | .spanOff{ 46 | display: inline-block; 47 | background: #ccc; 48 | width: 20px; 49 | height: 20px; 50 | border-radius: 100%; 51 | margin:0 10px; 52 | } -------------------------------------------------------------------------------- /frontWeb/components/home/views/cheapOrReducers/CheapOrReducers.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import PureRenderMixin from 'react-addons-pure-render-mixin' 3 | import style from './style.css' 4 | import {AdHeader} from "../../../common/AdHeader"; 5 | import {AdCell} from "../../../common/AdCell"; 6 | 7 | export default class CheapOrReducers extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 11 | this.renderAdCell = this.renderAdCell.bind(this) 12 | } 13 | 14 | render() { 15 | const {type,data} = this.props; 16 | return ( 17 |
18 | 19 |
20 | {data&&this.renderAdCell(data)} 21 |
22 |
23 | ) 24 | } 25 | 26 | renderAdCell(arr){ 27 | return arr.map((item,index)=>{ 28 | return( 29 | 30 | ) 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontWeb/components/home/views/cheapOrReducers/style.css: -------------------------------------------------------------------------------- 1 | .container{ 2 | background-color: #fff; 3 | padding:0 10px 30px; 4 | margin-bottom: 20px; 5 | } 6 | .cellContainer{ 7 | display: flex; 8 | flex-direction: row; 9 | } 10 | -------------------------------------------------------------------------------- /frontWeb/components/home/views/guessULike/GuessULike.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import PureRenderMixin from 'react-addons-pure-render-mixin' 3 | import style from './style.css' 4 | import {ListCell} from "../../../common/ListCell"; 5 | import LoadingMore from "../../../common/LoadingMore"; 6 | 7 | 8 | export default class GuessULike extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 12 | this.renderListCell = this.renderListCell.bind(this); 13 | } 14 | 15 | render() { 16 | const {data,loadMore,isLoading} = this.props; 17 | return ( 18 |
19 | 猜你喜欢 20 | {data&&this.renderListCell(data)} 21 | {!isLoading&&} 22 |
23 | ) 24 | } 25 | 26 | renderListCell(arrs){ 27 | return arrs.map((item,index)=>) 28 | } 29 | } 30 | 31 | GuessULike.propTypes = { 32 | data:PropTypes.arrayOf(PropTypes.object.isRequired), 33 | loadMore:PropTypes.func.isRequired, 34 | isLoading:PropTypes.bool.isRequired 35 | } 36 | -------------------------------------------------------------------------------- /frontWeb/components/home/views/guessULike/style.css: -------------------------------------------------------------------------------- 1 | .container{ 2 | background: #fff; 3 | font-size: 2.3rem; 4 | padding: 15px; 5 | } 6 | .totalTag{ 7 | color: #c3c3c3; 8 | font-size: 1.8rem; 9 | display: inline-block; 10 | margin-bottom: 20px; 11 | } -------------------------------------------------------------------------------- /frontWeb/components/home/views/homeHeader/HomeHeader.js: -------------------------------------------------------------------------------- 1 | import React,{Component,PropTypes} from 'react' 2 | import PureRenderMixin from 'react-addons-pure-render-mixin' 3 | import style from './homeHeader.css' 4 | import arrowDown from './arrow_down.png' 5 | import userIcon from './usered.png' 6 | import searchIcon from './search.png' 7 | 8 | export default class HomeHeader extends Component{ 9 | constructor(props){ 10 | super(props); 11 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this) 12 | } 13 | 14 | render(){ 15 | const {cityName,history} = this.props; 16 | return( 17 |
18 |
history.push('/city')}> 19 | {cityName} 20 | 21 |
22 |
23 | 24 | 25 |
26 | 27 |
28 | ) 29 | } 30 | 31 | } 32 | HomeHeader.propTypes={ 33 | cityName:PropTypes.string 34 | }; -------------------------------------------------------------------------------- /frontWeb/components/home/views/homeHeader/arrow_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/frontWeb/components/home/views/homeHeader/arrow_down.png -------------------------------------------------------------------------------- /frontWeb/components/home/views/homeHeader/homeHeader.css: -------------------------------------------------------------------------------- 1 | .homeHeaderContainer{ 2 | background: #ff6633; 3 | display: flex; 4 | flex-direction: row; 5 | justify-content: space-between; 6 | height: 100px; 7 | align-items: center; 8 | font-size:2.5rem; 9 | color: #fff; 10 | padding:1rem; 11 | position: fixed; 12 | width: 100%; 13 | z-index: 999; 14 | top: 0; 15 | } 16 | .arrowDown{ 17 | width: 3.5rem; 18 | height: auto; 19 | } 20 | .searchDiv{ 21 | background: #fff; 22 | height: 80px; 23 | width: 60%; 24 | border-radius: 50px; 25 | padding-left: 1rem; 26 | display: flex; 27 | align-items: center; 28 | } 29 | .searchIcon{ 30 | width: 4rem; 31 | height: auto; 32 | } 33 | .searchInput{ 34 | border: none; 35 | height: 100%; 36 | } 37 | 38 | .userIcon{ 39 | width: 3.5rem; 40 | margin-right: 30px; 41 | } -------------------------------------------------------------------------------- /frontWeb/components/home/views/homeHeader/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/frontWeb/components/home/views/homeHeader/search.png -------------------------------------------------------------------------------- /frontWeb/components/home/views/homeHeader/usered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/frontWeb/components/home/views/homeHeader/usered.png -------------------------------------------------------------------------------- /frontWeb/components/home/views/style.css: -------------------------------------------------------------------------------- 1 | .home{ 2 | background: #eee; 3 | padding-top: 100px; 4 | } -------------------------------------------------------------------------------- /frontWeb/components/notFound/actionTypes.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/frontWeb/components/notFound/actionTypes.js -------------------------------------------------------------------------------- /frontWeb/components/notFound/actions.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/frontWeb/components/notFound/actions.js -------------------------------------------------------------------------------- /frontWeb/components/notFound/index.js: -------------------------------------------------------------------------------- 1 | import view from './views/NotFound' 2 | import reducer from './reducer' 3 | import * as actions from './actions' 4 | 5 | export {view,reducer,actions} -------------------------------------------------------------------------------- /frontWeb/components/notFound/reducer.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/frontWeb/components/notFound/reducer.js -------------------------------------------------------------------------------- /frontWeb/components/notFound/views/NotFound.js: -------------------------------------------------------------------------------- 1 | import React,{Component,PropTypes} from 'react' 2 | 3 | class NotFound extends Component{ 4 | constructor(props){ 5 | super(props) 6 | } 7 | 8 | render(){ 9 | return( 10 |
NotFound
11 | ) 12 | } 13 | } 14 | 15 | 16 | export default NotFound; -------------------------------------------------------------------------------- /frontWeb/components/search/actionTypes.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/frontWeb/components/search/actionTypes.js -------------------------------------------------------------------------------- /frontWeb/components/search/actions.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/frontWeb/components/search/actions.js -------------------------------------------------------------------------------- /frontWeb/components/search/index.js: -------------------------------------------------------------------------------- 1 | import view from './views/Search' 2 | import reducer from './reducer' 3 | import * as actions from './actions' 4 | 5 | export {view,reducer,actions} -------------------------------------------------------------------------------- /frontWeb/components/search/reducer.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/frontWeb/components/search/reducer.js -------------------------------------------------------------------------------- /frontWeb/components/search/views/Search.js: -------------------------------------------------------------------------------- 1 | import React,{Component,PropTypes} from 'react' 2 | 3 | class Search extends Component{ 4 | constructor(props){ 5 | super(props) 6 | } 7 | 8 | render(){ 9 | return( 10 |
Search
11 | ) 12 | } 13 | } 14 | 15 | 16 | export default Search; -------------------------------------------------------------------------------- /frontWeb/components/user/actionTypes.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/frontWeb/components/user/actionTypes.js -------------------------------------------------------------------------------- /frontWeb/components/user/actions.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/frontWeb/components/user/actions.js -------------------------------------------------------------------------------- /frontWeb/components/user/index.js: -------------------------------------------------------------------------------- 1 | import view from './views/User' 2 | import reducer from './reducer' 3 | import * as actions from './actions' 4 | 5 | export {view,reducer,actions} -------------------------------------------------------------------------------- /frontWeb/components/user/reducer.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/frontWeb/components/user/reducer.js -------------------------------------------------------------------------------- /frontWeb/components/user/views/User.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import PureRenderMixin from 'react-addons-pure-render-mixin' 3 | 4 | export default class User extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 8 | } 9 | 10 | render() { 11 | 12 | return ( 13 |
14 | User 15 |
16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontWeb/components/wrap/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const USERINFO_UPDATE = 'USERINFO_UPDATE'; 2 | export const START_FETCH = 'START_FETCH'; 3 | export const FETCH_ERROR = 'FETCH_ERROR'; 4 | export const FETCH_END = 'FETCH_END'; 5 | export const GET_USER_LOCATION = 'GET_USER_LOCATION'; -------------------------------------------------------------------------------- /frontWeb/components/wrap/actions.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from './actionTypes' 2 | 3 | export function updateUserInfo(data) { 4 | return{ 5 | type:ActionTypes.USERINFO_UPDATE, 6 | data 7 | } 8 | } 9 | 10 | export function startFetch() { 11 | return{ 12 | type:ActionTypes.START_FETCH 13 | } 14 | } 15 | 16 | export function fetchError() { 17 | return{ 18 | type:ActionTypes.FETCH_ERROR 19 | } 20 | } 21 | 22 | export function getUserLocation(url) { 23 | return{ 24 | type:ActionTypes.GET_USER_LOCATION, 25 | url 26 | } 27 | } -------------------------------------------------------------------------------- /frontWeb/components/wrap/index.js: -------------------------------------------------------------------------------- 1 | import view from './views/DianpingApp' 2 | import reducer from './reducer' 3 | import * as actions from './actions' 4 | export {view,reducer,actions} -------------------------------------------------------------------------------- /frontWeb/components/wrap/reducer.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from './actionTypes' 2 | import {combineReducers} from 'redux' 3 | 4 | const initialState = {}; 5 | 6 | function userInfo(state = initialState,action) { 7 | switch (action.type){ 8 | case actionTypes.USERINFO_UPDATE: 9 | return action.data; 10 | default: 11 | return state; 12 | } 13 | } 14 | //start end error 15 | function fetchState(state='end',action) { 16 | switch (action.type){ 17 | case actionTypes.START_FETCH: 18 | return 'start'; 19 | case actionTypes.FETCH_ERROR: 20 | return 'error'; 21 | case actionTypes.FETCH_END: 22 | return 'end'; 23 | default: 24 | return state; 25 | } 26 | } 27 | 28 | const rootReducer = combineReducers({ 29 | userInfo, 30 | fetchState 31 | }); 32 | 33 | export default rootReducer 34 | -------------------------------------------------------------------------------- /frontWeb/components/wrap/views/DianpingApp.js: -------------------------------------------------------------------------------- 1 | //router 2 | import React, {Component} from 'react' 3 | import { 4 | BrowserRouter as Router, 5 | Route, 6 | Link, 7 | Switch 8 | } from 'react-router-dom' 9 | import {view as City} from '../../city/index' 10 | import {view as Detail} from '../../detail/index' 11 | import {view as Home} from '../../home/index' 12 | import {view as NotFound} from '../../notFound/index' 13 | import {view as Search} from '../../search/index' 14 | import {view as User} from '../../user/index' 15 | import PureRenderMixin from 'react-addons-pure-render-mixin' 16 | import {bindActionCreators} from 'redux' 17 | import {connect} from 'react-redux' 18 | import {updateUserInfo,getUserLocation} from '../actions' 19 | import './reset.css' 20 | import style from './style.css' 21 | 22 | class DianpingApp extends Component { 23 | constructor(props) { 24 | super(props); 25 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 26 | } 27 | 28 | render() { 29 | return ( 30 | 31 |
32 | { 33 | this.props.isLoading ? 34 |
35 | 36 |
37 | :null 38 | } 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 |
50 | ) 51 | } 52 | 53 | componentDidMount() { 54 | //获取地理位置信息 55 | this.props.getUserLocation({ 56 | url:'/api/getLocation' 57 | }); 58 | } 59 | } 60 | 61 | function mapStateToProps(state) { 62 | return { 63 | isLoading: state.wrap.fetchState === 'start' 64 | } 65 | } 66 | 67 | function mapDispatchToProps(dispatch) { 68 | return { 69 | updateUserInfo: bindActionCreators(updateUserInfo, dispatch), 70 | getUserLocation:bindActionCreators(getUserLocation,dispatch) 71 | } 72 | } 73 | 74 | export default connect( 75 | mapStateToProps, 76 | mapDispatchToProps 77 | )(DianpingApp) -------------------------------------------------------------------------------- /frontWeb/components/wrap/views/reset.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | /************************* 3 | * 4 | * author: Neal 5 | * version: 1.0 6 | * time: 2016-07-11 7 | * 8 | *************************/ 9 | html { -webkit-overflow-scrolling: touch; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%} 10 | article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section,body,div,h1,h2,h3,h4,h5,h6,hr,p,blockquote,dl,dt,dd,ul,ol,li,pre,fieldset,legend,button,input,textarea,form,th,td { margin: 0; padding: 0; vertical-align: baseline} 11 | article,aside,details,figcaption,figure,footer,header,hgroup,nav,section,summary { display: block} 12 | audio,canvas,video { display: inline-block; *display: inline; *zoom:1} 13 | body,button,input,select,textarea { font: 12px/1.5 Tahoma,Helvetica,Arial,"\5B8B\4F53","\5FAE\8F6F\96C5\9ED1",sans-serif} 14 | h1,h2,h3,h4,h5,h6 { font-size: 100%; font-weight: normal} 15 | address,cite,dfn,em,var,i { font-style: normal} 16 | ul,ol { list-style: none outside none} 17 | a { text-decoration: none; outline: 0} 18 | a:hover { text-decoration: underline; } 19 | a:active { text-decoration: none; } 20 | pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word} 21 | sub,sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline} 22 | sup { top: -0.5em} 23 | sub { bottom: -0.25em} 24 | fieldset,iframe { border: 0 none} 25 | img { border: 0 none; vertical-align: middle; -ms-interpolation-mode: bicubic; max-width: 100%;} 26 | button,input,select,textarea { font-family: inherit; font-size: 100%; vertical-align: baseline; *vertical-align: middle} 27 | button,input[type=button],input[type=submit],input[type="reset"] { -webkit-appearance: button; cursor: pointer; *overflow: visible} 28 | button[disabled],input[disabled] { cursor: default} 29 | button::-moz-focus-inner,button::-moz-focus-outer,input::-moz-focus-inner,input::-moz-focus-outer { border: 0 none; padding: 0; margin: 0} 30 | input[type=search] { -webkit-appearance: textfield; -moz-box-sizing: content-box; -webkit-box-sizing: content-box; box-sizing: content-box} 31 | input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration { -webkit-appearance: none} 32 | textarea { overflow: auto; vertical-align: top; resize: vertical} 33 | table { border-collapse: collapse; border-spacing: 0} 34 | strong,em,i { font-weight: normal} 35 | 36 | /**************** base ******************/ 37 | /* display */ 38 | .dn{display:none;} 39 | .di{display:inline;} 40 | .db{display:block;} 41 | .dib{display:inline-block;} /* if the element is block level(eg. div, li), using 'inline_any' instead */ 42 | /* float*/ 43 | .fl { float: left;} 44 | .fr { float: right;} 45 | /* text-align */ 46 | .tc{text-align:center;} 47 | .tr{text-align:right;} 48 | .tl{text-align:left;} 49 | /* vertical-align */ 50 | .vm{ display: inline-block; vertical-align:middle;} 51 | .vtb{ display: inline-block;vertical-align:text-bottom;} 52 | /* position */ 53 | .rel{position:relative;} 54 | .abs{position:absolute;} 55 | /* font-size */ 56 | .f12{font-size:12px;} 57 | .f14{font-size:14px;} 58 | .f16{font-size:16px;} 59 | .f18{font-size:18px;} 60 | .f20{font-size:20px;} 61 | .f24{font-size:24px;} 62 | /* 块状元素水平居中 */ 63 | .auto{margin-left:auto; margin-right:auto;} 64 | /* 清除浮 动*/ 65 | .clearfix{*zoom:1;} 66 | .clearfix:after{display:table; content:''; clear:both;} 67 | /* 单行文字溢出虚点显 示*/ 68 | .ellipsis{text-overflow:ellipsis; white-space:nowrap; overflow:hidden;} -------------------------------------------------------------------------------- /frontWeb/components/wrap/views/style.css: -------------------------------------------------------------------------------- 1 | .loadingContainer{ 2 | position: fixed; 3 | top:0; 4 | z-index: 33; 5 | height: 100%; 6 | width: 100%; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | } 11 | .imgIcon{ 12 | /*width: 60px;*/ 13 | /*height: auto;*/ 14 | } -------------------------------------------------------------------------------- /frontWeb/components/wrap/wrapSaga.js: -------------------------------------------------------------------------------- 1 | import {put,call,take} from 'redux-saga/effects' 2 | import * as wrapActionTypes from './actionTypes' 3 | 4 | import {get} from '../../fetchApi/get' 5 | 6 | export function* getLocation (url) { 7 | yield put({type:wrapActionTypes.START_FETCH}); 8 | try { 9 | return yield call(get,url) 10 | }catch (err){ 11 | yield put({type:wrapActionTypes.FETCH_ERROR}) 12 | }finally { 13 | yield put({type:wrapActionTypes.FETCH_END}) 14 | } 15 | } 16 | 17 | export function* getLocatioFlow() { 18 | while(true){ 19 | let request =yield take(wrapActionTypes.GET_USER_LOCATION); 20 | let response = yield call(getLocation,request.url); 21 | yield put({type:wrapActionTypes.USERINFO_UPDATE,data:response.data}) 22 | } 23 | } -------------------------------------------------------------------------------- /frontWeb/fetchApi/get.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export function get(url, params = '') { 4 | if (params) 5 | return axios(url); 6 | return axios(url,{params:params}) 7 | } -------------------------------------------------------------------------------- /frontWeb/fetchApi/post.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export function post(url,data) { 4 | return axios.post(url,data) 5 | } -------------------------------------------------------------------------------- /frontWeb/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {render} from 'react-dom' 3 | import {view as DianpingApp} from './components/wrap/index' 4 | import {AppContainer} from 'react-hot-loader' 5 | import {Provider} from 'react-redux' 6 | import configureStore from './store' 7 | 8 | let div = document.createElement('div'); 9 | div.setAttribute('id', 'app'); 10 | document.body.appendChild(div); 11 | 12 | const mountNode = document.getElementById('app'); 13 | const store = configureStore(); 14 | render( 15 | 16 | 17 | 18 | 19 | , 20 | mountNode 21 | ); 22 | 23 | if (module.hot && process.env.NODE_ENV !== 'production') { 24 | module.hot.accept(); 25 | } -------------------------------------------------------------------------------- /frontWeb/reducer.js: -------------------------------------------------------------------------------- 1 | import {combineReducers } from 'redux' 2 | import {reducer as wrap} from './components/wrap/index' 3 | import {reducer as city} from './components/city' 4 | import {reducer as detail} from './components/detail' 5 | import {reducer as home} from './components/home' 6 | import {reducer as notFound} from './components/notFound' 7 | import {reducer as search } from './components/search' 8 | import {reducer as user } from './components/user' 9 | 10 | export default combineReducers({ 11 | wrap, 12 | home, 13 | orderDetail:detail, 14 | cities:city 15 | }); 16 | 17 | -------------------------------------------------------------------------------- /frontWeb/rootSaga.js: -------------------------------------------------------------------------------- 1 | import {fork,call} from 'redux-saga/effects' 2 | 3 | import {getAdDataFlow,getULikeDataFlow} from './components/home/homeSaga' 4 | import {getLocatioFlow} from './components/wrap/wrapSaga' 5 | import {getDetailFolw} from './components/detail/detailSaga' 6 | import {getCitiesFlow} from './components/city/citySaga' 7 | 8 | export default function* rootSaga () { 9 | yield fork(getLocatioFlow); 10 | yield fork(getAdDataFlow); 11 | yield fork(getULikeDataFlow); 12 | yield fork(getDetailFolw); 13 | yield fork(getCitiesFlow); 14 | } -------------------------------------------------------------------------------- /frontWeb/store.js: -------------------------------------------------------------------------------- 1 | import {createStore,applyMiddleware, compose} from 'redux' 2 | import rootReducer from './reducer' 3 | import Perf from 'react-addons-perf' 4 | import createSagaMiddleware from 'redux-saga' 5 | import rootSaga from './rootSaga' 6 | 7 | const win = window; 8 | win.Perf = Perf; 9 | 10 | const sagaMiddleware = createSagaMiddleware(); 11 | const middlewares = []; 12 | 13 | const storeEnhancers = compose( 14 | applyMiddleware(...middlewares,sagaMiddleware), 15 | (win && win.devToolsExtension) ? win.devToolsExtension() : (f) => f, 16 | ); 17 | 18 | export default function configureStore(initialState={}) { 19 | const store = createStore(rootReducer, initialState,storeEnhancers); 20 | sagaMiddleware.run(rootSaga); 21 | if (module.hot) { 22 | // Enable Webpack hot module replacement for reducers 23 | module.hot.accept( () => { 24 | const nextRootReducer = require('./reducer'); 25 | store.replaceReducer(nextRootReducer); 26 | }); 27 | } 28 | return store; 29 | } 30 | -------------------------------------------------------------------------------- /hotReloadServer.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const pathLib = require('path'); 3 | const config = require('./config/config'); 4 | const resData = require('./serverData/resData'); 5 | let app = express(); 6 | 7 | app.use('/',require('connect-history-api-fallback')()); 8 | app.use('/',express.static(pathLib.resolve(__dirname,'..','build'))); 9 | app.get('/api/getLocation',function (req,res) { 10 | res.status(200).send({cityName:'北京'}); 11 | }); 12 | /** 13 | * 获取广告数据 14 | */ 15 | app.get('/api/getAdData',function (req,res) { 16 | res.status(200).send(resData.adData); 17 | }); 18 | app.get('/api/getULikeData',function (req,res) { 19 | res.status(200).send(resData.guessULike); 20 | }); 21 | app.get('/api/orderDetail',function (req,res) { 22 | res.status(200).send(resData.detail) 23 | }); 24 | app.get('/api/cities',function (req,res) { 25 | res.status(200).send(resData.cities) 26 | }); 27 | 28 | if(process.env.NODE_ENV !== 'production'){//开发环境下 29 | const webpack = require('webpack'); 30 | const webpackConfig = require('./webpack.config.dev'); 31 | const webpackCompiled = webpack(webpackConfig); 32 | 33 | //配置运行时打包 34 | const webpackDevMiddleware = require('webpack-dev-middleware'); 35 | app.use(webpackDevMiddleware(webpackCompiled,{ 36 | publicPath:'/',//必填项,由于index.html请求的buildxxx.js存放的位置映射到服务器的URI路径是根 37 | stats: {colors: true},//console统计日志带颜色输出 38 | lazy: false,//懒人加载模式。true表示不监控源码修改状态,收到请求才执行webpack的build。false表示监控源码状态,配套使用的watchOptions可以设置与之相关的参数。 39 | watchOptions: { 40 | aggregateTimeout: 300, 41 | poll: true 42 | } 43 | })); 44 | 45 | // 配置热更新 46 | const webpackHotMiddleware = require('webpack-hot-middleware'); 47 | app.use(webpackHotMiddleware(webpackCompiled)); 48 | } 49 | 50 | const server = app.listen(config.hotReloadPort,function () { 51 | let port = server.address().port; 52 | console.log(`Open http://${config.hotReloadHost}:%s`, port); 53 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react.fullstack.dp.demo", 3 | "version": "1.0.0", 4 | "description": "react.fullstack.dianping.demo", 5 | "main": "index.js", 6 | "scripts": { 7 | "build-client:dev": "webpack --config ./webpack.config.dev.js --progress --profile --colors", 8 | "build-client:prod": "webpack --config ./webpack.config.prod.js --progress --profile --colors", 9 | "start-hot-server": "node ./hotReloadServer.js", 10 | "dev": "npm run build-client:dev & npm run start-hot-server ", 11 | "prod": "npm run build-client:prod", 12 | "start": "node ./hotReloadServer.js", 13 | "build": "npm run prod" 14 | }, 15 | "repository": "git+https://github.com/Nealyang/React-Fullstack-Dianping-Demo.git", 16 | "keywords": [ 17 | "React", 18 | "react-router", 19 | "redux", 20 | "webpack2" 21 | ], 22 | "author": "Nealyang", 23 | "license": "ISC", 24 | "bugs": { 25 | "url": "https://github.com/Nealyang/React-Fullstack-Dianping-Demo/issues" 26 | }, 27 | "homepage": "https://github.com/Nealyang/React-Fullstack-Dianping-Demo#readme", 28 | "dependencies": { 29 | "axios": "^0.16.2", 30 | "express": "^4.15.4", 31 | "react": "^15.6.1", 32 | "react-addons-perf": "^15.4.2", 33 | "react-addons-pure-render-mixin": "^15.6.0", 34 | "react-dom": "^15.6.1", 35 | "react-redux": "^5.0.6", 36 | "react-router-dom": "^4.2.2", 37 | "react-swipe": "^5.0.8", 38 | "react-toastify": "^2.0.0-rc.3", 39 | "redux": "^3.7.2", 40 | "redux-immutable-state-invariant": "^2.0.0", 41 | "redux-saga": "^0.15.6", 42 | "swipe-js-iso": "^2.0.4" 43 | }, 44 | "devDependencies": { 45 | "autoprefixer": "^7.1.3", 46 | "babel-core": "^6.26.0", 47 | "babel-loader": "^7.1.2", 48 | "babel-plugin-react-transform": "^2.0.2", 49 | "babel-plugin-transform-remove-console": "^6.8.5", 50 | "babel-polyfill": "^6.26.0", 51 | "babel-preset-env": "^1.6.0", 52 | "babel-preset-es2015": "^6.24.1", 53 | "babel-preset-react": "^6.24.1", 54 | "babel-preset-react-hmre": "^1.1.1", 55 | "babel-preset-react-optimize": "^1.0.1", 56 | "babel-preset-stage-0": "^6.24.1", 57 | "babel-register": "^6.26.0", 58 | "babel-runtime": "^6.26.0", 59 | "clean-webpack-plugin": "^0.1.16", 60 | "concurrently": "^3.5.0", 61 | "connect-history-api-fallback": "^1.3.0", 62 | "cross-env": "^5.0.5", 63 | "css-loader": "^0.28.7", 64 | "extract-text-webpack-plugin": "^3.0.0", 65 | "file-loader": "^0.11.2", 66 | "html-webpack-plugin": "^2.30.1", 67 | "node-loader": "^0.6.0", 68 | "node-sass": "^4.5.3", 69 | "nodemon": "^1.11.0", 70 | "open-browser-webpack-plugin": "^0.0.5", 71 | "postcss-loader": "^2.0.6", 72 | "progress-bar-webpack-plugin": "^1.10.0", 73 | "react-hot-loader": "^3.0.0-beta.6", 74 | "react-transform-catch-errors": "^1.0.2", 75 | "react-transform-hmr": "^1.0.4", 76 | "redbox-react": "^1.5.0", 77 | "rimraf": "^2.6.1", 78 | "sass-loader": "^6.0.6", 79 | "style-loader": "^0.18.2", 80 | "url-loader": "^0.5.9", 81 | "webpack": "^3.5.5", 82 | "webpack-dev-middleware": "^1.12.0", 83 | "webpack-dev-server": "^2.7.1", 84 | "webpack-hot-middleware": "^2.18.2", 85 | "webpack-isomorphic-tools": "^3.0.3" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins:[ 3 | require('autoprefixer')({browsers:'last 2 versions'}) 4 | ] 5 | }; -------------------------------------------------------------------------------- /record.patch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/record.patch -------------------------------------------------------------------------------- /record/city.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/record/city.jpg -------------------------------------------------------------------------------- /record/detail_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/record/detail_1.jpg -------------------------------------------------------------------------------- /record/detail_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/record/detail_2.jpg -------------------------------------------------------------------------------- /record/framework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/record/framework.png -------------------------------------------------------------------------------- /record/home_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/record/home_1.jpg -------------------------------------------------------------------------------- /record/home_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/record/home_2.jpg -------------------------------------------------------------------------------- /record/loading.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/record/loading.jpg -------------------------------------------------------------------------------- /record/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/record/loading.png -------------------------------------------------------------------------------- /record/play.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/record/play.gif -------------------------------------------------------------------------------- /record/saga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/record/saga.png -------------------------------------------------------------------------------- /record/state_tree.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/record/state_tree.gif -------------------------------------------------------------------------------- /serverData/resData.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | adData: { 3 | ad: [ 4 | { 5 | name: '领10元礼券', 6 | subName: "中元节献礼", 7 | thumbnail: 'http://www.dpfile.com/sc/ares_pics/34e75fb3b9bff41ddf36ce01c4d0c293.png' 8 | }, 9 | { 10 | name: '超值折扣菜', 11 | subName: "低至1折起", 12 | thumbnail: 'http://www.dpfile.com/sc/ares_pics/89796a587c129aeb6bea8c6b30efa713.png' 13 | }, 14 | { 15 | name: '拼手气赢豪礼', 16 | subName: "大奖免费抽取", 17 | thumbnail: 'http://www.dpfile.com/sc/ares_pics/72d664b99eb72ae209c0c8ca452b610.png' 18 | }, 19 | { 20 | name: '58元红包', 21 | subName: "天天来就有", 22 | thumbnail: 'http://www.dpfile.com/sc/ares_pics/205326b96e2a31c2e79529548ff5dc24.png' 23 | } 24 | ], 25 | cheap: [ 26 | { 27 | thumbnail: 'https://p0.meituan.net/deal/75214c8211fcced9ecce7d06ad47531d20687.jpg%40120w_90h_1e_1c_1l%7Cwatermark%3D1%26%26r%3D1%26p%3D9%26x%3D20%26y%3D20', 28 | price: '4.99', 29 | subtract: '6', 30 | name: '田记老太婆摊摊面小吃店' 31 | }, 32 | { 33 | thumbnail: 'https://p1.meituan.net/deal/84c5e702f07d790ab7666b7d102bdb3452405.jpg%40120w_90h_1e_1c_1l%7Cwatermark%3D1%26%26r%3D1%26p%3D9%26x%3D20%26y%3D20', 34 | price: '9.58', 35 | subtract: '10', 36 | name: '羊佬上汤羊肉米线' 37 | }, 38 | { 39 | thumbnail: 'https://p0.meituan.net/deal/6b7786301af51465c433a4749c4d624f54111.jpg%40120w_90h_1e_1c_1l%7Cwatermark%3D1%26%26r%3D1%26p%3D9%26x%3D20%26y%3D20', 40 | price: '7', 41 | subtract: '9', 42 | name: '真功夫' 43 | }, 44 | ], 45 | reduces: [ 46 | { 47 | thumbnail: 'https://p1.meituan.net/deal/0b96d6009af9c36fe3a901a638412ee769091.jpg%40120w_90h_1e_1c_1l%7Cwatermark%3D1%26%26r%3D1%26p%3D9%26x%3D20%26y%3D20', 48 | price: '30.08', 49 | tag: '立减2', 50 | name: '醉花甲' 51 | }, 52 | { 53 | thumbnail: 'https://p1.meituan.net/deal/e2b5548d49533cc5150fe14ec18fed5d333671.jpg%40120w_90h_1e_1c_1l%7Cwatermark%3D1%26%26r%3D1%26p%3D9%26x%3D20%26y%3D20', 54 | price: '90', 55 | tag: '', 56 | name: '绘咖啡' 57 | }, 58 | { 59 | thumbnail: 'https://p1.meituan.net/deal/641b88485fa132c747302eb7d42f8f3a47163.jpg%40120w_90h_1e_1c_1l%7Cwatermark%3D1%26%26r%3D1%26p%3D9%26x%3D20%26y%3D20', 60 | price: '98', 61 | subtract: '立减2', 62 | name: '真功夫' 63 | } 64 | ], 65 | }, 66 | guessULike: [ 67 | { 68 | name: '江同学烤猪蹄', 69 | distance: '721m', 70 | desc: '[4店通用] 10元代金券1张,可叠加', 71 | price: '8.9', 72 | exPrice: '10', 73 | cell: '7147', 74 | tag: '免预约', 75 | thumbnail: 'https://p1.meituan.net/deal/2b601d2c32d9ee15f96ccd04076a7fb474647.jpg%40180w_180h_1e_1c_1l_80q%7Cwatermark%3D0' 76 | }, 77 | { 78 | name: 'S瘦身工作室', 79 | distance: '389m', 80 | desc: '[大望路] 单人保湿舒压净排套餐7选2', 81 | price: '11', 82 | exPrice: '1960', 83 | cell: '9', 84 | tag: '', 85 | thumbnail: 'https://p0.meituan.net/dpdeal/953bd949ee0c1511699c67056e115a4245109.jpg%40180w_180h_1e_1c_1l_80q%7Cwatermark%3D0' 86 | }, 87 | { 88 | name: '安妮意大利餐厅', 89 | distance: '432m', 90 | desc: '[9店通用] 下午茶,建议单人使用', 91 | price: '25', 92 | exPrice: '43', 93 | cell: '648', 94 | tag: '免预约', 95 | thumbnail: 'https://p0.meituan.net/deal/8b66a93dcd6dcbd7c729ea971d72b9a729068.jpg%40180w_180h_1e_1c_1l_80q%7Cwatermark%3D0' 96 | }, 97 | { 98 | name: '北京纯K-SHOW TIME', 99 | distance: '<100m', 100 | desc: '[大望路] 时段4选1单人欢唱券', 101 | price: '34.9', 102 | exPrice: '152', 103 | cell: '23221', 104 | tag: '', 105 | thumbnail: 'https://p0.meituan.net/dpdeal/6480243ae9f7ef6cfaf4ac013b3fa8be80989.jpg%40180w_180h_1e_1c_1l_80q%7Cwatermark%3D0' 106 | }, 107 | { 108 | name: '新疆阿达西餐厅', 109 | distance: '848m', 110 | desc: '[小庄/红庙] 烧烤全家福套餐,建议1-2人使用', 111 | price: '46', 112 | exPrice: '58', 113 | cell: '54', 114 | tag: '免预约', 115 | thumbnail: 'https://p1.meituan.net/deal/d31a8b970bb1c3201914dc731ea21f1a126360.jpg%40180w_180h_1e_1c_1l_80q%7Cwatermark%3D0' 116 | }, 117 | { 118 | name: '图站咖啡', 119 | distance: '358m', 120 | desc: '[大望路] 带骨小牛排单人餐', 121 | price: '18', 122 | exPrice: '15', 123 | cell: '28', 124 | tag: '免预约', 125 | thumbnail: 'https://p0.meituan.net/deal/c1fef6c35b8f00fef4c9680cef0b720c39190.jpg%40180w_180h_1e_1c_1l_80q%7Cwatermark%3D0' 126 | }, 127 | { 128 | name: '紫光园', 129 | distance: '899m', 130 | desc: '[红庙北里等14店] 烤鸭1套,提供免费WiFi加', 131 | price: '75', 132 | exPrice: '98', 133 | cell: '17147', 134 | tag: '', 135 | thumbnail: 'https://p1.meituan.net/deal/9d74d45f3a42472fea3c6a4dd08de5de60408.jpg%40180w_180h_1e_1c_1l_80q%7Cwatermark%3D0' 136 | }, 137 | { 138 | name: '上善河豚料理', 139 | distance: '272m', 140 | desc: '[国贸] 100元代金券1张,可叠加', 141 | price: '80', 142 | exPrice: '100', 143 | cell: '531', 144 | tag: '免预约', 145 | thumbnail: 'https://p0.meituan.net/deal/4f088c525af9ee76b039eeb778ab9e02110242.jpg%40180w_180h_1e_1c_1l_80q%7Cwatermark%3D0' 146 | }, 147 | { 148 | name: '叶叶菩提', 149 | distance: '311m', 150 | desc: '[大望路] 精选下午茶1份,提供免费WiFi', 151 | price: '158', 152 | exPrice: '198', 153 | cell: '110', 154 | tag: '', 155 | thumbnail: 'https://p1.meituan.net/deal/3ecf5131de418090e9e2124faad65d1891765.jpg%40180w_180h_1e_1c_1l_80q%7Cwatermark%3D0' 156 | }, 157 | { 158 | name: '佰伦斯游泳健身联盟', 159 | distance: '<100m', 160 | desc: '[5店通用] 佰伦斯单人游泳+健身通票一张', 161 | price: '65', 162 | exPrice: '80', 163 | cell: '35', 164 | tag: '免预约', 165 | thumbnail: 'https://p0.meituan.net/dpdeal/14858bab0a08daaaac929408ba2aa25a92604.jpg%40180w_180h_1e_1c_1l_80q%7Cwatermark%3D0' 166 | }, 167 | { 168 | name: '京八珍', 169 | distance: '346m', 170 | desc: '[45店通用] 50元代金券1张,可叠加', 171 | price: '43.6', 172 | exPrice: '50', 173 | cell: '59348', 174 | tag: '免预约', 175 | thumbnail: 'https://p0.meituan.net/deal/5f4e2559e993adc85a5175f5fd8a287f35000.jpg%40180w_180h_1e_1c_1l_80q%7Cwatermark%3D0' 176 | }, 177 | { 178 | name: '苗方清颜', 179 | distance: '563m', 180 | desc: '[2店通用] 单人痘肌护理/淡化痘印/磁泉深层补水套餐3选1', 181 | price: '18', 182 | exPrice: '488', 183 | cell: '1371', 184 | tag: '', 185 | thumbnail: 'https://p0.meituan.net/dpdeal/726a9e3a2766de7f32b12be25f2c3acc68313.jpg%40180w_180h_1e_1c_1l_80q%7Cwatermark%3D0' 186 | } 187 | ], 188 | detail: { 189 | imgs:[ 190 | 'https://p0.meituan.net/deal/22a8fa02b961a350139f122389f73e5f117067.jpg%40450w_280h_1e_1c_1l%7Cwatermark%3D1%26%26r%3D1%26p%3D9%26x%3D2%26y%3D2%26relative%3D1%26o%3D20', 191 | 'https://p1.meituan.net/deal/dd1b6b64a0061a3fd4fc04004cf9127b296845.jpg%40450w_280h_1e_1c_1l%7Cwatermark%3D1%26%26r%3D1%26p%3D9%26x%3D2%26y%3D2%26relative%3D1%26o%3D20', 192 | 'https://p1.meituan.net/deal/54008a2e6143af277590dfc3183fc01d31147.jpg%40450w_280h_1e_1c_1l%7Cwatermark%3D1%26%26r%3D1%26p%3D9%26x%3D2%26y%3D2%26relative%3D1%26o%3D20', 193 | 'https://p0.meituan.net/deal/04ed9293e93d49699065bc9e2b634ecc28571.jpg%40450w_280h_1e_1c_1l%7Cwatermark%3D1%26%26r%3D1%26p%3D9%26x%3D2%26y%3D2%26relative%3D1%26o%3D20', 194 | 'https://p1.meituan.net/deal/d2a8229118b41a64fecad26595555d3425155.jpg%40450w_280h_1e_1c_1l%7Cwatermark%3D1%26%26r%3D1%26p%3D9%26x%3D2%26y%3D2%26relative%3D1%26o%3D20' 195 | ], 196 | recommends:[ 197 | {thumbnail:'https://p1.meituan.net/deal/3a5df590599df6a86b803d509df0161b48985.jpg%40450w_280h_1e_1c_1l%7Cwatermark%3D1%26%26r%3D1%26p%3D9%26x%3D2%26y%3D2%26relative%3D1%26o%3D20',name:'伊豆野菜村',price:'158',exPrice:'168'}, 198 | {thumbnail:'https://p1.meituan.net/deal/91b3517c6cab016fb71a23b98fb56acf56588.jpg%40450w_280h_1e_1c_1l%7Cwatermark%3D1%26%26r%3D1%26p%3D9%26x%3D2%26y%3D2%26relative%3D1%26o%3D20',name:'南京大排档',price:'89',exPrice:'100'}, 199 | {thumbnail:'https://p0.meituan.net/dpdeal/9bd1b2ce2dbb0f4c5abc99e51e28918e46343.jpg%40450w_280h_1e_1c_1l%7Cwatermark%3D1%26%26r%3D1%26p%3D9%26x%3D2%26y%3D2%26relative%3D1%26o%3D20',name:'伊豆野菜村',price:'49',exPrice:'408'}, 200 | ], 201 | orderDetail:{ 202 | youxiaoqi:['2017-07-18至2017-09-30'], 203 | chuwairiqi:['有效期内周末、法定节假日可用'], 204 | shiyongshijian:['团购券使用时间:11:00-21:30'], 205 | yuyuetixing:['无需预约,消费高峰时可能需要等位'], 206 | guizetixing:['每桌最多使用1张团购券','每张团购券建议2人使用'], 207 | baojian:['店内无包间'], 208 | tangshiwaidai:['堂食外带均可,可以打包,打包费详情咨询商家'], 209 | wenxintishi:['团购用户不可同时享受商家其他优惠','酒水饮料等问题,请致电商家咨询,以商家反馈为准'] 210 | } 211 | }, 212 | cities:[ 213 | {name:'北京'}, 214 | {name:'成都'}, 215 | {name:'重庆'}, 216 | {name:'广州'}, 217 | {name:'杭州'}, 218 | {name:'南京'}, 219 | {name:'上海'}, 220 | {name:'深圳'}, 221 | {name:'苏州'}, 222 | {name:'天津'}, 223 | {name:'武汉'}, 224 | {name:'西安'} 225 | 226 | ] 227 | }; -------------------------------------------------------------------------------- /util/Util.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/util/Util.js -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const pathLib = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const webpack = require('webpack'); 4 | const OpenBrowser = require('open-browser-webpack-plugin'); 5 | const ExtractText = require('extract-text-webpack-plugin'); 6 | const CleanPlugin = require('clean-webpack-plugin'); 7 | const ProgressBarPlugin = require('progress-bar-webpack-plugin'); 8 | const config = require('./config/config'); 9 | module.exports = { 10 | entry: { 11 | index: [ 12 | 'babel-polyfill', 13 | 'react-hot-loader/patch', 14 | 'webpack-hot-middleware/client?path=http://localhost:8000/__webpack_hmr', 15 | pathLib.resolve(__dirname,'frontWeb', 'index.js') 16 | ], 17 | vendor: ['react','react-dom','react-router-dom','redux','react-redux','redux-saga','swipe-js-iso','react-swipe','react-addons-pure-render-mixin'] 18 | }, 19 | output: { 20 | path: pathLib.resolve(__dirname, 'build'), 21 | publicPath: "/", 22 | filename: '[name].[hash:8].js' 23 | }, 24 | devtool:'eval-source-map', 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.jsx?$/, 29 | exclude: /node_modules/, 30 | use: ['babel-loader'] 31 | }, 32 | { 33 | test: /\.css$/, 34 | exclude: /node_modules/, 35 | use:ExtractText.extract({ 36 | fallback: "style-loader", 37 | use: [ 38 | { 39 | loader: 'css-loader', 40 | options: { 41 | modules: true, 42 | localIdentName: '[path][name]-[local]-[hash:base64:5]', 43 | importLoaders: 1 44 | } 45 | }, 46 | 'postcss-loader' 47 | ] 48 | }) 49 | }, 50 | { 51 | test:/\.(png|jpg|gif|JPG|GIF|PNG|BMP|bmp|JPEG|jpeg)$/, 52 | exclude:/node_modules/, 53 | use:[ 54 | { 55 | loader:'url-loader', 56 | options: { 57 | limit:8192 58 | } 59 | } 60 | ] 61 | }, 62 | { 63 | test: /\.(eot|woff|ttf|woff2|svg)$/, 64 | use: 'url-loader' 65 | } 66 | ] 67 | }, 68 | plugins: [ 69 | new CleanPlugin(['build']), 70 | new ProgressBarPlugin(), 71 | new webpack.optimize.AggressiveMergingPlugin(), 72 | new webpack.DefinePlugin({ 73 | "progress.env.NODE_ENV":JSON.stringify('development') 74 | }), 75 | new HtmlWebpackPlugin({ 76 | title: "My app", 77 | showErrors: true, 78 | }), 79 | new webpack.HotModuleReplacementPlugin(), 80 | new webpack.NoEmitOnErrorsPlugin(),//保证出错时页面不阻塞,且会在编译结束后报错 81 | new ExtractText({ 82 | filename:'bundle.[hash].css', 83 | disable:false, 84 | allChunks:true 85 | }), 86 | new OpenBrowser({url:`http://${config.hotReloadHost}:${config.hotReloadPort}`}), 87 | new webpack.HashedModuleIdsPlugin(), 88 | new webpack.optimize.CommonsChunkPlugin({ 89 | name: 'vendor', 90 | minChunks: function (module) { 91 | return module.context && module.context.indexOf('node_modules') !== -1; 92 | } 93 | }), 94 | new webpack.optimize.CommonsChunkPlugin({ 95 | name: "manifest" 96 | }) 97 | ], 98 | resolve: { 99 | extensions: ['.js', '.json', '.sass', '.scss', '.less', 'jsx'] 100 | } 101 | }; -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const pathLib = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const webpack = require('webpack'); 4 | const OpenBrowser = require('open-browser-webpack-plugin'); 5 | const ExtractText = require('extract-text-webpack-plugin'); 6 | const CleanPlugin = require('clean-webpack-plugin'); 7 | module.exports = { 8 | entry: { 9 | index: [ 10 | pathLib.resolve(__dirname, 'frontWeb', 'index.js') 11 | ], 12 | vendor: ['react','react-dom','react-router-dom'] 13 | }, 14 | devtool:'source-map', 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.jsx?$/, 19 | exclude: /node_modules/, 20 | use: ['babel-loader'] 21 | }, 22 | { 23 | test: /\.css$/, 24 | exclude: /node_modules/, 25 | use:ExtractText.extract({ 26 | fallback: "style-loader", 27 | use: [ 28 | { 29 | loader: 'css-loader', 30 | options: { 31 | modules: true, 32 | localIdentName: '[path][name]-[local]-[hash:base64:5]', 33 | importLoaders: 1 34 | } 35 | }, 36 | 'postcss-loader' 37 | ] 38 | }) 39 | }, 40 | { 41 | test:/\.(png|jpg|gif|JPG|GIF|PNG|BMP|bmp|JPEG|jpeg)$/, 42 | exclude:/node_modules/, 43 | use:[ 44 | { 45 | loader:'url-loader', 46 | options: { 47 | limit:8192 48 | } 49 | } 50 | ] 51 | }, 52 | { 53 | test: /\.(eot|woff|ttf|woff2|svg)$/, 54 | use: 'url-loader' 55 | } 56 | ] 57 | }, 58 | plugins: [ 59 | new CleanPlugin(['build']), 60 | new webpack.DefinePlugin({ 61 | "progress.env.NODE_ENV":JSON.stringify('production') 62 | }), 63 | new HtmlWebpackPlugin({ 64 | title: "My app", 65 | showErrors: true, 66 | }), 67 | new ExtractText({ 68 | filename:'bundle.[contenthash].css', 69 | disable:false, 70 | allChunks:true 71 | }), 72 | new webpack.HashedModuleIdsPlugin(), 73 | new webpack.optimize.CommonsChunkPlugin({ 74 | name: 'vendor', 75 | minChunks: function (module) { 76 | return module.context && module.context.indexOf('node_modules') !== -1; 77 | } 78 | }), 79 | new webpack.optimize.CommonsChunkPlugin({ 80 | name: "manifest" 81 | }), 82 | new webpack.optimize.UglifyJsPlugin({ 83 | sourceMap: true, 84 | compress: { 85 | // 在 UglifyJs 删除没有用到的代码时输出警告 86 | warnings: false, 87 | }, 88 | }), 89 | ], 90 | output: { 91 | path: pathLib.resolve(__dirname, 'build'), 92 | publicPath: "./", 93 | filename: '[name].[chunkhash].js' 94 | }, 95 | resolve: { 96 | extensions: ['.js', '.json', '.sass', '.scss', '.less', 'jsx'] 97 | } 98 | }; -------------------------------------------------------------------------------- /webpack_chunk.patch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nealyang/React-Fullstack-Dianping-Demo/e0d0187b0d5e19c917c024618a31d31d2ac46e9e/webpack_chunk.patch --------------------------------------------------------------------------------