主题列表:juejin, github, smartblue, cyanosis, channing-cyan, fancy, hydrogen, condensed-night-purple, greenwillow, v-green, vue-pro, healer-readable, mk-cute, jzman, geek-black, awesome-green
贡献主题:https://github.com/xitu/juejin-markdown-themes
theme: vue-pro
highlight:
前言
几乎所有的项目都需要登录,无论是权限限制、个性化定制、信息安全等需求,都要通过登录系统来获取用户信息,以便提供后续服务。
而一个公司可能会有多个不同的项目,每个项目后端都是共用同一套用户系统的话,就势必会有通用登录的需求出现。
通用登录的方式有很多种,下面我们仅探讨前端的实现方案。
项目子域名不同,共用一个父域
通过设置 cookie 的 domain 属性,可以使得 cookie 携带的内容在父子域名下共享。
根据这个特性,登录之后将 token 保存在 cookie 里面,所有子项目可以共享 token。
将登陆系统单独提出来做成一个单独的项目,其他所有的项目在未登录的情况下重定向到独立的登录系统,登录之后再根据来源跳转到对应的页面,简单的实现如下:
// 子项目在判断未登录的时候,跳转对应的登录项目并将当前的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 个部分:
- 登录 DOM 渲染
- 请求模块
- 登录使用到的事件模块
- 登录事件之后的回调(成功、失败等)
登录 DOM 渲染模块
预先将登录的静态 html 写好。然后将写好的模板以模板字符串保存,样式以内联样式写入。
this.domTpl = ``;
统一的登录界面,可以预先添加一些模块定制化,比如登录 logo,背景图片等,会更加通用一些。
另外为了保证 sdk 的体积与加载速度,尽可能的少用大图素材,小的素材直接 base64 引入,背景大图这种比较大的资源,采用 cdn 引入。
请求模块
为了保证较高的兼容性,以及 sdk 的大小,所以直接采用原生的 xhr 请求,不使用额外的 ajax 请求库与 fetch。
// 发送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;
}
};
}
登录使用到的事件模块
需要内置的事件如下:
- 验证码发送
- 手机、账号、验证码校验
- 登录请求
- 页面关闭
- 提示交互
- 一些可选的额外功能(例如:是否需要勾选协议验证等)
// 登陆相关事件
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,在项目中,直接引入即可:
登录
效果如下:
如上,一个通用的登录 sdk 开发完毕,总体压缩之后的大小为 9kb 左右。如果感觉还不够的话,可以使用 es5 语法开发,体积可以再压缩一些。
可优化点
- 可以设置初始化 sdk 之后,自动、手动判断登录态,根据本身需进行登录业务处理
- 根据自身的项目需求,对通用的 sdk 进一步定制化
写在最后
上述是将登录业务剥离之后,独立开发、部署的一些简单的方案,如果有更好的方案或优化点,欢迎探讨。
项目示例代码明天会上传到Github, 有兴趣可以下载玩玩,自己定制一个。