前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >前端进阶:如何设计统一登录业务

前端进阶:如何设计统一登录业务

作者头像
Cookieboty
发布2021-01-06 10:18:27
1.2K0
发布2021-01-06 10:18:27
举报
文章被收录于专栏:前端小兵成长营

前言

几乎所有的项目都需要登录,无论是权限限制、个性化定制、信息安全等需求,都要通过登录系统来获取用户信息,以便提供后续服务。

而一个公司可能会有多个不同的项目,每个项目后端都是共用同一套用户系统的话,就势必会有通用登录的需求出现。

通用登录的方式有很多种,下面我们仅探讨前端的实现方案。

项目子域名不同,共用一个父域

通过设置 cookie 的 domian 属性,可以使得 cookie 携带的内容在父子域名下共享。

根据这个特性,登录之后将 token 保存在 cookie 里面,所有子项目可以共享 token。

将登陆系统单独提出来做成一个单独的项目,其他所有的项目在未登录的情况下重定向到独立的登录系统,登录之后再根据来源跳转到对应的页面,简单的实现如下:

代码语言:javascript
复制
// 子项目在判断未登录的时候,跳转对应的登录项目并将当前的url作为参数带给登录系统
location.replace('https://login.abc.com?redirectUrl' + window.location.href)

// 登录系统在登录之后,根据redirectUrl跳回对应的项目
location.replace(redirectUrl)

这种方式是最为简单的,并且由于登录是独立的项目,也可以将个性化的定制放到项目中,只需要在其他项目跳转的时候除了 redirectUrl 外,多附带项目类型参数(参数名随便取)就可以针对不同的子系统定制个性化的登录界面。

同域,但根据网关来区分项目

实现效果同上,但是由于是同域,所以可操作性的地方就更多,token 不仅仅限制于 cookie,任何本地存储的方式都可以使用,例如 sessionStorage、localStorage 等本地缓存都行。

“一般使用此方式的都是 pc 端,定制化高,但是同时登录项目的资源会比较多,加载速度有影响。 ”

NPM

将登录的组件、接口、逻辑全部打包成 npm 包,使用到的项目可以按需引入之后,调用统一的登录方式。

“就跟写组件业务一样,把登录当成一个独立的业务组件来写,缺点是当登录业务升级的时候,所有有关的项目都需要重新构建、发布。 ”

CDN SDK

上一篇的初级前端进阶里面有谈到过,sdk 的统一登录方案,这里就拿出来详细说下,顺便附带部分代码讲解。

“其实总的来说,没啥难度,就是将整个登录业务封装一下,做的更为通用罢了。 ”

首先,分析一下,登录业务需要拆分成如下 4 个部分:

  1. 登录 DOM 渲染
  2. 请求模块
  3. 登录使用到的事件模块
  4. 登录事件之后的回调(成功、失败等)
登录 DOM 渲染模块

预先将登录的静态 html 写好。然后将写好的模板以模板字符串保存,样式以内联样式写入。

代码语言:javascript
复制
this.domTpl = `<div style="position: fixed; top: 0; left: 0; background: #fff; width: 100%; height: 100%; z-index: 9999;font-family: 'PingFangSC-Regular'">
    ${this.close ? `<div id="closeIcon" style="position: absolute; right: 10px; top: 10px"><p style="height: 20px; width: 20px;" >X</p></div>` : ''}
    ${this.imgUrl.loginImgStart ? `<div class="logo" style="text-align: center; padding-top: 60px;">
      <img src=${this.imgUrl.loginImgUrl} style="width: 36.6vw; height: 36.6vw" />
    </div>` : ''}
    <div style="width: 78.6vw; margin: 0 auto; margin-top: 16px;">
      <input id="phone" type="text" name="phone" placeholder="请输入手机号码"
             style="width: 100%;font-size: 16px; padding-top: 22px; -webkit-tap-highlight-color: rgba(255, 255, 255, 0);
             outline: none;border: none;border-bottom: 1px solid rgba(232,232,232,1);padding-bottom: 10px;" />
    </div>
    <div style="width: 78.6vw; margin: 0 auto; display: flex;">
      <input id="code" type="text" placeholder="请输入验证码"
             style="width: calc(100% - 94px); font-size: 16px; padding-top: 22px; -webkit-tap-highlight-color: rgba(255, 255, 255, 0);
             outline: none;border: none;border-bottom: 1px solid rgba(232,232,232,1);padding-bottom: 10px;" />
      <p class="Obtain" style="width: 84px;border:1px solid rgba(42,112,254,1); font-size: 12px;padding: 5px 12px; text-align: center;margin: 20px 0 0px 0;
                           color: #2A70FE;border-radius:8px;">获取验证码</p>
    </div>
    <div style="width: 78.6vw; margin: 0 auto;margin-top: 45px;position: relative;">
      <div class="tipModel" style="display: none; position: absolute; top: -24px; left: 0; right: 0; color: #FF495F; font-size: 12px; text-align: center; margin-bottom: 12px;">123</div>
      <p class="loginButton" style="font-size: 17px;background:rgba(203,205,209,1);box-shadow:0px 1px 4px 0px rgba(82,88,102,0.2);border-radius:4px; text-align: center;
                font-family: 'PingFangSC-Regular';font-weight:400;color:rgba(255,255,255,1);line-height:40px;margin-block-start: 0;margin-block-end: 0;">登录</p>
    </div>
    ${this.agreement.start ? `<div style="width: 78.6vw; margin: 0 auto;margin-top: 12px;">
      <div id="notes" style="display: flex;align-content: center;">
        <i id="regulations" style="display: block;background: url(${this.regulations}); background-size: cover; width: 16px; height: 16px;margin-right: 5px;"></i>
        <p style="color: #7A8599;font-size: 12px;margin-block-start: 0;margin-block-end: 0;">已阅读并同意<a href=${this.agreement.serverUrl} style="color: #2A70FE;text-decoration:none;">《用户服务协议》</a>和<a href=${this.agreement.privacyUrl} style="color: #2A70FE;text-decoration:none;">《隐私政策》</a></p>
      </div>
    </div>` : ''}
</div>`;

