likes
comments
collection
share

(07)Header 组件开发——⑦ AJAX 获取推荐数据 | React.js 项目实战:PC 端“简书”开发

作者站长头像
站长
· 阅读数 4
转载请注明出处,未经同意,不可修改文章内容。

🔥🔥🔥“前端一万小时”两大明星专栏——“从零基础到轻松就业”、“前端面试刷题”,已于本月大改版,合二为一,干货满满,欢迎点击公众号菜单栏各模块了解。

1 需求分析

之前的 6 篇文章,我们算是搭建好了整个项目的架子,接下来我们就可以放开手脚开始实现交互需求了。

本篇要实现的需求为:当“聚焦”在 SearchPanel 样式组件上时,加载出 PanelLabels 样式组件里边的所有 LabelLink 数据

我们可以看下“简书”官网在这一块的效果展示(当点击 input“搜索框”时,它会发一个“请求 trending_search ”(且只在第一次点击时会发送这个“请求”),其“返回值”和 PanelLabels 里的值是一一对应的): (07)Header 组件开发——⑦ AJAX 获取推荐数据 | React.js 项目实战:PC 端“简书”开发 所以,我们会用到 AJAX 来获取“异步数据”,并予以展示。

既然是一个中大型正式项目,我们就会用到 Redux-thunk 等“中间件”协助我们开发。

以下文字,我们就先配置 Redux-thunk,然后 mock “推荐”数据,最后获取这个 mock 的数据并展示出来!

2 安装和配置 Redux-thunk

1️⃣安装 Redux-thunk 并重新启动: (07)Header 组件开发——⑦ AJAX 获取推荐数据 | React.js 项目实战:PC 端“简书”开发

2️⃣打开 src 目录下 store 中的 index.js 文件:

/*
2️⃣-①:从 redux 中引入 applyMiddleware 方法。
这个方法使得我们可以使用“中间件”;
 */
import { createStore, compose, applyMiddleware } from "redux";  

import reducer from "./reducer"; 

// 2️⃣-②:从 redux-thunk 库中引入 thunk 模块;
import thunk from "redux-thunk";

const composeEnhancers =

  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;



const enhancer = composeEnhancers(  

  applyMiddleware(thunk) // ❗️2️⃣-③:顺便把 thunk 通过 applyMiddleware 执行一下!

);

const store = createStore(
  reducer,
 
  enhancer 
  
  
);  

export default store; 

返回页面查看效果(页面没报错,即 Redux-thunk 安装和配置成功): (07)Header 组件开发——⑦ AJAX 获取推荐数据 | React.js 项目实战:PC 端“简书”开发

3 mock “推荐”数据

有了 Redux-thunk,我们就可以在 action 中进行“异步”的操作了。

作为一个“前后端分离”的项目,在 AJAX 请求数据前,我们得自己 mock 一些数据辅助我们开发。

❓实际项目中应该怎样 mock 数据呢?

答:❗️假如一开始我们就和后端的伙伴约定好了此处需求的“接口”(如 /api/headerList.json ),此时我们就可以利用 Create-react-app 提供给我们的 public 目录来放置“模拟数据”的 api 文件夹。

当我们用 AJAX 请求路径为 /api/headerList.json 的数据文件时,Create-react-app 底层搭建的 Node 服务器会首先到“工程目录”下查看是否有对应的“路由”。

如果找不到(前后端联调前,肯定是找不到的),它就会去 public 目录下查找 api 目录下的 headerList.json 文件,并显示出来。

待前端整个项目开发结束,和后端进行项目联调时,那会儿后端已把真实的“数据接口”开发完毕,我们就只需要做一件事:将 public 目录下的 api 文件删除,程序自动就会去获取并显示真实的接口数据。

OK,既然知道了 mock 数据的方式,接下来我们就开始操作吧。

3️⃣-①:在项目的 public 目录下新增一个 api 文件夹,同时在文件夹下新增一个 headerList.json 文件; (07)Header 组件开发——⑦ AJAX 获取推荐数据 | React.js 项目实战:PC 端“简书”开发

