王志远,微医前端技术部
前言
数据流至关重要,不然我们的数据处理只能停留单页面级别,无法在应用级进行扩展。简单举个例子,用户登录后,用户头像需要放在头部,每个页面都需要用到,这时就需要数据共享了。我们刚刚实现了用户的增删改查,现在就来实现登陆吧。
这里我们选择用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.tsx的getInitialProps中即可,步骤如下
- 请求用户信息接口,这里需要注意,应该发请求的 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);