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

1,280 阅读3分钟

场景

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

  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,谢谢

本文正在参加「金石计划」