从零开始构建用户模块:前端开发实践
场景
在大多数前端应用中都会有自己的用户模块,对于前端应用中的用户模块来说,需要从多个方面功能考虑,以掘金为例,可能需要下面这些功能:
- 多种登录方式,账号密码,手机验证码,第三方登录等
- 展示类信息,用户头像、用户名、个人介绍等
- 用户权限控制,可能需要附带角色信息等
- 发起请求可能还需要带上token等
接下来我们来一步步实现一个简单的用户模块
需求分析
用户模型
针对这些需求我们可以列出一个用户模型,包括下面这些参数
展示信息:
- username 用户名
- avatar 头像
- introduction 个人介绍
角色信息:
- role
鉴权:
token
这个user模型对于前端应用来说应该是全局唯一的,我们这里可以用singleton,标注为全局单例
import { singleton } from '@clean-js/presenter';
@singleton()
export class User {
username = '';
avatar = '';
introduction = '';
role = 'member';
token = '';
init(data: Partial<Omit<User, 'init'>>) {
Object.assign(this, data);
}
}
用户服务
接着可以针对我们的用户场景来构建用户服务类。
如下面这个UserService:
- 注入了全局单例的User
- loginWithMobile 提供了手机号验证码登录方法,这里我们用一个mock代码来模拟请求登录
- updateUserInfo 用来获取用户信息,如头像,用户名之类的。从后端拉取信息之后我们会更新单例User
import { injectable } from '@clean-js/presenter';
import { User } from '../entity/user';
@injectable()
export class UserService {
constructor(private user: User) {}
/**
* 手机号验证码登录
*/
loginWithMobile(mobile: string, code: string) {
// mock 请求接口登录
return new Promise((resolve) => {
setTimeout(() => {
this.user.init({
token: 'abcdefg',
});
resolve(true);
}, 1000);
});
}
updateUserInfo() {
// mock 请求接口登录
return new Promise<User>((resolve) => {
setTimeout(() => {
this.user.init({
avatar:
'https://p3-passport.byteimg.com/img/user-avatar/2245576e2112372252f4fbd62c7c9014~180x180.awebp',
introduction: '欢乐堡什么都有,唯独没有欢乐',
username: '鱼露',
role: 'member',
});
resolve(this.user);
}, 1000);
});
}
}
界面状态
我们以登录界面和个人中心页面为例
登录界面
在登录界面需要这些页面状态和方法
View State:
- loading: boolean; 页面loading
- mobile: string; 输入手机号
- code: string; 输入验证码
methods:
- showLoading
- hideLoading
- login
import { history } from 'umi';
import { UserService } from '@/module/user/service/user';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { injectable, Presenter } from '@clean-js/presenter';
import { usePresenter } from '@clean-js/react-presenter';
import { Button, Form, Input, message, Space } from 'antd';
interface IViewState {
loading: boolean;
mobile: string;
code: string;
}
@injectable()
class PagePresenter extends Presenter<IViewState> {
constructor(private userService: UserService) {
super();
this.state = {
loading: false,
mobile: '',
code: '',
};
}
_loadingCount = 0;
showLoading() {
if (this._loadingCount === 0) {
this.setState((s) => {
s.loading = true;
});
}
this._loadingCount += 1;
}
hideLoading() {
this._loadingCount -= 1;
if (this._loadingCount === 0) {
this.setState((s) => {
s.loading = false;
});
}
}
login = () => {
const { mobile, code } = this.state;
this.showLoading();
return this.userService
.loginWithMobile(mobile, code)
.then((res) => {
if (res) {
message.success('登录成功');
}
})
.finally(() => {
this.hideLoading();
});
};
}
export default function LoginPage() {
const { p } = usePresenter(PagePresenter);
return (
<div>
<Form
name="normal_login"
initialValues={{ email: 'admin@admin.com', password: 'admin' }}
onFinish={() => {
console.log(p, '==p');
p.login().then(() => {
setTimeout(() => {
history.push('/profile');
}, 1000);
});
}}
>
<Form.Item
name="email"
rules={[{ required: true, message: 'Please input your email!' }]}
>
<Input
prefix={<UserOutlined className="site-form-item-icon" />}
placeholder="email"
/>
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: 'Please input your Password!' }]}
>
<Input
prefix={<LockOutlined className="site-form-item-icon" />}
type="password"
placeholder="Password"
/>
</Form.Item>
<Form.Item>
<Space>
<Button
type="primary"
htmlType="submit"
className="login-form-button"
>
Log in
</Button>
</Space>
</Form.Item>
</Form>
</div>
);
}
如上代码所示,一个登录页面就完成了,接下来我们实现一下个人中心页面
个人中心
import { UserService } from '@/module/user/service/user';
import { injectable, Presenter } from '@clean-js/presenter';
import { usePresenter } from '@clean-js/react-presenter';
import { Image, Spin } from 'antd';
import { useEffect } from 'react';
interface IViewState {
loading: boolean;
username: string;
avatar: string;
introduction: string;
}
@injectable()
class PagePresenter extends Presenter<IViewState> {
constructor(private userS: UserService) {
super();
this.state = {
loading: false,
username: '',
avatar: '',
introduction: '',
};
}
_loadingCount = 0;
showLoading() {
if (this._loadingCount === 0) {
this.setState((s) => {
s.loading = true;
});
}
this._loadingCount += 1;
}
hideLoading() {
this._loadingCount -= 1;
if (this._loadingCount === 0) {
this.setState((s) => {
s.loading = false;
});
}
}
/**
* 拉取用户信息
*/
getUserInfo() {
this.showLoading();
this.userS
.updateUserInfo()
.then((u) => {
this.setState((s) => {
s.avatar = u.avatar;
s.username = u.username;
s.introduction = u.introduction;
});
})
.finally(() => {
this.hideLoading();
});
}
}
const ProfilePage = () => {
const { p } = usePresenter(PagePresenter);
useEffect(() => {
p.getUserInfo();
}, []);
return (
<Spin spinning={p.state.loading}>
<p>
avatar: <Image src={p.state.avatar} width={100} alt="avatar"></Image>
</p>
<p>username: {p.state.username}</p>
<p>introduction: {p.state.introduction}</p>
</Spin>
);
};
export default ProfilePage;
在这个ProfilePage中,我们初始化时会执行p.getUserInfo();
期间会切换loading的状态,并映射到页面的Spin组件中,执行完成后,更新页面的用户信息,用于展示
总结
至此,一个简单的用户模块就实现啦,整个用户模块以及页面的依赖关系可以查看下面这个UML图,
各位大佬,记得一键三连,给个star,谢谢
作者:鱼露
来源:juejin.cn/post/7208818303673679933
来源:juejin.cn/post/7208818303673679933