技术思考 Vue【第三方登录】

563 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

第三方登录 —— 以QQ登录为例

参考文档

  1. 准备工作(opens new window)
  2. QQ互联JS_SDK(opens new window)

开发者流程

  1. 准备一个已经备案的网站需要有QQ登录的逻辑(登录页面,回跳页面)
  2. 然后在QQ互联上进行 身份认证 ,并且 审核通过
  3. 在QQ互联上创建应用,应用需要域名,备案号,回调地址等
  4. 等待人工审核,审核通过会得到应用ID 应用key 回调地址

核心步骤

  1. 修改 vite.config.ts 配置。
  2. 修改电脑的 host 文件。
  3. 配置 VueRouter 和 vue 组件。

修改 vite 配置

查看 vite 官网,有很多关于 vite 做服务器时相关选项,如下图所示,感兴趣的可以自行去阅读。

image.png

根据文档,修改 vite.config.ts 文件,修改端口号,基准路径,开启跨域等:

export default defineConfig({
  // 配置开发服务器
  server: {
    // QQ三方登录的回调uri为:http://www.corho.com:8080/#/login/callback
    // vite 中配置: www.corho.com:8080
    host: "www.corho.com",
    port: 8080,
    // 其他有价值的配置项
    open: true, // 帮我们打开浏览器
    cors: true, // 允许开发时 ajax 跨域
  },
});

修改 host 文件

这一步必须操作,如果不设置,会从互联网中寻找网站地址,而不是从本地去寻找。

注意:

做这一步会有杀毒软件爆出警告,因为早期的病毒就是通过修改 host 文件让用户访问假网站,而这次是我们自己的操作,因此可以完全信任。

  1. 找到 C:\Windows\System32\drivers\etc 下 hosts 文件
  2. 在文件中加入 127.0.0.1 www.corho.com
  3. 保存即可

如果提示没有权限

  1. 将hosts文件移到桌面,然后进行修改,确认保存。
  2. 将桌面hosts文件替换c盘文件

小技巧:如果不想让女朋友用自己的电脑逛淘宝,可以设置 127.0.0.1 www.taobao.com,这样他搜索淘宝只会跳转到本地自己搭建的服务器(每日一个分手小技巧)

关于mac OS 系统操作方法:

  1. 打开命令行窗口
  2. 输入:sudo vim /etc/hosts
  3. 按下:i 键
  4. 输入:127.0.0.1 www.corho.com
  5. 按下:esc
  6. 按下:shift + :
  7. 输入:wq 回车即可

配置组件

配置一个组件用于书写QQ登录的页面,配置路由。

DNS解析概念

DNS解析: 将域名解析成ip地址的过程。

想看一个网站 www.jd.com ,电脑不知道什么是 www.jd.com,需要询问的

  1. 先问本地 hosts 文件(一般不改) 如果本地配置了 域名 和 地址的映射关系,优先使用 hosts 中的映射(如下所示)

    127.0.0.1           www.jd.com
    
  2. 如果本地hosts文件里面没配(默认一般都没配)比如:找www.baidu.com

    会找线上的 dns 服务器, dns 服务器就像一个字典, 字典中记录大量的 网站域名 和 网站ip 的对应关系
    
    dns 服务器
    112.80.248.75      www.baidu.com
    xxx.xx.xxx.xx      www.xxx.com
    

登录实现

首先需要在 index.html 中引入QQ登录的第三方包

<script src="http://connect.qq.com/qc_jssdk.js" data-appid="100556005" data-redirecturi="http://www.corho.com:8080/#/login/callback"></script>

然后需要用到几个 API 获取用户的相关信息。

需要用到的 3 个 API

  • QC.Login.check :检查用户是否登录
  • QC.Login.getMe :获取 QQ 用户唯一标识 openId
  • QC.api("get_user_info").success :获取信息

查阅 官方文档,找到需要的方法相关示例,代码如下所示,每个代码的作用见注释。

// 1. 检查用户是否已登录
if (QC.Login.check()) {
  // 2. 获取 QQ 用户唯一标识 openId
  QC.Login.getMe((openId) => {
    console.log("openId", openId);
  });
  // 3. 获取用户资料
  QC.api("get_user_info").success((res: unknown) => {
    console.log("获取用户资料", res);
  });
}

运行起来发现虽然控制台有打印效果了,但是编译器却在报错,查看报错,有 ESlint 报错,也有 ts 报错。一一解决。

