- 搭建项目环境
- 使用styled-components
- 头部区域编写
- 头部区域字体图标补充
- 搜索框动画
- 使用React-Redux管理数据
- 使用combineReducers拆分数据管理
- 拆分actionCreators和constants
- immutable和redux-immutable的使用
- 热门搜索样式布局
- 热门搜索获取数据
- 热门搜索显示修改
- 分页显示热门搜索
- 热门搜索换页动画
- 避免多次请求
- 路由及登录控制
搭建项目环境
使用脚手架工具大家简书项目: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"></span>
</NavItem>
<SearchWrapper>
<NavSearch></NavSearch>
<span className="iconfont"></span>
</SearchWrapper>
</Nav>
<Addition>
<Button className='writing'>
<span className="iconfont"></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"></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'}
>

</span>
</SearchWrapper>
</Nav>
<Addition>
<Button className='writing'>
<span className="iconfont"></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 redux
和npm 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"></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'}
>

</span>
</SearchWrapper>
</Nav>
<Addition>
<Button className='writing'>
<span className="iconfont"></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"></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'}
>

</span>
</SearchWrapper>
</Nav>
<Addition>
<Button className='writing'>
<span className="iconfont"></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"></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'}
>

</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"></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"></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'}
>

</span>
{getListArea(props.focused)}
</SearchWrapper>
</Nav>
<Addition>
<Button className='writing'>
<span className="iconfont"></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"></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'}
>

</span>
{this.getListArea()}
</SearchWrapper>
</Nav>
<Addition>
<Button className='writing'>
<span className="iconfont"></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"></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'}
>

</span>
{this.getListArea()}
</SearchWrapper>
</Nav>
<Addition>
<Button className='writing'>
<span className="iconfont"></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"></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'}
>

</span>
{this.getListArea()}
</SearchWrapper>
</Nav>
<Addition>
<Button className='writing'>
<span className="iconfont"></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"></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"></span>
换一批
</SearchInfoSwitch>
</SearchInfoTitle>
...
<span className={focused ? 'focused iconfont zoom' : 'iconfont zoom'}></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"></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,后续有时间会作详细补充。