前后端再联调
前端:两种方法防止重复点击按钮
方案一:点击之后使得按钮不可以点击,这个逻辑其实可以写到button里面,如果button被点击了,就把自己沉默(一段时间不能再次点击了)。若用户不想这样可以加一个参数来处理。
方案二:用户自己传入是否禁用。详细代码见链接。
写了一个hooks进行封装。
typescript
import { ref } from "vue"
export const useBool = (initialValue: boolean) => {
const bool = ref(initialValue)
return {
ref: bool,
toggle: () => bool.value = !bool.value,
on: () => bool.value = true,
off: () => bool.value = false,
}
}
然后再Button组件中添加自我沉默的功能。
typescript
import { computed, defineComponent, PropType, ref } from 'vue';
import s from './Button.module.scss';
export const Button = defineComponent({
props: {
onClick: {
type: Function as PropType<(e: MouseEvent) => void>
},
level: {
type: String as PropType<'important' | 'normal' | 'danger'>,
default: 'important'
},
type: {
type: String as PropType<'submit' | 'button'>,
default: 'button'
},
disabled: {
type: Boolean,
default: false
},
autoSelfDisabled: {
type: Boolean,
default: false
}
},
setup: (props, context) => {
const selfDisabled = ref(false)
const _disabled = computed(() => {
if(props.autoSelfDisabled === false) {
return props.disabled
}
if(selfDisabled.value) {
return true
} else {
return props.disabled
}
})
const onClick = () => {
props.onClick?.()
selfDisabled.value = true
setTimeout(()=>{
selfDisabled.value = false
},500)
}
return () => (
<button disabled={_disabled.value} type={props.type} class={[s.button, s[props.level]]} onClick={onClick}>
{context.slots.default?.()}
</button>
)
}
})
修改点击发送验证按钮的点击事件。
typescript
const { ref: refDisabled, toggle, on: disabled, off: enable } = useBool(false)
const onClickSendValidationCode = async() => {
disabled()
const response = await http
.post('/validation_codes', { email: formData.email })
.catch(onError)
.finally(enable)
refValidationCode.value.startCount()
}
前端:完善登录表单细节
写了一个判断对象中键值为数组时候是否为空的函数。
typescript
export function hasError(errors: Record<string,string[]>) {
// 可以使用reduce 但是reduce无法终止循环
// return Object.values(errors).reduce((result,value)=> result += value.length,0) > 0
let result = false
for(let key in errors) {
if(errors[key].length > 0) {
result = true
break
}
}
return result
}
然后在SignInPage.tsx
中用于处理提交表单时候的校验。提交的按钮需要设置为type: 'submit'
,具体代码见链接。
typescript
const onSubmit = async (e: Event) => {
e.preventDefault()
Object.assign(errors, {
email: [], code: []
})
Object.assign(errors, validate(formData, [
{ key: 'email', type: 'required', message: '必填' },
{ key: 'email', type: 'pattern', regex: /^\w+@[a-z0-9]+\.[a-z]{2,4}$/, message: '必须是邮箱地址' },
{ key: 'code', type: 'required', message: '必填' },
{ key: 'code', type: 'pattern' , regex: /^\d{6}$/, message: '必须是六位数字'}
]))
if(!hasError(errors)){
const response = await http.post('/session', formData)
}
}
前端:将jwt保存至localStrogae
在SignInPage.tsx
引入自己创建的hashHistroy,import { history } from'../shared/history'
,将jwt存在localStorage中,然后路由跳转。详细代码见链接。
typescript
const onSubmit = async (e: Event) => {
e.preventDefault()
Object.assign(errors, {
email: [], code: []
})
Object.assign(errors, validate(formData, [
{ key: 'email', type: 'required', message: '必填' },
{ key: 'email', type: 'pattern', regex: /^\w+@[a-z0-9]+\.[a-z]{2,4}$/, message: '必须是邮箱地址' },
{ key: 'code', type: 'required', message: '必填' },
{ key: 'code', type: 'pattern' , regex: /^\d{6}$/, message: '必须是六位数字'}
]))
if(!hasError(errors)){
const response = await http.post<{ jwt: string }>('/session', formData)
localStorage.set('jwt', response.data.jwt)
history.push('/')
}
}
后端:双重校验
ruby中ActiveModel
是不再数据库内的,无法把它存下来,ActiveRecord
是在数据库内的对象。创建一个app/models/session.rb
文件。 可以发现创建的Session
和其他models不一样,没有继承任何东西。若是要使其拥有Model的方法则使用include
。
由于Session是不在数据库内的要实现自动读取,需要使用attr_accessor
,它是一个读写访问器,然后创建了email
和code
两个属性访问器,然后添加校验。
ruby
class Session
include ActiveModel::Model
attr_accessor :email, :code
validates :email, :code, presence: true
validates :email, format: {with: /\A.+@.+\z/}
# 自定义校验
validate :check_validation_code
def check_validation_code
return if Rails.env.test? and self.code == '123456'
# 若code为空直接return
return if self.code.empty?
# 如果不存在code 则报错
self.errors.add :email, :not_found unless
ValidationCode.exists? email: self.email, code: self.code, used_at: nil
end
end
同时也要修改controller。
ruby
require 'jwt'
class Api::V1::SessionsController < ApplicationController
def create
session = Session.new params.permit :email, :code
if session.valid?
user = User.find_or_create_by email: session.email
render json: { jwt: user.generate_jwt }
else
render json: { errors: session.errors },status: :unprocessable_entity
end
end
end
并更新中文错误提示。
ruby
zh-CN:
activerecord:
errors:
messages:
invalid: 格式不正确
blank: 必填
not_found: 找不到对应的记录
models:
validation_code:
attributes:
email:
invalid: 邮件地址格式不正确
activemodel:
errors:
messages:
invalid: 格式不正确
blank: 必填
not_found: 找不到对应的记录
详细代码内容,见链接。
后端:解决发送code的bug问题
发现发送的code和数据库的code没有对应上。并将验证码放在标题上。
修改model中的内容。
ruby
class ValidationCode < ApplicationRecord
validates :email, presence: true
# email 必须是合法的邮箱地址
validates :email, format: { with: /\A.+@.+\z/ }
# - after_initialize :generate_code 不应该用after_initialize 这是初始化之后
before_create :generate_code
after_create :send_email
enum kind: { sign_in: 0, reset_password: 1 }
def generate_code
self.code = SecureRandom.random_number.to_s[2..7]
end
def send_email
UserMailer.welcome_email(self.email)&.deliver
end
end
然后修改mailers内的发送主题带上验证码。
ruby
class UserMailer < ApplicationMailer
def welcome_email(email)
validation_code = ValidationCode.order(created_at: :desc).find_by_email(email)
@code = validation_code.code
mail(to: email, subject: "[#{@code}]山竹记账验证码")
end
end
修改邮件发送的内容。
ruby
<!DOCTYPE html>
<html>
<head>
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
</head>
<body>
你正在登录山竹记账,验证码是:<code><%= @code %></code>
</body>
</html>