(18)登录页开发——② “登录”功能实现 | React.js 项目实战:PC 端“简书”开发

4,460 阅读14分钟
转载请注明出处,未经同意,不可修改文章内容。

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


1 “登录”功能需求分析

  • 需求 1:点击“登录”后,跳转至“登录页”;

18-01.png

  • 需求 2:实现“登录”和“登录验证”的逻辑;

18-02.png

  • 需求 3:点击“登录”按钮,当“登录验证”成功后,整个页面跳转至“首页”;
  • 需求 4:且同时,“登录”显示为“退出”;

18-03.png

  • 需求 5:在登录状态下,点击“退出”,将退出登录状态,并将“退出”显示为“登录”。

18-04.png

2 需求实现

2.1 基本架子搭建

拿到“登录页”,我们首先要考虑它有哪些“数据”。因为,对于一个以“数据”驱动的框架来说,“数据”是我们任何时候都要关注的重点。

1️⃣打开 src 目录 pages 下的文件夹 login ,login 下会有很多只属于 Login 组件的“数据”(至少会有一个“登录”与否的状态—— login: true/false ),我们就可以将这些“数据”放在 login 里进行管理。 在 login 目录下新建一个文件夹 store18-05.png

2️⃣紧接着,在 store 文件夹下创建一个 index.js 文件,它的作用为——❗️它是这整个 store 的“出口”文件(“出口”文件的好处为:它可以简化引用文件时的“路径”,也便于后期维护~): 18-06.png

3️⃣在 store 文件夹下创建一个 reducer.js 文件,让 reducer.js 来管理 login 下的“数据”: 18-07.png

4️⃣同理,在 store 文件夹下创建 actionTypes.jsactionCreators.js 文件备用: 18-08.png

小架子搭起后,我们按流程编写各文件里的代码。

5️⃣编写 reducer.js 中的代码: 5️⃣-①: reducer.js 返回一个“函数”;

import {fromJS} from "immutable"; /*
																	❗️从 immutable 中引入 fromJS 方法,这个“方法”可以
                                  帮我们把一个“JS 对象”转化为一个“immutable 对象”!
                            			 */

const defaultState = fromJS(

);

export default (state=defaultState, action) => {
  
  return state;
}

5️⃣-②:将 Login 组件的“登录状态”( truefalse )这个“数据”放到 reducer.js 中进行管理;

import {fromJS} from "immutable";  

const defaultState = fromJS({
	login: false // ❗️初始为“未登录”!
});

export default (state=defaultState, action) => {
  
  return state;
}

5️⃣-③:打开 login 目录下 store 中的“出口”文件 index.js ,定义一些通用的需要传递出去的东西;

import reducer from "./reducer";
import * as actionTypes from "./actionTypes";
import * as actionCreators from "./actionCreators";

export {reducer, actionTypes, actionCreators};

❗️5️⃣-④:返回 src 目录下 store 中的 reducer.js 文件,引入上一步创建的 reducer;

import {combineReducers} from "redux-immutable";

import {reducer as headerReducer} from "../common/header/store"; 

import {reducer as homeReducer} from "../pages/home/store";

import {reducer as detailReducer} from "../pages/detail/store";  

import {reducer as loginReducer} from "../pages/login/store"; // ❗️❗️❗️

const reducer = combineReducers({ 
  header: headerReducer,
  home: homeReducer,
  detail: detailReducer,
  
  login: loginReducer // ❗️❗️❗️
})


export default reducer;

返回页面控制台查看是否有“数据”了(“数据”已存在): 18-09.gif

6️⃣OK,通过以上步骤,store 里边就有“数据”了。按照 Redux 和 React-redux 的工作流程,一旦有了数据,Login 组件就可以连接 store,去 store 里边取数据并进行后续一系列的逻辑操作了!

6️⃣-①:按照流程,这一步应该是让“Login 组件”拥有获取 store 中数据的“能力”。但这一步,我们在《登录页开发——① 登录页面布局》中已经实现了——我们已经把 Login 组件放到了 Provider 组件之中。

打开 src 目录下的 App.js 文件查看:

import React, { Component } from "react";

import {GlobalStyle} from "./style";

import {GlobalIconStyle} from "./statics/iconfont/iconfont";

import {BrowserRouter, Route} from "react-router-dom";

import Header from "./common/header";

import Home from "./pages/home";
import Detail from "./pages/detail";


