简书项目
Header组件开发
样式问题Styled-Components
- react项目文件中的css文件都是一个引用全局受用,这样就会造成样式混乱管理的问题
我们引入Styled-Components结合Reset.css
// (1)安装styled-components
npm install styled-components
// (2)项目目录下创建一个style.js文件来存放样式
// 引入对应全局样式的 api
import { createGlobalStyle } from "styled-components";
// (3)创建一个组件,以组件的形式导出对应样式
const GlobalStyle = createGlobalStyle`
...Reset.css //统一规范样式
`;
export default GlobalStyle;
// (4)以组件的形式使用添加到对应JSX语法块中
<Fragment>
<div className="body">minyue</div>
<GlobalStyle />
</Fragment>
完成header组件布局
- 在src目录下新建
common文件夹
来装公共样式 - common文件夹下再新建
header文件夹
,新建index.js文件
,样式文件style.js
来设置头部样式
这里我们使用styled-components布局方式—-组件化布局方式
><HeaderWrapper> <Logo></Logo> <Nav> <NavItem className="left">首页</NavItem> <NavItem className="left">下载APP</NavItem> <NavSearch></NavSearch> <NavItem className="right">登录</NavItem> <NavItem className="right">Aa</NavItem> </Nav> ></HeaderWrapper>
- 样式作为一个个块的组件搭建起来,在样式文件样式文件中完成标签类型的设定,组件形式导出
>// (1)引入包文件 >import styled from "styled-components"; >import logoPic from "../../statics/logo.png"; >// (2)组件形式编写样式组件 >export const Logo = styled.a.attrs({ href: "/", >})` position: absolute; top: 0; left: 0; display: block; width: 100px; height: 56px; background: url(${logoPic}); background-size: contain; >`; >export const NavItem = styled.div` line-height: 56px; padding: 0 15px; font-size: 17px; color: #333; &.left { float: left; } &.right { float: right; } &.active { color: #ea6f5a; } >`; >export const NavSearch = styled.input.attrs({ placeholder: "search", >})` 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; } >`;
注意事项
- 图片的插入方式!!!
- 设置标签的其他默认属性,及其样式的方法(href,placeholder等)
- 如何使用选择器(
&.className{}
)
字体图标的使用
和图片一样作为静态数据,我们存放在
statics
中iconfont
下删掉没有用的文件,将iconfont.css
改为iconfont.js
,把字体图标的样式当做一个全局组件使用createGlobalStyle
函数。
App.js
import React, { Fragment } from "react";
import { GlobalStyle } from "./style";
import { GlobalIconFont } from "./statics/iconfont/iconfont";
import Header from "./common/header/index.js";
function App() {
return (
<Fragment>
<Header />
<GlobalStyle />
<GlobalIconFont />
</Fragment>
);
}
export default App;
iconfont.js
import { createGlobalStyle } from "styled-components";
export const GlobalIconFont = createGlobalStyle`
....
`;
style.js
import { createGlobalStyle } from "styled-components";
// 不同浏览器的统一样式规范
export const GlobalStyle = createGlobalStyle`
.....
`;
header/style.js
样式问题 | |
---|---|
.className | 下一级含有该类,下一级元素的样式设定 |
&.className | &(表示当前元素),同级元素含有对应类的元素进行样式设定 |
.attrs({ 默认属性 }) | 对当前元素默认属性的设置 |
&::placeholder {} | 对当前元素的伪类元素进行样式设定(默认内容属性,类似元素的伪类属性) |
搜索框的动态效果
使用 react-redux实现
- 这里就需要一个状态,和两个action
- 安装react-transition-group
npm install react-transition-group --save
- 写搜索框样式
<SearchWrapper>
<CSSTransition
in={props.focused}
timeout={200}
classNames="slide"
>
<NavSearch
className={props.focused ? 'focused': ''}
onFocus={props.handleInputFocus}
onBlur={props.handleInputBlur}
></NavSearch>
</CSSTransition>
<i className={props.focused ? 'focused iconfont': 'iconfont'}>