3️⃣-②:编写 mock 数据 headerList.json 中的内容;

4 获取并显示数据

打开 header 目录下的 index.js 文件:

import React, {Component} from "react";

import {
  HeaderWrapper,
  Logo,
  
  Navbar,
  ItemList,
  LinkList,
  
  SearchArea,
  SearchInput,
  SearchPanel,
  PanelTitle,
  PanelChange,
  PanelLabels,
  LabelLink,
  
  Extra,
  ExtraLink
  
} from "./style";

import { connect } from "react-redux";

import {actionCreators} from "./store";


class Header extends Component {
  render() {
    return (
      <HeaderWrapper>
        <Logo>
          <img src="https://qdywxs.github.io/jianshu-images/logo.png" alt="logo" />
        </Logo>

        <Navbar className="clearfix">
          <ItemList className="active">
            <LinkList href="/">
              首页
            </LinkList>
          </ItemList>

          <ItemList>
            <LinkList href="/">
              下载APP
            </LinkList>           
          </ItemList>
        </Navbar>
      
        <SearchArea>
          {/*
           ❗️4️⃣-①:给 SearchInput 添加一个 onfocus 事件,
           当“聚焦”时,获取 AJAX 数据;
            */}
          <SearchInput
      			onFocus={this.props.handleInputFocus}
          />
      
          <span className="iconfont icon-search">&#xe63e;</span>
      
          <SearchPanel>
            <PanelTitle>
              热门搜索
      
              <PanelChange
                onMouseDown={this.props.handleMouseDown}
                onMouseUp={this.props.handleMouseUp}
              > 
                <span className={this.props.refresh ? "iconfont refresh" : "iconfont"}>&#xe65f;</span>

                换一批
              </PanelChange>
            </PanelTitle>
      
            <PanelLabels className="clearfix">
              <LabelLink href="/">
                区块链
              </LabelLink>
              <LabelLink href="/">
                故事
              </LabelLink>
              <LabelLink href="/">
                小程序
              </LabelLink>
              <LabelLink href="/">
                前端一万小时
              </LabelLink>
            </PanelLabels>
          </SearchPanel>
        </SearchArea>
      
      
        <Extra>
          <span className="iconfont icon-textsize" >&#xe739;</span>
          <ExtraLink className="login" href="/">
            登录
          </ExtraLink>
          <ExtraLink className="register" href="/">
            注册
          </ExtraLink> 
      
          <ExtraLink className="writing" href="/">
            <span className="iconfont icon-pen">&#xe600;</span>
            写文章
          </ExtraLink>     
        </Extra>
      </HeaderWrapper>
    )
  }

}

const mapStateToProps = (state) => { 
  return { 
    refresh: state.getIn(["header", "refresh"])
  }
}

const mapDispatchToProps = (dispatch) => {  
  return {
    handleMouseDown() { 
      const action = actionCreators.changeClassNameAction(); 
      dispatch(action)
    
    },

    handleMouseUp() {
      const action = actionCreators.resumeClassNameAction();
      dispatch(action)
    }, // ❗️注意这里的 , 不能少!
    
    
    /*
    4️⃣-②:Redux-thunk 中,“异步”代码我们是放在 action 中进行。
    这里我们仅作方法的“调用”;
     */
    handleInputFocus() {
      const action = actionCreators.initLabelAction();
      dispatch(action)
    }
  }
}

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

4️⃣-③:打开 header 目录下 store 中的 actionCreators.js 文件,定义 action:

import {CHANGE_CLASS_NAME, RESUME_CLASS_NAME} from "./actionTypes";

export const changeClassNameAction = () => ({
  type: CHANGE_CLASS_NAME
})

export const resumeClassNameAction = () => ({
  type: RESUME_CLASS_NAME
})

// 4️⃣-④:在 action 中添加 AJAX“异步”代码;
export const initLabelAction = () => {
  
}

