React学习记录四:简书网站搭建


搭建项目环境

使用脚手架工具大家简书项目:npx create-create-app jianshu

删除多余文件和代码,简化、保留src/App.js、src/index.css、src/index.js。

使用styled-components

React中,如果在一个js文件中引入css文件,则该css文件全局生效,即在其它js文件也可引用该样式,这样会导致样式使用混乱。因此,在React项目中推荐使用style-components进行样式管理。

github中搜索styled-components,打开官方文档查看,通过npm install --save styled-components进行安装。使用styled-components时,首先按js文件使用,修改index.css为style.js。对全局样式,使用injectGlobal方法。

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './style.js'  // 使用styled-components首先按js引入
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

App.js

import React from 'react';

function App() {
  return (
    <div>
        Hello World!
    </div>
  );
}

export default App;

style.js

import {injectGlobal} from 'styled-components';

injectGlobal`
  body {
    margin: 0;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
      'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
      sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    background-color:green;  // 测试
  }
  
  code {
    font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
      monospace;
  }
`

注意:当前styled-components中已废除injectGlobal方法,以createGlobalStyle代替。官方说明

A helper function to generate a special StyledComponent that handles global styles. Normally, styled components are automatically scoped to a local CSS class and therefore isolated from other components. In the case of createGlobalStyle, this limitation is removed and things like CSS resets or base stylesheets can be applied.

Returns a StyledComponent that does not accept children. Place it at the top of your React tree and the global styles will be injected when the component is “rendered”.

官方案例:

import { createGlobalStyle } from 'styled-components'

const GlobalStyle = createGlobalStyle`
  body {
    color: ${props => (props.whiteColor ? 'white' : 'black')};
  }
`

// later in your app

<React.Fragment>
  <GlobalStyle whiteColor />
  <Navigation /> {/* example of other top-level stuff */}
</React.Fragment>

修改injectGlobal使用createGlobalStyle:

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

App.js

import React, {Fragment} from 'react';
import {GlobalStyle} from './style'

function App() {
  return (
    <Fragment>
        <GlobalStyle />
        <div>
            Hello World!
        </div>
    </Fragment>
  );
}

export default App;

style.js

import {createGlobalStyle} from 'styled-components';

export const GlobalStyle = createGlobalStyle`
  body {
    margin: 0;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
      'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
      sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    background-color:green;  // 测试
  }
  
  code {
    font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
      monospace;
  }
`

此时发现全局样式与icing生效。搜索reset.css文件,使用reset样式替代React自身的全局样式。地址:https://meyerweb.com/eric/tools/css/reset/

头部区域编写

考虑头部区域是首页和详情页公有区域,因此将头部放至公共区域。

在src目录下新建common/header文件夹,下创建index.js和style.js文件。代码如下:

App.js

import React, {Fragment} from 'react';
import {GlobalStyle} from './style'
import Header from './common/header'

function App() {
  return (
    <Fragment>
        <GlobalStyle />
        <Header/>
    </Fragment>
  );
}

export default App;

common/header/index.js

import React, {Component} from 'react';
import {
    HeaderWrapper,
    Logo,
    Nav,
    NavItem,
    NavSearch,
    Addition,
    Button
} from "./style";

class Header extends Component {
    render() {
        return (
            <HeaderWrapper>
                <Logo/>
                <Nav>
                    <NavItem className='left active'>首页</NavItem>
                    <NavItem className='left'>下载App</NavItem>
                    <NavItem className='right'>登录</NavItem>
                    <NavItem className='right'>Aa</NavItem>
                    <NavSearch></NavSearch>
                </Nav>
                <Addition>
                    <Button className='writing'>写文章</Button>
                    <Button className='reg'>注册</Button>
                </Addition>
            </HeaderWrapper>
        );
    }
}

export default Header;

common/header/style.js

import styled from 'styled-components';
import logoPic from '../../statics/logo.png'

// 通过styled创建带样式的元素
export const HeaderWrapper = styled.div`
    position: relative;
    height: 56px;
    border-bottom: 1px solid #f0f0f0;
`;

export const Logo = styled.a.attrs({
    href: '/'
})`
    position: absolute;
    top: 0;
    left: 0;
    display: block;
    height: 56px;
    width: 100px;
    background: url(${logoPic});  // 注意图片的使用方式
    background-size: contain;
`;

export const Nav = styled.div`
    width: 960px;
    height: 100%;
    margin: 0 auto;
`;

export const NavItem = styled.div`
    line-height: 56px;
    padding: 0 15px;
    font-size: 17px;
    color: #333;
    &.left {
      float:left;
    }
    &.right {
      float: right;
      color: #969696;
    }
    &.active {
      color: #ea6f5a;
    }
`;

export const NavSearch = styled.input.attrs({
    placeholder: '搜索'
})`
    width: 160px;
    height: 38px;
    padding: 0 20px;
    margin-top: 9px;
    margin-left: 20px;
    box-sizing: border-box;
    border: none;
    outline: none;
    border-radius: 19px;
    background: #eee;
    font-size: 14px;
    &::placeholder {
        color: #999;
    }
`;

export const Addition = styled.div`
    position: absolute;
    right: 0;
    top: 0;
    height: 56px;
`;

export const Button = styled.div`
    float: right;
    margin-top: 9px;
    margin-right: 20px;
    padding: 0 20px;
    line-height: 38px;
    border-radius: 19px;
    border: 1px solid #ec6149;
    font-size: 14px;
    &.reg {
        color: #ec6149;
    }
    &.writing {
        color: #fff;
        background: #ec6149;
    }
`;

此时刷新页面可以看到头部区域已基本成形。

头部区域字体图标补充

字体图标使用阿里矢量库。注册登录https://www.iconfont.cn/,在”图标管理” — “我的项目”下创建项目。(搜索)找到需要的字体图标,点击添加至购物车,然后点击右上角购物车图标,点击添加至项目,然后进入”我的项目”中点击下载至本地并解压。

在static路径下创建iconfont目录,把解压的.eot、.svg、.ttf、.woff、.woff2以及iconfont.css文件拷贝到iconfont下(原文件可以删除),修改iconfont.css内容,将@font-face下修改为相对路径,去除.iconfont外多余类样式。

将iconfont.css改为iconfont.js,引入styled-components。修改相关代码,添加搜索、写文章、Aa处字体图标:

App.js

import React, {Fragment} from 'react';
import {GlobalStyle} from './style'
import {IconFontGlobalStyle} from './statics/iconfont/iconfont'
import Header from './common/header'

function App() {
  return (
    <Fragment>
        <GlobalStyle />
        <IconFontGlobalStyle />
        <Header/>
    </Fragment>
  );
}

export default App;

iconfont.js

import {createGlobalStyle} from 'styled-components';

