第7章 项目实战:Header组件开发
7-1 项目目录搭建:Styled-Components 与 Reset.css 的结合使用
安装
https://reactjs.org/
npx create-react-app jianshu
cd jianshu
npm start
打包,并在本地起一个 server 运行*
npm run build
npm i -g serve
serve -s build
删除一些文件
src
——App.css
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #09d3ac;
}
src
——App.test.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});
修改一些文件
在项目的入口文件引入样式
src
——index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
给根组件添加一个简单的颜色样式
src
——index.css
body {
margin: 0;
padding: 0;
}
.dell {
background: orange;
}
这种样式的引入方式,全局组件可用
src
——App.js
import React from 'react';
function App() {
return (
<div className="dell">
Hello,World!
</div>
);
}
export default App;
避免样式冲突
引入第三方模块 styled-components 对样式进行管理
npm i styled-components
npm i typescript
通过 styled-components 构建全局样式
index.css —— style.js
import { createGlobalStyle } from 'styled-components';
export const GlobalStyle = createGlobalStyle`
body {
margin: 0;
padding: 0;
}
.dell {
background: orange;
}
`
在入口文件引入全局样式
src
——index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './style.js';
ReactDOM.render(<App />, document.getElementById('root'));
在根组件中使用全局样式
- 根组件相当于 index.html
src
——App.js
import React from 'react';
import { GlobalStyle } from './style';
function App() {
return (
<div className="dell">
<GlobalStyle />
Hello,World!
</div>
);
}
export default App;
在所有的浏览器中统一样式 reset.css
https://meyerweb.com/eric/tools/css/reset/
src
——style.js
import { createGlobalStyle } from 'styled-components';
export const GlobalStyle = createGlobalStyle`
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
`;
7-2 使用 styled-components 完成 Header 组件布局(1)
因为头部是公共区块,所以文件夹命名为 common
src
——common
————header
——————index.js
import React from 'react';
function Header() {
return (
<div>
Header
</div>
);
}
export default Header;
在入口组件里引用我们自定义的 Header 组件
src
——App.js
import React from 'react';
import { GlobalStyle } from './style';
import Header from './common/header';
function App() {
return (
<div>
<GlobalStyle />
<Header />
</div>
)
}
export default App;
开始写头部样式:组件局部样式的应用
src
——common
————header
——————style.js
import styled from 'styled-components';
export const HeaderWrapper = styled.div`
height: 56px;
background: red;
`;
src
——common
————header
——————index.js
import React from 'react';
import { HeaderWrapper } from './style';
function Header() {
return (
<HeaderWrapper>
Header
</HeaderWrapper>
);
}
export default Header;
将静态文件统一放在 statics 目录里管理
src
——statics
——————logo.png
引入 Logo 组件
src
——common
————header
——————index.js
import React from 'react';
import { HeaderWrapper, Logo } from './style';
function Header() {
return (
<HeaderWrapper>
<Logo />
</HeaderWrapper>
);
}
export default Header;
定义 Logo 组件
src
——common
————header
——————style.js
import styled from 'styled-components';
export const HeaderWrapper = styled.div`
position: relative;
height: 56px;
border-bottom: 1px solid #f0f0f0;
`;
export const Logo = styled.a`
position: absolute;
top: 0;
left: 0;
display: block;
width: 100px;
height: 56px;
background: orange;
`;
引入图片地址的语法
src
——common
————header
——————style.js
import styled from 'styled-components';
import logoPic from '../../statics/logo.png';
// ...
export const Logo = styled.a`
position: absolute;
top: 0;
left: 0;
display: block;
width: 100px;
height: 56px;
background-image: url(${logoPic});
background-size: contain;
cursor: pointer;
`;
a标签添加跳转链接
import styled from 'styled-components';
import logoPic from '../../statics/logo.png';
// ...
export const Logo = styled.a.attrs({
href: "/"
})`
position: absolute;
...
cursor: pointer;
`;
导航样式
src
——common
————header
——————style.js
import styled from 'styled-components';
// ...
export const Nav = styled.div`
width: 960px;
height: 100%;
margin: 0 auto;
`;
7-3 使用styled-components完成Header组件布局(2)
导航项既有相同点、又有不同点,通过 class 来区分
src
——common
————header
——————index.js
import React from 'react';
import {
HeaderWrapper,
Logo,
Nav,
NavItem
} from './style';
function Header() {
return (
<HeaderWrapper>
<Logo/>
<Nav>
<NavItem className="left active">首页</NavItem>
<NavItem className="left">下载App</NavItem>
<NavItem className="right">登录</NavItem>
<NavItem className="right">Aa</NavItem>
</Nav>
</HeaderWrapper>
);
}
export default Header;
src
——common
————header
——————style.js
import styled from 'styled-components';
// ...
export const NavItem = styled.div`
padding: 0 15px;
font-size: 17px;
line-height: 56px;
color: #333;
&.left {
float: left;
}
&.right {
float: right;
color: #969696;
}
&.active {
color: #ea6f5a;
}
`;
引入搜索框
src
——index.js
import React from 'react';
import {
HeaderWrapper,
Logo,
Nav,
NavItem,
NavSearch
} from './style';
function Header() {
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>
</HeaderWrapper>
);
}
export default Header;
定义搜索框样式
src
——common
————header
——————style.js
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;
}
`;
引入最右侧的两个按钮
src
——index.js
import React from 'react';
import {
HeaderWrapper,
Logo,
Nav,
NavItem,
NavSearch,
Addition,
Button
} from './style';
function Header() {
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='writting'>写文章</Button>
<Button className='reg'>注册</Button>
</Addition>
</HeaderWrapper>
)
}
export default Header;
定义按钮样式
src
——common
————header
——————style.js
// ...
export const Nav = styled.div`
...
padding-right: 70px;
box-sizing: border-box;
...
`;
// ...
export const Addition = styled.div`
position: absolute;
right: 0px;
top: 0px;
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;
}
&.writting {
color: #fff;
background: #ec6149;
}
`;
7-4 使用iconfont嵌入头部图标
iconfont.cn

















import './statics/iconfont/iconfont.css'




<i className="iconfont"></i>
<i className="iconfont"></i>
<i className="iconfont"></i>
给搜索框增加放大镜图标
src
——index.js
import React from 'react';
import {
HeaderWrapper,
Logo,
Nav,
NavItem,
NavSearch,
Addition,
Button,
NavSearchWrapper
} from './style';
function Header() {
return (
<HeaderWrapper>
<Logo/>
<Nav>
<NavItem className="left active">首页</NavItem>
<NavItem className="left">下载App</NavItem>
<NavItem className="right">登录</NavItem>
<NavItem className="right">
<i className="iconfont"></i>
</NavItem>
<NavSearchWrapper>
<NavSearch></NavSearch>
<span className='iconfont'></span>
</NavSearchWrapper>
</Nav>
<Addition>
<Button className='writting'>
<i className="iconfont"></i>
写文章
</Button>
<Button className='reg'>注册</Button>
</Addition>
</HeaderWrapper>
);
}
export default Header;
给搜索框增加 wrapper 样式
控制放大镜在搜索框里的位置
src
——common
————header
——————style.js
export const NavSearchWrapper = styled.div`
position: relative;
float: left;
.iconfont {
position: absolute;
right:5px;
bottom: 5px;
width: 30px;
line-height: 30px;
border-radius: 15px;
text-align: center;
}
`;
7-5 搜索框动画效果实现
搜索框样式微调
src
——common
————header
——————style.js
export const NavSearch = styled.input.attrs({
placeholder: '搜索'
})`
...
padding: 0 40px 0 20px;
color: #666;
...
`;
给搜索框加 onFocus 事件,改变框的长度
src
——index.js
import React, { Component } from 'react';
import {
HeaderWrapper,
Logo,
Nav,
NavItem,
NavSearch,
Addition,
Button,
NavSearchWrapper
} 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>
<NavSearchWrapper>
<NavSearch
className={this.state.focused ? 'focused' : ''}
onFocus={this.handleInputFocus}
onBlur={this.handleInputBlur}
></NavSearch>
<i className={this.state.focused ? 'focused iconfont' : 'iconfont'}></i>
</NavSearchWrapper>
</Nav>
<Addition>
<Button className='writting'>
<span className='iconfont'></span>
写文章
</Button>
<Button>注册</Button>
</Addition>
</HeaderWrapper>
)
}
handleInputFocus() {
this.setState({
focused: true
})
}
handleInputBlur() {
this.setState({
focused: false
})
}
}
export default Header;
增加搜索框和放大镜聚焦样式
src
——common
————header
——————style.js
// ...
export const NavSearch = styled.input.attrs({
placeholder: '搜索'
})`
padding: 0 40px 0 20px;
...
&::placeholder {
color: #999;
}
&.focused {
width: 240px;
}
`;
// ...
export const NavSearchWrapper = styled.div`
position: relative;
float: left;
.iconfont {
...
text-align: center;
&.focused {
background: #777;
color: #FFF;
}
}
`;
给搜索框添加动画效果
npm i react-transition-group
import { CSSTransition } from 'react-transition-group';
// ...
<NavSearchWrapper>
<CSSTransition
in={this.state.focused}
timeout={500}
classNames="slide"
>
<NavSearch
className={this.state.focused ? 'focused' : ''}
onFocus={this.handleInputFocus}
onBlur={this.handleInputBlur}
></NavSearch>
</CSSTransition>
<i className={this.state.focused ? 'iconfont focused' : 'iconfont'}></i>
</NavSearchWrapper>
// ...
src
——common
————header
——————style.js
const NavSearch = styled.input.attrs({
placeholder: '搜索'
})`
padding: 0 40px 0 20px;
...
&.focused {
width: 240px;
}
&.slide-enter {
transition: all .5s ease-out;
}
&.slide-enter-active {
width: 240px;
}
&.slide-exit {
transition: all .5s ease-out;
}
&.slide-exit-active {
width: 160px;
}
`;
7-6使用React-Redux进行应用数据的管理
1.创建 store
npm i redux
npm i react-redux
src
——store
————index.js
import { createStore } from 'redux';
import reducer from './reducer'
const store = createStore(reducer);
export default store;
src
——store
————reducer.js
const defaultState = {};
export default (state = defaultState, action) => {
return state;
}
2.引用 store
src
——App.js
import { Provider } from 'react-redux';
import store from './store';
<Provider store={store}>
<Header>
</Provider>
3.连接 store
src
——common
————header
——————index.js
import { connect } from 'react-redux';
// ...
const mapStateToProps = (state) => {
return {
}
}
const mapDispatchToProps = (dispatch) => {
return {
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Header);
4.将默认数据放到 redux 仓库进行管理
src
——common
————header
——————index.js
constructor(props) {
super(props);
// this.state = {
// focused: false
// }
this.handleInputFocus = this.handleInputFocus.bind(this);
this.handleInputBlur = this.handleInputBlur.bind(this);
}
src
——store
————reducer.js
const defaultState = {
focused: false
};
5.将 store 里的数据映射到组件的 Props
src
——common
————header
——————index.js
const mapStateToProps = (state) => {
return {
focused: state.focused
}
}
6.将 this.state 都换成 this.props
src
——common
————header
——————index.js
<NavSearchWrapper>
<CSSTransition
in={this.props.focused}
...
>
<NavSearch
className={this.props.focused ? 'focused' : ''}
onFocus={this.props.handleInputFocus}
onBlur={this.props.handleInputBlur}
></NavSearch>
</CSSTransition>
<i className={this.props.focused ? 'iconfont focused' : 'iconfont'}></i>
</NavSearchWrapper>
7.修改 redux 里数据的流程:
创建 action —— 给到 store——— 给到 reducer —— 返回给 store —— 数据发生更新 —— 页面跟着变化
src
——common
————header
——————index.js
// constructor(props) {
// super(props);
// this.state = {
// focused: false
// }
// this.handleInputFocus = this.handleInputFocus.bind(this);
// this.handleInputBlur = this.handleInputBlur.bind(this);
// }
// handleInputFocus() {
// this.setState({
// focused: true
// })
// }
//
// handleInputBlur() {
// this.setState({
// focused: false
// })
// }
const mapDispatchToProps = (dispatch) => {
return {
handleInputFocus() {
const action = {
type: 'search_focus',
}
dispatch(action);
},
handleInputBlur() {
const action = {
type: 'search_blur'
}
dispatch(action);
}
}
}
src
——store
————reducer.js
export default (state = defaultState, action) => {
if (action.type === 'search_focus') {
return {
focused: true
}
}
if (action.type === 'search_blur') {
return {
focused: false
}
}
return state;
}
8.变身无状态组件
src
——common
————header
——————index.js
const Header = (props) => {
return (
<HeaderWrapper>
...
</HeaderWrapper>
)
}
function Header(props) {
return (
<HeaderWrapper>
...
</HeaderWrapper>
)
}
9.将 this.props 改为 props
<NavSearchWrapper>
<CSSTransition
in={props.focused}
...
>
<NavSearch
className={props.focused ? 'focused' : ''}
onFocus={props.handleInputFocus}
onBlur={props.handleInputBlur}
></NavSearch>
</CSSTransition>
<i className={props.focused ? 'iconfont focused' : 'iconfont'}></i>
</NavSearchWrapper>
10.去掉 { Component } 的引入
import React from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import {
HeaderWrapper,
Logo,
Nav,
NavItem,
NavSearch,
Addition,
Button,
NavSearchWrapper
} from './style';
function 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>
<NavSearchWrapper>
<CSSTransition
in={props.focused}
timeout={500}
classNames="slide"
>
<NavSearch
className={props.focused ? 'focused' : ''}
onFocus={props.handleInputFocus}
onBlur={props.handleInputBlur}
></NavSearch>
</CSSTransition>
<i className={props.focused ? 'iconfont focused' : 'iconfont'}></i>
</NavSearchWrapper>
</Nav>
<Addition>
<Button className='writting'>
<span className='iconfont'></span>
写文章
</Button>
<Button>注册</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);
7-7使用combineReducers完成对数据的拆分管理
1.设置开发者工具:redux-devtoola-extension
https://github.com/zalmoxisus/redux-devtools-extension
src
——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;
2.一个文件的代码超过300行,就证明设计有问题
Reducer 是图书馆管理员对应的手册,把手册拆分成多个小手册来管理
src
——common
————header
——————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;
};
3.在根目录下的 Reducer 对所有分类手册进行整合
src
——store
————reducer.js
import { combineReducers } from 'redux';
import headerReducer from '../common/header/store/reducer';
const reducer = combineReducers({
header: headerReducer
})
export default reducer;
4.从 Store 获取数据的时候要多加一层
src
——common
————header
——————index.js
const mapStateToProps = (state) => {
return {
focused: state.header.focused
}
}
5.优化分类手册的整合路径
src
——common
————header
——————store
————————index.js
import reducer from './reducer';
export { reducer };
src
——store
————reducer.js
import { reducer as headerReducer } from '../common/header/store';
7-8 actionCreators与constants的拆分
用 actionCreators 统一创建 action
src
——common
————header
——————store
————————actionCreators.js
export const searchFocus = () => ({
type: 'search_focus'
});
export const searchFocus = () => ({
type: 'search_blur'
});
src
——common
————header
——————index.js
import * as actionCreators from './store/actionCreators';
// ...
const mapDispatchToProps = (dispatch) => {
return {
handleInputFocus() {
dispatch(actionCreators.searchFocus());
},
handleInputBlur() {
dispatch(actionCreators.searchBlur());
}
}
}
统一管理常量
src
——common
————header
——————store
————————constants.js
export const SEARCH_FOCUS = 'header/SEARCH_FOCUS';
export const SEARCH_BLUR = 'header/SEARCH_BLUR';
src
——common
————header
——————store
————————actionCreators.js
import * as constants from './constants';
export const searchFocus = () => ({
type: constants.SEARCH_FOCUS
});
export const searchBlur = () => ({
type: constants.SEARCH_BLUR
});
src
——common
————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;
};
整合 store 下路径
src
——common
————header
——————store
————————index.js
import reducer from './reducer';
import * as actionCreators from './actionCreators';
import * as constants from './constants';
export { reducer, actionCreators, constants };
src
——common
————header
——————index.js
import { actionCreators } from './store';
// ...
7-9 使用 Immutable.js 来管理store中的数据
https://immutable-js.github.io/immutable-js/docs/
https://github.com/immutable-js/immutable-js
npm i immutable
1.将 state 变成 immutable 对象
src
——common
————header
——————store
————————reducer.js
import * as constants from './constants';
import { fromJS } from 'immutable';
const defaultState = fromJS({
focused: false
});
// ...
2.immutable 对象的获取方式
列表循环的获取方式要改为:item.get('imgUrl'),不能为点语法,切记!!!不要忘记!!!
mapStateToProps里的获取方式为:list: state.home.get('blogList')
src
——common
————header
——————index.js
//...
const mapStateToProps = (state) => {
return {
focused: state.header.get('focused')
}
}
// ...
3.immutable 对象的 set 方法会返回一个 state 的副本
src
——common
————header
——————store
————————reducer.js
//...
export default (state = defaultState, action) => {
if (action.type === constants.SEARCH_FOCUS) {
return state.set('focused', true);
}
if (action.type === constants.SEARCH_BLUR) {
return state.set('focused', false);
}
return state;
};
//...
7-10 使用redux-immutable统一数据格式
list: state.home.get('blogList') => list: state.get('home').get('blogList')
=> list: state.getIn(['home', 'blogList'])
npm i redux-immutable
src
——store
————reducer.js
import { combineReducers } from 'redux-immutable';
//...
src
——common
————header
——————index.js
//...
const mapStateToProps = (state) => {
return {
focused: state.get('header').get('focused')
}
}
// ...
const mapStateToProps = (state) => {
return {
focused: state.getIn(['header', 'focused'])
}
}
到这里项目中最难的部分已经搞定了,框架已经搭好
https://github.com/wtyqer/jianshu-react-framework
网友评论