注册
web

从零开始构建用户模块:前端开发实践

场景


在大多数前端应用中都会有自己的用户模块,对于前端应用中的用户模块来说,需要从多个方面功能考虑,以掘金为例,可能需要下面这些功能:



  1. 多种登录方式,账号密码,手机验证码,第三方登录等
  2. 展示类信息,用户头像、用户名、个人介绍等
  3. 用户权限控制,可能需要附带角色信息等
  4. 发起请求可能还需要带上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

0 个评论

要回复文章请先登录注册