</i>
</SearchWrapper>
//对应搜索框下
&.focused {
width: 240px;
}
&.slide-enter {
width: 160px;
}
&.slide-enter-active {
transition: all 0.2s ease-out;
width: 240px;
}
&.slide-exit {
width: 240px;
}
&.slide-exit-active {
width: 160px;
transition: all 0.2s ease-out;
}
使用react-redux管理数据
- 先安装redux,react-redux
npm install redux;
npm install react-redux;
- App.js中引入Privider,连接store
import store from "./store";
import { Provider } from "react-redux";
//使用
<Provider store={store}>
<Header />
<GlobalStyle />
<GlobalIconFont />
</Provider>
- header/index.js中连接store
import { connect } from "react-redux";
//使用
const mapStateToProps = (state) => {
return {
focused: state.focused,
};
};
const mapDispathToProps = (dispatch) => {
return {
handleInputFocus() {
const action = {
type: "search_focus",
};
dispatch(action);
},
handleInputBlur() {
const action = {
type: "search_blur",
};
dispatch(action);
},
};
};
export default connect(mapStateToProps, mapDispathToProps)(Header);
- 注意将文章中的state用props替换
- src下创建store编写index.js/reducer.js
(1)reducer是一个纯函数,返回一个对象
(2)index.js中使用createStore,compose API(参考react-devtools使用方法)
combineReducers数据拆分管理
- 把src下的store/reducer.js下放到对应区块的子store中,更加方便管理数据
- 在header下新建store
//reducer移入之前的内容
....
//导出路径过长,新建一个index.js导出对应reducer,就会是调用路径减少两层
import reducer from './reducer';
export { reducer };
- src下的总reducer,汇总功能
//引入combineReducers()
//es6语法 ’as‘ 名称替代写法(名称重复时使用)
import { combineReducers } from 'redux';
import { reducer as headerReducer } from '../common/header/store';
const reducer = combineReducers({
header: headerReducer
});
export default reducer;
- 注意
focused: state.header.focused,
要加中间header
immutable.js管理数据
- 为了确保reducer中不会返回一个”有问题“的”state“,我们设法把defaultState设定成有权限的数据类型
引入immutable.js
的 fromJS()
- 安装
//分支reducer需要 fromJS函数
npm install immutable
//总reducer需要 新的合并函数
npm install redux-immutable
- header/store/reducer.js
// 把state模板设置为immutable对象
import { fromJS } from "immutable";
const defaultState = fromJS({
focused: false,
});
- 返回对象时要用对应immutable对象提供的
return state.set("focused", true);
- 总reducer
//相当于combineReducers函数升级了
import { combineReducers } from "redux-immutable";
- 更改数据引用方式
我们使用了immutable后,store下面数据的引用不再使用’.属性‘来实现 需要通过immutable提供的API
// props和state连接时
// focused: state.get("header").get("focused"),
focused: state.getIn(["header", "focused"]),
Ajax获取热门搜索框数据
- 异步获取数据,引入中间件redux-thunk
npm install redux-thunk
- 修改store/index.js
//引入applyMiddleware
//引入thunk
import { createStore, compose, applyMiddleware } from "redux";
import thunk from "redux-thunk";
//在composeEnhancers中添加中间件
const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)));
- 热门搜索框效果添加
- index.js中
// (1) 把搜索框写入一个函数组件,当focus(true)时返回样式,并添加到searchWrapper中
getListArea() {
const { focused, list } = this.props;
if (focused) {
return (
<SearchInfo>
<SearchInfoTitle>
热门搜索
<SearchInfoSwitch>换一批</SearchInfoSwitch>
</SearchInfoTitle>
<SearchInfoList>
{list.map((item) => {
return <SearchInfoItem key={item}>{item}</SearchInfoItem>;
})}
</SearchInfoList>
</SearchInfo>
);
} else {
return null;
}
}
// (2)style.js样式!!!
// (3)数据需要一个list
list: state.getIn(["header", "list"]),
dispatch(actionCreators.getList());
- 数据的异步获取
- actionCreaters.js
// 异步获取数据请求
// (1)安装axios模块
npm install axios
//getList 可以返回一个函数了!!
//模拟数据可以放在public/api下(程序会检索到这里)
export const getList = () => {
return (dispatch) => {
axios
.get("/api/headerList.json")
.then((res) => {
const data = res.data;
dispatch(changeList(data.data));
})
.catch(() => {
console.log("error");
});
};
};
const changeList = (data) => ({
type: actionTypes.CHANGE_LIST,
data: fromJS(data),
//data类型匹配!!
});
- reducer.js
// (1)defaultState 中添加list项 注意他是一个immutable类型
// (2)处理程序添加一项
case actionTypes.CHANGE_LIST:
return state.set("list", action.data); //data也应该是immutable类型
- 注意这里Ajax请求只需要请求一次
但是只要我们点击输入框就会触发Ajax请求,我们需要修改,当list为空时才发起请求!!
>// 将list传入函数 handleInputFocus >handleInputFocus(list) { list.size === 0 && dispatch(actionCreators.getList()); dispatch(actionCreators.searchFocus()); >},
- immutable对象没有length只有size判断list的长度
流程
1.添加结构(发现要什么数据,什么行为) | index.js |
---|---|
2.connect的两函数实现数据连接,行为传递 | index.js |
3.actionCreaters对行为做集中处理,返回action,调用dispatch传递行为(异步行为,返回一个函数,函数中会再次调用actionCreaters中的函数返回行为对象,并在返回的函数中实现dispatch) | actionCreaters.js |
4.reducer中添加默认数据种类,添加处理程序(使用immutable时,注意数据类型的匹配) | reducer.js |
5.检查完善(结构化赋值,等),数据文件的编写 | actionTypes.js(public/api/..) |
bug解决
- 我们可以看到,点击热门搜索框,内容搜索框失焦,热门搜索框也跟着关闭了??
index.js
// (1)给热门搜索框增加一个mouseIn判断值
if (focused || mouseIn) {
return (
<SearchInfo
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
...
)
}
//添加一个mouseIn的数据,走一遍流程
热门搜索换页功能
- 添加换页iconfont
- 注意,页面中有多个字体图标样式修改时,我们要对每一个设定自己特定的类名!!!,避免样式冲突
// 例如
<i
className={
this.props.focused
? "focused iconfont search"
: "iconfont search"
}
>