export const IconFontGlobalStyle = createGlobalStyle`
    @font-face {
      font-family: "iconfont";
      src: url('./iconfont.eot?t=1580292140050'); /* IE9 */
      src: url('./iconfont.eot?t=1580292140050#iefix') format('embedded-opentype'), /* IE6-IE8 */
      url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAASwAAsAAAAACSwAAARhAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDMgqFSIR1ATYCJAMUCwwABCAFhG0HTBsFCBFVnEXJfhzYMeQ33GTK0ziMw7Cg/XcG4paXpGX99MUKbypTEdgc5iLIxL0NEEAQdGs6xrYIk/MtS2Knn/5H0u700pRxHqdRCiFBOK7u9TWlaSxY12JxeZfWOoWbBTBUgWLBUipmAhLg+j+Yf7C/DK86zL1Lh4EA4PBDDEhJWdW6kCDAHEUAkK6dOrSClDFCaKATSBpNxUIBZCpESHQCvQhgSvB68gZJRAIoRAbzqJrtS9ui4Dl7/ozpVb2mzAA9nB0AvwxgAGIACADpXensChYlY8DA1e4CCgBwSKCgUJ1Cnqc9f6aqkEg3pOaWE6Agf3gCQDQwlfaYXJASgOfM4QEFnocoUFSlYT0IVj3D4gejFD01B0rfAkieoA2BpDE8CJUkRfZwt7O3l2NDnHh9+7q8i9yo8Ww/vtCjYasDsgnghB/lxsvE0KCBgRtz6u1cC/062XSCkCFWa32rsa7ZbDP09SwWhqmBzXatoX7Dme4tZ3KjdrauyRxdmzmySZlVd/Z6wwbTOv3aNj1QB9TWbjY374dEJF5jtQ4yatvPnqO0m+U+cwhfba67cqvP7P3cGLEX+AZmRTkYETNznjJLNunmnMiXV4VbnjSw3fHTGdZ3ND+sP1PbPz5nwezV7qMh6o9qy56j22VR5Dm2aJtNmXXzaPvyaxEl19o5qq031s55NerRotad8mzCn7w+fU9lxBOxMK5HZP1ODs3kKgVZT5s36HvBU/njFfop1A9prb18Szt3LvWt6LfYxnxd585pm1/FxaRsxS9X95nEQ2vzSYkaqd78pskmT789rOdf4l/Xvx727x9GV6+hQy3Mkw/jRAxuVW7eALp0CR3AYAxyjXQJg5H/RXcjgbqkpwUPSEpPDw4JQM4JPav5PbRTRrZrxqUAd9/Wi7ZuzxycM8fxSLpb9ZxeFZP7tZUcBpO63vT01apiaO8zB9bval8vXKO9p1Q6ssu9U6H4cFph7UOHelRt57rFfVTXynt3FtWSJmeOTOu9bn3lsVu1yc2tdLNmrtA25X3QsiaNamIgAEBV2VEWsP6yMUwafo7tY7L6X9WPMcOfNAiRYdt9WjjlfJPkJxi53QA7KCDSbkq/gwqdRf+Nfq0G3D+pJjFN4pRCx6j+V0KiWiZQUrWGA/4Wr6sfjEqhImTcEiUBqgJfYCQheIEcAyJZCmhI8oGLdlgucyO6yOwARHkhIJy9gXJ0BBhnH7xA/oJI6w8azjHg6jfdUBZWp8lG0Ch6sE9IHJdkW3QS5h8xanIjaUnAvKLUSQSWs0U+c8ASBWJKfYtWqgQkXMAeHYd5zlAJp+h0FqtW6/mcqu40c1y0JooJZCjkAesJJBxWIq+6mhSef4QijZwhDSVV/iskakn7YGlm0QF/UJadSi6le+0msqIUMcOIYAWwx3aUW0kGqup+KeSomXhAvLI2J5Woq3JGX13c4xYAztxUjBJGBCISDejNgMwjMeXEzykjb9KE7TF8NoVhm7RaAAAA') format('woff2'),
      url('./iconfont.woff?t=1580292140050') format('woff'),
      url('./iconfont.ttf?t=1580292140050') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
      url('./iconfont.svg?t=1580292140050#iconfont') format('svg'); /* iOS 4.1- */
    }
    
    .iconfont {
      font-family: "iconfont" !important;
      font-size: 16px;
      font-style: normal;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
    }
`;

common/header/index.js

import React, {Component} from 'react';
import {
    HeaderWrapper,
    Logo,
    Nav,
    NavItem,
    NavSearch,
    Addition,
    Button,
    SearchWrapper
} from "./style";

class Header extends Component {
    render() {
        return (
            <HeaderWrapper>
                <Logo/>
                <Nav>
                    <NavItem className='left active'>首页</NavItem>
                    <NavItem className='left'>下载App</NavItem>
                    <NavItem className='right'>登录</NavItem>
                    <NavItem className='right'>
                        <span className="iconfont">&#xe636;</span>
                    </NavItem>
                    <SearchWrapper>
                        <NavSearch></NavSearch>
                        <span className="iconfont">&#xe624;</span>
                    </SearchWrapper>
                </Nav>
                <Addition>
                    <Button className='writing'>
                        <span className="iconfont">&#xe6e5;</span>
                        写文章</Button>
                    <Button className='reg'>注册</Button>
                </Addition>
            </HeaderWrapper>
        );
    }
}

export default Header;

common/header/style.js

import styled from 'styled-components';
import logoPic from '../../statics/logo.png'

// 通过styled创建带样式的元素
export const HeaderWrapper = styled.div`
    position: relative;
    height: 56px;
    border-bottom: 1px solid #f0f0f0;
`;

export const Logo = styled.a.attrs({
    href: '/'
})`
    position: absolute;
    top: 0;
    left: 0;
    display: block;
    height: 56px;
    width: 100px;
    background: url(${logoPic});  // 注意图片的使用方式
    background-size: contain;
`;

export const Nav = styled.div`
    width: 960px;
    height: 100%;
    margin: 0 auto;
`;

export const NavItem = styled.div`
    line-height: 56px;
    padding: 0 15px;
    font-size: 17px;
    color: #333;
    &.left {
      float:left;
    }
    &.right {
      float: right;
      color: #969696;
    }
    &.active {
      color: #ea6f5a;
    }
`;

export const SearchWrapper = styled.div`
    position: relative;
    float:left;
    .iconfont {
        position: absolute;
        right: 5px;
        bottom: 5px;
        width: 30px;
        line-height: 30px;
        border-radius: 15px;
        text-align: center;
    }
`;

export const NavSearch = styled.input.attrs({
    placeholder: '搜索'
})`
    width: 160px;
    height: 38px;
    padding: 0 20px;
    margin-top: 9px;
    margin-left: 20px;
    box-sizing: border-box;
    border: none;
    outline: none;
    border-radius: 19px;
    background: #eee;
    font-size: 14px;
    &::placeholder {
        color: #999;
    }
`;

export const Addition = styled.div`
    position: absolute;
    right: 0;
    top: 0;
    height: 56px;
`;

export const Button = styled.div`
    float: right;
    margin-top: 9px;
    margin-right: 20px;
    padding: 0 20px;
    line-height: 38px;
    border-radius: 19px;
    border: 1px solid #ec6149;
    font-size: 14px;
    &.reg {
        color: #ec6149;
    }
    &.writing {
        color: #fff;
        background: #ec6149;
    }
`;