4️⃣-⑤:先得安装 axios,才能进一步编写“异步”函数; (07)Header 组件开发——⑦ AJAX 获取推荐数据 | React.js 项目实战:PC 端“简书”开发

4️⃣-⑥:返回 header 目录下 store 中的 actionCreators.ja 文件,编写“异步”代码;

import {CHANGE_CLASS_NAME, RESUME_CLASS_NAME} from "./actionTypes";

// 4️⃣-⑦:引入 axios 模块;
import axios from "axios";

export const changeClassNameAction = () => ({
  type: CHANGE_CLASS_NAME
})

export const resumeClassNameAction = () => ({
  type: RESUME_CLASS_NAME
})

// 4️⃣-⑧:编写“异步”函数;
export const initLabelAction = () => {
  return(dispatch) => {
  	axios.get("/api/headerList.json")
    	.then((res) => {
    		const data = res.data;
        
        // 4️⃣-⑨:在控制台打印一下这个“数据”,看是否已获取到!
        console.log(data)
    	})
    	.catch(() => {alert("error")})
  }
}

返回页面控制台查看(数据的确获取到了,但最后几秒有 bug——重复点击,重复获取数据,这个 bug 稍后再解决,这里先记下): (07)Header 组件开发——⑦ AJAX 获取推荐数据 | React.js 项目实战:PC 端“简书”开发

4️⃣-⑩:既然 AJAX 能获取到数据,我们就可以利用这些数据了。打开 header 目录下 store 中的 reducer.js 文件,添加一个新数据 list: [] 来表示 PanelLabels 样式组件中的数据项;

import {CHANGE_CLASS_NAME, RESUME_CLASS_NAME} from "./actionTypes";

import {fromJS} from "immutable";

const defaultState = fromJS({  
  refresh: false,
  
  // ❗️添加一个新数据,初始值为空,其具体实际数值为 AJAX 获取到的“数据”!
  list: []
})

export default (state=defaultState, action) => {
  if(action.type === CHANGE_CLASS_NAME) {
    return state.set("refresh", true);     
  }
  
  if(action.type === RESUME_CLASS_NAME) {
    return state.set("refresh", false);   
  }
   
  return state;
}

4️⃣-⑪:用 AJAX 获取到的数据替换上一步中初始的“空数组”。又是“修改数据”的套路,那我们继续走 Redux 的工作流程;

4️⃣-⑫:打开 header 目录下 store 中的 actionTypes.js 文件:

export const CHANGE_CLASS_NAME = "change_class_name";
export const RESUME_CLASS_NAME ="resume_class_name";

export const CHANGE_LIST="change_list"; // ❗️定义好常量~

4️⃣-⑬:打开 header 目录下 store 中的 actionCreators.js 文件,编写 action:

// 4️⃣-⑭:先引入“常量”;
import {CHANGE_CLASS_NAME, RESUME_CLASS_NAME, CHANGE_LIST} from "./actionTypes";


import axios from "axios";

// 4️⃣-⑱:引入 fromJS 方法;
import {fromJS} from "immutable";

export const changeClassNameAction = () => ({
  type: CHANGE_CLASS_NAME
})

export const resumeClassNameAction = () => ({
  type: RESUME_CLASS_NAME
})

// 4️⃣-⑯:在这里定义 action;
const changeListAction = (data) => ({
  type: CHANGE_LIST,
  
  /*
  ❗️❗️❗️4️⃣-⑰:这里请一定注意,这里的 data 是从“接口”获取到的“数组”对象,
  它是一个“JS 对象”。
  但在上边的第“4️⃣-⑩”步中,list 数据项被 fromJS 修改成了“immutable 对象”,
  因此,这里也应该将 data 转换为“immutable 对象”!
   */
  /*
  4️⃣-⑲:将 data 转化为“immutable 对象”~
  data: data  
   */
  data: fromJS(data)
})

export const initLabelAction = () => {
  return(dispatch) => {
    axios.get("/api/headerList.json")
      .then((res) => {
        const data = res.data;

        // 4️⃣-⑮:获取到数据后,需要去替换初始的空数组;
        const action = changeListAction(data.data);
      
        dispatch(action) // ❗️将这个 action 发送给 reducer!
      })
      .catch(() => {alert("error")})
  }
}