eslint报错是因为没有这个全局变量,在eslint文件添加全局变量。

// eslintrc.cjs

module.exports = {
  ...
  // 全局变量
  globals: {
    QC: true,
  },
}

ts则是类型报错,需要给 QC 变量添加类型声明。

// env.d.ts

// QC 类型声明 - QQ 登录模块
declare namespace QC {
  const Login: {
    // QC.Login.check()
    check: () => boolean;
    // QC.Login.getMe((openId) => {
    //   console.log("获取QQ用户openId", openId);
    // });
    getMe: (callback: (openId: string) => void) => void;
  };
  // QC.api("get_user_info").success((res: unknown) => {
  //   console.log("获取QQ用户资料", res);
  // });
  function api(s: string): {
    success: (res: unknown) => void;
  };
}

渲染用户信息

声明类型

打印返回值的数据,查看其类型,创建数据对象的 ts 类型。

export interface Data {
  ret: number;
  msg: string;
  is_lost: number;
  nickname: string;
  gender: string;
  gender_type: number;
  province: string;
  city: string;
  year: string;
  constellation: string;
  figureurl: string;
  figureurl_1: string;
  figureurl_2: string;
  figureurl_qq_1: string;
  figureurl_qq_2: string;
  figureurl_qq: string;
  figureurl_type: string;
  is_yellow_vip: string;
  vip: string;
  yellow_vip_level: string;
  level: string;
  is_yellow_year_vip: string;
}

export interface UserInfo {
  status: string;
  fmt: string;
  ret: number;
  code: number;
  data: Data;
  seq: string;
  dataText: string;
}

渲染信息

在组件中引入类型,创建一个变量存储数据的返回值。

import { ref } from 'vue';
import type { UserInfo } from '@/type';

// 1. 检查用户是否已登录
if (QC.Login.check()) {
  // 2. 获取 QQ 用户唯一标识 openId
  QC.Login.getMe((openId) => {
    console.log('openId', openId);
  });
  // 3. 获取用户资料
  QC.api('get_user_info').success((res: UserInfo) => {
    console.log('获取用户资料', res);
    userinfo.value = res;
  });
}

const userinfo = ref<UserInfo>();

最后根据获取到的数据通过父传子传递给子组件,让子组件对页面进行动态数据渲染。

多状态登录

有账号未绑定

实现思路

  1. 进行手机号码校验
  2. 验证码按钮倒计时效果(可先放放)
  3. 进行短信验证码发送 (🚨必须调用接口)
  4. 进行绑定,完成后把当前用户数据存入Pinia,跳转到首页

手机号码校验

手机号码校验可以使用正则表达式进行校验,触发事件函数时对手机号进行校验,失败则终止函数返回提示,成功则发送获取验证码请求。

  1. 声明 reactive 对象,存储手机号 mobile 和验证码 code.
    <script>
    // 准备表单数据
    const form = reactive({
      mobile: "13535337057",
      code: "",
    });
    </script>
    
  2. 在组件中双向绑定数据。
    <input
       v-model="form.mobile"
      class="input"
      type="text"
      placeholder="绑定的手机号"
    />
    <input
       v-model="form.code"
      class="input"
      type="text"
      placeholder="短信验证码"
    />
    
  3. 点击发送验证码按钮校验手机号
<script>
const sendCode = () => {
  if (!/^1[3-9]\d{9}$/.test(form.mobile)) {
    return message({ type: 'error', text: '手机号有误' });
  }
  message({ type: 'success', text: '已发送验证码' });
};
</script>

<span @click="sendCode" class="code">发送验证码</span>

发送验证码业务

注意:

这个发送的动作必须要有!也就是接口必须要调用才可以绑定。

接口文档:三方登录_发送已有账号短信

Path:  /login/social/code

Method:  GET

请求参数

Query

参数名称是否必须示例备注
mobile13211112222手机号

点击发送验证码按钮校验手机号是否正确,如果正确,则调用接口发送数据,获取验证码。

  1. pinia 中封装一个获取验证码接口的函数

    import { http } from '@/utils/request';
    import { defineStore } from 'pinia';
    
    const useLoginStore = defineStore('member', {
      actions: {
        async loginSocialCode(mobile: string) {
          await http('GET', '/login/social/code', { mobile });
        },
      },
    });
    
  2. 引入接口函数,点击按钮后调用

    const {  member } = useStore();
    const sendCode = async () => {
      // 1. 进行手机号格式校验
      if (!/^1[3-9]\d{9}$/.test(form.mobile)) {
        // 校验不通过,提醒用户
        return message({ type: "warn", text: "手机号码格式错误~" });
      }
      // 2. 调用获取验证码接口
      await member.loginSocialCode(form.mobile);
      // 调用成功,用户提示
      message({ type: "success", text: "验证码已发送~" });
    };
    

