├── .gitignore ├── app ├── config │ ├── README.md │ └── localStorageKey.jsx ├── containers │ ├── Home │ │ ├── README.md │ │ ├── subPage │ │ │ ├── style.scss │ │ │ ├── Reco.jsx │ │ │ └── GuessInterest.jsx │ │ └── index.jsx │ ├── City │ │ ├── README.md │ │ ├── style.scss │ │ └── index.jsx │ ├── Detail │ │ ├── README.md │ │ └── index.jsx │ ├── Search │ │ ├── README.md │ │ ├── index.jsx │ │ └── subPage │ │ │ └── SearchList.jsx │ ├── README.md │ ├── User │ │ └── index.jsx │ ├── 404.jsx │ └── index.jsx ├── fetch │ ├── README.md │ ├── get.jsx │ ├── Home │ │ └── index.jsx │ ├── Search │ │ └── index.jsx │ └── post.jsx ├── util │ ├── README.md │ └── localStorage.jsx ├── components │ ├── CityList │ │ ├── README.md │ │ ├── style.scss │ │ └── index.jsx │ ├── Category │ │ ├── README.md │ │ ├── style.scss │ │ └── index.jsx │ ├── Header │ │ ├── README.md │ │ ├── style.scss │ │ └── index.jsx │ ├── HomeReco │ │ ├── README.md │ │ ├── style.scss │ │ └── index.jsx │ ├── LoadMore │ │ ├── README.md │ │ ├── style.scss │ │ └── index.jsx │ ├── SearchInput │ │ ├── README.md │ │ ├── style.scss │ │ └── index.jsx │ ├── CurrentCity │ │ ├── README.md │ │ ├── style.scss │ │ └── index.jsx │ ├── GoodsList │ │ ├── README.md │ │ ├── index.jsx │ │ ├── Item │ │ │ └── index.jsx │ │ └── style.scss │ ├── SearchHeader │ │ ├── README.md │ │ ├── style.scss │ │ └── index.jsx │ └── HomeHeader │ │ ├── README.md │ │ ├── style.scss │ │ └── index.jsx ├── router │ ├── README.md │ └── routerMap.jsx ├── reducers │ ├── README.md │ ├── index.jsx │ └── userInfo.jsx ├── actions │ ├── README.md │ └── userInfo.jsx ├── constants │ ├── README.md │ └── userInfo.jsx ├── static │ ├── fonts │ │ ├── iconfont.eot │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ └── iconfont.svg │ └── scss │ │ ├── common.scss │ │ ├── reset.scss │ │ └── font.scss ├── store │ └── configureStore.jsx ├── index.html └── index.jsx ├── mock ├── README.md ├── Home │ ├── reco.js │ └── guessInterest.js ├── Search │ └── searchList.js └── server.js ├── postcss.config.js ├── .babelrc ├── .eslintrc ├── README.md ├── package.json ├── webpack.config.js └── webpack.config.production.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build -------------------------------------------------------------------------------- /app/config/README.md: -------------------------------------------------------------------------------- 1 | 常用变量名的缩写配置 -------------------------------------------------------------------------------- /app/containers/Home/README.md: -------------------------------------------------------------------------------- 1 | 主页 -------------------------------------------------------------------------------- /app/fetch/README.md: -------------------------------------------------------------------------------- 1 | get / post 数据 -------------------------------------------------------------------------------- /app/util/README.md: -------------------------------------------------------------------------------- 1 | 全站需要的一些工具函数 -------------------------------------------------------------------------------- /mock/README.md: -------------------------------------------------------------------------------- 1 | 模拟get / post 数据 -------------------------------------------------------------------------------- /app/components/CityList/README.md: -------------------------------------------------------------------------------- 1 | 城市列表 -------------------------------------------------------------------------------- /app/containers/City/README.md: -------------------------------------------------------------------------------- 1 | 选择城市页面 -------------------------------------------------------------------------------- /app/containers/Detail/README.md: -------------------------------------------------------------------------------- 1 | 详细页 -------------------------------------------------------------------------------- /app/containers/Search/README.md: -------------------------------------------------------------------------------- 1 | 搜索页 -------------------------------------------------------------------------------- /app/components/Category/README.md: -------------------------------------------------------------------------------- 1 | 轮播图形式的类别组件 -------------------------------------------------------------------------------- /app/components/Header/README.md: -------------------------------------------------------------------------------- 1 | 全站Header公共组件 -------------------------------------------------------------------------------- /app/components/HomeReco/README.md: -------------------------------------------------------------------------------- 1 | 首页广告推荐位组件 -------------------------------------------------------------------------------- /app/components/LoadMore/README.md: -------------------------------------------------------------------------------- 1 | 加载更多组件 -------------------------------------------------------------------------------- /app/components/SearchInput/README.md: -------------------------------------------------------------------------------- 1 | 搜索框 -------------------------------------------------------------------------------- /app/router/README.md: -------------------------------------------------------------------------------- 1 | 这是路由map 2 | 所有的路由都在这里 -------------------------------------------------------------------------------- /app/components/CurrentCity/README.md: -------------------------------------------------------------------------------- 1 | 显示当前城市的大白块组件 -------------------------------------------------------------------------------- /app/components/GoodsList/README.md: -------------------------------------------------------------------------------- 1 | Home页面的猜你喜欢组件 -------------------------------------------------------------------------------- /app/components/SearchHeader/README.md: -------------------------------------------------------------------------------- 1 | 搜索页的Header -------------------------------------------------------------------------------- /app/reducers/README.md: -------------------------------------------------------------------------------- 1 | 存放react-redux的readucer -------------------------------------------------------------------------------- /app/components/HomeHeader/README.md: -------------------------------------------------------------------------------- 1 | Home页面用的Header -------------------------------------------------------------------------------- /app/actions/README.md: -------------------------------------------------------------------------------- 1 | react-redux的actions 2 | 和reducer一一对应 -------------------------------------------------------------------------------- /app/constants/README.md: -------------------------------------------------------------------------------- 1 | react-redux的常用变量名映射 2 | 与reducer一一对应 -------------------------------------------------------------------------------- /app/containers/README.md: -------------------------------------------------------------------------------- 1 | 一个文件夹对应一个页面 2 | subpage中为该页面需要的smart组件 -------------------------------------------------------------------------------- /app/config/localStorageKey.jsx: -------------------------------------------------------------------------------- 1 | export const CITYNAME = 'USER_CURRENT_CITY_NAME'; -------------------------------------------------------------------------------- /app/constants/userInfo.jsx: -------------------------------------------------------------------------------- 1 | export const USERINFO_UPDATE_CITYNAME = 'USERINFO_UPDATE_CITYNAME'; -------------------------------------------------------------------------------- /app/components/SearchHeader/style.scss: -------------------------------------------------------------------------------- 1 | .searchHeader { 2 | .searchInput { 3 | margin-left: 30px; 4 | } 5 | } -------------------------------------------------------------------------------- /app/static/fonts/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeLittlePrince/react-webapp-spa/HEAD/app/static/fonts/iconfont.eot -------------------------------------------------------------------------------- /app/static/fonts/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeLittlePrince/react-webapp-spa/HEAD/app/static/fonts/iconfont.ttf -------------------------------------------------------------------------------- /app/static/fonts/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeLittlePrince/react-webapp-spa/HEAD/app/static/fonts/iconfont.woff -------------------------------------------------------------------------------- /app/components/LoadMore/style.scss: -------------------------------------------------------------------------------- 1 | .loadMore { 2 | font-size: 18px; 3 | line-height: 40px; 4 | color: #999; 5 | text-align: center; 6 | } -------------------------------------------------------------------------------- /app/containers/City/style.scss: -------------------------------------------------------------------------------- 1 | .city { 2 | .header { 3 | h3 { 4 | font-size: 16px; 5 | padding: 5px 0; 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /app/reducers/index.jsx: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import userInfo from './userInfo'; 4 | 5 | export default combineReducers({ 6 | userInfo 7 | }); -------------------------------------------------------------------------------- /app/actions/userInfo.jsx: -------------------------------------------------------------------------------- 1 | import * as actionTypes from 'constants/userInfo'; 2 | 3 | export function updateUserCity(cityName) { 4 | return { type: actionTypes.USERINFO_UPDATE_CITYNAME, cityName }; 5 | } -------------------------------------------------------------------------------- /app/components/CurrentCity/style.scss: -------------------------------------------------------------------------------- 1 | .currentCity { 2 | color: #333; 3 | padding: 30px; 4 | text-align: center; 5 | font-size: 30px; 6 | background-color: #fff; 7 | border-bottom: 1px solid #e0e0e0; 8 | } -------------------------------------------------------------------------------- /app/containers/User/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class User extends React.PureComponent { 4 | render() { 5 | return ( 6 |
User
7 | ); 8 | } 9 | } 10 | 11 | export default User; -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer') 4 | ], 5 | // 配置autoprefix 6 | browsers: [ 7 | "iOS >= 8", 8 | "Firefox >= 20", 9 | "Android > 4.4", 10 | "ie >= 9" 11 | ] 12 | } -------------------------------------------------------------------------------- /app/containers/404.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class NotFound extends React.PureComponent { 4 | render() { 5 | return ( 6 |
7 |