4️⃣-⑳:打开 header 目录下 store 中的 reducer.js 文件:

// 4️⃣-㉑:先引入“常量”;
import {CHANGE_CLASS_NAME, RESUME_CLASS_NAME, CHANGE_LIST} from "./actionTypes";

import {fromJS} from "immutable";

const defaultState = fromJS({  /*❗️*/
  refresh: false,
  
  list: []
})

export default (state=defaultState, action) => {
  if(action.type === CHANGE_CLASS_NAME) {
    return state.set("refresh", true); 
  }
  
  if(action.type === RESUME_CLASS_NAME) {
    return state.set("refresh", false); 
  }
  
  // 4️⃣-㉒:编写替换“数据”的逻辑;
  if(action.type === CHANGE_LIST) {
    return state.set("list", action.data)
  }
   
  return state;
}

返回页面控值台查看(list 的数据项已经被替换了): (07)Header 组件开发——⑦ AJAX 获取推荐数据 | React.js 项目实战:PC 端“简书”开发

OK,既然“数据”项 list 已成功被替换,接下来就好办了,我们只需要将 PanelLabels 样式组件里的内容,用 list 替换掉就可以了。

5️⃣打开 header 目录下的 index.js 文件:

import React, {Component} from "react";

import {
  HeaderWrapper,
  Logo,
  
  Navbar,
  ItemList,
  LinkList,
  
  SearchArea,
  SearchInput,
  SearchPanel,
  PanelTitle,
  PanelChange,
  PanelLabels,
  LabelLink,
  
  Extra,
  ExtraLink
  
} from "./style";

import { connect } from "react-redux";

import {actionCreators} from "./store";


class Header extends Component {
  render() {
    return (
      <HeaderWrapper>
        <Logo>
          <img src="https://qdywxs.github.io/jianshu-images/logo.png" alt="logo" />
        </Logo>

        <Navbar className="clearfix">
          <ItemList className="active">
            <LinkList href="/">
              首页
            </LinkList>
          </ItemList>

          <ItemList>
            <LinkList href="/">
              下载APP
            </LinkList>           
          </ItemList>
        </Navbar>
      
        <SearchArea>

          <SearchInput
            onFocus={this.props.handleInputFocus}
          />
      
          <span className="iconfont icon-search">&#xe63e;</span>
      
          <SearchPanel>
            <PanelTitle>
              热门搜索
      
              <PanelChange
                onMouseDown={this.props.handleMouseDown}
                onMouseUp={this.props.handleMouseUp}
              > 
                <span className={this.props.refresh ? "iconfont refresh" : "iconfont"}>&#xe65f;</span>

                换一批
              </PanelChange>
            </PanelTitle>
      
            <PanelLabels className="clearfix">
              {/*
               5️⃣-②:将这些写死的“数据”删除掉~
              <LabelLink href="/">
                区块链
              </LabelLink>
              <LabelLink href="/">
                故事
              </LabelLink>
              <LabelLink href="/">
                小程序
              </LabelLink>
              <LabelLink href="/">
                前端一万小时
              </LabelLink>
                */}
              {/*
               5️⃣-③:替换为“数据项 list”中的内容。❗️注意:虽然 list 
               是“immutable 对象”,但 immutable 依然给我们提供了
               一样功能的 map 方法;
                */}
              {
                this.props.list.map((item) => {
                  return <LabelLink key={item} href="/">{item}</LabelLink>
                })
              }
            </PanelLabels>
          </SearchPanel>
        </SearchArea>
      
      
        <Extra>
          <span className="iconfont icon-textsize" >&#xe739;</span>
          <ExtraLink className="login" href="/">
            登录
          </ExtraLink>
          <ExtraLink className="register" href="/">
            注册
          </ExtraLink> 
      
          <ExtraLink className="writing" href="/">
            <span className="iconfont icon-pen">&#xe600;</span>
            写文章
          </ExtraLink>     
        </Extra>
      </HeaderWrapper>
    )
  }

}