搜索框动画

通过npm install react-transition-group --save安装react-transition-group。修改common/header/index.js和style.js:

import React, {Component} from 'react';
import {CSSTransition} from 'react-transition-group';
import {
    HeaderWrapper,
    Logo,
    Nav,
    NavItem,
    NavSearch,
    Addition,
    Button,
    SearchWrapper
} from "./style";

class Header extends Component {

    constructor(props) {
        super(props);
        this.state = {
            focused: false,
        };
        this.handleInputFocus = this.handleInputFocus.bind(this);
        this.handleInputBlur = this.handleInputBlur.bind(this);
    }

    render() {
        return (
            <HeaderWrapper>
                <Logo/>
                <Nav>
                    <NavItem className='left active'>首页</NavItem>
                    <NavItem className='left'>下载App</NavItem>
                    <NavItem className='right'>登录</NavItem>
                    <NavItem className='right'>
                        <span className="iconfont">&#xe636;</span>
                    </NavItem>
                    <SearchWrapper>
                        <CSSTransition
                            in={this.state.focused}
                            timeout={200}
                            classNames='slide'
                        >
                            <NavSearch
                                className={this.state.focused ? 'focused' : ''}
                                onFocus={this.handleInputFocus}
                                onBlur={this.handleInputBlur}
                            >
                            </NavSearch>
                        </CSSTransition>
                        <span
                            className={this.state.focused ? 'focused iconfont' : 'iconfont'}
                        >
                            &#xe624;
                        </span>
                    </SearchWrapper>
                </Nav>
                <Addition>
                    <Button className='writing'>
                        <span className="iconfont">&#xe6e5;</span>
                        写文章</Button>
                    <Button className='reg'>注册</Button>
                </Addition>
            </HeaderWrapper>
        );
    };

    handleInputFocus() {
        this.setState({
            focused: true,
        })
    };

    handleInputBlur() {
        this.setState({
            focused: false,
        })
    }
}

export default Header;
import styled from 'styled-components';
import logoPic from '../../statics/logo.png'

// 通过styled创建带样式的元素
export const HeaderWrapper = styled.div`
    position: relative;
    height: 56px;
    border-bottom: 1px solid #f0f0f0;
`;

export const Logo = styled.a.attrs({
    href: '/'
})`
    position: absolute;
    top: 0;
    left: 0;
    display: block;
    height: 56px;
    width: 100px;
    background: url(${logoPic});  // 注意图片的使用方式
    background-size: contain;
`;

export const Nav = styled.div`
    width: 960px;
    height: 100%;
    margin: 0 auto;
`;

export const NavItem = styled.div`
    line-height: 56px;
    padding: 0 15px;
    font-size: 17px;
    color: #333;
    &.left {
      float:left;
    }
    &.right {
      float: right;
      color: #969696;
    }
    &.active {
      color: #ea6f5a;
    }
`;

export const SearchWrapper = styled.div`
    position: relative;
    float:left;
    .iconfont {
        position: absolute;
        right: 5px;
        bottom: 5px;
        width: 30px;
        line-height: 30px;
        border-radius: 15px;
        text-align: center;
        &.focused {
            background: #777;
            color: #fff;
        }
    }
`;

export const NavSearch = styled.input.attrs({
    placeholder: '搜索'
})`
    width: 160px;
    height: 38px;
    padding: 0 30px 0 20px;
    margin-top: 9px;
    margin-left: 20px;
    box-sizing: border-box;
    border: none;
    outline: none;
    border-radius: 19px;
    background: #eee;
    font-size: 14px;
    color: #666;
    &::placeholder {
        color: #999;
    }
    &.focused {
        width: 240px;
        .iconfont
    }
    &.slide-enter {
        transition: all .2s ease-in-out;
    }
    &.slide-enter-active {
        width: 240px;   
    }
    &.slide-exit {
        transition: all .2s ease-in-out;
    }
    &.slide-exit-active {
        width: 160px;
    }
`;

export const Addition = styled.div`
    position: absolute;
    right: 0;
    top: 0;
    height: 56px;
`;

export const Button = styled.div`
    float: right;
    margin-top: 9px;
    margin-right: 20px;
    padding: 0 20px;
    line-height: 38px;
    border-radius: 19px;
    border: 1px solid #ec6149;
    font-size: 14px;
    &.reg {
        color: #ec6149;
    }
    &.writing {
        color: #fff;
        background: #ec6149;
    }
`;

使用React-Redux管理数据

使用npm install --save reduxnpm install --save react-redux安装Redux和React-Redux。

在src下创建store文件夹,下面创建index.js和reducer.js。

App.js

import React from 'react';
import {GlobalStyle} from './style';
import {IconFontGlobalStyle} from './statics/iconfont/iconfont';
import Header from './common/header';
import store from './store';
import {Provider} from 'react-redux';

function App() {
  return (
    <Provider store={store}>
        <GlobalStyle />
        <IconFontGlobalStyle />
        <Header/>
    </Provider>
  );
}

export default App;

store/index.js

import {createStore} from 'redux';
import reducer from './reducer'

const store = createStore(reducer);

export default store;

store/reducer.js

const defaultState = {
    focused: false,
};

export default (state = defaultState, action)=>{
    if (action.type === 'search_focus') {
        return {
            focused: true,
        }
    };
    if (action.type === 'search_blur') {
        return {
            focused: false,
        }
    }
    return state;
};

common/header/index.js

import React from 'react';
import {CSSTransition} from 'react-transition-group';
import {connect} from 'react-redux';
import {
    HeaderWrapper,
    Logo,
    Nav,
    NavItem,
    NavSearch,
    Addition,
    Button,
    SearchWrapper
} from "./style";

const Header = (props) => {
    return (
        <HeaderWrapper>
            <Logo/>
            <Nav>
                <NavItem className='left active'>首页</NavItem>
                <NavItem className='left'>下载App</NavItem>
                <NavItem className='right'>登录</NavItem>
                <NavItem className='right'>
                    <span className="iconfont">&#xe636;</span>
                </NavItem>
                <SearchWrapper>
                    <CSSTransition
                        in={props.focused}
                        timeout={200}
                        classNames='slide'
                    >
                        <NavSearch
                            className={props.focused ? 'focused' : ''}
                            onFocus={props.handleInputFocus}
                            onBlur={props.handleInputBlur}
                        >
                        </NavSearch>
                    </CSSTransition>
                    <span
                        className={props.focused ? 'focused iconfont' : 'iconfont'}
                    >
                            &#xe624;
                        </span>
                </SearchWrapper>
            </Nav>
            <Addition>
                <Button className='writing'>
                    <span className="iconfont">&#xe6e5;</span>
                    写文章</Button>
                <Button className='reg'>注册</Button>
            </Addition>
        </HeaderWrapper>
    )
};

const mapStateToProps = (state)=>{
    return {
        focused: state.focused,
    }
};

const mapDispatchToProps = (dispatch) => {
    return {
        handleInputFocus() {
            const action = {
                type: 'search_focus'
            };
            dispatch(action);
        },
        handleInputBlur() {
            const action = {
                type: 'search_blur'
            };
            dispatch(action);
        }
    }
};