import Login from "./pages/login";


import { Provider } from "react-redux";

import store from "./store";

class App extends Component  {  
  render() {  
    return (
      <div>
        <GlobalStyle />
        <GlobalIconStyle />
 
        <Provider store={store}>             
          <BrowserRouter>
            <div>
              <Header />
              <Route path="/" exact component={Home}></Route>
              <Route path="/detail/:id" exact component={Detail}></Route>

              <Route path="/login" exact component={Login}></Route> {/* ❗️❗️❗️ */}

            </div>
          </BrowserRouter>
        </Provider>

      </div>
    );
  }
}

export default App; 

OK,“能力”也有了,接下来我们就开始挨个实现上边提的需求!

2.2 需求 1——点击“登录”后,跳转至“登录页”

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

import React, {Component} from "react";

import {Link} from "react-router-dom";  

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 {
  
  getPanels() {
    const newList = this.props.list.toJS();
    const pageLabels = [];  
    
    if(newList.length) {  
      for(let i=(this.props.page - 1)*10; i<this.props.page*10; i++) { 
        pageLabels.push(  
          <Link to="/"  key={newList[i]}>  
            <LabelLink>  
              {newList[i]} 
            </LabelLink>
          </Link>
        )
      }
      return pageLabels; 
    }
  } 
  
  render() {
    return (
      <HeaderWrapper>

        <Link to="/">  
          <Logo>
            <img src="https://qdywxs.github.io/jianshu-images/logo.png" alt="logo" />
          </Logo>
        </Link>

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

          <ItemList>
            <Link to="/">  
              <LinkList>
                下载APP
              </LinkList>  
            </Link>         
          </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}

                onClick={() => this.props.handleChangePage(this.props.page, this.props.totalPage)}
              >  
                  
                <span className={this.props.refresh ? "iconfont refresh" : "iconfont"}>&#xe65f;</span>
                换一批
              </PanelChange>
            </PanelTitle>
      
            <PanelLabels className="clearfix">
              {this.getPanels()}
            </PanelLabels>
          </SearchPanel>
        </SearchArea>
      
      
        <Extra>
          <span className="iconfont icon-textsize" >&#xe739;</span>

          {/*
           ❗️❗️❗️2️⃣将“登录”的 Link 标签的跳转“路径”改为 /login;
           先注释掉下面的写法~
           <Link to="/">
            */}
					<Link to="/login">
            
            <ExtraLink className="login">
              登录
            </ExtraLink>
          </Link>

          <Link to="/">  
            <ExtraLink className="register">
              注册
            </ExtraLink> 
          </Link>

          <Link to="/">  
            <ExtraLink className="writing">
              <span className="iconfont icon-pen">&#xe600;</span>
              写文章
            </ExtraLink>  
          </Link>  

        </Extra>
      </HeaderWrapper>
    )
  }
}

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

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

    handleMouseUp() {
      const action = actionCreators.resumeClassNameAction();
      dispatch(action)
    },
    
    
    handleInputFocus(list) {  
      if(list.size === 0) { 
        const action = actionCreators.initLabelAction();
        dispatch(action)
      }
    },
    
    handleChangePage(page, totalPage) {
      if(page < totalPage) {
        dispatch(actionCreators.changePageAction(page + 1))
      }else {
        dispatch(actionCreators.changePageAction(1))
      }
    }
  }
}

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

返回页面查看(点击“登录”时,成功跳转至 login 页): 18-10.gif

2.3 需求 2——实现“登录”和“登录验证”的逻辑

🏆“登录”的背后逻辑为:我们需要发送“异步”请求调用 AJAX 接口,将用户输入的“账户”和“密码”传递给后端,然后后端进行比对:

  • 一致,则可以登录;
  • 不一致,则“登录”失败。

1️⃣mock 数据——在项目 public 目录下 api 文件夹中新增一个 loginData.json 文件: 18-11.png

2️⃣编写 mock 数据 loginData.json 中的内容(❗️我们默认任何“账户”和“密码”都能登录成功):

{
  "success": true,
  "data": true
}

3️⃣打开 login 目录下的 index.js 文件:

❓❓❓我们需要思考一个问题——要做“登录验证”,即用用户输入的“账号”和“密码”来跟后端存储的“账号”和“密码”作比对。可怎样才能获取到用户输入的“账户”和“密码”呢?

答:
🔗前置知识:《React 进阶——⑤ React 中 ref 的使用》