unionId 参数父传子

实现验证码获取后QQ登录最后一个需要的参数是 unionId ,这个前面登陆实现步骤已经获取到,可以声明一个变量存储,再传递给子组件使用。

<script setup lang="ts">
const userInfo = ref<UserInfo>();
const unionId = ref("");
// 1. 检查用户是否已登录
if (QC.Login.check()) {
  // 2. 获取 QQ 用户唯一标识 openId
  QC.Login.getMe((openId) => {
    unionId.value = openId;
  });
  // 3. 获取用户资料
  QC.api("get_user_info").success((res: UserInfo) => {
    userInfo.value = res;
  });
}
</script>

<template>
  <section class="container">
    <div class="tab-content">
      <KeepAlive>
        <component
          :userInfo="userInfo"
          :unionId="unionId"
          :is="isBind ? CallbackBind : CallbackRegister"
        />
      </KeepAlive>
    </div>
  </section>
</template>

子组件 Props 获取。

<script setup lang="ts">
interface Props {
  userInfo: UserInfo;
  unionId: string;
}
</script>

QQ登录并绑定手机号

接口文档:三方登录_账号绑定

Path:  /login/social/bind

Method:  POST

请求参数

Body

名称类型是否必须默认值备注其他信息
unionIdstring必须三方标识QQ登录后的 openId
mobilestring必须手机号
codestring必须验证码

现在三个参数我们都已经拿到了,可以实现登录业务了,先封装一个接口函数,用于发送请求。

async loginSocialBind(data: {
  unionId: string;
  mobile: string;
  code: string;
}) {
  const res = await http<Profile>('POST', '/login/social/bind', data);
  // console.log(res);
  this.profile = res.data.result;
  const { target = '/' } = router.currentRoute.value.query;
  router.push(target as string);
  message({ type: 'success', text: '登录成功~' });
},

请求成功后要保存数据,跳转页面。

组件中为绑定按钮添加点击事件,点击事件处理函数中调用函数发送请求。

const loginFn = () => {
  member.loginSocialBind({
    mobile: form.mobile,
    code: form.code,
    unionId: unionId,
  });
};

有账号已绑定

接口描述:三方直接登录

Path:  /login/social

Method:  POST

请求参数

Body

名称类型是否必须备注
unionIdstring必须三方标识
sourceinteger必须注册来源 注册来源,1为pc,2为webapp,3为微信小程序,4为Android,5为ios,6为qq,7为微信

查看文档,已绑定的事件需要两个参数,一个是 unionId ,在登录实现中可获取到,一个是注册来源,这里我们是QQ注册,因此选6.

先封装一个函数。

async loginsocial(data: { unionId: string; source: number }) {
  const res = await http<Profile>('post', '/login/social', data);
  console.log(res);
  this.profile = res.data.result;

  const { target = '/' } = router.currentRoute.value.query;
  router.push(target as string);
  message({ type: 'success', text: '登录成功~' });
},

在登录实现获取到用户id后立即调用接口发请求。

// 1. 检查用户是否已登录
if (QC.Login.check()) {
  // 2. 获取 QQ 用户唯一标识 openId
  QC.Login.getMe((openId) => {
    console.log('openId', openId);
    unionId.value = openId;
    // 🚨 获取 openId 后,尝试直接登录
    member.loginsocial({ unionId: openId, source: 6 });
  });
  // 3. 获取用户资料
  QC.api('get_user_info').success((res: UserInfo) => {
    console.log('获取用户资料', res);
    userinfo.value = res;
  });
}

优化

注册来源我们可以注册成一个枚举,用枚举来实现语义化调用。枚举通过关键字 enum 注册。枚举有自增的属性,只要第一个赋值为1,后续会累赠。

export enum loginScoure {
  PC = 1,
  WebApp,
  MiniProgram,
  Android,
  IOS,
  QQ,
  WeChat,
}

现在可以调用这个对象点语法获取QQ的注册来源,等价于6