0%

简书项目

简书项目

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组件布局

  1. 在src目录下新建common文件夹来装公共样式
  2. 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;
 }
>`;

注意事项

  1. 图片的插入方式!!!
  2. 设置标签的其他默认属性,及其样式的方法(href,placeholder等)
  3. 如何使用选择器(&.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
  1. 安装react-transition-group
npm install react-transition-group --save
  1. 写搜索框样式
<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'}>
    &#xe614;
  </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管理数据

  1. 先安装redux,react-redux
npm install redux;
npm install react-redux;
  1. App.js中引入Privider,连接store
import store from "./store";
import { Provider } from "react-redux";

//使用
<Provider store={store}>
  <Header />
  <GlobalStyle />
  <GlobalIconFont />
</Provider>
  1. 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替换
  1. src下创建store编写index.js/reducer.js
(1)reducer是一个纯函数,返回一个对象
(2)index.js中使用createStore,compose  API(参考react-devtools使用方法)

combineReducers数据拆分管理

  • 把src下的store/reducer.js下放到对应区块的子store中,更加方便管理数据
  1. 在header下新建store
//reducer移入之前的内容
....

//导出路径过长,新建一个index.js导出对应reducer,就会是调用路径减少两层
import reducer from './reducer';
export { reducer };
  1. 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()

  1. 安装
//分支reducer需要 fromJS函数
npm install immutable

//总reducer需要 新的合并函数
npm install redux-immutable
  1. header/store/reducer.js
// 把state模板设置为immutable对象

import { fromJS } from "immutable";

const defaultState = fromJS({
  focused: false,
});
  • 返回对象时要用对应immutable对象提供的
return state.set("focused", true);
  1. 总reducer
//相当于combineReducers函数升级了

import { combineReducers } from "redux-immutable";
  1. 更改数据引用方式

我们使用了immutable后,store下面数据的引用不再使用’.属性‘来实现 需要通过immutable提供的API

// props和state连接时

// focused: state.get("header").get("focused"),
focused: state.getIn(["header", "focused"]),

Ajax获取热门搜索框数据

  1. 异步获取数据,引入中间件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)));
  1. 热门搜索框效果添加
  • 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());
  1. 数据的异步获取
  • 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的数据,走一遍流程

热门搜索换页功能

  1. 添加换页iconfont
  • 注意,页面中有多个字体图标样式修改时,我们要对每一个设定自己特定的类名!!!,避免样式冲突
// 例如
<i
  className={
    this.props.focused
    ? "focused iconfont search"
    : "iconfont search"
  }
>
  &#xe637;
</i>

<i
  className="iconfont spin"
>
  &#xe772;
</i>
  1. 实现分页思路

我们获取到的数据是一大堆数据数组的集合,我们可以把它们分为固定长度为一组,预定存储当前页数,和总页数两个数据在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方法(返回替换后的新字符串接受两个参数,一个实匹配字符串,一个是替换字符串)
  1. 正则表达式表示的匹配字符串
  2. 普通字符串
>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,实现内容切换
  1. 安装
npm install react-router-dom
  1. 引入对应模块
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”)

文件架构搭建

  1. 新建pages文件夹
    • 存放各个页面
  2. pages文件夹下,新建detail详情页和home主页
  3. detail和home都要创建对应的index.js和styles.js
  4. 页面内容过多可以分组件开发
    • 如: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内容
  1. 建立store连接
import { connect } from "react-redux";

const mapState = (state) => ({
  list: state.get("home").get("topicList"),
});

export default connect(mapState, null)(Topic);
  1. 结构样式编写
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";
  1. 添加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中

  1. 引入axios模块
  2. 导出getHomeInfo(返回一个函数)
  3. 函数中还会带着数据再次调用dispatch方法(返回action)
  4. dispatch方法将action派发给reducer进行处理
componentDidMount() {
	this.props.changeHomeData();
}
const mapDispatch = (dispatch) => ({
  changeHomeData() {
    dispatch(actionCreators.getHomeInfo());
  },
});

加载更多模块设计

  • 点击加载更多按钮,加载出更多list列表项
  1. 结构代码添加 List.js
<LoadMore onClick={() => getMoreList(page)}更多文字</LoadMore>
  1. 样式文件添加对应样式 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;
`;
  1. 绑定 dispatch函数
getMoreList(page) {
	dispatch(actionCreators.getMoreList(page));
},
  1. 修改 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));
    });
  };
};
  1. reducer.js
  • 添加处理程序
case actionTypes.ADD_ARTICLE_LIST:
	return state.merge({
		articleList: state.get("articleList").concat(action.list),
		articlePage: action.nextPage,
});

性能优化

  1. react-redux使用connect连接store和页面,一旦store改变了,就会重新渲染使用connect所连接的所有render部分。

    • 为解决这样的性能消耗,react帮我们预定义了一个纯组件PureComponent

    其中相当于内置了shouldComponentUpdate这个生命周期函数!!,进行一层浅比较,所以对于一些引用数据类型可能会有坑,但对于使用immutable.js管理的数据,可以得到很好的兼容性!!!

  2. 单页面跳转

    • 在react-router-dom中有一个Link组件 to 属性可以实现单页面跳转功能
      >//使用方法 :外面加一个
      ><Link to="/">	//回到根页面
    ....
      ></Link>

    注意点:使用路由跳转的组件都应该放在<BrowserRouter>组件内部

详情页/登录功能开发