前言

上一章节我们实现了底部导航栏,并且创建了三个主页面,这三个页面是需要展示底部导航栏,而我们本章节要制作的「登录注册页面」便是不需要底部导航栏的单独页面。

本教程已有线上地址在线地址,同学们可以在实战部分,对照着线上页面进行学习。

知识点

组件:CellInputButtonCheckBox

注册页面

img

我们的系统是面向多用户的,换句话说也就是一个纯正的 C 端项目,任何人都可以通过网站,注册一个新的账号。接下来开始注册页面的编写。

首先新建 Login 文件夹,在文件夹内添加两个文件 index.jsxstyle.module.less,我们先把注册页面的静态页面切出来,首先给 index.jsx 添加如下代码:

1
2
3
4
5
6
7
8
9
import React from "react";

import s from "./style.module.less";

const Login = () => {
return <div className={s.auth}>注册</div>;
};

export default Login;

为它添加一个路由配置,打开 router/index.js 添加如下:

1
2
3
4
5
6
import Login from '@/container/Login'
...
{
path: "/login",
component: Login
}

重启项目,如下所示代表登录注册页面创建成功了:

img

接下来为 Login/index.jsx 添加静态页面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import React from "react";
import { Cell, Input, Button, Checkbox } from "zarm";
import CustomIcon from "@/components/CustomIcon";

import s from "./style.module.less";

const Login = () => {
return (
<div className={s.auth}>
<div className={s.head} />
<div className={s.tab}>
<span>注册</span>
</div>
<div className={s.form}>
<Cell icon={<CustomIcon type="zhanghao" />}>
<Input clearable type="text" placeholder="请输入账号" />
</Cell>
<Cell icon={<CustomIcon type="mima" />}>
<Input clearable type="password" placeholder="请输入密码" />
</Cell>
<Cell icon={<CustomIcon type="mima" />}>
<Input clearable type="text" placeholder="请输入验证码" />
</Cell>
</div>
<div className={s.operation}>
<div className={s.agree}>
<Checkbox />
<label className="text-light">
阅读并同意<a>《掘掘手札条款》</a>
</label>
</div>
<Button block theme="primary">
注册
</Button>
</div>
</div>
);
};

export default Login;

文末已为同学们提供下本章节 demo 代码,样式部分不再详细说明。

上述代码中,关键部分是账号输入、密码输入、验证码输入,这三个输入框是需要获取数据作为接口的参数提交上去的。

很多时候,服务端没有开发好接口的时候,我们前端要做的任务就是先还原 UI 稿,把该切的页面都切出来,并且预留好需要给接口提交的数据交互,比如上述三个输入框。

样式编写部分,要注意的一点是 :global 这个关键词。由于我们采用的是 CSS Module 的形式进行开发,也就是你在页面中声明的类名都会根据当前页面,打一个唯一的 hash 值,比如我们页面中声明的 className={s.form},最终在浏览器中显示的是这样的:

img

_form_kpur3_30 是已经被编译过的样式,这样做的目的是避免和别的页面的样式重名,这是目前样式管理的一个诟病,当多人参与项目开发的时候,很难做到不污染全局样式名称,除非很小心的命名样式名称。

所以经过编译之后,想要修改 .form 下的 .za-cell,如下写法,将无法修改成功:

1
2
3
4
5
.form {
.za-cell {
color: red;
}
}

原因是,上述写法,.za-cell 会被编译加上 hash,组件库 Zarm 内的 dom 类名还是叫 za-cell,如上图所示。所以为了不加 hash,就需要这样操作:

1
2
3
4
5
6
7
.form {
:global {
.za-cell {
color: red;
}
}
}

这样 .za-cell 就不会被加上 hash,如下图所示:

img

完成上述页面布局之后,你会看到这样一个效果:

img

少了一个验证码,我们使用插件 react-captcha-code,我们通过 npm 下载它:

1
npm i react-captcha-code -S

在代码中引入:

1
2
3
4
5
6
7
8
9
10
...
import Captcha from "react-captcha-code"
...
<Input
clearable
type="text"
placeholder="请输入验证码"
onChange={(value) => setVerify(value)}
/>
<Captcha charNum={4} />

浏览器展示如下所示:

img

此时我们已经切完注册页面需要的内容。

我们给页面加上相应的逻辑,首先是账号、密码、验证码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
...
const [username, setUsername] = useState(''); // 账号
const [password, setPassword] = useState(''); // 密码
const [verify, setVerify] = useState(''); // 验证码
...
<Input
clearable
type="text"
placeholder="请输入账号"
onChange={(value) => setUsername(value)}
/>
...
<Input
clearable
type="password"
placeholder="请输入密码"
onChange={(value) => setPassword(value)}
/>
...
<Input
clearable
type="text"
placeholder="请输入验证码"
onChange={(value) => setVerify(value)}
/>

当输入框内容修改的时候,onChange 会被触发,接受的回调函数参数,便是变化的输入值,此时我们将其保存在声明的变量中。

我们输入的验证码是需要和验证码图片里的验证码匹配的,所以我们还需要拿到图片里的验证码,我们作如下操作:

1
2
3
4
5
6
7
8
9
10
import React, { useCallback } from 'react'
...
const [captcha, setCaptcha] = useState(''); // 验证码变化后存储值
// 验证码变化,回调方法
const handleChange = useCallback((captcha) => {
console.log('captcha', captcha)
setCaptcha(captcha)
}, []);
...
<Captcha charNum={4} onChange={handleChange} />

当验证码变化的时候,便能获取到相应的值。修改完上述代码,我们不妨测试一下:

img

到此,注册需要的参数都有了,我们开始编写注册方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { Cell, Input, Button, Checkbox, Toast } from 'zarm'
import { post } from '@/utils'
...
const onSubmit = async () => {
if (!username) {
Toast.show('请输入账号')
return
}
if (!password) {
Toast.show('请输入密码')
return
}
if (!verify) {
Toast.show('请输入验证码')
return
};
if (verify != captcha) {
Toast.show('验证码错误')
return
};
try {
const { data } = await post('/api/user/register', {
username,
password
});
Toast.show('注册成功');
} catch (error) {
Toast.show('系统错误');
}
};
...
<Button onClick={onSubmit} block theme="primary">注册</Button>

上述代码中,因为我们使用的是 async await 做异步处理,所以需要通过 try catch 来捕获异步处理过程中出现的错误,如果使用 Promise 的回调函数,则无需使用 try catch,改动如下:

1
2
3
4
5
6
post("/api/user/register", {
username,
password,
}).then((res) => {
// do something
});

尝试使用之前注册过的用户名,注册一个账号:

img

服务端给出正确的报错,我们再用一个未注册过的用户名:

img

此时我们大致将注册功能实现了。这里我不再展开讲样式部分,因为这样会使得文章中出现过多的重复代码,不以阅读,大家尽量根据标签的类名去查找 css 样式部分。

登录页面

登录页面的逻辑我们直接做到同一个页面中,通过一个 type 参数作为判断条件,判断当前状态是登录页面或是注册页面。

话不多说我们添加代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
...
import cx from 'classnames'
...

const Login = () => {
...
const [type, setType] = useState('login'); // 登录注册类型

return <div className={s.auth}>
...
<div className={s.tab}>
<span className={cx({ [s.avtive]: type == 'login' })} onClick={() => setType('login')}>登录</span>
<span className={cx({ [s.avtive]: type == 'register' })} onClick={() => setType('register')}>注册</span>
</div>
</div>
<div className={s.form}>
...
{
type == 'register' ? <Cell icon={<CustomIcon type="mima" />}>
<Input
clearable
type="text"
placeholder="请输入验证码"
onChange={(value) => setVerify(value)}
/>
<Captcha ref={captchaRef} charNum={4} onChange={handleChange} />
</Cell> : null
}
</div>
<div className={s.operation}>
{
type == 'register' ? <div className={s.agree}>
<Checkbox />
<label className="text-light">阅读并同意<a>《掘掘手札条款》</a></label>
</div> : null
}
<Button onClick={onSubmit} block theme="primary">{type == 'login' ? '登录' : '注册'}</Button>
</div>
}

注意,如果引入了新的工具包,请自行安装,如上述代码就需要安装 classnames。可以通过 npm i classnames -S 指令

代码分析:

上述代码中,通过 type 属性区分注册和登录。

首先是 tab 切换,通过 classname 来判断是否是当前高亮,用于样式控制。

其次,当 type == 'register' 的时候,才把验证码展示出来,因为登录这边咱们就不设置验证码,只在注册的时候显示。

最后是事件的判断,如果 type == 'login',则按钮文案显示为 登录,否则为 注册

此时点击触发的 onSubmit 事件也很关键,同样需要通过 type 判断是登录还是注册,修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const onSubmit = async () => {
if (!username) {
Toast.show("请输入账号");
return;
}
if (!password) {
Toast.show("请输入密码");
return;
}
try {
// 判断是否是登录状态
if (type == "login") {
// 执行登录接口,获取 token
const { data } = await post("/api/user/login", {
username,
password,
});
// 将 token 写入 localStorage
localStorage.setItem("token", data.token);
} else {
if (!verify) {
Toast.show("请输入验证码");
return;
}
if (verify != captcha) {
Toast.show("验证码错误");
return;
}
const { data } = await post("/api/user/register", {
username,
password,
});
Toast.show("注册成功");
// 注册成功,自动将 tab 切换到 login 状态
setType("login");
}
} catch (error) {
Toast.show("系统错误");
}
};

由于登录注册的账号和密码是同一参数,我们这边就直接复用了逻辑,并通过 type 判断调用哪一个接口。

重启项目,验证登录接口是否成功,如果成功则会返回 token 信息,如下图所示:

img

此时,我们本地的 localStorage 里,已经存下了 token,如下图所示:

img

保存 token 的形式有很多,你可以引入状态管理插件来对这些数据进行存储,但是这里我们对其进行简单处理,减少项目多余的负担,保证课程的完成度。有时候,成功的将课程完成,也是一种成就感。

总结

到此,我们的登录注册页面算是完成了,我们拿到的 token 是有时效性的,我在后台设置的是 24 小时的时效,如果过期了,请求其他接口时,就会报错,从而通过逻辑判断重新回到登录页面。下一章节,我会详细分析这块内容。