export default connect(mapStateToProps, mapDispatchToProps)(Header);

使用combineReducers拆分数据管理

为方便使用Redux调试工具,github搜索redux-devtools-extension,查看文档说明,修改store/index.js:

import {createStore, compose} from 'redux';
import reducer from './reducer'

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(reducer, composeEnhancers());

export default store;

此时在控制台可以查看redux数据变化:state下面只有focused一个数据。

实际项目中,数据会很多,如果都在store/reducer下面进行数据定义和逻辑处理,会使reducer.js冗长,不利于代码查看和调试。因此,推荐使用combineReducers对数据进行拆分管理。在common/header下创建store,store下创建reducer.js,将原reducer.js文件内容复制到该文件中。修改原store/reducer.js代码:

store/reducer.js

import {combineReducers} from 'redux';
import HeaderReducer from '../common/header/store/reducer';

const reducer = combineReducers({
    header: HeaderReducer,
});

export default reducer;

刷新页面可以看到state里不再直接是focused,而是由header/focused,多了header层。这样在数据多时可以比较调理的查看和调试。

注意,此时搜索框动画并不能够正常使用,这是因为mapStateToProps中使用数据仍未修改。修改如下:

...
const mapStateToProps = (state)=>{
    return {
        focused: state.header.focused,
    }
};
...

此时可以正常使用。

对header/store进行调整修改,完整代码为:

common/header/store/index .js

import reducer from './reducer';

export {reducer}

store/reducer.js

import {combineReducers} from 'redux';
import {reducer as HeaderReducer} from '../common/header/store';

const reducer = combineReducers({
    header: HeaderReducer,
});

export default reducer;

拆分actionCreators和constants

在header/store下创建constans.js和actionCreators.js。修改代码:

header/store/constants.js

export const SEARCH_FOCUS = 'header/SEARCH_FOCUS';
export const SEARCH_BLUR = 'header/SEARCH_BLUR';

header/store/actionCreators.js

import * as constants from './constants';

export const searchFocus = () => ({
    type: constants.SEARCH_FOCUS,
});

export const searchBlur = () => ({
    type: constants.SEARCH_BLUR,
});

header/store/reducer.js

import * as constants from './constants';

const defaultState = {
    focused: false,
};

export default (state = defaultState, action)=>{
    if (action.type === constants.SEARCH_FOCUS) {
        return {
            focused: true,
        }
    };
    if (action.type === constants.SEARCH_BLUR) {
        return {
            focused: false,
        }
    }
    return state;
};

header/store/index.js

import reducer from './reducer';
import * as actionCreators from './actionCreators';
import * as constants from './constants';

export {reducer, actionCreators, constants}

header/index.js

import React from 'react';
import {CSSTransition} from 'react-transition-group';
import {connect} from 'react-redux';
import {
    HeaderWrapper,
    Logo,
    Nav,
    NavItem,
    NavSearch,
    Addition,
    Button,
    SearchWrapper
} from "./style";
import {actionCreators} from './store';  // 在index.js中已向外暴露

const Header = (props) => {
    return (
        <HeaderWrapper>
            <Logo/>
            <Nav>
                <NavItem className='left active'>首页</NavItem>
                <NavItem className='left'>下载App</NavItem>
                <NavItem className='right'>登录</NavItem>
                <NavItem className='right'>
                    <span className="iconfont">&#xe636;</span>
                </NavItem>
                <SearchWrapper>
                    <CSSTransition
                        in={props.focused}
                        timeout={200}
                        classNames='slide'
                    >
                        <NavSearch
                            className={props.focused ? 'focused' : ''}
                            onFocus={props.handleInputFocus}
                            onBlur={props.handleInputBlur}
                        >
                        </NavSearch>
                    </CSSTransition>
                    <span
                        className={props.focused ? 'focused iconfont' : 'iconfont'}
                    >
                            &#xe624;
                        </span>
                </SearchWrapper>
            </Nav>
            <Addition>
                <Button className='writing'>
                    <span className="iconfont">&#xe6e5;</span>
                    写文章</Button>
                <Button className='reg'>注册</Button>
            </Addition>
        </HeaderWrapper>
    )
};

const mapStateToProps = (state)=>{
    return {
        focused: state.header.focused,
    }
};

const mapDispatchToProps = (dispatch) => {
    return {
        handleInputFocus() {
            dispatch(actionCreators.searchFocus());
        },
        handleInputBlur() {
            dispatch(actionCreators.searchBlur());
        }
    }
};

export default connect(mapStateToProps, mapDispatchToProps)(Header);

immutable和redux-immutable的使用

React中数据修改时不建议修改原数据,为在项目中防止将原数据修改,推荐使用immutable对象的数据。

github搜索immutable查看文档,使用npm install immutable --save安装immutable。修改普通数据为immutable数据:

header/store/reducer.js

import * as constants from './constants';
// immutable提供fromJS方法将普通对象转化为immutable对象
import {fromJS} from 'immutable';

const defaultState = fromJS({
    focused: false,
});

export default (state = defaultState, action)=>{
    if (action.type === constants.SEARCH_FOCUS) {
        // immutable对象的set方法,会结合之前immutable对象的值和设置的值返回一个全新的对象
        return state.set('focused', true);
    };
    if (action.type === constants.SEARCH_BLUR) {
        return state.set('focused', false);
    }
    return state;
};

header/index.js

...
const mapStateToProps = (state)=>{
    return {
        // immutable对象使用get方法获取数据
        focused: state.header.get('focused'),
    }
};
...

此时项目正常使用。但实际上,state.header.get('focused')这种书写方式本身仍存在问题,因为通过.方式仍是普通js对象的书写方式,而后面有使用immutable的get方式获取,不合理。

为将state也创建为immutable对象,使用redux-immutable来创建reducer。通过npm install redux-immutable --save安装redux-immutable。修改创建reducer时代码:

import {combineReducers} from 'redux-immutable';  // 修改为redux-immutable
import {reducer as HeaderReducer} from '../common/header/store';

const reducer = combineReducers({
    header: HeaderReducer,
});

export default reducer;

修改使用时:

...
const mapStateToProps = (state)=>{
    return {
        // focused: state.get('header').get('focused'),
        // 等价于
        focused: state.getIn(['header', 'focused'])
    }
};
...

热门搜索样式布局

修改header下面index.js和style.js,实现热门搜索的样式展示:

import React from 'react';
import {CSSTransition} from 'react-transition-group';
import {connect} from 'react-redux';
import {
    HeaderWrapper,
    Logo,
    Nav,
    NavItem,
    NavSearch,
    Addition,
    Button,
    SearchWrapper,
    SearchInfo,
    SearchInfoTitle,
    SearchInfoSwitch,
    SearchInfoItem
} from "./style";
import {actionCreators} from './store';