想要获取用户的输入值,我们很容易能想到:

  1. 获取到“账号”和“密码”这两个 input 框的 DOM 节点;
  2. 有了 DOM 节点,我们就能很容易得到 input 框 value 值——用户输入的“账号”、“密码”。

3️⃣-①:按“前置知识”,我们应该以如下方式使用 ref 来获取到 input 框的 DOM 节点;

import React, {Component} from "react";


import {
  LoginWrapper,
  LoginBox,
  Input,
  Button
} from "./style"


class Login extends Component {
  render() {
    return(
      <LoginWrapper>
        <LoginBox>
          
      		{/*
           ❗️3️⃣-②:首先,给 input 元素增加一个 ref 属性,它等于一个“箭头函数”;
           “箭头函数”的格式为:
           3️⃣-②-1:箭头的左边,“箭头函数”会自动接收一个“参数”,名字可以随便取(我取名为 input);
           3️⃣-②-2:箭头的右边,是一个固定的写法 this.text = input,即“账号”等于 input;
           3️⃣-②-3:❗️❗️❗️这整行代码的意思为——我构建一个“引用”ref,这个“引用”叫做
           this.text,它指向 input 框对应的 DOM 节点。
          	*/}
          <Input type="text" placeholder="账号" ref={(input) => {this.text = input}} />
      		
          {/* ❗️3️⃣-③:同理,给“密码框”的 input 元素也增加一个 ref 属性! */}
          <Input type="password" placeholder="密码" ref={(input) => {this.password = input}}  />
      
          <Button type="submit">登陆</Button>
        </LoginBox>
      </LoginWrapper>
    )
  }
}

export default Login;

🏆如此一来, this.text.valuethis.password.value 就分别表示用户输入的“账号”和“密码”。

4️⃣既然拿到了用户的输入“值”,接下就又是发送“异步”请求、修改“数据”的“套路”了,继续在 login 目录下的 index.js 文件中编码:

import React, {Component} from "react";

import {
  LoginWrapper,
  LoginBox,
  Input,
  Button
} from "./style"

/*
4️⃣-①:从 react-redux 中引入 connect 方法(它也是 React-redux 的核心 API 之一),
connect 的目的很明确——就是“连接”的意思!
 */
import {connect} from "react-redux";

// ❗️❗️❗️引入 actionCreators!
import {actionCreators} from "./store";

class Login extends Component {
  render() {
    return(
      <LoginWrapper>
        <LoginBox>
          <Input type="text" placeholder="账号" ref={(input) => {this.text = input}} />
          <Input type="password" placeholder="密码" ref={(input) => {this.password = input}}  />
          
          {/*
           5️⃣给“登录”按钮绑定一个“点击”事件,
           ❓可这个“事件”应该怎样被调用呢?
            */}
          {/*
           5️⃣-②:因此,这里可以通过 this.props.handleLogin 来调用
           store 中的 handleLogin;
           ❗️❗️❗️同时,通过“箭头函数”将“登录”和“密码”对应的“DOM 节点”传递出去!
            */}
          <Button type="submit" onClick={() => this.props.handleLogin(this.text, this.password)}>登陆</Button>
        </LoginBox>
      </LoginWrapper>
    )
  }
}

/*
 ❗️❗️❗️4️⃣-⑤:接下来,我们定义哪些“用户的操作”
 应该当作 action,并传给 store;
 */
const mapDispatchToProps = (dispatch) => { /*
                                           4️⃣-⑥:把 store 里的“dispatch 方法”作为
                                           参数传递给 mapDispatchToProps;
                                            */
  return {
    handleLogin(accountElem, passwordElem) { /*
                                       ❗️5️⃣-①:在这里定义 handleLogin 会被当作 action 
                                       传给 store;
                                              */
                                       /*
                                       5️⃣-③:相应地,handleLogin 就会接收到
                                       两个参数,accountElem 和 passwordElem;
                                        */
      /*
      5️⃣-④:Redux-thunk 中,“异步”代码我们是放在 action 中进行。这里
      我们仅作方法的“调用”;
      ❗️❗️❗️当然,“参数”也需要传递过去!这里传递给后端验证的“参数”是
      用户在 input 框真正输入的“值”,故“参数”为 accountElem.value 
      和 passwordElem.value。
      
      ❗️注意一定要在本文件上边去引入 actionCreators!
       */
      const action = actionCreators.loginAction(accountElem.value, passwordElem.value);
      
      dispatch(action);
    }
  }

}


