这次谈谈一个登录页面的设计。在之前写过的项目中,我都是把表单放在一个页面也没有考虑到用户等待过程中的loading提示。这次重新规划一下,将展示组件与控制拆分,同时用状态机实现状态管理
状态分析
前几天刚好看到一篇文章前端状态管理请三思,觉得挺有意思的,原文作者利用状态机的思想,预先设想好所有状态和状态的迁移,优雅的管理页面登录状态避免过多变量的使用。本文参考作者的思想和代码,实现一个简单的登录页面。状态分析如下:
- 初始登录页面是展示登录的表单(login form)
- 当提交(submit)数据过程后,页面变为等待数据响应状态(loading)
- 数据响应有两种状态,成功(success)页面跳转到首页;失败(failure)页面提示错误
- 当登录成功,只有先退出登录(logout)之后才能重新登录
- 当登录失败,重新提交(submit)回到加载状态(loading)
- logout之后回到login form状态
依旧是模仿掘金app登录页面的一个实现效果:
定义状态机
const machine = {
states: {
'login form': {
submit: 'loading'
},
loading: {
success: 'profile',
failure: 'error'
},
profile: {
viewProfile: 'profile',
logout: 'login form'
},
error: {
submit: 'loading'
}
}
}
复制代码
实现一个状态控制函数,返回下一个状态
const stateTransformer = function(currentState, stepUp) {
let nextState
if (machine.states[currentState][stepUp]) {
nextState = machine.states[currentState][stepUp]
}
console.log(`${currentState} + ${stepUp} --> ${nextState}`)
return nextState || currentState
}
复制代码
我们把状态控制的变量存储在redux中,定义一个简单的auth模块如下,stateChanger纯函数用于控制currentState的状态迁移,每次操作结果返回进行状态变换
export default {
namespace: 'auth',
state: {
currentState: 'login form'
},
reducers: {
stateChanger(state, {stepUp}) {
return {
...state,
currentState: stateTransformer(state.currentState, stepUp)
}
}
},
effects: dispatch => ({
async loginByPhoneNumber(playload, state) {
dispatch.auth.stateChanger({stepUp: 'submit'})
let {data} = await api.auth.loginByPhoneNumber(playload)
if (data.s === 0) {
dispatch.auth.stateChanger({stepUp: 'success'})
saveData('juejin_token', data.token)
} else {
dispatch.auth.stateChanger({stepUp: 'failure'})
Toast.info('用户名或密码错误', 2)
}
}
})
}
复制代码
那么在组件中,我们很容易写一个控制状态变化的组件
render() {
let {currentState} = this.props
return (
<>
{(() => {
switch (currentState) {
case 'loading':
return (
//加载中展示组件
)
case 'profile':
return <Redirect to={'/'} />//返回首页
default:
return (
//登录表单
)
}
})()}
</>
)
}
复制代码
具体配置补充
为了配合项目的用户登录验证,我们重新搭建一个本地服务,在react配置路由的代理转发,具体地,在根目录下新建文件src/setupProxy.js,将/api
开头请求转发到服务器
const proxy = require('http-proxy-middleware')
module.exports = function(app) {
app.use(proxy('/api', {target: 'http://localhost:8989/', changeOrigin: true}))
}
复制代码
services/api定义数据接口
export async function loginByPhoneNumber({phoneNumber, password}) {
return post('/api/auth/type/phoneNumber', {
body: {
phoneNumber,
password
}
})
}
复制代码
后端实现一个简单的中间件路由
const Koa = require('koa')
const router = require('./router')
router.post('/auth/type/phoneNumber', async (ctx, next) => {
var {phoneNumber, password} = await parse.json(ctx.req)
if (phoneNumber === '15111111111' && password === '123456') {
let token = generateToken({uid: phoneNumber, password})
ctx.response.body = JSON.stringify({
s: 0,
m: `账号登录成功错误`,
d: '',
token
})
} else {
ctx.response.body = JSON.stringify({s: 1, m: '账号信息错误', d: ''})
}
})
复制代码
项目地址
总之是个人的一个小实践,大家在登录页面的管理中有什么更好的做法吗?还有什么复杂的情况需要考虑