统一的登录界面,可以预先添加一些模块定制化,比如登录 logo,背景图片等,会更加通用一些。

另外为了保证 sdk 的体积与加载速度,尽可能的少用大图素材,小的素材直接 base64 引入,背景大图这种比较大的资源,采用 cdn 引入。

请求模块

为了保证较高的兼容性,以及 sdk 的大小,所以直接采用原生的 xhr 请求,不使用额外的 ajax 请求库与 fetch。

代码语言:javascript
复制
// 发送ajax请求
createXMLHttpRequest(url, errFun) {
    let xmlHttp = new XMLHttpRequest();
    xmlHttp.open("POST", url, false);
    xmlHttp.setRequestHeader('content-type', 'application/json');
    xmlHttp.send(this.paramsEven());
    return xmlHttp.onreadystatechange = () => {
      if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
        let data = JSON.parse(xmlHttp.responseText);
        if (data.code !== 0) {
          return errFun(data.errMsg);
        }
        if (url === this.dataStorage.url) {
          this[`${this.dataStorage.storage}Even`](data.data.token); // 根据配置缓存方法,将缓存存到制定的位置
          if (this.success) this.success(data.data.token); // 直接成功回调,把 token 传给调用者
        }
        return data;
      }
    };
}
登录使用到的事件模块

需要内置的事件如下:

  1. 验证码发送
  2. 手机、账号、验证码校验
  3. 登录请求
  4. 页面关闭
  5. 提示交互
  6. 一些可选的额外功能(例如:是否需要勾选协议验证等)