/*
❗️4️⃣-②:之前我们直接导出的是 Login,可用了 React-redux 后,就不能这样写了! 
export default Login;
 */

/*
4️⃣-③:取而代之,我们是导出 connect 方法(
❗️注意看我们给 connect 方法传递了哪些参数!); 
 */
export default connect(null, mapDispatchToProps)(Login); /*
                                           4️⃣-④:我们一共要给 connect 传递 3 个参数!
                                           Login 表示:connect 会让 “Login 组件”和store 
                                           进行“连接”;
                                           
                                           null 表示:这里还会接收一个名叫 
                                           mapStateToProps 的参数,由于这里暂时不需要去
                                           获取“数据”,故用 null 占位;
                                           
                                           mapDispatchToProps 表示:我们把 store 的
                                           dispatch 方法“挂载”到 Login 组件的 props 上。
                                           即,我们可以定义哪些“用户的操作”应该当作 action,
                                           并传给 store!
                                                         */

5️⃣-⑤:打开 login 目录下 store 中的 actionCreators.js 文件,定义这个 action;
🔗前置知识:《HTML——③ HTML 表单详解》——弄懂 getpost 的区别,以及当我们点“登录”这个按钮时,浏览器做了什么事情!

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

// 5️⃣-⑥:在 action 中添加 AJAX“异步”代码(❗️❗️❗️注意接收那两个“参数”);
export const loginAction = (account, password) => {
  
  // 5️⃣-⑧:编写“异步”函数进行“登录验证”(❗️❗️❗️既然是“验证”,我们就得给后端带几个“参数”过去);
  return(dispatch) => {
    axios.get("/api/loginData.json?account=" + account + "&password=" + password) 
    // ❗️❗️❗️当然,实际上是应该通过 POST 的方式向后端“传数据”,但这里为了演示的方便,我们选用 GET!
    
    	.then((res) => {
    		const result = res.data.data;
    	})
      .catch(() => {alert("error")})
  }
}

返回页面查看,输入“账号”和“密码”,点击“登录”,看看“验证”的情况(成功验证):

18-12.gif

6️⃣-①:打开 login 目录下 store 中的 actionTypes.js 文件;

// ❗️定义好常量~
export const CHANGE_LOGIN_DATA_ACTION = "change_login_data_action";

6️⃣-②:返回 login 目录下 store 中的 actionCreators.js 文件;

import axios from "axios";

// 6️⃣-③:先引入“常量”;
import {CHANGE_LOGIN_DATA_ACTION} from "./actionTypes";

// 6️⃣-⑥:在这里定义这个“同步”的 action;
const changeLoginDataAction = () => ({
  type: CHANGE_LOGIN_DATA_ACTION,
  login: true 
})

export const loginAction = (account, password) => {  
  return(dispatch) => {
    axios.get("/api/loginData.json?account=" + account + "&password=" + password) 
      .then((res) => {
        const result = res.data.data;
      
        // 6️⃣-④:获取到数据后,可以通过一个条件判断来适时修改 login 的值;
        if(result) { /*
                     6️⃣-⑤:从我们 mock 的数据来看,无论输入任何字符都可以登录成功。
                     所以走下边这个的流程;
                      */
          const action = changeLoginDataAction();
          
          dispatch(action); // 6️⃣-⑦:将这个 action 发送给 reducer!
          
        }else {
          alert("登录失败!")
        }
      })
      .catch(() => {alert("error")})
  }
}

7️⃣打开 login 目录下 store 中的 reducer.js 文件:

import {fromJS} from "immutable";  

// 7️⃣-①:先引入“常量”;
import {CHANGE_LOGIN_DATA_ACTION} from "./actionTypes";

const defaultState = fromJS({
  login: false // ❗️初始为“未登录”!
});

export default (state=defaultState, action) => {
  // 7️⃣-②:编写替换“数据”的逻辑;
  if(action.type === CHANGE_LOGIN_DATA_ACTION) {
    return state.set("login", action.login);
  }
  
  return state;
}

返回页面查看(“数据”确实变了——用户输入“账号”和“密码”,然后点击“登录”后,数据 login 的值从 false 变为了 true ): 18-13.gif

2.4 “需求 3”和“需求 4”——成功验证后,进行“页面”的“状态”的相应跳转

