Skip to content

前后端再联调

前端:两种方法防止重复点击按钮

方案一:点击之后使得按钮不可以点击,这个逻辑其实可以写到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,它是一个读写访问器,然后创建了emailcode两个属性访问器,然后添加校验。

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>