代码语言:javascript
复制
// 登陆相关事件
bindAction() {
// 手机号正则
let checkPhone = (phone) => {
  if (!(/^1(3|4|5|6|7|8|9)\d{9}$/.test(phone))) {
    return false;
  } else {
    return true;
  }
};

// 弹窗
let tipModel = {
  show: (tipFont) => {
    let tipModel = document.getElementsByClassName('tipModel')[0];
    tipModel.innerHTML = tipFont;
    tipModel.style.display = 'block';
  },
  hide: () => {
    document.getElementsByClassName('tipModel')[0].style.display = 'none';
  }
};

// 验证码相关
let ObtainFun = () => {
  let ObtainStart = document.getElementsByClassName('ObtainStart')[0];
  let time = 50;
  ObtainStart.innerHTML = `${time} S`;
  ObtainStart.style.borderColor = 'rgba(245,246,247,1)';
  ObtainStart.style.background = 'rgba(245,246,247,1)';
  time = time - 1;
  let interval = setInterval(() => {
    ObtainStart.innerHTML = `${time} S`;
    time = time - 1;
    if (time < 0) {
      ObtainStart.innerHTML = `获取验证码`;
      clearInterval(interval);
      document.getElementsByClassName('ObtainStart')[0].className = 'Obtain';
      let Obtain = document.getElementsByClassName('Obtain')[0];
      Obtain.style.borderColor = '#2A70FE';
      Obtain.style.background = '#fff';
    }
  }, 1000)
};

// 验证码事件
document.getElementsByClassName('Obtain')[0].onclick = () => {
  let phone = document.getElementById('phone').value;
  if (!checkPhone(phone)) {
    tipModel.show('请输入正确的手机号码');
    return false;
  }
  let dataInfo = {};
  if (document.getElementsByClassName('Obtain')[0]) {
    dataInfo = this.createXMLHttpRequest(this.dataStorage.verifyCodeUrl, tipModel.show)();
  }
  if (dataInfo.code === 0) {
    document.getElementsByClassName('Obtain')[0].className = 'ObtainStart';
    ObtainFun();
  }
};

// closeIcon事件
if (this.close) {
  document.getElementById('closeIcon').onclick = () => {
    this.hide();
  };
}

// 判断验证码是否存在
document.getElementById('code').oninput = () => {
  let codeVal = document.getElementById('code').value;
  if (codeVal) {
    let loginButton = document.getElementsByClassName('loginButton')[0];
    loginButton.style.background = '#3D424D';
    loginButton.style.color = '#fff';
  }
};

// 登陆事件
document.getElementsByClassName('loginButton')[0].onclick = () => {
  if (!document.getElementById('phone').value || !document.getElementById('code').value) {
    return tipModel.show('请输入正确的手机号码和验证码');
  }
  if (this.agreement.start && document.getElementById('regulations').style.backgroundImage !== `url("${this.regulationsStart}")`) {
    return tipModel.show('请阅读用户相关条例');
  }
  this.createXMLHttpRequest(this.dataStorage.url, tipModel.show)();
};

// 用户条例事件
if (this.agreement.start) {
  document.getElementById('notes').addEventListener('click', () => {
    let regulations = document.getElementById('regulations');
    let regulationsBackground = regulations.style.backgroundImage;
    if (regulationsBackground === `url("${this.regulations}")`) {
      regulations.style.backgroundImage = `url("${this.regulationsStart}")`;
    } else {
      regulations.style.backgroundImage = `url(${this.regulations})`;
    }
  }, false)
}
}
登录事件之后的回调(成功、失败等)

在初始化的时候,可以将需要的回调方法传入,再在对应的场景下,执行对应的回调事件。

如上,已经完成了一个简单、通用的登录 sdk,在项目中,直接引入即可:

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
    <meta name="viewport"
        content="width=device-width, initial-scale=1, user-scalable=no, shrink-to-fit=no,viewport-fit=cover"/>
    <title>登录</title>
  </head>
  <body style="margin: 0;"></body>
  <script type="text/javascript" src="./js/login.js"></script>
  <script>
    Login.init({
      imgUrl: {
        loginImgStart: true,
        loginImgUrl: "https://mirror-gold-cdn.xitu.io/168e088524247c4bcc7?imageView2/1/w/180/h/180/q/85/format/webp/interlace/1",
        loginImgStyleWidth: "130px",
        loginImgStyleHeight: "130px"
      },
      agreement: {
        start: true,
        serverUrl: '',
        privacyUrl: ''
      },
      close: true,
      success() {
        console.log('success')
      },
      error() {
        console.log('error')
      },
      dataStorage: {
        path: 'https://login.com'
      }
    })
  </script>
</html>

效果如下:

如上,一个通用的登录 sdk 开发完毕,总体压缩之后的大小为 9kb 左右。如果感觉还不够的话,可以使用 es5 语法开发,体积可以再压缩一些。

可优化点
  1. 可以设置初始化 sdk 之后,自动、手动判断登录态,根据本身需进行登录业务处理
  2. 根据自身的项目需求,对通用的 sdk 进一步定制化

写在最后

上述是将登录业务剥离之后,独立开发、部署的一些简单的方案,如果有更好的方案或优化点,欢迎探讨。

项目示例代码明天会上传到Github, 有兴趣可以下载玩玩,自己定制一个。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-12-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端小兵成长营 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 项目子域名不同,共用一个父域
  • 同域,但根据网关来区分项目
  • NPM
  • CDN SDK
    • 登录 DOM 渲染模块
      • 请求模块
        • 登录使用到的事件模块
          • 登录事件之后的回调(成功、失败等)
            • 可优化点
            • 写在最后
            相关产品与服务
            验证码
            腾讯云新一代行为验证码(Captcha),基于十道安全栅栏, 为网页、App、小程序开发者打造立体、全面的人机验证。最大程度保护注册登录、活动秒杀、点赞发帖、数据保护等各大场景下业务安全的同时,提供更精细化的用户体验。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档