2.4.1 需求 3——点击“登录”按钮,当“登录验证”成功后,整个页面跳转至“首页”

🏆需求分析:怎么调回至“首页”呢?

  1. 在 login 目录下的 index.js 中,通过 mapStateToProps 将 store 里的“数据”映射到 Login 组件的 props 上;
  2. 接着,在 Login 组件的“render 函数” 作一个条件判断——当“登录”状态为 false 时,才渲染相关“样式组件”;否则(“登录”状态为 true 时),跳转至“首页”。

1️⃣打开 login 目录下的 index.js 文件:

import React, {Component} from "react";

import {
  LoginWrapper,
  LoginBox,
  Input,
  Button
} from "./style"

import {connect} from "react-redux";

import {actionCreators} from "./store";

/*
❗️❗️❗️1️⃣-⑨:幸运的是,react-router-dom 为我们提供了一个“组件”——Redirect,
它可以直接“重定向”至目的页;
 */
import {Redirect} from "react-router-dom";


class Login extends Component {
  render() {
    
    /*
    ❗️❗️❗️1️⃣-⑤:“映射”上了过后,我们就可以利用 this.props.login 来做一个
    条件判断;
    注释掉下面的代码~
    return(
      <LoginWrapper>
        <LoginBox>
          <Input type="text" placeholder="账号" ref={(input) => {this.text = input}} />
          <Input type="password" placeholder="密码" ref={(input) => {this.password = input}}  />
          
          <Button type="submit" onClick={() => this.props.handleLogin(this.text, this.password)}>登陆</Button>
        </LoginBox>
      </LoginWrapper>
    )
     */
    if(!this.props.login) { // ❗️❗️❗️1️⃣-⑥:当“未登录”(未通过验证)时,渲染以下“样式组件”;
      return(
        <LoginWrapper>
          <LoginBox>
            <Input type="text" placeholder="账号" ref={(input) => {this.text = input}} />
            <Input type="password" placeholder="密码" ref={(input) => {this.password = input}}  />
            <Button type="submit" onClick={() => this.props.handleLogin(this.text, this.password)}>登陆</Button>
          </LoginBox>
        </LoginWrapper>
      ) 
    }else { // ❗️1️⃣-⑦:否则执行以下逻辑(重定向到“首页”);
      // ❓1️⃣-⑧:怎样“重定向”到首页呢?
      
      // 1️⃣-⑩:我们用“Redirect 组件”来“重定向”至“首页”;
      return <Redirect to="/" />
    }
  }
}

// 1️⃣-②:接下来,我们先定义“连接”的规则;
const mapStateToProps = (state) => ({ /*
                                      1️⃣-③:把 store 里的“数据 state”作为“参数”
                                      传递给 mapStateToProps;
                                       */
  
  login: state.getIn(["login", "login"]) /*
                                         ❗️1️⃣-④:规则的具体做法为——将 store 里
                                         的 login 映射到“Login 组件”里的 props
                                         的 login 中去;
                                          */
})


const mapDispatchToProps = (dispatch) => {
  return {
    handleLogin(accountElem, passwordElem) { 
      const action = actionCreators.loginAction(accountElem.value, passwordElem.value);
      dispatch(action);
    }
  }
}


/*
❗️1️⃣-①:给 connect 传递另一个重要“参数”mapStateToProps。
根据上边一系列的操作,Login 组件已经有“能力”取得 store 中的“数据”,
有“能力”后,Login 组件就可以去“连接”store,并从 store 中去取“数据”了。

但“连接”是需要“规则”的,而具体的“规则”就在这个 mapStateToProps 里面(❗️直译为:把 store 里
的“数据 state”映射到“Login 组件”的 props 里)!
注释掉下面这行代码~
export default connect(null, mapDispatchToProps)(Login); 
 */
export default connect(mapStateToProps, mapDispatchToProps)(Login);

返回页面查看(需求得以正确实现): 18-14.gif

2.4.2 需求 4——且同时,“Header 组件”里的“登录”显示为“退出”

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

import React, {Component} from "react";

