只需cv,实战中学习next | 集成redux -- 实现登陆逻辑

168 阅读5分钟

王志远,微医前端技术部

前言

数据流至关重要,不然我们的数据处理只能停留单页面级别,无法在应用级进行扩展。简单举个例子,用户登录后,用户头像需要放在头部,每个页面都需要用到,这时就需要数据共享了。我们刚刚实现了用户的增删改查,现在就来实现登陆吧。

这里我们选择用redux进行实现。

开始实现

实现思路

  • 实现登陆页:登陆时触发登陆接口查看数据库中是否有对应用户,成功则跳转至首页
  • 实现接入 redux:
    • 实现 store 的客户端复用、服务端数据隔离
    • 实现用户信息 action,在登陆成功时触发存入用户信息至 store

安装依赖

yarn add redux@4.0.5 react-redux@7.1.3

具体实现:实现登陆页

login.tsx实现如下内容

import router from "next/router";
import { Form, Input, Button, Icon, message } from "antd";
import axios from "../utils/axios";
function Login(props) {
  const { getFieldDecorator } = props.form;
  async function handleSubmit(event) {
    event.preventDefault();
    let values = props.form.getFieldsValue();
    let response = await axios.post("/api/login", values);
    if (response.data.code === 0) {
      message.success("登录成功");
      router.push("/");
    } else {
      message.error("登录失败");
    }
  }
  return (
    <Form
      onSubmit={handleSubmit}
      className="login-form"
      style={{ maxWidth: "300px", margin: "20px auto" }}
    >
      <Form.Item>
        {getFieldDecorator("username", {
          rules: [{ required: true, message: "Please input your username!" }],
        })(
          <Input
            prefix={<Icon type="user" style={{ color: "rgba(0,0,0,.25)" }} />}
            placeholder="Username"
          />
        )}
      </Form.Item>
      <Form.Item>
        {getFieldDecorator("password", {
          rules: [{ required: true, message: "Please input your Password!" }],
        })(
          <Input
            prefix={<Icon type="lock" style={{ color: "rgba(0,0,0,.25)" }} />}
            type="password"
            placeholder="Password"
          />
        )}
      </Form.Item>
      <Form.Item>
        <Button type="primary" htmlType="submit" className="login-form-button">
          登录
        </Button>
      </Form.Item>
    </Form>
  );
}
const WrappedLogin = Form.create({ name: "Login" })(Login);
export default WrappedLogin;

接口接入,server/index.tsx中新增路由

app.post("/api/login", async (req, res) => {
  let user = req.body;
  let dbUser = await Models.UserModel.findOne(user);
  if (dbUser) {
    req.session.currentUser = dbUser;
    res.send({ code: 0, data: dbUser });
  } else {
    res.send({ code: 1, error: "登录失败" });
  }
});

实现效果

具体实现:实现接入 redux

初始化 redux

实现 redux 的常规流程,就不过多赘述了

  • store 中的 acyion-types
  • store 中的 action

新建 store 中的 acyion-types,store/acyion-types.tsx

export const SET_USER_INFO = "SET_USER_INFO";
/**
 * 为什么要引入 redux
 * 登录之后可以把用户信息存放到 redux 中,方便所有的页面和组件共享
 */

新建 store 中的 action,即store/index.tsx

import { createStore } from "redux";
import * as TYPES from "./action-types";
let initialState = {
  currentUser: null,
};
const reducer = (state = initialState, action) => {
  switch (action.type) {
    case TYPES.SET_USER_INFO:
      return { currentUser: action.payload };
    default:
      return state;
  }
};
/**
 * 一般来说我们的仓库只有一份
 * 这个代码会在服务器端执行。
 * 每个客户端访问服务器的时候,都要创建一个新的仓库
 * @param initialState
 */
export default function (initialState) {
  return createStore(reducer, initialState);
}

next 接入 redux,实现 store 的客户端复用、服务端数据隔离

我们要注意,redux 数据在客户端渲染时,数据是要共通的;而在服务端,可能有多个客户端请求,这就需要响应多个不同的 store 实例,即数据隔离。

我们获取 store 的动作放在getInitialProps中,而挂载在实例的动作我们放在构造函数中(因为getInitialProps 中不存在实例对象)获取 store 这个函数(getStore)需要实现我们刚刚说的【客户端复用、服务端数据隔离】,实现思路如下

  • 如果是服务端,则直接创建并返回
  • 如果是客户端,则判断缓存(window[REDUX_STORE])是否存在,存在就返回缓存,不存在就创建并挂载在缓存上
function getStore(initialState) {
  if (typeof window == "undefined") {
    //如果 在服务器端运行的。那么直接创建新仓库返回
    return createStore(initialState);
  } else {
    //如果此代码是在客户端执行的,第一次会创建,以后每次都复用上一次创建的
    if (!window[__REDUX_STORE__]) {
      window[__REDUX_STORE__] = createStore(initialState);
    }
    return window[__REDUX_STORE__];
  }
}

具体实现:实现用户信息请求及存入

接口触发

触发接口很简单,只要写在_app.tsxgetInitialProps中即可,步骤如下

  • 请求用户信息接口,这里需要注意,应该发请求的 cookie 携带上
  • 触发对应 type(dispatch)

具体实现如下