const Header = (props) => {
    return (
        <HeaderWrapper>
            <Logo/>
            <Nav>
                <NavItem className='left active'>首页</NavItem>
                <NavItem className='left'>下载App</NavItem>
                <NavItem className='right'>登录</NavItem>
                <NavItem className='right'>
                    <span className="iconfont">&#xe636;</span>
                </NavItem>
                <SearchWrapper>
                    <CSSTransition
                        in={props.focused}
                        timeout={200}
                        classNames='slide'
                    >
                        <NavSearch
                            className={props.focused ? 'focused' : ''}
                            onFocus={props.handleInputFocus}
                            onBlur={props.handleInputBlur}
                        >
                        </NavSearch>
                    </CSSTransition>
                    <span
                        className={props.focused ? 'focused iconfont' : 'iconfont'}
                    >
                            &#xe624;
                        </span>
                    <SearchInfo>
                        <SearchInfoTitle>
                            热门搜索
                            <SearchInfoSwitch>
                                换一批
                            </SearchInfoSwitch>
                        </SearchInfoTitle>
                        <div>
                            <SearchInfoItem>教育</SearchInfoItem>
                            <SearchInfoItem>教育</SearchInfoItem>
                            <SearchInfoItem>教育</SearchInfoItem>
                            <SearchInfoItem>教育</SearchInfoItem>
                            <SearchInfoItem>教育</SearchInfoItem>
                            <SearchInfoItem>教育</SearchInfoItem>
                            <SearchInfoItem>教育</SearchInfoItem>
                            <SearchInfoItem>教育</SearchInfoItem>
                        </div>
                    </SearchInfo>
                </SearchWrapper>
            </Nav>
            <Addition>
                <Button className='writing'>
                    <span className="iconfont">&#xe6e5;</span>
                    写文章</Button>
                <Button className='reg'>注册</Button>
            </Addition>
        </HeaderWrapper>
    )
};

const mapStateToProps = (state)=>{
    return {
        focused: state.getIn(['header', 'focused'])
    }
};

const mapDispatchToProps = (dispatch) => {
    return {
        handleInputFocus() {
            dispatch(actionCreators.searchFocus());
        },
        handleInputBlur() {
            dispatch(actionCreators.searchBlur());
        }
    }
};

export default connect(mapStateToProps, mapDispatchToProps)(Header);
import styled from 'styled-components';
import logoPic from '../../statics/logo.png'

export const HeaderWrapper = styled.div`
    position: relative;
    height: 56px;
    border-bottom: 1px solid #f0f0f0;
`;

export const Logo = styled.a.attrs({
    href: '/'
})`
    position: absolute;
    top: 0;
    left: 0;
    display: block;
    height: 56px;
    width: 100px;
    background: url(${logoPic});  // 注意图片的使用方式
    background-size: contain;
`;

export const Nav = styled.div`
    width: 960px;
    height: 100%;
    margin: 0 auto;
`;

export const NavItem = styled.div`
    line-height: 56px;
    padding: 0 15px;
    font-size: 17px;
    color: #333;
    &.left {
      float:left;
    }
    &.right {
      float: right;
      color: #969696;
    }
    &.active {
      color: #ea6f5a;
    }
`;

export const SearchWrapper = styled.div`
    position: relative;
    float:left;
    .iconfont {
        position: absolute;
        right: 5px;
        bottom: 5px;
        width: 30px;
        line-height: 30px;
        border-radius: 15px;
        text-align: center;
        &.focused {
            background: #777;
            color: #fff;
        }
    }
`;

export const SearchInfo = styled.div`
  position: absolute;
  left: 0;
  top: 56px;
  width: 240px;
  padding: 0 20px;
  box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
`;

export const SearchInfoTitle = styled.div`
  margin-top: 20px;
  margin-bottom: 15px;
  line-height: 20px;
  font-size: 14px;
  color: #969696;
`;

export const SearchInfoSwitch = styled.span`
  font-size: 13px;
  float: right;
`;

export const SearchInfoItem = styled.a`
  display: block;
  float:left;
  line-height: 20px;
  padding: 0 5px;
  margin-right: 10px;
  margin-bottom: 15px;
  font-size: 12px;
  border: 1px solid #ddd;
  color: #787878;
  border-radius: 3px;
`;

export const NavSearch = styled.input.attrs({
    placeholder: '搜索'
})`
    width: 160px;
    height: 38px;
    padding: 0 30px 0 20px;
    margin-top: 9px;
    margin-left: 20px;
    box-sizing: border-box;
    border: none;
    outline: none;
    border-radius: 19px;
    background: #eee;
    font-size: 14px;
    color: #666;
    &::placeholder {
        color: #999;
    }
    &.focused {
        width: 240px;
        .iconfont
    }
    &.slide-enter {
        transition: all .2s ease-in-out;
    }
    &.slide-enter-active {
        width: 240px;   
    }
    &.slide-exit {
        transition: all .2s ease-in-out;
    }
    &.slide-exit-active {
        width: 160px;
    }
`;

export const Addition = styled.div`
    position: absolute;
    right: 0;
    top: 0;
    height: 56px;
`;

export const Button = styled.div`
    float: right;
    margin-top: 9px;
    margin-right: 20px;
    padding: 0 20px;
    line-height: 38px;
    border-radius: 19px;
    border: 1px solid #ec6149;
    font-size: 14px;
    &.reg {
        color: #ec6149;
    }
    &.writing {
        color: #fff;
        background: #ec6149;
    }
`;

为实现聚焦时显示热门搜索,失焦时隐藏,修改index.js:

import React from 'react';
import {CSSTransition} from 'react-transition-group';
import {connect} from 'react-redux';
import {
    HeaderWrapper,
    Logo,
    Nav,
    NavItem,
    NavSearch,
    Addition,
    Button,
    SearchWrapper,
    SearchInfo,
    SearchInfoTitle,
    SearchInfoSwitch,
    SearchInfoItem
} from "./style";
import {actionCreators} from './store';

const getListArea = (show) => {
    if (show) {
        return (
            <SearchInfo>
                <SearchInfoTitle>
                    热门搜索
                    <SearchInfoSwitch>
                        换一批
                    </SearchInfoSwitch>
                </SearchInfoTitle>
                <div>
                    <SearchInfoItem>教育</SearchInfoItem>
                    <SearchInfoItem>教育</SearchInfoItem>
                    <SearchInfoItem>教育</SearchInfoItem>
                    <SearchInfoItem>教育</SearchInfoItem>
                    <SearchInfoItem>教育</SearchInfoItem>
                    <SearchInfoItem>教育</SearchInfoItem>
                    <SearchInfoItem>教育</SearchInfoItem>
                    <SearchInfoItem>教育</SearchInfoItem>
                </div>
            </SearchInfo>
        );
    } else {
        return null;
    }
}