import {Link} from "react-router-dom";  

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 {
  
  getPanels() {
    const newList = this.props.list.toJS();
    const pageLabels = [];  
    
    if(newList.length) {  
      for(let i=(this.props.page - 1)*10; i<this.props.page*10; i++) { 
        pageLabels.push(  
          <Link to="/"  key={newList[i]}>  
            <LabelLink>  
              {newList[i]} 
            </LabelLink>
          </Link>
        )
      }
      return pageLabels; 
    }
  } 
  
  render() {
    return (
      <HeaderWrapper>

        <Link to="/">  
          <Logo>
            <img src="https://qdywxs.github.io/jianshu-images/logo.png" alt="logo" />
          </Logo>
        </Link>

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

          <ItemList>
            <Link to="/">  
              <LinkList>
                下载APP
              </LinkList>  
            </Link>         
          </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}

                onClick={() => this.props.handleChangePage(this.props.page, this.props.totalPage)}
              >  
                  
                <span className={this.props.refresh ? "iconfont refresh" : "iconfont"}>&#xe65f;</span>
                换一批
              </PanelChange>
            </PanelTitle>
      
            <PanelLabels className="clearfix">
              {this.getPanels()}
            </PanelLabels>
          </SearchPanel>
        </SearchArea>
      
      
        <Extra>
          <span className="iconfont icon-textsize" >&#xe739;</span>
          
          {/*
           ❗️2️⃣-②:通过“三元运算符”的方式,作一个判断——当 this.props.login 为 true 时(
           即,“登录状态”),显示“退出”;否则(未登录时),显示“登录”!
            */}
          {this.props.login ? <ExtraLink className="login">退出</ExtraLink> : 
            <Link to="/login">
              <ExtraLink className="login">
                登录
              </ExtraLink>
            </Link>
          }

          <Link to="/">  
            <ExtraLink className="register">
              注册
            </ExtraLink> 
          </Link>

          <Link to="/">  
            <ExtraLink className="writing">
              <span className="iconfont icon-pen">&#xe600;</span>
              写文章
            </ExtraLink>  
          </Link>  

        </Extra>
      </HeaderWrapper>
    )
  }
}

const mapStateToProps = (state) => { 
  return { 
    refresh: state.getIn(["header", "refresh"]),
    list: state.getIn(["header", "list"]),
    
    page: state.getIn(["header", "page"]),
    totalPage: state.getIn(["header", "totalPage"]),
    
    /*
    ❗️❗️❗️2️⃣-①:由于 Header 组件早就和 store 建立好了“连接”,
    这里我们就可以直接从 login 里取到“数据”项 login,并“映射”
    到 Header 组件里的 props 的 login 中去;
     */
    login: state.getIn(["login", "login"])
    
  }
}

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

    handleMouseUp() {
      const action = actionCreators.resumeClassNameAction();
      dispatch(action)
    },
    
    
    handleInputFocus(list) {  
      if(list.size === 0) { 
        const action = actionCreators.initLabelAction();
        dispatch(action)
      }
    },
    
    handleChangePage(page, totalPage) {
      if(page < totalPage) {
        dispatch(actionCreators.changePageAction(page + 1))
      }else {
        dispatch(actionCreators.changePageAction(1))
      }
    }
  }
}

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

返回页面查看(“退出”正常显示): 18-15.gif

2.5 需求 5——在登录状态下,点击“退出”,将退出登录状态,并将“退出”显示为“登录”

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

import React, {Component} from "react";

import {Link} from "react-router-dom";  

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";

// ❗️❗️❗️1️⃣-⑤:从正确的位置引入 actionCreators!
import {actionCreators as loginActionCreators} from "../../pages/login/store";

class Header extends Component {
  
  getPanels() {
    const newList = this.props.list.toJS();
    const pageLabels = [];  
    
    if(newList.length) {  
      for(let i=(this.props.page - 1)*10; i<this.props.page*10; i++) { 
        pageLabels.push(  
          <Link to="/"  key={newList[i]}>  
            <LabelLink>  
              {newList[i]} 
            </LabelLink>
          </Link>
        )
      }
      return pageLabels; 
    }
  } 
  
