侧边栏壁纸
博主头像
术业有道之编程博主等级

亦是三月纷飞雨,亦是人间惊鸿客。亦是秋霜去叶多,亦是风华正当时。

  • 累计撰写 99 篇文章
  • 累计创建 50 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

vue3+typeScript+element-plus学习之二

Administrator
2022-04-21 / 0 评论 / 0 点赞 / 230 阅读 / 13770 字

写在前面

接上一篇,本次增加了以下功能(第三项用前端思维难以理解,所以放到最后,方便忽略)

  • 路由守卫
  • 加入element 表单验证
  • 基于oop思想封装业务层ts实现,提供默认实现与自定义实现

一、路由守卫

作用:当用户没有登录时,将其定向到登录页,不能做其他页面的操作。

router/index.ts中增加:

// 先加入相关引用
import {AUTH} from "@/ts/common/auth"
import HomeView from '../views/HomeView.vue'
import LoginView from '../views/LoginView.vue'

//配置相关的页面 requireAuth属性,为true表示需要被守卫验证
const routes: Array<RouteRecordRaw> = [
  {
    path: AUTH.LOGIN_PATH,
    name: 'login',
    component: LoginView,
    meta: {
      title: "登录页",
      requireAuth: false // 标识该路由是否需要登录
    }
  },
  {
    path: '/home',
    name: 'home',
    component: HomeView,
    meta: {
      title: "首页",
      requireAuth: true // 标识该路由是否需要登录
    }
  },
  {
    path: '/about',
    name: 'about',
    component: () => import('../views/AboutView.vue')
  }
]

//在这个函数中增加守卫验证代码
router.beforeEach(
  (to, from, next) => {
    /**
     * 未登录则跳转到登录页
     * 未登录跳转到登录页,也可以通过axios的响应拦截器去实现,但是会先在当前页面渲染一下,再跳转到登录页,会有个闪动的现象
     * 这里通过路由守卫的方式,不会在当前页闪现一下,但是需要在每个路由组件添加一个是否需要登录的标识位,如本项目中的requireAuth字段
     */
    if (to.matched.some((auth) => auth.meta.requireAuth)) {
      if (AUTH.isXtoken()) {
        next()
      } else {
        console.log("未登录")
        next({
          path: AUTH.LOGIN_PATH
        });
      }
    } else {
      next()
    }
  })

二、加入element 表单验证

作用:验证准备提交的参数是否合法,并且给出错误提示

下面都是LoginView.vue的相关代码(代码太多,我简化了)

  • data方法中增加一个rules块来描述字段的规则及提示信息,字段要与formData块的字段一致
  • data方法的return块中增加一个formData块来定义表单字段及默认值
  • data方法的return块中增加validate字段来方便控制表单提交的前进或停止动作