const Header = (props) => {
    return (
        <HeaderWrapper>
            <Logo/>
            <Nav>
                <NavItem className='left active'>首页</NavItem>
                <NavItem className='left'>下载App</NavItem>
                <NavItem className='right'>登录</NavItem>
                <NavItem className='right'>
                    <span className="iconfont">&#xe636;</span>
                </NavItem>
                <SearchWrapper>
                    <CSSTransition
                        in={props.focused}
                        timeout={200}
                        classNames='slide'
                    >
                        <NavSearch
                            className={props.focused ? 'focused' : ''}
                            onFocus={props.handleInputFocus}
                            onBlur={props.handleInputBlur}
                        >
                        </NavSearch>
                    </CSSTransition>
                    <span
                        className={props.focused ? 'focused iconfont' : 'iconfont'}
                    >
                            &#xe624;
                        </span>
                    {getListArea(props.focused)}
                </SearchWrapper>
            </Nav>
            <Addition>
                <Button className='writing'>
                    <span className="iconfont">&#xe6e5;</span>
                    写文章</Button>
                <Button className='reg'>注册</Button>
            </Addition>
        </HeaderWrapper>
    )
};

const mapStateToProps = (state)=>{
    return {
        focused: state.getIn(['header', 'focused'])
    }
};

const mapDispatchToProps = (dispatch) => {
    return {
        handleInputFocus() {
            dispatch(actionCreators.searchFocus());
        },
        handleInputBlur() {
            dispatch(actionCreators.searchBlur());
        }
    }
};

export default connect(mapStateToProps, mapDispatchToProps)(Header);

热门搜索获取数据

这里获取数据使用redux-thunk中间件。通过npm install redux-thunk --save安装redux-thunk,使用npm install axios --save安装axios。

首先修改store创建:

import {createStore, compose, applyMiddleware} from 'redux';
import thunk from 'redux-thunk'
import reducer from './reducer'

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(reducer, composeEnhancers(
    applyMiddleware(thunk)  // 使用thunk中间件
));

export default store;

将Header恢复为有状态组件,并且修改代码:

import React, {Component} from 'react';
import {CSSTransition} from 'react-transition-group';
import {connect} from 'react-redux';
import {
    HeaderWrapper,
    Logo,
    Nav,
    NavItem,
    NavSearch,
    Addition,
    Button,
    SearchWrapper,
    SearchInfo,
    SearchInfoTitle,
    SearchInfoSwitch,
    SearchInfoItem
} from "./style";
import {actionCreators} from './store';

class Header extends Component {

    getListArea () {
        const {focused, list} = this.props;
        if (focused) {
            return (
                <SearchInfo>
                    <SearchInfoTitle>
                        热门搜索
                        <SearchInfoSwitch>
                            换一批
                        </SearchInfoSwitch>
                    </SearchInfoTitle>
                    <div>
                        {
                            list.map((item)=>{
                                return <SearchInfoItem key={item}>{item}</SearchInfoItem>
                            })
                        }
                    </div>
                </SearchInfo>
            );
        } else {
            return null;
        }
    }

    render() {
        const {focused, handleInputFocus, handleInputBlur} = this.props;
        return (
            <HeaderWrapper>
                <Logo/>
                <Nav>
                    <NavItem className='left active'>首页</NavItem>
                    <NavItem className='left'>下载App</NavItem>
                    <NavItem className='right'>登录</NavItem>
                    <NavItem className='right'>
                        <span className="iconfont">&#xe636;</span>
                    </NavItem>
                    <SearchWrapper>
                        <CSSTransition
                            in={focused}
                            timeout={200}
                            classNames='slide'
                        >
                            <NavSearch
                                className={focused ? 'focused' : ''}
                                onFocus={handleInputFocus}
                                onBlur={handleInputBlur}
                            >
                            </NavSearch>
                        </CSSTransition>
                        <span
                            className={focused ? 'focused iconfont' : 'iconfont'}
                        >
                            &#xe624;
                        </span>
                        {this.getListArea()}
                    </SearchWrapper>
                </Nav>
                <Addition>
                    <Button className='writing'>
                        <span className="iconfont">&#xe6e5;</span>
                        写文章</Button>
                    <Button className='reg'>注册</Button>
                </Addition>
            </HeaderWrapper>
        )
    }
}

const mapStateToProps = (state)=>{
    return {
        focused: state.getIn(['header', 'focused']),
        list: state.getIn(['header', 'list'])
    }
};

const mapDispatchToProps = (dispatch) => {
    return {
        handleInputFocus() {
            dispatch(actionCreators.getList())
            dispatch(actionCreators.searchFocus());
        },
        handleInputBlur() {
            dispatch(actionCreators.searchBlur());
        }
    }
};

export default connect(mapStateToProps, mapDispatchToProps)(Header);

修改header/store下文件:

import * as constants from './constants';
import {fromJS} from 'immutable';
import axios from 'axios';

const changeList = (data) => ({
    type: constants.HEADER_LIST,
    data: fromJS(data)  // 注意这里要把数据改为immutable对象
});

export const searchFocus = () => ({
    type: constants.SEARCH_FOCUS,
});

export const searchBlur = () => ({
    type: constants.SEARCH_BLUR,
});

export const getList = () => {
    return (dispatch) => {
        axios.get('/api/headerList.json').then((res)=>{
            const data = res.data;
            dispatch(changeList(data.data));
        }).catch(()=>{
            console.log('error!')
        });
    }
}
export const SEARCH_FOCUS = 'header/SEARCH_FOCUS';
export const SEARCH_BLUR = 'header/SEARCH_BLUR';
export const HEADER_LIST = 'header/HEADER_LIST';
import * as constants from './constants';
import {fromJS} from 'immutable';

const defaultState = fromJS({
    focused: false,
    list: []
});

export default (state = defaultState, action)=>{
    switch (action.type) {
        case constants.SEARCH_FOCUS:
            return state.set('focused', true);
        case constants.SEARCH_BLUR:
            return state.set('focused', false);
        case constants.HEADER_LIST:
            return state.set('list', action.data);
        default:
            return state;
    }
};

热门搜索显示修改

当前热门搜索项在搜索框失去焦点时就隐藏,无法点击,因此需要修改逻辑。

添加mouseIn属性,当鼠标移出热门搜索显示区域时才隐藏热门搜索。

import * as constants from './constants';
import {fromJS} from 'immutable';

const defaultState = fromJS({
    focused: false,
    list: [],
    mouseIn: false
});

export default (state = defaultState, action)=>{
    switch (action.type) {
        case constants.SEARCH_FOCUS:
            return state.set('focused', true);
        case constants.SEARCH_BLUR:
            return state.set('focused', false);
        case constants.HEADER_LIST:
            return state.set('list', action.data);
        case constants.MOUSE_ENTER:
            return state.set('mouseIn', true);
        case constants.MOUSE_LEAVE:
            return state.set('mouseIn', false);
        default:
            return state;
    }
}
import * as constants from './constants';
import {fromJS} from 'immutable';
import axios from 'axios';

const changeList = (data) => ({
    type: constants.HEADER_LIST,
    data: fromJS(data)
});

export const searchFocus = () => ({
    type: constants.SEARCH_FOCUS,
});

export const searchBlur = () => ({
    type: constants.SEARCH_BLUR,
});

export const mouseEnter = () => ({
    type: constants.MOUSE_ENTER,
});

export const mouseLeave = () => ({
    type: constants.MOUSE_LEAVE,
});