Not Found~~~

8 |
9 | ); 10 | } 11 | } 12 | 13 | export default NotFound; -------------------------------------------------------------------------------- /app/fetch/get.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 封装fetch的get请求 3 | * @param {*string} url 请求地址 4 | * @return fetch返回的数据 5 | */ 6 | export default function get(url) { 7 | return fetch(url, { 8 | headers: { 9 | 'Accept': 'application/json, text/plain, */*', 10 | } 11 | }); 12 | } -------------------------------------------------------------------------------- /app/containers/Home/subPage/style.scss: -------------------------------------------------------------------------------- 1 | .home-recommend { 2 | margin-top: 15px; 3 | } 4 | 5 | .home-guessInterest { 6 | margin-top: 15px; 7 | h3 { 8 | background-color: #fff; 9 | font-size: 18px; 10 | border-bottom: 1px solid #eee; 11 | padding: 10px; 12 | } 13 | } -------------------------------------------------------------------------------- /app/store/configureStore.jsx: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | import Reducers from 'reducers'; 3 | 4 | export default function () { 5 | return createStore( 6 | Reducers, 7 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() // 这句是为了chrome redux插件用的 8 | ); 9 | } -------------------------------------------------------------------------------- /app/components/CurrentCity/index.jsx: -------------------------------------------------------------------------------- 1 | import './style'; 2 | import React from 'react'; 3 | 4 | class CurrentCity extends React.PureComponent { 5 | render() { 6 | return ( 7 |
{this.props.cityName}
8 | ); 9 | } 10 | } 11 | 12 | export default CurrentCity; -------------------------------------------------------------------------------- /app/fetch/Home/index.jsx: -------------------------------------------------------------------------------- 1 | import get from '../get'; 2 | 3 | export function getRecoData() { 4 | return get('/api/home/reco'); 5 | } 6 | 7 | export function getGuessInterestData(city, page) { 8 | return get('api/home/guessInterest/' 9 | + encodeURIComponent(city) 10 | + '/' 11 | + page); 12 | } -------------------------------------------------------------------------------- /app/containers/Detail/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | class Detail extends React.Component { 3 | render() { 4 | return ( 5 |
6 | This is Detail! And the param is {this.props.match.params.id} 7 |
8 | ); 9 | } 10 | } 11 | 12 | export default Detail; -------------------------------------------------------------------------------- /app/static/scss/common.scss: -------------------------------------------------------------------------------- 1 | @import './reset.scss'; 2 | @import './font.scss'; 3 | 4 | body { 5 | background-color: #f6f6f6; 6 | line-height: 1.2; 7 | } 8 | 9 | .l-clearfix:after { 10 | display: table; 11 | content: ''; 12 | clear: both; 13 | } 14 | .l-left { 15 | float: left; 16 | } 17 | .l-right { 18 | float: right; 19 | } -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | react-webapp 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /app/components/Header/style.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | position: relative; 3 | background-color: #323232; 4 | padding: 10px; 5 | color: #fff; 6 | font-size: 16px; 7 | line-height: 1; 8 | text-align: center; 9 | .icon-arrow-left { 10 | position: absolute; 11 | left: 10px; 12 | font-size: 20px; 13 | margin: 5px 0; 14 | } 15 | } -------------------------------------------------------------------------------- /app/reducers/userInfo.jsx: -------------------------------------------------------------------------------- 1 | const initState = {}; 2 | 3 | export default function userInfo(state = initState, action) { 4 | switch (action.type) { 5 | case 'USERINFO_UPDATE_CITYNAME': 6 | return { 7 | ...state, 8 | cityName: action.cityName 9 | }; 10 | default: 11 | return state; 12 | } 13 | } -------------------------------------------------------------------------------- /app/fetch/Search/index.jsx: -------------------------------------------------------------------------------- 1 | import get from '../get'; 2 | 3 | export function getSearchListData(city, page, category, keywords) { 4 | let url = 'api/search/SearchList/' 5 | + encodeURIComponent(city) + '/' 6 | + page + '/' 7 | + encodeURIComponent(category); 8 | if (keywords) { 9 | url += '/' + encodeURIComponent(keywords); 10 | } 11 | return get(url); 12 | } -------------------------------------------------------------------------------- /app/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import RouterMap from './router/routerMap'; 4 | import { Provider } from 'react-redux'; 5 | import configureStore from './store/configureStore'; 6 | 7 | const store = configureStore(); 8 | 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | document.getElementById('app') 14 | ); -------------------------------------------------------------------------------- /app/components/HomeHeader/style.scss: -------------------------------------------------------------------------------- 1 | .home-header { 2 | background-color: #323232; 3 | padding: 10px; 4 | color: #fff; 5 | font-size: 16px; 6 | line-height: 1; 7 | .location { 8 | color: #fff; 9 | width: 60px; 10 | text-align: left; 11 | margin: 5px 0; 12 | } 13 | .searchInput { 14 | width: auto; 15 | margin: 0 30px 0 60px; 16 | } 17 | .user { 18 | margin: 5px 0; 19 | } 20 | } -------------------------------------------------------------------------------- /app/components/HomeReco/style.scss: -------------------------------------------------------------------------------- 1 | .home-recommend { 2 | background-color: #fff; 3 | h3 { 4 | font-size: 18px; 5 | border-bottom: 1px solid #eee; 6 | padding: 10px; 7 | } 8 | ul { 9 | padding: 10px; 10 | font-size: 0; 11 | li { 12 | width: 33.3%; 13 | display: inline-block; 14 | padding: 5px; 15 | img { 16 | width: 100%; 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /app/components/Header/index.jsx: -------------------------------------------------------------------------------- 1 | import './style'; 2 | import React from 'react'; 3 | 4 | class Header extends React.PureComponent { 5 | 6 | /** 7 | * 返回上一页 8 | */ 9 | clickHandler() { 10 | window.history.back(); 11 | } 12 | 13 | render() { 14 | return ( 15 |
16 | 17 | {this.props.children} 18 |
19 | ); 20 | } 21 | } 22 | 23 | export default Header; -------------------------------------------------------------------------------- /app/components/CityList/style.scss: -------------------------------------------------------------------------------- 1 | .cityList { 2 | color: #333; 3 | padding: 15px 15px 20px; 4 | background-color: #fff; 5 | h3 { 6 | font-size: 18px; 7 | } 8 | li { 9 | float: left; 10 | width: 33.3%; 11 | margin-top: 20px; 12 | text-align: center; 13 | span { 14 | display: inline-block; 15 | width: 90%; 16 | font: 14px; 17 | line-height: 2; 18 | color: #fff; 19 | background-color: #ff6fa2; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | [ 5 | "env", 6 | { 7 | "targets": { 8 | "browsers": [ 9 | "> 1%", 10 | "last 2 versions", 11 | "not ie <= 8" 12 | ] 13 | }, 14 | "useBuiltIns": true 15 | } 16 | ] 17 | ], 18 | "plugins": [ 19 | "transform-object-rest-spread", 20 | "react-html-attrs", 21 | "syntax-dynamic-import" 22 | ] 23 | } -------------------------------------------------------------------------------- /app/components/SearchInput/style.scss: -------------------------------------------------------------------------------- 1 | .searchInput { 2 | .search { 3 | background-color: #fff; 4 | border-radius: 15px; 5 | overflow: hidden; 6 | padding: 5px 10px; 7 | i { 8 | font-size: 16px; 9 | color: #ccc; 10 | vertical-align: middle; 11 | } 12 | input { 13 | width: 90%; 14 | padding-left: 5px; 15 | font-size: 14px; 16 | outline: none; 17 | border: none; 18 | font-weight: normal; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /app/containers/Search/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SearchHeader from 'components/SearchHeader'; 3 | import SearchList from './subPage/SearchList'; 4 | 5 | class Search extends React.PureComponent { 6 | render() { 7 | const params = this.props.match.params; 8 | return ( 9 |
10 | 11 | 12 |
13 | ); 14 | } 15 | } 16 | 17 | export default Search; -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "experimentalObjectRestSpread": true, 11 | "jsx": true 12 | }, 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "react" 17 | ], 18 | "parser": "babel-eslint", 19 | "rules": { 20 | "indent": 0, 21 | "linebreak-style": 0, 22 | "quotes": 0, 23 | "no-extra-semi": 0, 24 | "no-unused-expressions": 0, 25 | "no-unused-vars": 0, 26 | "no-console": 0, 27 | "no-mixed-spaces-and-tabs": 0, 28 | "no-cond-assign": 0 29 | } 30 | } -------------------------------------------------------------------------------- /app/components/GoodsList/index.jsx: -------------------------------------------------------------------------------- 1 | import './style'; 2 | import React from 'react'; 3 | import Item from './Item'; 4 | import PropTypes from 'prop-types'; 5 | 6 | class GoodsList extends React.PureComponent { 7 | render() { 8 | return ( 9 |
10 | 17 |
18 | ); 19 | } 20 | } 21 | 22 | GoodsList.propTypes = { 23 | data: PropTypes.array 24 | }; 25 | 26 | GoodsList.defaultProps = { 27 | data: [] 28 | }; 29 | 30 | export default GoodsList; -------------------------------------------------------------------------------- /app/util/localStorage.jsx: -------------------------------------------------------------------------------- 1 | export default { 2 | getItem: key => { 3 | let value; 4 | try { 5 | value = localStorage.getItem(key) 6 | } catch (ex) { 7 | // 开发环境下提示error 8 | if (__DEV__) { 9 | console.error('localStorage.getItem报错, ', ex.message) 10 | } 11 | } 12 | return value; 13 | }, 14 | setItem: (key, value) => { 15 | try { 16 | // ios safari 无痕模式下,直接使用 localStorage.setItem 会报错 17 | localStorage.setItem(key, value) 18 | } catch (ex) { 19 | // 开发环境下提示 error 20 | /*global __DEV__*/ 21 | if (__DEV__) { 22 | console.error('localStorage.setItem报错, ', ex.message) 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /app/components/Category/style.scss: -------------------------------------------------------------------------------- 1 | .home-category { 2 | background-color: #333; 3 | padding-bottom: 10px; 4 | .items { 5 | width: 100%; 6 | height: auto; 7 | text-align: center; 8 | font-size: 0; 9 | color: #111; 10 | img { 11 | width: 100%; 12 | } 13 | } 14 | .index{ 15 | margin-top: 10px; 16 | width: 100%; 17 | height: auto; 18 | text-align: center; 19 | li { 20 | list-style: none; 21 | display: inline-block; 22 | height: 8px; 23 | width: 8px; 24 | border-radius: 4px; 25 | background-color: #fff; 26 | margin: 0 3px; 27 | &.active { 28 | background-color: #ff6fa2; 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/containers/Home/index.jsx: -------------------------------------------------------------------------------- 1 | import './subPage/style'; 2 | import React from 'react'; 3 | import { connect } from 'react-redux'; 4 | import HomeHeader from 'components/HomeHeader'; 5 | import Category from 'components/Category'; 6 | import Reco from './subPage/Reco'; 7 | import GuessInterest from './subPage/GuessInterest'; 8 | 9 | class Home extends React.PureComponent { 10 | render() { 11 | return ( 12 |
13 | 14 | 15 | 16 | 17 |
18 | ); 19 | } 20 | } 21 | 22 | function mapStateToProps(state) { 23 | return { 24 | cityName: state.userInfo.cityName 25 | }; 26 | } 27 | 28 | export default connect(mapStateToProps)(Home); -------------------------------------------------------------------------------- /app/static/scss/reset.scss: -------------------------------------------------------------------------------- 1 | *, *:before, *:after { 2 | /* suppressing the tap highlight */ 3 | -webkit-tap-highlight-color: rgba(0,0,0,0); 4 | 5 | /* this is a personal preference */ 6 | box-sizing: border-box; 7 | padding: 0; 8 | margin: 0; 9 | -webkit-font-smoothing: antialiased; 10 | } 11 | 12 | *:focus { 13 | /* the default outline doesn't play well with a mobile application, 14 | I usually start without it, 15 | but don't forget to research further to make your mobile app accessible. */ 16 | outline: 0; 17 | } 18 | 19 | input { 20 | border-radius: 0; 21 | } 22 | 23 | html, body { 24 | /* we don't want to allow users to select text everywhere, 25 | you can enable it on the places you think appropriate */ 26 | user-select: none; 27 | } 28 | 29 | /* 个人需要 */ 30 | a { 31 | text-decoration: none; 32 | } 33 | li { 34 | list-style: none; 35 | } -------------------------------------------------------------------------------- /app/components/SearchHeader/index.jsx: -------------------------------------------------------------------------------- 1 | import './style'; 2 | import React from 'react'; 3 | import Header from 'components/Header'; 4 | import SearchInput from 'components/SearchInput'; 5 | 6 | class SearchHeader extends React.PureComponent { 7 | submitHandler(keywords) { 8 | let category = this.props.category; 9 | // https://github.com/ReactTraining/react-router/issues/1982 解决人:PFight 10 | // 解决react-router v4改变查询参数并不会刷新或者说重载组件的问题 11 | this.props.history.push('/empty'); 12 | setTimeout(() => { 13 | this.props.history.replace('/search/' + category + '/' + encodeURIComponent(keywords)); 14 | }); 15 | } 16 | render() { 17 | return ( 18 |
19 |
20 | 21 |
22 |
23 | ); 24 | } 25 | } 26 | 27 | export default SearchHeader; -------------------------------------------------------------------------------- /app/components/GoodsList/Item/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class Item extends React.PureComponent { 5 | render() { 6 | let { item } = this.props; 7 | return ( 8 |
  • 9 | 10 |
    11 | {item.title} 12 |
    13 |
    14 |
    15 |

    {item.title}

    16 | {item.created} 17 |
    18 |

    {item.desc}

    19 |

    ¥{item.price}

    20 |
    21 |
    22 |
  • 23 | ); 24 | } 25 | } 26 | 27 | Item.propTypes = { 28 | item: PropTypes.object.isRequired 29 | }; 30 | 31 | export default Item; -------------------------------------------------------------------------------- /app/components/HomeReco/index.jsx: -------------------------------------------------------------------------------- 1 | import './style'; 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | class HomeReco extends React.PureComponent { 6 | render() { 7 | return ( 8 |
    9 |

    {this.props.title}

    10 | 22 |
    23 | ); 24 | } 25 | } 26 | 27 | HomeReco.propTypes = { 28 | title: PropTypes.string, 29 | recoData: PropTypes.array 30 | }; 31 | 32 | HomeReco.defaultProps = { 33 | title: '', 34 | recoData: [] 35 | }; 36 | 37 | export default HomeReco; -------------------------------------------------------------------------------- /mock/Home/reco.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | title: '单肩包', 4 | img: 'http://ou41niivx.bkt.clouddn.com/bag.png?imageView2/2/w/200', 5 | link: 'https://bcy.net/zhipin/detail/7883' 6 | }, 7 | { 8 | title: '明信片', 9 | img: 'http://ou41niivx.bkt.clouddn.com/postcard.png?imageView2/2/w/200', 10 | link: 'https://bcy.net/zhipin/detail/8179' 11 | }, 12 | { 13 | title: '隔热杯', 14 | img: 'http://ou41niivx.bkt.clouddn.com/cup.png?imageView2/2/w/200', 15 | link: 'https://bcy.net/zhipin/detail/7495' 16 | }, 17 | { 18 | title: '抱枕', 19 | img: 'http://ou41niivx.bkt.clouddn.com/pillow.png?imageView2/2/w/200', 20 | link: 'https://bcy.net/zhipin/detail/3507' 21 | }, 22 | { 23 | title: '手机壳', 24 | img: 'http://ou41niivx.bkt.clouddn.com/phoneshell.png?imageView2/2/w/200', 25 | link: 'https://bcy.net/zhipin/detail/7358' 26 | }, 27 | { 28 | title: '框画', 29 | img: 'http://ou41niivx.bkt.clouddn.com/picFrame.png?imageView2/2/w/200', 30 | link: 'https://bcy.net/zhipin/detail/7015' 31 | } 32 | ] -------------------------------------------------------------------------------- /app/fetch/post.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 将对象转换为a=1&b=2这样的字符串 3 | * @param {*object} obj 需要转换的对象 4 | * @return {*string} 返回转换结果 5 | */ 6 | function obj2params(obj) { 7 | var result = ''; 8 | var item; 9 | for (item in obj) { 10 | result += '&' + item + '=' + encodeURIComponent(obj[item]); 11 | } 12 | if (result) { 13 | result = result.slice(1); 14 | } 15 | return result; 16 | } 17 | /** 18 | * 对fetch的post封装 19 | * @param {*string} url 请求地址 20 | * @param {*object} paramsObj 请求附带的参数 21 | * @return fetch返回的数据 22 | */ 23 | export default function post(url, paramsObj) { 24 | return fetch(url, { 25 | credentials: 'include', 26 | /* 27 | Credentials' Description: 28 | omit: Never send cookies. (default) 29 | same-origin: Send cookies if the URL is on the same origin as the calling script. 30 | include: Always send cookies, even for cross- origin calls. 31 | */ 32 | headers: { 33 | 'Accept': 'application/json, text/plain, */*', 34 | 'Content-Type': 'application/x-www-form-urlencoded' 35 | }, 36 | body: obj2params(paramsObj) 37 | }) 38 | } -------------------------------------------------------------------------------- /app/components/HomeHeader/index.jsx: -------------------------------------------------------------------------------- 1 | import './style'; 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { Link } from 'react-router-dom'; 5 | import SearchInput from 'components/SearchInput'; 6 | 7 | class HomeHeader extends React.PureComponent { 8 | 9 | submitHandler(keywords) { 10 | this.props.history.push('/search/' + 'all/' + encodeURIComponent(keywords)) 11 | } 12 | 13 | render() { 14 | return ( 15 |
    16 | 17 |
    18 | {this.props.cityName} 19 |
    20 | 21 |
    22 | 23 |
    24 | 25 |
    26 | ); 27 | } 28 | } 29 | 30 | HomeHeader.propTypes = { 31 | cityName: PropTypes.string 32 | }; 33 | 34 | HomeHeader.defaultProps = { 35 | cityName: '杭州' 36 | }; 37 | 38 | export default HomeHeader; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 如何运行 2 | 1. 开发环境:先运行npm run mock开启本地模拟数据,然后npm run dev 3 | 1. 发布环境:运行npm run build 4 | 5 | # react-webapp-spa 6 | #### 目前已完成: 7 | 1. webpack v3 前端工程:dev、mock、build 8 | - dev为本地开发环境,使用eslint、postcss、webpack-dev-server等工具 9 | - mock为本地模拟数据,通过koa2来处理本地的前端请求 10 | - build是发布环境,npm run build以后会生成build目录及相关文件。会将package.json里的dependencies打包成vendor.[hash].js,页面中js代码打包为app.[hash].js,scss打包为app.[hash].css,给图片和font加hash,然后压缩CSS、JS、图片。 11 | 1. react + react-redux + react-router v4 实现页面首页、城市页、搜索结果页、轮播图、下拉加载更多、搜索等功能。 12 | 1. react 热更新 13 | 1. 使用dynamic import将JS按页面代码分离,加速了首屏显示 14 | 1. scope hosting 15 | 16 | #### 之后要做: 17 | 1. SSR,为了SEO和防止一开始白屏 18 | 1. 移植项目到react-naive 19 | 1. 发布几篇详细的文章 20 | 21 | #### 预览图 22 | 23 | #### 1 24 | 25 | 26 | #### 2 27 | 29 | 30 | #### 3 31 | 33 | 34 | #### 4 35 | 37 | -------------------------------------------------------------------------------- /app/containers/Home/subPage/Reco.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import HomeReco from 'components/HomeReco'; 3 | import { getRecoData } from 'fetch/Home'; 4 | 5 | class AD extends React.PureComponent { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | recoData: [] 10 | } 11 | } 12 | 13 | /** 14 | * fetch推荐数据,然后更新state 15 | */ 16 | _getRecoData() { 17 | getRecoData() 18 | .then(res => { 19 | return res.json(); 20 | }) 21 | .then(json => { 22 | this.setState({ 23 | recoData: json 24 | }); 25 | }) 26 | .catch(ex => { 27 | /*global __DEV__*/ 28 | if (__DEV__) { 29 | // 虽然这个项目中数据是前端定的,几乎不可能报错 30 | // 但是,如果proxy到线下后端的服务器拿模拟数据,就会可能出现数据结构不符等问题 31 | // 所以,fetch数据一定要catch一下 32 | console.error('首页半次元周边推荐数据报错:', ex.message); 33 | } 34 | }); 35 | } 36 | 37 | componentDidMount() { 38 | this._getRecoData(); 39 | } 40 | 41 | render() { 42 | return ( 43 |
    44 | {this.state.recoData.length 45 | ? 46 | :
    加载中...
    47 | } 48 |
    49 | ); 50 | } 51 | } 52 | 53 | export default AD; -------------------------------------------------------------------------------- /app/components/SearchInput/index.jsx: -------------------------------------------------------------------------------- 1 | import './style'; 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | class SearchInput extends React.PureComponent { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | value: '' 10 | }; 11 | } 12 | 13 | // 点击searchInput内,都focus输入框 14 | clickHandler() { 15 | this.refs.input.focus(); 16 | } 17 | 18 | // 输入回车后,submit 19 | keyUpHandler(e) { 20 | e.preventDefault(); 21 | this.props.onSubmit(this.state.value); 22 | } 23 | 24 | // 受控组件处理 25 | changeHandler(e) { 26 | let value = e.target.value; 27 | this.setState({ 28 | value 29 | }); 30 | } 31 | 32 | render() { 33 | return ( 34 |
    35 |
    36 | 42 |
    43 |
    44 | ); 45 | } 46 | } 47 | 48 | SearchInput.propTypes = { 49 | onSubmit: PropTypes.func.isRequired 50 | }; 51 | 52 | export default SearchInput; -------------------------------------------------------------------------------- /app/containers/City/index.jsx: -------------------------------------------------------------------------------- 1 | import './style'; 2 | import React from 'react'; 3 | import { connect } from 'react-redux'; 4 | import Header from 'components/Header'; 5 | import CurrentCity from 'components/CurrentCity'; 6 | import CityList from 'components/CityList'; 7 | import { updateUserCity } from 'actions/userInfo'; 8 | import localStorage from 'util/localStorage'; 9 | 10 | class City extends React.PureComponent { 11 | 12 | /** 13 | * 更新Redux userInfo的cityName,以及localStorage的城市名 14 | * @param {*string} newCityName 新的城市名,由子组件回调提供 15 | */ 16 | updateCityHandler(newCityName) { 17 | // 更新redux 18 | this.props.onUpdateCity(newCityName); 19 | // 更新localStorage 20 | localStorage.setItem('cityName', newCityName); 21 | // 返回主页 22 | this.props.history.push('/'); 23 | } 24 | 25 | render() { 26 | return ( 27 |
    28 |
    29 |

    选择城市

    30 |
    31 | 32 | 33 |
    34 | ); 35 | } 36 | } 37 | 38 | function mapStateToProps(state) { 39 | return { 40 | cityName: state.userInfo.cityName 41 | }; 42 | } 43 | 44 | function mapDispatchToProps(dispatch) { 45 | return { 46 | onUpdateCity: cityName => { 47 | dispatch(updateUserCity(cityName)) 48 | } 49 | }; 50 | } 51 | 52 | export default connect(mapStateToProps, mapDispatchToProps)(City); -------------------------------------------------------------------------------- /mock/Search/searchList.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | hasMore: true, 3 | data: [ 4 | { 5 | title: '小林家的龙女仆 康娜 马克杯', 6 | img: 'http://ou41niivx.bkt.clouddn.com/search-1.png?imageView2/2/w/200', 7 | price: '38.00', 8 | created: '2017-2-22', 9 | link: 'https://bcy.net/zhipin/detail/5574', 10 | desc: '小林家的龙女仆康娜马克杯,38块,你买不了吃亏,38块,你买不了上当~' 11 | }, 12 | { 13 | title: '作品集锦2016.4-2017.6', 14 | img: 'http://ou41niivx.bkt.clouddn.com/search-2.png?imageView2/2/w/200', 15 | price: '48.00', 16 | created: '2017-2-20', 17 | link: 'https://bcy.net/zhipin/detail/7951', 18 | desc: '很棒的作品集锦!!!' 19 | }, 20 | { 21 | title: '扑克少女隔热杯', 22 | img: 'http://ou41niivx.bkt.clouddn.com/search-3.png?imageView2/2/w/200', 23 | price: '48.00', 24 | created: '2017-1-22', 25 | link: 'https://bcy.net/zhipin/detail/7495', 26 | desc: '清仓大甩卖,清仓大甩卖' 27 | }, 28 | { 29 | title: '史迪仔鼠标垫', 30 | img: 'http://ou41niivx.bkt.clouddn.com/search-4.png?imageView2/2/w/200', 31 | price: '28.00', 32 | created: '2017-1-18', 33 | link: 'https://bcy.net/zhipin/detail/7753', 34 | desc: '史迪仔强势来袭,你懂的~' 35 | }, 36 | { 37 | title: '王者荣耀法师联盟A徽章套装(4个/套)', 38 | img: 'http://ou41niivx.bkt.clouddn.com/search-5.png?imageView2/2/w/200', 39 | price: '28.00', 40 | created: '2017-1-17', 41 | link: 'https://bcy.net/zhipin/detail/8988', 42 | desc: '王者荣耀!王者农药!药药药!' 43 | } 44 | ] 45 | }; 46 | -------------------------------------------------------------------------------- /mock/Home/guessInterest.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | hasMore: true, 3 | data: [ 4 | { 5 | title: '干物妹小埋手机壳', 6 | img: 'http://ou41niivx.bkt.clouddn.com/interest-1.png?imageView2/2/w/200', 7 | price: '38.00', 8 | created: '2017-2-22', 9 | link: 'https://bcy.net/zhipin/detail/1020', 10 | desc: '超萌的干物妹小埋手机壳,38块,你买不了吃亏,38块,你买不了上当~' 11 | }, 12 | { 13 | title: '唯美插画书签', 14 | img: 'http://ou41niivx.bkt.clouddn.com/interest-2.png?imageView2/2/w/200', 15 | price: '38.00', 16 | created: '2017-2-20', 17 | link: 'https://bcy.net/zhipin/detail/8080', 18 | desc: '很棒的唯美插画书签~' 19 | }, 20 | { 21 | title: 'MogGen-猫将徽章套装(4个/套)', 22 | img: 'http://ou41niivx.bkt.clouddn.com/interest-3.png?imageView2/2/w/200', 23 | price: '28.00', 24 | created: '2017-1-22', 25 | link: 'https://bcy.net/zhipin/detail/8030', 26 | desc: '清仓大甩卖,清仓大甩卖' 27 | }, 28 | { 29 | title: '埃罗芒阿老师等身抱枕', 30 | img: 'http://ou41niivx.bkt.clouddn.com/interest-4.png?imageView2/2/w/200', 31 | price: '168.00', 32 | created: '2017-1-18', 33 | link: 'https://bcy.net/zhipin/detail/8338', 34 | desc: '埃罗芒阿老师等身抱枕,你懂的~' 35 | }, 36 | { 37 | title: '神烦催更超大鼠标垫-绘师款', 38 | img: 'http://ou41niivx.bkt.clouddn.com/interest-5.png?imageView2/2/w/200', 39 | price: '58.00', 40 | created: '2017-1-17', 41 | link: 'https://bcy.net/zhipin/detail/2031', 42 | desc: '还不投稿?勾搭了吗?小黄本看了?还不投稿?' 43 | } 44 | ] 45 | }; 46 | -------------------------------------------------------------------------------- /app/containers/index.jsx: -------------------------------------------------------------------------------- 1 | import 'static/scss/common.scss'; 2 | import React from 'react'; 3 | import { connect } from 'react-redux'; 4 | import { withRouter } from 'react-router-dom'; 5 | import get from 'fetch/get'; 6 | import LocalStorage from 'util/localStorage'; 7 | import { CITYNAME } from 'config/localStorageKey'; 8 | import * as userInfoActions from 'actions/userInfo'; 9 | 10 | class App extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | isInitDone: false 15 | } 16 | } 17 | 18 | /** 19 | * @return 返回获取到的城市名字 20 | */ 21 | _getCityName() { 22 | let cityName = LocalStorage.getItem(CITYNAME); 23 | if (cityName == null) { 24 | cityName = '上海'; 25 | } 26 | return cityName; 27 | } 28 | 29 | componentDidMount() { 30 | // 从localStorage中获取本地城市数据 31 | let cityName = this._getCityName(); 32 | // 数据需要放到redux中 33 | this.props.onUpdateCity(cityName); 34 | // 更新加载中状态 35 | this.setState({ 36 | isInitDone: true 37 | }); 38 | } 39 | 40 | render() { 41 | return ( 42 |
    43 | {this.state.isInitDone 44 | ? this.props.children 45 | :
    加载中...
    46 | } 47 |
    48 | ); 49 | } 50 | } 51 | 52 | const mapStateToProps = state => { 53 | return { 54 | cityName: state.cityName 55 | } 56 | } 57 | 58 | const mapDispatchToProps = dispatch => { 59 | return { 60 | onUpdateCity: cityName => { 61 | dispatch(userInfoActions.updateUserCity(cityName)); 62 | } 63 | } 64 | } 65 | 66 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(App)); -------------------------------------------------------------------------------- /app/components/CityList/index.jsx: -------------------------------------------------------------------------------- 1 | import './style'; 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | class CityList extends React.PureComponent { 6 | 7 | /** 8 | * 选择城市以后,更新城市数据 9 | * @param {*object} e 事件event 10 | */ 11 | ClickHandler (e) { 12 | let { target } = e; 13 | // 事件代理 14 | if (target.nodeName === 'SPAN') { 15 | let newCityName = target.textContent; 16 | if (this.props.onUpdateCity) { 17 | this.props.onUpdateCity(newCityName); 18 | } 19 | } 20 | } 21 | 22 | render() { 23 | return ( 24 |
    25 |

    热门城市

    26 | 49 |
    50 | ); 51 | } 52 | } 53 | 54 | CityList.propTypes = { 55 | onUpdateCity: PropTypes.func 56 | }; 57 | 58 | export default CityList; -------------------------------------------------------------------------------- /app/components/GoodsList/style.scss: -------------------------------------------------------------------------------- 1 | .goodsList { 2 | background-color: #fff; 3 | 4 | ul { 5 | padding: 10px 10px 0; 6 | li { 7 | padding: 5px; 8 | border-bottom: 1px solid #f1f1f1; 9 | &:after { 10 | display: table; 11 | content: ''; 12 | clear: both; 13 | } 14 | a { 15 | color: #333; 16 | } 17 | .left { 18 | margin-right: 10px; 19 | float: left; 20 | width: 120px; 21 | img { 22 | width: 100%; 23 | } 24 | line-height: 1; 25 | } 26 | .right { 27 | .head { 28 | height: 24px; 29 | &:after { 30 | display: table; 31 | content: ''; 32 | clear: both; 33 | } 34 | h4 { 35 | max-width: 40%; 36 | float: left; 37 | font-size: 16px; 38 | line-height: 1.2; 39 | overflow: hidden; 40 | text-overflow: ellipsis; 41 | white-space: nowrap; 42 | word-wrap: normal; 43 | } 44 | span { 45 | float: right; 46 | font-size: 12px; 47 | line-height: 16px; 48 | } 49 | } 50 | .desc { 51 | font-size: 14px; 52 | line-height: 1.4; 53 | } 54 | .price { 55 | margin-top: 5px; 56 | color: #ff6fa2; 57 | font-size: 20px; 58 | font-weight: 600; 59 | } 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-webapp", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "mock": "node ./mock/server.js", 8 | "dev": "NODE_ENV=dev webpack-dev-server --progress --colors --open", 9 | "build": "NODE_ENV=production webpack -p --config ./webpack.config.production.js --progress --colors" 10 | }, 11 | "author": "Luozi", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "autoprefixer": "^7.1.2", 15 | "babel-core": "^6.26.3", 16 | "babel-eslint": "^7.2.3", 17 | "babel-loader": "^7.1.1", 18 | "babel-plugin-react-html-attrs": "^2.0.0", 19 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 20 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 21 | "babel-polyfill": "^6.26.0", 22 | "babel-preset-env": "^1.7.0", 23 | "babel-preset-react": "^6.24.1", 24 | "boom": "^5.2.0", 25 | "clean-webpack-plugin": "^0.1.16", 26 | "css-loader": "^0.28.4", 27 | "eslint": "^4.19.1", 28 | "eslint-loader": "^1.9.0", 29 | "eslint-plugin-react": "^7.1.0", 30 | "extract-text-webpack-plugin": "^3.0.0", 31 | "file-loader": "^0.11.2", 32 | "html-webpack-plugin": "^2.29.0", 33 | "image-webpack-loader": "^3.3.1", 34 | "koa": "^2.5.1", 35 | "koa-bodyparser": "^4.2.0", 36 | "koa-router": "^7.2.1", 37 | "node-sass": "^4.9.2", 38 | "postcss": "^6.0.8", 39 | "postcss-loader": "^2.0.6", 40 | "react-addons-perf": "^15.4.2", 41 | "react-hot-loader": "^1.3.1", 42 | "sass-loader": "^6.0.6", 43 | "style-loader": "^0.18.2", 44 | "url-loader": "^0.5.9", 45 | "webpack": "^3.4.1", 46 | "webpack-dev-server": "^2.6.1" 47 | }, 48 | "dependencies": { 49 | "history": "^4.6.3", 50 | "prop-types": "^15.5.10", 51 | "react": "^15.6.1", 52 | "react-dom": "^15.6.1", 53 | "react-loadable": "^4.0.4", 54 | "react-redux": "^5.0.5", 55 | "react-router-dom": "^4.1.2", 56 | "react-swipe": "^5.0.8", 57 | "redux": "^3.7.2", 58 | "swipe-js-iso": "^2.0.3" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/components/Category/index.jsx: -------------------------------------------------------------------------------- 1 | import './style'; 2 | import React from 'react'; 3 | import ReactSwipe from 'react-swipe'; 4 | 5 | class Category extends React.PureComponent { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | index: 0 10 | }; 11 | } 12 | render() { 13 | const settings = { 14 | auto: 2500, 15 | callback: index => { 16 | this.setState({ index: index }); // 更新当前轮播图的index 17 | } 18 | }; 19 | return ( 20 |
    21 | 22 |
    23 | banner1 24 |
    25 |
    26 | banner2 27 |
    28 |
    29 | banner3 30 |
    31 |
    32 | banner4 33 |
    34 |
    35 | banner5 36 |
    37 |
    38 |
    39 |
  • 40 |
  • 41 |
  • 42 |
  • 43 |
  • 44 |
    45 |
    46 | ); 47 | } 48 | } 49 | 50 | export default Category; -------------------------------------------------------------------------------- /app/components/LoadMore/index.jsx: -------------------------------------------------------------------------------- 1 | import './style'; 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | let handlerID; // 为了清除window上的scroll事件 6 | 7 | class LoadMore extends React.PureComponent { 8 | /** 9 | * 监听滚动事件,并处理回调 10 | */ 11 | _loadMore() { 12 | let timeoutID; 13 | window.addEventListener('scroll', this._scrollHandler(timeoutID, this)); 14 | } 15 | 16 | /** 17 | * scroll事件的处理 18 | * @param {*number} timeoutID 计时器的ID,为了可以clear 19 | * @param {*object} context 上下文 20 | */ 21 | _scrollHandler(timeoutID, context) { 22 | return handlerID = e => { 23 | // 如果处于正在获取数据的状态,则不调用获取数据 24 | if (context.props.isLoading) { 25 | return; 26 | } 27 | // 如果hasMore为false了,说明没数据了,清除scroll的事件 28 | if (!context.props.hasMore) { 29 | window.removeEventListener('scroll', handlerID); 30 | } 31 | if (timeoutID) { 32 | clearTimeout(timeoutID) 33 | } 34 | // 节流 35 | timeoutID = setTimeout(this._callBack.bind(context), 50); 36 | } 37 | } 38 | 39 | /** 40 | * 当LoadMore元素出现在页面可视范围中,则调用获取数据 41 | */ 42 | _callBack() { 43 | let loadMoreNode = this.refs.loadMoreNode; 44 | let top = loadMoreNode.getBoundingClientRect().top, 45 | screenHeight = window.screen.height; 46 | if (top && top < screenHeight) { 47 | this.props.onLoadMore(); 48 | } 49 | } 50 | 51 | componentDidMount() { 52 | this._loadMore(); 53 | } 54 | 55 | componentWillUnmount() { 56 | window.removeEventListener('scroll', handlerID); 57 | } 58 | 59 | 60 | render() { 61 | let text; 62 | if (!this.props.hasMore) { 63 | text = '没有更多了~'; 64 | } else if (this.props.isLoading) { 65 | text = '加载中...'; 66 | } else { 67 | text = '上拉加载更多'; 68 | } 69 | return ( 70 |
    71 | {text} 72 |
    73 | ); 74 | } 75 | } 76 | 77 | LoadMore.propTypes = { 78 | isLoading: PropTypes.bool.isRequired, 79 | onLoadMore: PropTypes.func.isRequired 80 | }; 81 | 82 | export default LoadMore; -------------------------------------------------------------------------------- /app/router/routerMap.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HashRouter as Router, Route, Switch } from 'react-router-dom'; 3 | import createHistory from 'history/createBrowserHistory'; 4 | const history = createHistory(); 5 | 6 | import App from 'containers'; 7 | 8 | // 按路由拆分代码 9 | import Loadable from 'react-loadable'; 10 | const MyLoadingComponent = ({ isLoading, error }) => { 11 | // Handle the loading state 12 | if (isLoading) { 13 | return
    Loading...
    ; 14 | } 15 | // Handle the error state 16 | else if (error) { 17 | return
    Sorry, there was a problem loading the page.
    ; 18 | } 19 | else { 20 | return null; 21 | } 22 | }; 23 | const AsyncHome = Loadable({ 24 | loader: () => import('../containers/Home'), 25 | loading: MyLoadingComponent 26 | }); 27 | const AsyncCity = Loadable({ 28 | loader: () => import('../containers/City'), 29 | loading: MyLoadingComponent 30 | }); 31 | const AsyncDetail = Loadable({ 32 | loader: () => import('../containers/Detail'), 33 | loading: MyLoadingComponent 34 | }); 35 | const AsyncSearch = Loadable({ 36 | loader: () => import('../containers/Search'), 37 | loading: MyLoadingComponent 38 | }); 39 | const AsyncUser = Loadable({ 40 | loader: () => import('../containers/User'), 41 | loading: MyLoadingComponent 42 | }); 43 | const AsyncNotFound = Loadable({ 44 | loader: () => import('../containers/404'), 45 | loading: MyLoadingComponent 46 | }); 47 | 48 | // 路由配置 49 | class RouteMap extends React.Component { 50 | render() { 51 | return ( 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | ); 66 | // 说明 67 | // empty Route 68 | // https://github.com/ReactTraining/react-router/issues/1982 解决人:PFight 69 | // 解决react-router v4改变查询参数并不会刷新或者说重载组件的问题 70 | } 71 | } 72 | 73 | export default RouteMap; -------------------------------------------------------------------------------- /mock/server.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | 3 | // 使用router 4 | const Router = require('koa-router'); 5 | const Boom = require('boom'); 6 | const app = new Koa(); 7 | const router = new Router(); 8 | app.use(router.routes()); 9 | app.use(router.allowedMethods({ 10 | throw: true, 11 | notImplemented: () => new Boom.notImplemented(), 12 | methodNotAllowed: () => new Boom.methodNotAllowed() 13 | })); 14 | 15 | // 使用bodyparser 解析get,post的参数 16 | const bodyParser = require('koa-bodyparser'); 17 | app.use(bodyParser()); 18 | 19 | /* 首页数据 */ 20 | 21 | // 推荐制品周边数据 22 | const recoData = require('./Home/reco.js'); 23 | router.get('/api/home/reco', async(ctx, next) => { 24 | ctx.body = recoData; 25 | }); 26 | // 猜你喜欢数据 27 | const guessData = require('./Home/guessInterest.js'); 28 | router.get('/api/home/guessInterest/:city/:page', async(ctx, next) => { 29 | // 看上去数据的判断有点简单? 30 | // 其实,实际项目也是这么做的,只不过后端接口在联调阶段会proxy到线下后端的数据服务器接口 31 | // 我们只需要传给后端city和page这两个参数即可,复杂的判断后端会处理 32 | // 不过我们需要在开发的时候考虑到,mock数据中如果hasMore为false的情况 33 | console.log('当前城市:' + ctx.params.city); 34 | console.log('当前页码:' + ctx.params.page); 35 | // 假设请求page5就没有更多了 36 | guessData.hasMore = true; 37 | if (ctx.params.page == 5) { 38 | console.log('nani'); 39 | 40 | guessData.hasMore = false; 41 | } 42 | ctx.body = guessData; 43 | await next(); 44 | }); 45 | 46 | // 搜索结果页数据 【无关键字】 47 | const searchListData1 = require('./Search/searchList.js'); 48 | router.get('/api/search/searchList/:city/:page/:category', async (ctx, next) => { 49 | console.log('当前城市:' + ctx.params.city); 50 | console.log('当前页码:' + ctx.params.page); 51 | console.log('当前类别:' + ctx.params.category); 52 | // 假设请求page5就没有更多了 53 | searchListData1.hasMore = true; 54 | if (ctx.params.page == 5) { 55 | searchListData1.hasMore = false; 56 | } 57 | ctx.body = searchListData1; 58 | await next(); 59 | }); 60 | 61 | // 搜索结果页数据 【有关键字】 62 | const searchListData2 = require('./Search/searchList.js'); 63 | router.get('/api/search/searchList/:city/:page/:category/:keywords', async (ctx, next) => { 64 | console.log('当前城市:' + ctx.params.city); 65 | console.log('当前页码:' + ctx.params.page); 66 | console.log('当前类别:' + ctx.params.category); 67 | console.log('当前关键字:' + ctx.params.keywords); 68 | // 假设请求page5就没有更多了 69 | searchListData2.hasMore = true; 70 | if (ctx.params.page == 5) { 71 | searchListData2.hasMore = false; 72 | } 73 | ctx.body = searchListData2; 74 | await next(); 75 | }); 76 | 77 | // log error 78 | app.on('error', (err, ctx) => { 79 | console.log('server error', err, ctx); 80 | }); 81 | 82 | app.listen(7777); -------------------------------------------------------------------------------- /app/containers/Home/subPage/GuessInterest.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { getGuessInterestData } from 'fetch/Home'; 3 | import { connect } from 'react-redux'; 4 | import GoodsList from 'components/GoodsList'; 5 | import LoadMore from 'components/LoadMore'; 6 | 7 | class GuessInterest extends React.PureComponent { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | data: [], 12 | page: 0, 13 | hasMore: true, 14 | isLoading: false 15 | }; 16 | } 17 | 18 | /** 19 | * 获取猜你喜欢的数据,并更新state 20 | */ 21 | _getGuessInterestData() { 22 | // 记录状态 23 | this.setState({ 24 | isLoading: true 25 | }) 26 | // 获取数据 27 | let city = this.props.cityName; 28 | let page = this.state.page; 29 | let result = getGuessInterestData(city, page); 30 | this._processResult(result); 31 | } 32 | 33 | _processResult(result) { 34 | // 增加 page 计数 35 | const page = this.state.page; 36 | this.setState({ 37 | page: page + 1 38 | }) 39 | // 处理 fetch返回结果 40 | result.then(res => { 41 | return res.json(); 42 | }) 43 | .then(json => { 44 | this.setState({ 45 | hasMore: json.hasMore, 46 | data: this.state.data.concat(json.data) 47 | }); 48 | // 更新状态 49 | this.setState({ 50 | isLoading: false 51 | }); 52 | }) 53 | .catch(ex => { 54 | /*global __DEV__*/ 55 | if (__DEV__) { 56 | console.error('主页获取猜你喜欢数据出错:', ex.message); 57 | } 58 | // 更新状态 59 | this.setState({ 60 | isLoading: false 61 | }); 62 | }); 63 | } 64 | 65 | componentDidMount() { 66 | this._getGuessInterestData(); 67 | } 68 | 69 | // 处理加载更多的回调 70 | loadMoreHandler() { 71 | this._getGuessInterestData() 72 | } 73 | 74 | render() { 75 | return ( 76 |
    77 |

    猜你喜欢

    78 | 79 | 82 |
    83 | ); 84 | } 85 | } 86 | 87 | function mapStateToProps(state) { 88 | return { 89 | cityName: state.userInfo.cityName 90 | }; 91 | } 92 | 93 | export default connect(mapStateToProps)(GuessInterest); -------------------------------------------------------------------------------- /app/containers/Search/subPage/SearchList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { getSearchListData } from 'fetch/Search'; 3 | import { connect } from 'react-redux'; 4 | import GoodsList from 'components/GoodsList'; 5 | import LoadMore from 'components/LoadMore'; 6 | 7 | class SearchList extends React.PureComponent { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | data: [], 12 | page: 0, 13 | hasMore: true, 14 | isLoading: false 15 | }; 16 | } 17 | 18 | /** 19 | * 获取猜你喜欢的数据,并更新state 20 | */ 21 | _getSearchListData() { 22 | // 记录状态 23 | this.setState({ 24 | isLoading: true 25 | }); 26 | // 获取数据 27 | let city = this.props.cityName, 28 | page = this.state.page, 29 | category = this.props.category || 'all', 30 | keywords = this.props.keywords; 31 | let result = getSearchListData(city, page, category, keywords) 32 | this._processResult(result); 33 | } 34 | 35 | _processResult(result) { 36 | // 增加 page 计数 37 | const { page } = this.state; 38 | this.setState({ 39 | page: page + 1 40 | }) 41 | // 处理 fetch返回结果 42 | result.then(res => { 43 | return res.json(); 44 | }) 45 | .then(json => { 46 | this.setState({ 47 | hasMore: json.hasMore, 48 | data: this.state.data.concat(json.data) 49 | }); 50 | // 更新状态 51 | this.setState({ 52 | isLoading: false 53 | }); 54 | }) 55 | .catch(ex => { 56 | /*global __DEV__*/ 57 | if (__DEV__) { 58 | console.error('搜索页获取搜索列表数据出错:', ex.message); 59 | } 60 | // 更新状态 61 | this.setState({ 62 | isLoading: false 63 | }); 64 | }); 65 | } 66 | 67 | componentDidMount() { 68 | this._getSearchListData(); 69 | } 70 | 71 | // 处理加载更多的回调 72 | loadMoreHandler() { 73 | this._getSearchListData(); 74 | } 75 | 76 | render() { 77 | return ( 78 |
    79 | 80 | 83 |
    84 | ); 85 | } 86 | } 87 | 88 | function mapStateToProps(state) { 89 | return { 90 | cityName: state.userInfo.cityName 91 | }; 92 | } 93 | 94 | export default connect(mapStateToProps)(SearchList); -------------------------------------------------------------------------------- /app/static/scss/font.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; 3 | src: url('../fonts/iconfont.eot?t=1501690051389'); 4 | /* IE9*/ 5 | src: url('../fonts/iconfont.eot?t=1501690051389#iefix') format('embedded-opentype'), /* IE6-IE8 */ 6 | url('../fonts/iconfont.woff?t=1501690051389') format('woff'), /* chrome, firefox */ 7 | url('../fonts/iconfont.ttf?t=1501690051389') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ 8 | url('../fonts/iconfont.svg?t=1501690051389#iconfont') format('svg'); 9 | /* iOS 4.1- */ 10 | } 11 | 12 | [class^="icon-"], [class*=" icon-"] { 13 | /* use !important to prevent issues with browser extensions that change fonts */ 14 | font-family: 'iconfont' !important; 15 | speak: none; 16 | font-style: normal; 17 | font-weight: normal; 18 | font-variant: normal; 19 | text-transform: none; 20 | line-height: 1; 21 | /* Better Font Rendering =========== */ 22 | -webkit-font-smoothing: antialiased; 23 | -moz-osx-font-smoothing: grayscale; 24 | } 25 | 26 | .icon-shuipingzuo:before { 27 | content: "\e600"; 28 | } 29 | 30 | .icon-shuangyuzuo:before { 31 | content: "\e601"; 32 | } 33 | 34 | .icon-mojiezuo:before { 35 | content: "\e602"; 36 | } 37 | 38 | .icon-chunvzuo:before { 39 | content: "\e603"; 40 | } 41 | 42 | .icon-shizizuo:before { 43 | content: "\e604"; 44 | } 45 | 46 | .icon-juxiezuo:before { 47 | content: "\e605"; 48 | } 49 | 50 | .icon-tianhezuo:before { 51 | content: "\e606"; 52 | } 53 | 54 | .icon-baobei:before { 55 | content: "\e607"; 56 | } 57 | 58 | .icon-dianpu:before { 59 | content: "\e608"; 60 | } 61 | 62 | .icon-dazahui:before { 63 | content: "\e609"; 64 | } 65 | 66 | .icon-sheyinglvxing:before { 67 | content: "\e60a"; 68 | } 69 | 70 | .icon-gaoxiaoquwei:before { 71 | content: "\e60b"; 72 | } 73 | 74 | .icon-mingxing:before { 75 | content: "\e60c"; 76 | } 77 | 78 | .icon-chongwu:before { 79 | content: "\e60d"; 80 | } 81 | 82 | .icon-DIY:before { 83 | content: "\e60e"; 84 | } 85 | 86 | .icon-meishi:before { 87 | content: "\e60f"; 88 | } 89 | 90 | .icon-qinggan:before { 91 | content: "\e610"; 92 | } 93 | 94 | .icon-muying:before { 95 | content: "\e611"; 96 | } 97 | 98 | .icon-jiaju:before { 99 | content: "\e612"; 100 | } 101 | 102 | .icon-meifa:before { 103 | content: "\e613"; 104 | } 105 | 106 | .icon-meirong:before { 107 | content: "\e614"; 108 | } 109 | 110 | .icon-shuma:before { 111 | content: "\e615"; 112 | } 113 | 114 | .icon-shishang:before { 115 | content: "\e616"; 116 | } 117 | 118 | .icon-yundonghuwai:before { 119 | content: "\e617"; 120 | } 121 | 122 | .icon-nanzhuang:before { 123 | content: "\e618"; 124 | } 125 | 126 | .icon-peishi:before { 127 | content: "\e619"; 128 | } 129 | 130 | .icon-xiezi:before { 131 | content: "\e61a"; 132 | } 133 | 134 | .icon-baobao:before { 135 | content: "\e61b"; 136 | } 137 | 138 | .icon-nvzhuang:before { 139 | content: "\e61c"; 140 | } 141 | 142 | .icon-baobei1:before { 143 | content: "\e61d"; 144 | } 145 | 146 | .icon-C-pad:before { 147 | content: "\e6bd"; 148 | } 149 | 150 | .icon-share:before { 151 | content: "\e65a"; 152 | } 153 | 154 | .icon-key:before { 155 | content: "\e8a3"; 156 | } 157 | 158 | .icon-search:before { 159 | content: "\e8b8"; 160 | } 161 | 162 | .icon-collect-fill:before { 163 | content: "\e8c2"; 164 | } 165 | 166 | .icon-collect:before { 167 | content: "\e8c3"; 168 | } 169 | 170 | .icon-user:before { 171 | content: "\e8c8"; 172 | } 173 | 174 | .icon-image:before { 175 | content: "\e8d2"; 176 | } 177 | 178 | .icon-arrow-left:before { 179 | content: "\e8ef"; 180 | } 181 | 182 | .icon-arrow-right:before { 183 | content: "\e8f1"; 184 | } 185 | 186 | .icon-arrow-down:before { 187 | content: "\e8f2"; 188 | } 189 | 190 | .icon-location:before { 191 | content: "\e8ff"; 192 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /*global 2 | __DEV__ 3 | __dirname 4 | process 5 | */ 6 | const webpack = require('webpack'); 7 | const path = require('path'); 8 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 9 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 10 | 11 | module.exports = { 12 | devtool: 'cheap-module-eval-source-map', 13 | entry: [ 14 | 'babel-polyfill', 15 | path.join(__dirname, 'app/index.jsx'), 16 | ], 17 | output: { 18 | path: path.join(__dirname, 'dev'), 19 | filename: 'bundle.js' 20 | }, 21 | resolve: { 22 | extensions: ['.js', '.jsx', '.scss', '.css'], 23 | alias: { 24 | components: path.join(__dirname, 'app/components/'), 25 | containers: path.join(__dirname, 'app/containers/'), 26 | constants: path.join(__dirname, 'app/constants/'), 27 | actions: path.join(__dirname, 'app/actions/'), 28 | reducers: path.join(__dirname, 'app/reducers/'), 29 | util: path.join(__dirname, 'app/util/'), 30 | fetch: path.join(__dirname, 'app/fetch/'), 31 | config: path.join(__dirname, 'app/config/'), 32 | static: path.join(__dirname, 'app/static/') 33 | } 34 | }, 35 | module: { 36 | rules: [ 37 | { 38 | test: /\.(js|jsx)$/, 39 | exclude: /node_modules/, 40 | use: [ 41 | 'react-hot-loader', 42 | 'babel-loader' 43 | ] 44 | }, 45 | { 46 | test: /\.(jpg|jpeg|png|svg|gif|bmp)/i, 47 | use: [ 48 | 'url-loader?limit=5000' 49 | ] 50 | }, 51 | { 52 | test: /\.(png|woff|woff2|svg|ttf|eot)($|\?)/i, 53 | use: [ 54 | 'url-loader?limit=5000' 55 | ] 56 | }, 57 | { 58 | test: /\.(css|scss)$/, 59 | exclude: /node_modules/, 60 | use: ExtractTextPlugin.extract({ 61 | fallback: 'style-loader', 62 | //resolve-url-loader may be chained before sass-loader if necessary 63 | use: [ 64 | { 65 | loader: "css-loader", 66 | options: { 67 | sourceMap: true 68 | } 69 | }, 70 | { 71 | loader: 'postcss-loader', 72 | options: { 73 | sourceMap: true 74 | } 75 | }, 76 | { 77 | loader: "sass-loader", 78 | options: { 79 | sourceMap: true 80 | } 81 | } 82 | ] 83 | }) 84 | 85 | } 86 | ] 87 | }, 88 | plugins: [ 89 | // Scope hosting 90 | new webpack.optimize.ModuleConcatenationPlugin(), 91 | new ExtractTextPlugin({ 92 | filename: 'main.css', 93 | disable: true 94 | }), 95 | // html 模板插件 96 | new HtmlWebpackPlugin({ 97 | template: __dirname + '/app/index.html' 98 | }), 99 | // 热加载插件 100 | new webpack.HotModuleReplacementPlugin(), 101 | // 可在业务 js 代码中使用 __DEV__ 判断是否是dev模式(dev模式下可以提示错误、测试报告等, production模式不提示) 102 | new webpack.DefinePlugin({ 103 | __DEV__: JSON.stringify(JSON.parse((process.env.NODE_ENV == 'dev') || 'false')) 104 | }) 105 | ], 106 | devServer: { 107 | proxy: { 108 | // 凡是 `/api` 开头的 http 请求,都会被代理到 localhost:7777 上,由 koa 提供 mock 数据。 109 | // koa 代码在 ./mock 目录中,启动命令为 npm run mock 110 | '/api': { 111 | target: 'http://localhost:7777', 112 | secure: false 113 | } 114 | }, 115 | host: '0.0.0.0', 116 | port: '9999', 117 | disableHostCheck: true, // 为了手机可以访问 118 | contentBase: './dev', // 本地服务器所加载的页面所在的目录 119 | historyApiFallback: true, // 为了SPA应用服务 120 | inline: true, //实时刷新 121 | hot: true // 使用热加载插件 HotModuleReplacementPlugin 122 | } 123 | } -------------------------------------------------------------------------------- /webpack.config.production.js: -------------------------------------------------------------------------------- 1 | /*global 2 | __DEV__ 3 | __dirname 4 | process 5 | */ 6 | const webpack = require('webpack'); 7 | const path = require('path'); 8 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 9 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 11 | const pkg = require('./package.json'); 12 | 13 | module.exports = { 14 | devtool: 'cheap-module-source-map', 15 | entry: { 16 | app: path.join(__dirname, 'app/index.jsx'), 17 | // 将第三方依赖(node_modules)的库打包 18 | vendor: Object.keys(pkg.dependencies) 19 | }, 20 | output: { 21 | path: path.join(__dirname, 'build'), 22 | publicPath: path.join(__dirname, 'build/'), 23 | filename: 'js/[name].[chunkhash:8].js' 24 | }, 25 | resolve: { 26 | extensions: ['.js', '.jsx', '.scss', '.css'], 27 | alias: { 28 | components: path.join(__dirname, 'app/components/'), 29 | containers: path.join(__dirname, 'app/containers/'), 30 | constants: path.join(__dirname, 'app/constants/'), 31 | actions: path.join(__dirname, 'app/actions/'), 32 | reducers: path.join(__dirname, 'app/reducers/'), 33 | util: path.join(__dirname, 'app/util/'), 34 | fetch: path.join(__dirname, 'app/fetch/'), 35 | config: path.join(__dirname, 'app/config/'), 36 | static: path.join(__dirname, 'app/static/') 37 | } 38 | }, 39 | module: { 40 | rules: [ 41 | { 42 | test: /\.(js|jsx)$/, 43 | exclude: /node_modules/, 44 | use: [ 45 | 'babel-loader', 46 | 'eslint-loader' 47 | ] 48 | }, 49 | { 50 | test: /\.(jpg|jpeg|png|svg|gif|bmp)/i, 51 | use: [ 52 | 'url-loader?limit=5000&name=img/[name].[sha512:hash:base64:8].[ext]', 53 | 'image-webpack-loader?{pngquant:{quality: "65-90", speed: 4}, mozjpeg: {quality: 65}}' 54 | ] 55 | }, 56 | { 57 | test: /\.(woff|woff2|ttf|eot)($|\?)/i, 58 | use: [ 59 | 'url-loader?limit=5000&name=fonts/[name].[sha512:hash:base64:8].[ext]' 60 | ] 61 | }, 62 | { 63 | test: /\.(css|scss)$/, 64 | exclude: /node_modules/, 65 | use: ExtractTextPlugin.extract({ 66 | fallback: 'style-loader', 67 | //resolve-url-loader may be chained before sass-loader if necessary 68 | use: [ 69 | { 70 | loader: 'css-loader', 71 | options: { 72 | sourceMap: true 73 | } 74 | }, 75 | { 76 | loader: 'postcss-loader', 77 | options: { 78 | sourceMap: true 79 | } 80 | }, 81 | { 82 | loader: 'sass-loader', 83 | options: { 84 | sourceMap: true 85 | } 86 | } 87 | ] 88 | }) 89 | } 90 | ] 91 | }, 92 | plugins: [ 93 | // Scope hosting 94 | new webpack.optimize.ModuleConcatenationPlugin(), 95 | // 删除build文件夹 96 | new CleanWebpackPlugin('./build'), 97 | // 加署名 98 | new webpack.BannerPlugin("Copyright by luoziwo.cn"), 99 | // html 模板插件 100 | new HtmlWebpackPlugin({ 101 | template: __dirname + '/app/index.html', 102 | minify: { 103 | removeComments: true, 104 | collapseWhitespace: false 105 | } 106 | }), 107 | // 分离CSS和js 108 | new ExtractTextPlugin('css/[name].[chunkhash:8].css'), 109 | // 提供公共代码vendor 110 | new webpack.optimize.CommonsChunkPlugin({ 111 | name: 'vendor', 112 | filename: 'js/[name].[chunkhash:8].js' 113 | }), 114 | /// 定义为生产环境,编译 React 时压缩到最小 115 | new webpack.DefinePlugin({ 116 | 'process.env': { 117 | 'NODE_ENV': JSON.stringify(process.env.NODE_ENV) 118 | } 119 | }), 120 | // 可在业务 js 代码中使用 __DEV__ 判断是否是dev模式(dev模式下可以提示错误、测试报告等, production模式不提示) 121 | new webpack.DefinePlugin({ 122 | __DEV__: JSON.stringify(JSON.parse((process.env.NODE_ENV == 'dev') || 'false')) 123 | }) 124 | ] 125 | } -------------------------------------------------------------------------------- /app/static/fonts/iconfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Created by iconfont 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | --------------------------------------------------------------------------------