  render() {
    return (
      <HeaderWrapper>

        <Link to="/">  
          <Logo>
            <img src="https://qdywxs.github.io/jianshu-images/logo.png" alt="logo" />
          </Logo>
        </Link>

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

          <ItemList>
            <Link to="/">  
              <LinkList>
                下载APP
              </LinkList>  
            </Link>         
          </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}

                onClick={() => this.props.handleChangePage(this.props.page, this.props.totalPage)}
              >  
                  
                <span className={this.props.refresh ? "iconfont refresh" : "iconfont"}>&#xe65f;</span>
                换一批
              </PanelChange>
            </PanelTitle>
      
            <PanelLabels className="clearfix">
              {this.getPanels()}
            </PanelLabels>
          </SearchPanel>
        </SearchArea>
      
      
        <Extra>
          <span className="iconfont icon-textsize" >&#xe739;</span>
          
          {/*
           ❗️1️⃣-①:给“退出”按钮绑定一个“点击”事件,
           ❓可这个“事件”应该怎样被调用呢?
            */}
					{/*
           1️⃣-③:因此,这里可以通过 this.props.logout 来
           调用 store 中的 logout;
            */}
          {this.props.login ? <ExtraLink className="login" onClick={this.props.logout}>退出</ExtraLink> : 
            <Link to="/login">
              <ExtraLink className="login">
                登录
              </ExtraLink>
            </Link>
          }

          <Link to="/">  
            <ExtraLink className="register">
              注册
            </ExtraLink> 
          </Link>

          <Link to="/">  
            <ExtraLink className="writing">
              <span className="iconfont icon-pen">&#xe600;</span>
              写文章
            </ExtraLink>  
          </Link>  

        </Extra>
      </HeaderWrapper>
    )
  }
}

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

    login: state.getIn(["login", "login"])
    
  }
}

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

    handleMouseUp() {
      const action = actionCreators.resumeClassNameAction();
      dispatch(action)
    },
    
    
    handleInputFocus(list) {  
      if(list.size === 0) { 
        const action = actionCreators.initLabelAction();
        dispatch(action)
      }
    },
    
    handleChangePage(page, totalPage) {
      if(page < totalPage) {
        dispatch(actionCreators.changePageAction(page + 1))
      }else {
        dispatch(actionCreators.changePageAction(1))
      }
    },
    
    // ❗️❗️❗️1️⃣-②:在这里定义 logout 会被当作 action 传给 store;
    logout() {
    	/*
      ❓1️⃣-④:接下来,我们需要在这里“调用” actionCreators 里定义的 action,但由于这个
      action 是去改变 Login 组件里的“数据”(login 值),所以我们要从 login 页面里的
      actionCreators 里引入 action!
       */
      
      // 1️⃣-⑥:调用 loginActionCreators 里的 logout;
      const action = loginActionCreators.logout();
      dispatch(action);
    }
  }
}

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

2️⃣请记得去 login 目录下 store 中的 actionTypes.jsactionCreators.js 文件中分别定义“常量”和 action:

  • actionTypes.js
export const CHANGE_LOGIN_DATA_ACTION = "change_login_data_action";

// ❗️定义好常量~
export const LOGOUT = "logout";
  • actionCreators.js
import axios from "axios";

// 2️⃣-①:先引入“常量”;
import {CHANGE_LOGIN_DATA_ACTION, LOGOUT} from "./actionTypes";

const changeLoginDataAction = () => ({
  type: CHANGE_LOGIN_DATA_ACTION,
  login: true 
})

export const loginAction = (account, password) => {  
  return(dispatch) => {
    axios.get("/api/loginData.json?account=" + account + "&password=" + password) 
      .then((res) => {
        const result = res.data.data;
        if(result) { 
          const action = changeLoginDataAction();
          dispatch(action);   
        }else {
          alert("登录失败!")
        }
      })
      .catch(() => {alert("error")})
  }
}

// 2️⃣-②:定义 action;
export const logout = () => ({
  type: LOGOUT,
  login: false
})

3️⃣打开 login 目录下 store 中的 reducer.js 文件:

import {fromJS} from "immutable";  

// 3️⃣-①:先引入“常量”;
import {CHANGE_LOGIN_DATA_ACTION, LOGOUT} from "./actionTypes";

const defaultState = fromJS({
  login: false // ❗️初始为“未登录”!
});

export default (state=defaultState, action) => {
  if(action.type === CHANGE_LOGIN_DATA_ACTION) {
    return state.set("login", action.login);
  };
  
  if(action.type === LOGOUT) {
  	// 3️⃣-②:编写替换“数据”的逻辑;
    return state.set("login", action.login);
  }
  
  return state;
}

返回页面查看(“退出”功能正常运行): 18-16.gif

本篇篇幅较长,但主要还是跑“流程”,本篇一定要掌握,因为 React 编程的“套路”都在这里边运用上了。

祝好,qdywxs ♥ you!