export const getList = () => {
    return (dispatch) => {
        axios.get('/api/headerList.json').then((res)=>{
            const data = res.data;
            dispatch(changeList(data.data));
        }).catch(()=>{
            console.log('error!')
        });
    }
}
export const SEARCH_FOCUS = 'header/SEARCH_FOCUS';
export const SEARCH_BLUR = 'header/SEARCH_BLUR';
export const HEADER_LIST = 'header/HEADER_LIST';
export const MOUSE_ENTER = 'header/MOUSE_ENTER';
export const MOUSE_LEAVE = 'header/MOUSE_LEAVE';
import React, {Component} from 'react';
import {CSSTransition} from 'react-transition-group';
import {connect} from 'react-redux';
import {
    HeaderWrapper,
    Logo,
    Nav,
    NavItem,
    NavSearch,
    Addition,
    Button,
    SearchWrapper,
    SearchInfo,
    SearchInfoTitle,
    SearchInfoSwitch,
    SearchInfoItem
} from "./style";
import {actionCreators} from './store';

class Header extends Component {

    getListArea () {
        const {focused, list, mouseIn, handleMouseEnter, handleMouseLeave} = this.props;
        if (focused || mouseIn) {
            return (
                <SearchInfo
                    onMouseEnter={handleMouseEnter}
                    onMouseLeave={handleMouseLeave}
                >
                    <SearchInfoTitle>
                        热门搜索
                        <SearchInfoSwitch>
                            换一批
                        </SearchInfoSwitch>
                    </SearchInfoTitle>
                    <div>
                        {
                            list.map((item)=>{
                                return <SearchInfoItem key={item}>{item}</SearchInfoItem>
                            })
                        }
                    </div>
                </SearchInfo>
            );
        } else {
            return null;
        }
    }

    render() {
        const {focused, handleInputFocus, handleInputBlur} = this.props;
        return (
            <HeaderWrapper>
                <Logo/>
                <Nav>
                    <NavItem className='left active'>首页</NavItem>
                    <NavItem className='left'>下载App</NavItem>
                    <NavItem className='right'>登录</NavItem>
                    <NavItem className='right'>
                        <span className="iconfont">&#xe636;</span>
                    </NavItem>
                    <SearchWrapper>
                        <CSSTransition
                            in={focused}
                            timeout={200}
                            classNames='slide'
                        >
                            <NavSearch
                                className={focused ? 'focused' : ''}
                                onFocus={handleInputFocus}
                                onBlur={handleInputBlur}
                            >
                            </NavSearch>
                        </CSSTransition>
                        <span
                            className={focused ? 'focused iconfont' : 'iconfont'}
                        >
                            &#xe624;
                        </span>
                        {this.getListArea()}
                    </SearchWrapper>
                </Nav>
                <Addition>
                    <Button className='writing'>
                        <span className="iconfont">&#xe6e5;</span>
                        写文章</Button>
                    <Button className='reg'>注册</Button>
                </Addition>
            </HeaderWrapper>
        )
    }
}

const mapStateToProps = (state)=>{
    return {
        focused: state.getIn(['header', 'focused']),
        list: state.getIn(['header', 'list']),
        mouseIn: state.getIn(['header', 'mouseIn'])
    }
};

const mapDispatchToProps = (dispatch) => {
    return {
        handleInputFocus() {
            dispatch(actionCreators.getList())
            dispatch(actionCreators.searchFocus());
        },
        handleInputBlur() {
            dispatch(actionCreators.searchBlur());
        },
        handleMouseEnter() {
            dispatch(actionCreators.mouseEnter());
        },
        handleMouseLeave() {
            dispatch(actionCreators.mouseLeave());
        }
    }
};

export default connect(mapStateToProps, mapDispatchToProps)(Header);

分页显示热门搜索

添加page和totalPage属性。修改代码:

header/index.js

import React, {Component} from 'react';
import {CSSTransition} from 'react-transition-group';
import {connect} from 'react-redux';
import {
    HeaderWrapper,
    Logo,
    Nav,
    NavItem,
    NavSearch,
    Addition,
    Button,
    SearchWrapper,
    SearchInfo,
    SearchInfoTitle,
    SearchInfoSwitch,
    SearchInfoItem
} from "./style";
import {actionCreators} from './store';

class Header extends Component {

    getListArea () {
        const {focused, list, mouseIn, page, totalPage, handleMouseEnter, handleMouseLeave, handleChangePage} = this.props;
        const newList = list.toJS();
        const pageList = [];
        if (newList.length) {
            for (let i = (page-1)*10;i < Math.min(page*10, newList.length); i++) {
                pageList.push(
                    <SearchInfoItem key={newList[i]}>{newList[i]}</SearchInfoItem>
                )
            }
        }
        if (focused || mouseIn) {
            return (
                <SearchInfo
                    onMouseEnter={handleMouseEnter}
                    onMouseLeave={handleMouseLeave}
                >
                    <SearchInfoTitle>
                        热门搜索
                        <SearchInfoSwitch onClick={()=>handleChangePage(page, totalPage)}>
                            换一批
                        </SearchInfoSwitch>
                    </SearchInfoTitle>
                    <div>
                        {pageList}
                    </div>
                </SearchInfo>
            );
        } else {
            return null;
        }
    }

    render() {
        const {focused, handleInputFocus, handleInputBlur} = this.props;
        return (
            <HeaderWrapper>
                <Logo/>
                <Nav>
                    <NavItem className='left active'>首页</NavItem>
                    <NavItem className='left'>下载App</NavItem>
                    <NavItem className='right'>登录</NavItem>
                    <NavItem className='right'>
                        <span className="iconfont">&#xe636;</span>
                    </NavItem>
                    <SearchWrapper>
                        <CSSTransition
                            in={focused}
                            timeout={200}
                            classNames='slide'
                        >
                            <NavSearch
                                className={focused ? 'focused' : ''}
                                onFocus={handleInputFocus}
                                onBlur={handleInputBlur}
                            >
                            </NavSearch>
                        </CSSTransition>
                        <span
                            className={focused ? 'focused iconfont' : 'iconfont'}
                        >
                            &#xe624;
                        </span>
                        {this.getListArea()}
                    </SearchWrapper>
                </Nav>
                <Addition>
                    <Button className='writing'>
                        <span className="iconfont">&#xe6e5;</span>
                        写文章</Button>
                    <Button className='reg'>注册</Button>
                </Addition>
            </HeaderWrapper>
        )
    }
}

const mapStateToProps = (state)=>{
    return {
        focused: state.getIn(['header', 'focused']),
        list: state.getIn(['header', 'list']),
        mouseIn: state.getIn(['header', 'mouseIn']),
        page: state.getIn(['header', 'page']),
        totalPage: state.getIn(['header', 'totalPage'])
    }
};