const mapStateToProps = (state) => { 
  return { 
    refresh: state.getIn(["header", "refresh"]),
    
    // ❗️5️⃣-①:从 header 下取得 list 数据;
    list: state.getIn(["header", "list"])
  }
}

const mapDispatchToProps = (dispatch) => {  
  return {
    handleMouseDown() { 
      const action = actionCreators.changeClassNameAction(); 
      dispatch(action)
    
    },

    handleMouseUp() {
      const action = actionCreators.resumeClassNameAction();
      dispatch(action)
    },
    
    
    handleInputFocus() {
      const action = actionCreators.initLabelAction();
      dispatch(action)
    }
  }
}

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

返回页面查看效果(“数据”正常显示,只是视频的最后几秒的 bug 依然没解决): (07)Header 组件开发——⑦ AJAX 获取推荐数据 | React.js 项目实战:PC 端“简书”开发

5 避免无意义的请求发送

既然需求都实现了,我们现在就可以来解决一下“重复发送请求”的 bug。

6️⃣打开 header 目录下的 index.js 文件:

import React, {Component} from "react";

import {
  HeaderWrapper,
  Logo,
  
  Navbar,
  ItemList,
  LinkList,
  
  SearchArea,
  SearchInput,
  SearchPanel,
  PanelTitle,
  PanelChange,
  PanelLabels,
  LabelLink,
  
  Extra,
  ExtraLink
  
} from "./style";

import { connect } from "react-redux";

import {actionCreators} from "./store";


class Header extends Component {
  render() {
    return (
      <HeaderWrapper>
        <Logo>
          <img src="https://qdywxs.github.io/jianshu-images/logo.png" alt="logo" />
        </Logo>

        <Navbar className="clearfix">
          <ItemList className="active">
            <LinkList href="/">
              首页
            </LinkList>
          </ItemList>

          <ItemList>
            <LinkList href="/">
              下载APP
            </LinkList>           
          </ItemList>
        </Navbar>
      
        <SearchArea>

          <SearchInput
            onFocus={() => this.props.handleInputFocus(this.props.list)}
          /> {/*
            	❗️❗️❗️6️⃣-①:在给元素绑定 onfocus 事件时,我们可以同时给事件方法
              传递一个 this.props.list 参数;
               
              onFocus={this.props.handleInputFocus}
               */}
      
          <span className="iconfont icon-search">&#xe63e;</span>
      
          <SearchPanel>
            <PanelTitle>
              热门搜索
      
              <PanelChange
                onMouseDown={this.props.handleMouseDown}
                onMouseUp={this.props.handleMouseUp}
              > 
                <span className={this.props.refresh ? "iconfont refresh" : "iconfont"}>&#xe65f;</span>

                换一批
              </PanelChange>
            </PanelTitle>
      
            <PanelLabels className="clearfix">
              {
                this.props.list.map((item) => {
                  return <LabelLink key={item} href="/">{item}</LabelLink>
                })
              }
            </PanelLabels>
          </SearchPanel>
        </SearchArea>
      
      
        <Extra>
          <span className="iconfont icon-textsize" >&#xe739;</span>
          <ExtraLink className="login" href="/">
            登录
          </ExtraLink>
          <ExtraLink className="register" href="/">
            注册
          </ExtraLink> 
      
          <ExtraLink className="writing" href="/">
            <span className="iconfont icon-pen">&#xe600;</span>
            写文章
          </ExtraLink>     
        </Extra>
      </HeaderWrapper>
    )
  }

}

const mapStateToProps = (state) => { 
  return { 
    refresh: state.getIn(["header", "refresh"]),
    
    list: state.getIn(["header", "list"])
  }
}