let options: any = {
  url: "/api/currentUser",
};
//如果此方法是在服务器执行的,那么会有 ctx.req 属性,它代表本次 node 请求对象
if (ctx.req && ctx.req.headers.cookie) {
  options.headers = options.headers || {};
  options.headers.cookie = ctx.req.headers.cookie;
}
let response = await axios(options);
if (response.data.code === 0) {
  // 当前登录的用户
  let currentUser = response.data.data;
  store.dispatch({ type: TYEPS.SET_USER_INFO, payload: currentUser });
}
接口实现

接口接入,server/index.tsx中新增路由

app.get("/api/currentUser", async (req, res) => {
  let currentUser = req.session.currentUser;
  if (currentUser) {
    res.send({ code: 0, data: currentUser });
  } else {
    res.send({ code: 1, error: "当前用户未登录" });
  }
});
store 挂载

获取后将 store 挂载在实例上,这个动作在客户端只需要挂载一次,所以就放在构造器中(getInitialProps 在客户端切换路由时也会被执行,所以会被执行多次)

constructor(props) {
    super(props);
    console.log("constructor LayoutApp");
    //此构造函数只会在客户端执行一次 这里的 initialState 是 getInitialProps 返回的
    this.store = getStore(props.initialState);
  }
最终_app.tsx

这里使用 react-redux 的 providers,这样所有子组件都能通过connect获取这个属性,最终的_app.tsx如下

import App, { Container } from "next/app";
import Link from "next/link";
import { Layout, Menu, Icon, Avatar, Spin } from "antd";
import "antd/dist/antd.css";
import { withRouter } from "next/router";
const { Header, Footer } = Layout;
import * as TYEPS from "../store/action-types";
import axios from "../utils/axios";
import createStore from "../store";
import { Provider } from "react-redux";
const __REDUX_STORE__ = "__REDUX_STORE__";
function getStore(initialState) {
  if (typeof window == "undefined") {
    //如果 在服务器端运行的。那么直接创建新仓库返回
    return createStore(initialState);
  } else {
    //如果此代码是在客户端执行的,第一次会创建,以后每次都复用上一次创建的
    if (!window[__REDUX_STORE__]) {
      window[__REDUX_STORE__] = createStore(initialState);
    }
    return window[__REDUX_STORE__];
  }
}
class LayoutApp extends App<any> {
  store: any;
  constructor(props) {
    super(props);
    //此构造函数只会在客户端执行一次 这里的 initialState 是 getInitialProps 返回的
    this.store = getStore(props.initialState);
  }
  // 在页面级别渲染时只会被执行一次,即服务端渲染或每次切换客户端渲染都会被执行,但服务端渲染时客户端不会执行
  static async getInitialProps({ Component, ctx }) {
    let store = getStore({});
    let pageProps = {};
    console.log("2. getInitialProps");
    let options: any = {
      url: "/api/currentUser",
    };
    //如果此方法是在服务器执行的,那么会有 ctx.req 属性,它代表本次 node 请求对象
    if (ctx.req && ctx.req.headers.cookie) {
      options.headers = options.headers || {};
      options.headers.cookie = ctx.req.headers.cookie;
    }
    let response = await axios(options);
    if (response.data.code === 0) {
      // 当前登录的用户
      let currentUser = response.data.data;
      store.dispatch({ type: TYEPS.SET_USER_INFO, payload: currentUser });
    }
    if (Component.getInitialProps) {
      // 执行当前页面的 getInitialProps
      let data = await Component.getInitialProps(ctx);
      pageProps = { ...data };
    }
    return { pageProps, initialState: store.getState() };
  }
  render() {
    console.log("3.LayoutApp.render");
    let { Component, pageProps } = this.props as any;
    let { currentUser } = this.store.getState();
    let pathname = this.props.router.pathname;
    pathname = "/" + pathname.split("/")[1];
    return (
      <Provider store={this.store}>
        <style jsx>
          {`
            a {
              display: inline-block !important;
            }
          `}
        </style>
        <Layout>
          <Header className="header">
            <Menu
              theme="dark"
              mode="horizontal"
              style={{ lineHeight: "64px", display: "inline-block" }}
              selectedKeys={[pathname]}
              defaultSelectedKeys={[pathname]}
            >
              <Menu.Item key="/">
                <Icon type="home" />
                <Link href="/">
                  <a>首页</a>
                </Link>
              </Menu.Item>
              <Menu.Item key="/user">
                <Icon type="/user" />{" "}
                <Link href="/user">
                  <a>用户管理</a>
                </Link>
              </Menu.Item>
              <Menu.Item key="/profile">
                <Icon type="profile" />
                <Link href="/profile">
                  <a>个人中心</a>
                </Link>
              </Menu.Item>
              <Menu.Item key="/login">
                <Icon type="login" />
                <Link href="/login">
                  <a>登录</a>
                </Link>
              </Menu.Item>
            </Menu>
            {currentUser && (
              <div
                style={{
                  display: "inline-block",
                  float: "right",
                  color: "red",
                }}
              >
                <Avatar style={{ color: "#F00", backgroundColor: "#CCC" }}>
                  {currentUser.username}
                </Avatar>
              </div>
            )}
          </Header>
          <Component {...pageProps} />
          <Footer style={{ textAlign: "center" }}>@copyright wzyan</Footer>
        </Layout>
      </Provider>
    );
  }
}
export default withRouter(LayoutApp);

实现效果

2022-05-03 12.49.07