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

复制代码
转载请注明出处,未经同意,不可修改文章内容。

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

1 需求分析

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

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

我们可以看下"简书"官网在这一块的效果展示(当点击 input"搜索框"时,它会发一个"请求 trending_search "(且只在第一次点击时会发送这个"请求"),其"返回值"和 PanelLabels 里的值是一一对应的): 所以,我们会用到 AJAX 来获取"异步数据",并予以展示。

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

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

2 安装和配置 Redux-thunk

1️⃣安装 Redux-thunk 并重新启动:

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

javascript 复制代码
/*
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 安装和配置成功):

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 文件;

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

json 复制代码
{
  "success": true,
  "data": ["公众号 | 前端一万小时","原生 JS","Vue","React.js","JavaScript","Oli","蚂蚁金服","阿里","qdywxs","olizhao","语雀","Matplotlib","ggplot","知乎:前端一万小时","2020 前端面试","Oliver","Theano","SciPy","PyTorch","Plotly","公众号:前端一万小时","Keras","Gensim","Vue 云笔记","React 简书","掘金:itsOli","JS 入门","JS 初识","JS 进阶","OliZhao","ID:qdywxs","mock 数据","前","端","一","万","小","时","Oli-Zhao","Jobs","欢","迎","加","入","语","雀","私","有","库","加油!"]
}

4 获取并显示数据

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

jsx 复制代码
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:

javascript 复制代码
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,才能进一步编写"异步"函数;

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

javascript 复制代码
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 稍后再解决,这里先记下):

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

javascript 复制代码
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 文件:

javascript 复制代码
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:

javascript 复制代码
// 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 文件:

javascript 复制代码
// 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 的数据项已经被替换了):

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

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

jsx 复制代码
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 依然没解决):

5 避免无意义的请求发送

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

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

jsx 复制代码
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):

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

jsx 复制代码
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 请求只发送了一次):

祝好,qdywxs ♥ you!

相关推荐
hongkid8 分钟前
React Native 如何打包正式apk
javascript·react native·react.js
李少兄11 分钟前
简单讲讲 SVG:前端开发中的矢量图形
前端·svg
前端小万12 分钟前
告别 CJS 库加载兼容坑
前端·前端工程化
恋猫de小郭12 分钟前
Flutter 3.38.1 之后,因为某些框架低级错误导致提交 Store 被拒
android·前端·flutter
JarvanMo16 分钟前
Flutter 需要 Hooks 吗?
前端
光影少年26 分钟前
前端如何虚拟列表优化?
前端·react native·react.js
Moment28 分钟前
一杯茶时间带你基于 Yjs 和 reactflow 构建协同流程图编辑器 😍😍😍
前端·后端·面试
菩提祖师_42 分钟前
量子机器学习在时间序列预测中的应用
开发语言·javascript·爬虫·flutter
invicinble1 小时前
对于前端数据的生命周期的认识
前端
PieroPc1 小时前
用FastAPI 后端 和 HTML/CSS/JavaScript 前端写一个博客系统 例
前端·html·fastapi