const mapDispatchToProps = (dispatch) => {  
  return {
    handleMouseDown() { 
      const action = actionCreators.changeClassNameAction(); 
      dispatch(action)
    
    },

    handleMouseUp() {
      const action = actionCreators.resumeClassNameAction();
      dispatch(action)
    },
    
    
    handleInputFocus(list) { // 6️⃣-②:注意在这里接收 list;
      
      // ❗️6️⃣-③:我们可以打印一下这个 list 都有些什么东西;
      console.log(list);
      
      const action = actionCreators.initLabelAction();
      dispatch(action)
    }
  }
}

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

返回控制台查看(除了第一次请求时,list 的 size 为 0,其他都是 50): (07)Header 组件开发——⑦ AJAX 获取推荐数据 | React.js 项目实战:PC 端“简书”开发

🚀利用这一特点,我们可以用 size 作一个判断(仅当 size === 0 时,我们才发送 AJAX 请求。即,没“数据”的时候才请求“数据”,有“数据”后就不要再请求了)。返回 header 目录下的 index.js 文件:

import React, {Component} from "react";

import {
  HeaderWrapper,
  Logo,
  
  Navbar,
  ItemList,
  LinkList,
  
  SearchArea,
  SearchInput,
  SearchPanel,
  PanelTitle,
  PanelChange,
  PanelLabels,
  LabelLink,
  
  Extra,
  ExtraLink
  
} from "./style";

import { connect } from "react-redux";

import {actionCreators} from "./store";


class Header extends Component {
  render() {
    return (
      <HeaderWrapper>
        <Logo>
          <img src="https://qdywxs.github.io/jianshu-images/logo.png" alt="logo" />
        </Logo>

        <Navbar className="clearfix">
          <ItemList className="active">
            <LinkList href="/">
              首页
            </LinkList>
          </ItemList>

          <ItemList>
            <LinkList href="/">
              下载APP
            </LinkList>           
          </ItemList>
        </Navbar>
      
        <SearchArea>

          <SearchInput
            onFocus={() => this.props.handleInputFocus(this.props.list)}
          />
      
          <span className="iconfont icon-search">&#xe63e;</span>
      
          <SearchPanel>
            <PanelTitle>
              热门搜索
      
              <PanelChange
                onMouseDown={this.props.handleMouseDown}
                onMouseUp={this.props.handleMouseUp}
              > 
                <span className={this.props.refresh ? "iconfont refresh" : "iconfont"}>&#xe65f;</span>

                换一批
              </PanelChange>
            </PanelTitle>
      
            <PanelLabels className="clearfix">
              {
                this.props.list.map((item) => {
                  return <LabelLink key={item} href="/">{item}</LabelLink>
                })
              }
            </PanelLabels>
          </SearchPanel>
        </SearchArea>
      
      
        <Extra>
          <span className="iconfont icon-textsize" >&#xe739;</span>
          <ExtraLink className="login" href="/">
            登录
          </ExtraLink>
          <ExtraLink className="register" href="/">
            注册
          </ExtraLink> 
      
          <ExtraLink className="writing" href="/">
            <span className="iconfont icon-pen">&#xe600;</span>
            写文章
          </ExtraLink>     
        </Extra>
      </HeaderWrapper>
    )
  }

}

const mapStateToProps = (state) => { 
  return { 
    refresh: state.getIn(["header", "refresh"]),
    
    list: state.getIn(["header", "list"])
  }
}

const mapDispatchToProps = (dispatch) => {  
  return {
    handleMouseDown() { 
      const action = actionCreators.changeClassNameAction(); 
      dispatch(action)
    
    },

    handleMouseUp() {
      const action = actionCreators.resumeClassNameAction();
      dispatch(action)
    },
    
    
    handleInputFocus(list) {  
      if(list.size === 0) { // ❗️6️⃣-④:仅当 size === 0 时,我们才发送 AJAX 请求!
        const action = actionCreators.initLabelAction();
        dispatch(action)
      }
    }
  }
}

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

返回页面查看效果(一切正常显示,且 AJAX 请求只发送了一次): (07)Header 组件开发——⑦ AJAX 获取推荐数据 | React.js 项目实战:PC 端“简书”开发

祝好,qdywxs ♥ you!