export default defineComponent({
  name: "LoginView",
  data() {
    return {
      isLoading: false,
      validate: false,
      formData: {
        name: "",
        pwd: ""
      },
      rules: {
        name: [
          {required: true, message: '请输入账户名', trigger: 'blur'},
          {min: 1, max: 5, message: '长度在 1 到 5 个字符', trigger: 'blur'}
        ],
        pwd: [
          {required: true, message: '请输入密码', trigger: 'blur'},
          {min: 1, max: 5, message: '长度在 1 到 5 个字符', trigger: 'blur'}
        ],
      }
    }
  }
}
  • 在页面上的el-form表单上加上:model属性、:rules属性,属性值就是上面data块中定义的
  • 在页面上的el-form表单上加上ref属性,属性值可以随意写,建议最好是rule字符串开头,后面跟上model属性,好区分一点
  • el-form-item上增加prop属性,属性值是rules块中定义的
  • 另外el-button上我做了一个:loading,这个太简单就不说了
<el-form :inline="true" :model="formData" :rules="rules" ref="ruleForm" class="login_form">
             <el-form-item label="账号" prop="name">
                <el-input v-model="formData.name" placeholder="请输入账号"></el-input>
              </el-form-item>
<el-button type="primary" class="ht_btn login_btn" :loading="isLoading" @click="login">login
              </el-button>  
</el-form>
  • defineComponent函数methods块中增加validateForm函数
 methods: {
    //需要验证这个表单只需要调用就行
    validateForm() {
    //这里必须要重新申明对象为any,网上说因为form不是响应式的
      let ruleForm: any = this.$refs.ruleForm
      return ruleForm.validate((valid: boolean) => {
        this.validate = valid
      });
    },
login() {
    //验证表单
      this.validateForm()
      if (!this.validate) return
   // 开始登录
   }
}
  • 附上一张效果图
    validateForm验证效果图.png

三、基于oop思想封装业务层ts实现,提供默认实现与自定义实现

  • 为什么要这么做:
    • LoginView.vue离业务最近,其次是对应的logIn.ts
    • 我想让这两者更关注业务实现(向哪里以什么方式提交什么数据,成功或失败的处理函数)
    • 最大的一点就是减少代码量,少写代码,默认实现,同时保留自定义扩展

基于以上的想法,我从login.ts中做了一层OOP抽象,出现了一个serviceRequest.ts,它可以帮你完成绝大部分工作,并且具有默认实现,以自定义实现,有需要你还可以重写它的方法来达到你的预期。

我很贴心的照顾了习惯使用promise的开发者,哈哈哈

  • 以下是serviceRequest.ts代码
import {ElMessage} from "element-plus";

/**
 * 定义一个公共业务接口
 */
export interface BaseServiceInterface {
  /**
   * 设置即将执行的渲染回调
   * @param successCallback 成功回调
   * @param failedCallback 失败回调
   */
  nextCallback(successCallback?: Function, failedCallback?: Function): void;

  /**
   * promise 处理
   * 当 nativeCallback 不为 null 时具有最高优先级
   * @param promise 基于http请求的promise对象
   * @param nativeCallback 用promise原本的方式回调处理
   */
  promiseHandle(promise: Promise<any>, nativeCallback?: Function): void;
}

/**
 * 默认实现公共业务接口
 */
export class BaseService implements BaseServiceInterface {
  constructor(
    public successCallback?: Function,
    public failedCallback?: Function
  ) {
  }

  /**
   * 将回调函数设置值
   * @param successCallback 成功回调
   * @param failedCallback 失败回调
   */
  nextCallback(successCallback?: Function, failedCallback?: Function) {
    this.successCallback = successCallback
    this.failedCallback = failedCallback
  }

  /**
   * promise 默认处理
   * 当 nativeCallback 不为 null 时覆盖 promiseHandle 默认处理,直接执行 nativeCallback
   * @param promise 基于 http 请求的 promise 对象
   * @param nativeCallback 用 promise 原本的方式回调处理
   */
  promiseHandle(promise: Promise<any>, nativeCallback?: Function) {
    if (nativeCallback != null) {
      nativeCallback(promise)
    } else {
      promise.then(res => {
        //成功回调
        if (this.successCallback != null) {
          this.successCallback(res)
          this.successCallback = () => {
          }
        }
      }).catch(err => {
        //失败回调
        if (this.failedCallback != null) {
          this.failedCallback(err)
          this.failedCallback = () => {
          }
        }
        ElMessage.error(err.message)
      })
    }
  }
}

基于这个serviceRequest.ts,重写了login.ts代码,效果就在这里了,请看现在的login.ts有多简单

  • 以下是login.ts代码
import service from "@/ts/common/http"
import {BaseService} from "@/ts/common/serviceRequest";

/**
 * 定义了当前 ts 登录模块的业务方法
 *
 */
interface LoginInterface {
  /**
   * 登录
   * @param login 登录的参数
   * @param nativeCallback 可选,用 promise 原本的方式回调处理的函数
   */
  login(login: LoginBO, nativeCallback?: Function): void;

  /**
   * 退出登录
   * @param nativeCallback 可选,用 promise 原本的方式回调处理的函数
   */
  out(nativeCallback?: Function): void;

  /**
   * 验证登录是否有效
   * @param nativeCallback 可选,用 promise 原本的方式回调处理的函数
   */
  checkToken(nativeCallback?: Function): void;
}

/**
 * 定义 login 请求的入参
 */
export class LoginBO {
  constructor(public name: string, //账号
              public pwd: string, //密码
              public platform?: number //平台(pc:1)
  ) {
  }
}

/**
 * 定义 login 请求的业务出参,相当于 AResponse.data;http.ts 会自动转义的
 */
export class UserVO {
  constructor(public code?: string, //状态码
              public userNo?: string, //用户编号
              public mobile?: string, //手机号
              public newUser?: boolean, //是新用户(是:true,否:false)
              public deletedUser?: boolean, //是已禁用用户(是:true,否:false)
              public nick?: string, //用户昵称
              public photo?: string, //用户头像
              public xtoken?: string, //X-TOKEN
              public successful?: boolean //是否登录成功
  ) {
  }
}

/**
 * 实现 LoginInterface
 * 继承 BaseService 获得基础实现
 */
class LoginService extends BaseService implements LoginInterface {

  /**
   * 注意:这个方法可以删除,不是必须的,这里写上只是演示方法重写
   * 若 base 类不满足需求,可以重写父类实现
   * 子类重写 nextCallback 方法
   */
  override nextCallback(successCallback?: Function, failedCallback?: Function) {
    console.log("LoginService nextCallback")
    super.nextCallback(successCallback, failedCallback)
  }

  /**
   * 注意:这个方法可以删除,不是必须的,这里写上只是演示方法重写
   * 若 base 类不满足需求,可以重写父类实现
   * 子类重写 promiseHandle 方法
   */
  override promiseHandle(promise: Promise<any>, nativeCallback?: Function) {
    console.log("LoginService promiseHandle")
    super.promiseHandle(promise, nativeCallback)
  }

  /**
   * 通过调用 promiseHandle 实现 login 请求
   * @param loginBO 登录参数
   * @param nativeCallback 若不为空则覆盖 promiseHandle 默认处理,直接执行 nativeCallback
   */
  login(loginBO: LoginBO, nativeCallback?: Function) {
    this.promiseHandle(service.post('/login/', loginBO), nativeCallback)
  }

  /**
   * 通过调用 promiseHandle 实现 checkToken 请求
   * @param nativeCallback 若不为空则覆盖 promiseHandle 默认处理,直接执行 nativeCallback
   */
  checkToken(nativeCallback?: Function) {
    this.promiseHandle(service.get('/login/check_token'), nativeCallback)
  }
  /**
   * 通过调用 promiseHandle 实现 out 请求
   * @param nativeCallback 若不为空则覆盖 promiseHandle 默认处理,直接执行 nativeCallback
   */
  out(nativeCallback?: Function) {
    this.promiseHandle(service.get('/login/out'), nativeCallback)
  }
}

export let loginService = new LoginService()

代码重点在 class LoginService最后的3个业务函数,只需要一行代码即可。基于login.tsLoginView.vue的使用方式有很多玩法了

  • 以下是LoginView.vue的代码
methods: {
    validateForm() {
      let ruleForm: any = this.$refs.ruleForm
      return ruleForm.validate((valid: boolean) => {
        this.validate = valid
      });
    },
    login() {
      // 表单验证
      this.validateForm()
      if (!this.validate) return
      // 按钮加上 loading 属性
      this.isLoading = true
      // 用最原始的方式使用 promise 来处理 Callback 
      // 并且自动转义 promise<T> 对应的返回数据模型
      // 此 Callback 会覆盖 nextCallback 的执行
      loginService.login(new LoginBO(this.formData.name, this.formData.pwd),
        (promise: Promise<AResponse<UserVO>>) => {
          promise.then(aResponse => {
            // 成功的处理函数
            this.isLoading = false
            AUTH.setXtoken(aResponse.data!!.xtoken!!)
            console.log(aResponse)
            this.$router.push({name: "home"})
          }).catch(aResponse => {
            // 失败的处理函数
            this.isLoading = false
            console.log(aResponse)
          })
        })
    },
    login2() {
      // 表单验证
      this.validateForm()
      if (!this.validate) return
      // 按钮加上 loading 属性
      this.isLoading = true
      // 先调用 nextCallback 设置函数
      // 使用 loginService 的默认实现 来处理 Callback ,这里我传递了成功和失败的2个 Callback
      // 并且自动转义 promise<T> 对应的返回数据模型
      loginService.nextCallback((aResponse: AResponse<UserVO>) => {
          // 成功的处理函数
          this.isLoading = false
          AUTH.setXtoken(aResponse.data!!.xtoken!!)
          console.log(aResponse)
          this.$router.push({name: "home"})
        },
        (aResponse: AResponse<UserVO>) => {
          // 失败的处理函数
          this.isLoading = false
          console.log(aResponse)
        })
      // 调用登录接口
      loginService.login(new LoginBO(this.formData.name, this.formData.pwd))
    },
    out() {
      // 先调用 nextCallback 设置函数
      // 使用 loginService 的默认实现 来处理 Callback ,这里我只传递了成功的 Callback ,
      // 并且自动转义 promise<T> 对应的返回数据模型
      loginService.nextCallback((aResponse: AResponse<void>) => {
        // 成功的处理函数
        AUTH.removeXtoken()
        console.log(aResponse)
      })
      // 调用退出登录接口
      loginService.out()
    },
    checkToken() {
      // 使用 loginService的默认实现 来处理 Callback ,
      // 这里我只传递了一个 Callback 不管成不成功都可以执行,
      // 同时会给这个箭头函数一个 promise ,想干啥自己写就行,
      // 并且自动转义 promise<T> 对应的返回数据模型
      loginService.checkToken((aResponse: AResponse<Boolean>) => {
        // 通用处理函数
        console.log(aResponse)
      })
    }

  }

上面展示了4种方式使用login.ts提供的接口完成请求和返回函数的执行。但是可以看到LoginView.vue的代码不管采用哪种写法,都没有明显的变化,原因是重业务,业务本身是高度定制化,没办法再抽象的,所以优化LoginView.vue代码量很困难,只能去优化它对应的ts

额外说明:

如果要新写一个模块,我只需要新写一个ts文件和一个对应的页面就行,重点是ts文件中代码量很少,几乎就是粘贴复制。

这里可能有人要杠,为什么还要新写一个ts,直接一个ts写完整个项目所有模块不行么?答案是行,但是不建议这么做。理由如下:

  • 代码量的减少目的是提升开发效率、代码维护成本变少
  • 基于oop抽离业务模块是为了清晰业务,明确每个ts的职责和功能,方便维护和代码审查,你不想别人看你的代码说跟翔一样吧?
  • 后端改动对前端影响更小,发起请求和数据模型都被封装自动转换了

四、关于大家评论说我用java的思想写前端

首先感谢大家对我学前端这事的关注,并且没有对我拙劣的前端技术各种抨击。我不是要卷各位前端大佬,只是想在更大的范围内做到技术闭环,仅此而已。

我觉得思想是程序员的最终经验,代码只是一个逻辑表达。oop的思想是不分技术和方向的。

个人公众号

0

评论区