</i>
<i
className="iconfont spin"
>

</i>
- 实现分页思路
我们获取到的数据是一大堆数据数组的集合,我们可以把它们分为固定长度为一组,预定存储当前页数,和总页数两个数据在store中
index.js
// (1)在getListArea函数中根据page来选择数据
const newList = list.toJS(); //immutable数据变为js标准数据
const newList = [];
if (newList.length) {
for (let i = (page-1) * 10; i < page * 10; i++) {
pageList.push(
<SearchInfoItem key={newList[i]}>{newList[i]}</SearchInfoItem>
)
}
}
// (2)点击事件在<SearchInfoSwitch>上添加
onClick={() => handleChangePage(page, totalPage, this.spinIcon)}
//这个点击事件需要传入三个参数,最后一个参数是对应的字体图标标签元素,这里需要使用ref属性获取!!
ref={(icon) => {
this.spinIcon = icon;
}}
// (3)数据连接部分修改
page: state.getIn(["header", "page"]),
totalPage: state.getIn(["header", "totalPage"]),
handleChangePage(page, totalPage, spin) {
let originAngle = +spin.style.transform.replace(/[^0-9]/gi, "");
spin.style.transform = "rotate(" + (originAngle + 360) + "deg)";
if (page < totalPage) {
dispatch(actionCreators.changePage(page + 1));
} else {
dispatch(actionCreators.changePage(1));
}
},
- String.replace方法(返回替换后的新字符串接受两个参数,一个实匹配字符串,一个是替换字符串)
- 正则表达式表示的匹配字符串
- 普通字符串
>const p = 'The quick brown fox jumps over the lazy dog. If the dog reacted, was it really lazy?'; >const regex = /dog/gi; >console.log(p.replace(regex, 'ferret')); >// expected output: "The quick brown fox jumps over the lazy ferret. If the ferret reacted, was it really lazy?" >console.log(p.replace('dog', 'monkey')); >// expected output: "The quick brown fox jumps over the lazy monkey. If the dog reacted, was it really lazy?"
- 正则表达式:末尾的gi表示(多次匹配,不考虑大小写)
actionCreaters.js
// (1)添加一个changePage函数
export const changePage = (page) => ({
type: actionTypes.CHANGE_PAGE,
page,
});
// (2)changeList中添加计算totalPage的action在异步获取数据的同时也确定了总的热门搜索的页数
totalPage: Math.ceil(data.length / 10),
- 对应更改actionTypes.js
reducer.js
// changeList action添加更改获取totalPage值
case actionTypes.CHANGE_LIST:
return state.merge({
list: action.data,
totalPage: action.totalPage,
});
// changePage action获取当前page值
case actionTypes.CHANGE_PAGE:
return state.set("page", action.page);
default:
- merge方法,返回一个对象,可以修改一组immutable类型的数据。代替get链的写法
首页开发
路由功能使用
- 对于单页面应用,更改对应不同的路径显示不同的div,实现内容切换
- 安装
npm install react-router-dom
- 引入对应模块
import { BrowserRouter, Route } from "react-router-dom";
- BrowserRouter是切换部分的总盒子
- Route是盒子内的切换子模块
<BrowserRouter>
<Route path="/" exact render={() => <div>home</div>}></Route>
<Route path="/detail" exact render={() => <div>detail</div>}></Route>
</BrowserRouter>
- exact属性: path完全匹配(不加的话,“/detail”会识别“/”和”/detail”)
文件架构搭建
- 新建pages文件夹
- 存放各个页面
- pages文件夹下,新建detail详情页和home主页
- detail和home都要创建对应的index.js和styles.js
- 页面内容过多可以分组件开发
- 如:home下新建compoments文件夹存放主页的各个小组件
App.js
//引入各个页面组件
import Home from "./pages/home";
import Detail from "./pages/detail";
//Route中 component属性指向相应组件,而不是render函数
<Route path="/" exact component={Home}></Route>
<Route path="/detail" exact component={Detail}></Route>
home/index.js
- 首页分为左右两个部分
//home 结构
<HomeWrapper>
<HomeLeft>
<Topic />
<List />
</HomeLeft>
<HomeRight>
<Recommend />
<Writer />
</HomeRight>
</HomeWrapper>
Topic部分设计
- 在Topic.js文件中编写Topic内容
- 建立store连接
import { connect } from "react-redux";
const mapState = (state) => ({
list: state.get("home").get("topicList"),
});
export default connect(mapState, null)(Topic);
- 结构样式编写
render() {
const { list } = this.props;
return (
<TopicWrapper>
{list.map((item) => (
<TopicItem key={item.get("id")}>
<img className="topic-pic" src={item.get("imgUrl")} />
{item.get("title")}
</TopicItem>
))}
</TopicWrapper>
);
}
- 首页的样式都写在home文件夹下的统一style.js中
import { TopicWrapper, TopicItem } from "../style";
- 添加reducer.js
// 需要使用到topicList列表 并且引入immutable
import { fromJS } from "immutable";
const defaultState = fromJS({
topicList: [
{
id: 1,
title: "社会热点",
imgUrl:
"绝对地址1",
},
{
id: 2,
title: "手绘",
imgUrl:
"绝对地址2",
},
],
});
export default (state = defaultState, action) => {
switch (action.type) {
default:
return state;
}
};
List部分设计
- 在List.js中设计List内容 流程和上面相同
分析一下就行:
- 页面设计三个部分:(标题,简介),图片(右)
list.map((item) => {
return (
<ListItem key={item.get('id')}>
<img alt='' className='pic' src={item.get('imgUrl')} />
<ListInfo>
<h3 className='title'>{item.get('title')}</h3>
<p className='desc'>{item.get('desc')}</p>
</ListInfo>
</ListItem>
);
})
connect连接数据仓库
修改reducer,
添加articleList列表数据
- 其他两个模块类似完成 Recommend.js和Writer.js
异步获取数据
- 在home/index.js中通过componentDidMount函数实现异步axios数据请求
// (1) 需要引入axios模块
// (2) 在componentDidMount中异步请求
componentDidMount() {
axios.get("/api/home.json").then((res) => {
const result = res.data.data;
const action = {
type: "change_home_data",
topicList: result.topicList,
articleList: result.articleList,
recommendList: result.recommendList,
};
this.props.changeHomeData(action);
});
}
// (3) 这里只用对dispatch方法做连接
const mapDispatch = (dispatch) => ({
changeHomeData(action) {
dispatch(action);
},
});
export default connect(null, mapDispatch)(Home);
异步操作中间件优化
- 之前我们已经引入redux-thunk模块,所以,我们可以在actioncreators.js中异步返回函数!
- 并且利用生命周期函数componentDidMount异步调用执行方法changeHomeData
将异步请求放再actionCreators.js中
- 引入axios模块
- 导出getHomeInfo(返回一个函数)
- 函数中还会带着数据再次调用dispatch方法(返回action)
- dispatch方法将action派发给reducer进行处理
componentDidMount() {
this.props.changeHomeData();
}
const mapDispatch = (dispatch) => ({
changeHomeData() {
dispatch(actionCreators.getHomeInfo());
},
});
加载更多模块设计
- 点击加载更多按钮,加载出更多list列表项
- 结构代码添加
List.js
<LoadMore onClick={() => getMoreList(page)}更多文字</LoadMore>
- 样式文件添加对应样式
style.js
export const LoadMore = styled.div`
width: 100%;
height: 40px;
line-height: 40px;
margin: 30px 0;
background: #a5a5a5;
text-align: center;
border-radius: 20px;
color: #fff;
cursor: pointer;
`;
- 绑定
dispatch
函数
getMoreList(page) {
dispatch(actionCreators.getMoreList(page));
},
- 修改
actionCreators.js
- 也是异步请求,但是要加上加载的页码数
const addHomeList = (list, nextPage) => ({
type: actionTypes.ADD_ARTICLE_LIST,
list: fromJS(list),
nextPage,
});
export const getMoreList = (page) => {
return (dispatch) => {
axios.get("/api/homeList.json?page=" + page).then((res) => {
const result = res.data.data;
dispatch(addHomeList(result, page + 1));
});
};
};
- reducer.js
- 添加处理程序
case actionTypes.ADD_ARTICLE_LIST:
return state.merge({
articleList: state.get("articleList").concat(action.list),
articlePage: action.nextPage,
});
性能优化
react-redux使用connect连接store和页面,一旦store改变了,就会重新渲染使用connect所连接的所有render部分。
- 为解决这样的性能消耗,react帮我们预定义了一个纯组件
PureComponent
其中相当于内置了shouldComponentUpdate这个生命周期函数!!,进行一层浅比较,所以对于一些引用数据类型可能会有坑,但对于使用immutable.js管理的数据,可以得到很好的兼容性!!!
- 为解决这样的性能消耗,react帮我们预定义了一个纯组件
单页面跳转
- 在react-router-dom中有一个
Link
组件to
属性可以实现单页面跳转功能
>//使用方法 :外面加一个 ><Link to="/"> //回到根页面 .... ></Link>
注意点:使用路由跳转的组件都应该放在
<BrowserRouter>
组件内部- 在react-router-dom中有一个