const mapDispatchToProps = (dispatch) => {
    return {
        handleInputFocus() {
            dispatch(actionCreators.getList())
            dispatch(actionCreators.searchFocus());
        },
        handleInputBlur() {
            dispatch(actionCreators.searchBlur());
        },
        handleMouseEnter() {
            dispatch(actionCreators.mouseEnter());
        },
        handleMouseLeave() {
            dispatch(actionCreators.mouseLeave());
        },
        handleChangePage(page, totalPage) {
            if (page < totalPage) {
                dispatch(actionCreators.changePage(page + 1));
            } else {
                dispatch(actionCreators.changePage(1));
            }
        }
    }
};

export default connect(mapStateToProps, mapDispatchToProps)(Header);

header/store/actionCreators.js

import * as constants from './constants';
import {fromJS} from 'immutable';
import axios from 'axios';

const changeList = (data) => ({
    type: constants.HEADER_LIST,
    data: fromJS(data),
    totalPage: Math.ceil(data.length / 10)  // 获取数据后获得总页数
});

export const searchFocus = () => ({
    type: constants.SEARCH_FOCUS,
});

export const searchBlur = () => ({
    type: constants.SEARCH_BLUR,
});

export const mouseEnter = () => ({
    type: constants.MOUSE_ENTER,
});

export const mouseLeave = () => ({
    type: constants.MOUSE_LEAVE,
});

export const changePage = (page) => ({
    type: constants.CHANGE_PAGE,
    page
});

export const getList = () => {
    return (dispatch) => {
        axios.get('/api/headerList.json').then((res)=>{
            const data = res.data;
            dispatch(changeList(data.data));
        }).catch(()=>{
            console.log('error!')
        });
    }
}

header/store/constants.js

export const SEARCH_FOCUS = 'header/SEARCH_FOCUS';
export const SEARCH_BLUR = 'header/SEARCH_BLUR';
export const HEADER_LIST = 'header/HEADER_LIST';
export const MOUSE_ENTER = 'header/MOUSE_ENTER';
export const MOUSE_LEAVE = 'header/MOUSE_LEAVE';
export const CHANGE_PAGE = 'header/CHANGE_PAGE';

header/store/reducer.js

import * as constants from './constants';
import {fromJS} from 'immutable';

const defaultState = fromJS({
    focused: false,
    list: [],
    mouseIn: false,
    page: 1,
    totalPage: 1
});

export default (state = defaultState, action)=>{
    switch (action.type) {
        case constants.SEARCH_FOCUS:
            return state.set('focused', true);
        case constants.SEARCH_BLUR:
            return state.set('focused', false);
        case constants.HEADER_LIST:
            // return state.set('list', action.data).set('totalPage', action.totalPage);
            // 修改多个数据时,等价于
            return state.merge({
                list: action.data,
                totalPage: action.totalPage
            });
        case constants.MOUSE_ENTER:
            return state.set('mouseIn', true);
        case constants.MOUSE_LEAVE:
            return state.set('mouseIn', false);
        case constants.CHANGE_PAGE:
            return state.set('page', action.page);
        default:
            return state;
    }
};

热门搜索换页动画

首先在阿里矢量库中添加旋转图标至项目,下载项目至本地,然后更新statics/iconfont/iconfont.js文件。然后在换一批前面添加旋转图标:

...
<SearchInfoTitle>
	热门搜索
	<SearchInfoSwitch onClick={()=>handleChangePage(page, totalPage)}>
		<span className="iconfont">&#xe637;</span>
		换一批
	</SearchInfoSwitch>
</SearchInfoTitle>
...

此时可以看到旋转按钮在热门搜索框的右下角,这是因为上面搜索图标的样式影响了。修改代码,给搜索图标添加room类名:

...
export const SearchWrapper = styled.div`
    position: relative;
    float:left;
    .zoom {
        position: absolute;
        right: 5px;
        bottom: 5px;
        width: 30px;
        line-height: 30px;
        border-radius: 15px;
        text-align: center;
        &.focused {
            background: #777;
            color: #fff;
        }
    }
`;
...
export const SearchInfoSwitch = styled.span`
  font-size: 13px;
  float: right;
  .spin {
    font-size: 12px;
    margin-right: 5px;
  }
`;
...
...
<SearchInfoTitle>
	热门搜索
	<SearchInfoSwitch onClick={()=>handleChangePage(page, totalPage)}>
		<span className="iconfont spin">&#xe637;</span>
		换一批
	</SearchInfoSwitch>
</SearchInfoTitle>
...
<span className={focused ? 'focused iconfont zoom' : 'iconfont zoom'}>&#xe624;</span>
...

此时换一批前面已经正常显示旋转按钮,接下来添加动画效果。

...
export const SearchInfoSwitch = styled.span`
  font-size: 13px;
  float: right;
  .spin {
    display: block;  // block才有transform
    float:left;
    font-size: 12px;
    margin-right: 5px;
    transition: all .2s ease-in;
    transform-origin: center center;
  }
`;
...
...
<SearchInfoTitle>
                        热门搜索
                        <SearchInfoSwitch onClick={()=>handleChangePage(page, totalPage, this.spinIcon)}>
                            <span ref={(icon)=>{this.spinIcon=icon}} className="iconfont spin">&#xe637;</span>  {/*使用ref属性获取元素*/}
                            换一批
                        </SearchInfoSwitch>
                    </SearchInfoTitle>
...
...
const mapDispatchToProps = (dispatch) => {
    return {
        handleInputFocus() {
            dispatch(actionCreators.getList());
            dispatch(actionCreators.searchFocus());
        },
        handleInputBlur() {
            dispatch(actionCreators.searchBlur());
        },
        handleMouseEnter() {
            dispatch(actionCreators.mouseEnter());
        },
        handleMouseLeave() {
            dispatch(actionCreators.mouseLeave());
        },
        handleChangePage(page, totalPage, spin) {
            let originAngle = spin.style.transform.replace(/[^0-9]/ig, '');
            if (originAngle) {
                originAngle = parseInt(originAngle, 10);
            } else {
                originAngle = 0;
            }
            spin.style.transform = 'rotate(' + (originAngle + 360) + 'deg)';
            if (page < totalPage) {
                dispatch(actionCreators.changePage(page + 1));
            } else {
                dispatch(actionCreators.changePage(1));
            }
        }
    }
}
...

避免多次请求

目前每次点击搜索框时都会发送请求获取热门搜索数据,为使请求只在第一次点击时发送,修改部分代码:

...
    render() {
        const {focused, handleInputFocus, handleInputBlur, list} = this.props;
        return (
            ...
                            <NavSearch
                                className={focused ? 'focused' : ''}
                                onFocus={()=>handleInputFocus(list)}  // 传入list
                                onBlur={handleInputBlur}
                            >
                            </NavSearch>
          	...
        )
    }
}
...
const mapDispatchToProps = (dispatch) => {
    return {
        handleInputFocus(list) {
            (list.size === 0) && dispatch(actionCreators.getList());  // 注意可以这样简写
            dispatch(actionCreators.searchFocus());
        },
        ....
};

export default connect(mapStateToProps, mapDispatchToProps)(Header);

另外为使鼠标移入换一批时样式,在SearchInfoSwitch下增加:cursor: pointer;

路由及登录控制

参考完整项目https://github.com/AdoredU/jianshu,后续有时间会作详细补充。