主題列表: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
貢獻(xiàn)主題:https://github.com/xitu/juejin-markdown-themes
theme: vue-pro
highlight:
前言
幾乎所有的項目都需要登錄仆潮,無論是權(quán)限限制、個性化定制苦银、信息安全等需求厅瞎,都要通過登錄系統(tǒng)來獲取用戶信息嫌吠,以便提供后續(xù)服務(wù)匾竿。
而一個公司可能會有多個不同的項目砌烁,每個項目后端都是共用同一套用戶系統(tǒng)的話,就勢必會有通用登錄的需求出現(xiàn)姑原。
通用登錄的方式有很多種悬而,下面我們僅探討前端的實現(xiàn)方案。
項目子域名不同页衙,共用一個父域
通過設(shè)置 cookie 的 domain 屬性,可以使得 cookie 攜帶的內(nèi)容在父子域名下共享阴绢。
根據(jù)這個特性店乐,登錄之后將 token 保存在 cookie 里面,所有子項目可以共享 token呻袭。
將登陸系統(tǒng)單獨(dú)提出來做成一個單獨(dú)的項目眨八,其他所有的項目在未登錄的情況下重定向到獨(dú)立的登錄系統(tǒng),登錄之后再根據(jù)來源跳轉(zhuǎn)到對應(yīng)的頁面左电,簡單的實現(xiàn)如下:
// 子項目在判斷未登錄的時候廉侧,跳轉(zhuǎn)對應(yīng)的登錄項目并將當(dāng)前的url作為參數(shù)帶給登錄系統(tǒng)
location.replace('https://login.abc.com?redirectUrl' + window.location.href)
// 登錄系統(tǒng)在登錄之后,根據(jù)redirectUrl跳回對應(yīng)的項目
location.replace(redirectUrl)
這種方式是最為簡單的篓足,并且由于登錄是獨(dú)立的項目段誊,也可以將個性化的定制放到項目中,只需要在其他項目跳轉(zhuǎn)的時候除了 redirectUrl 外栈拖,多附帶項目類型參數(shù)(參數(shù)名隨便攘帷)就可以針對不同的子系統(tǒng)定制個性化的登錄界面。
同域涩哟,但根據(jù)網(wǎng)關(guān)來區(qū)分項目
實現(xiàn)效果同上索赏,但是由于是同域,所以可操作性的地方就更多贴彼,token 不僅僅限制于 cookie潜腻,任何本地存儲的方式都可以使用,例如 sessionStorage器仗、localStorage 等本地緩存都行融涣。
一般使用此方式的都是 pc 端,定制化高精钮,但是同時登錄項目的資源會比較多暴心,加載速度有影響。
NPM
將登錄的組件杂拨、接口专普、邏輯全部打包成 npm 包,使用到的項目可以按需引入之后弹沽,調(diào)用統(tǒng)一的登錄方式檀夹。
就跟寫組件業(yè)務(wù)一樣筋粗,把登錄當(dāng)成一個獨(dú)立的業(yè)務(wù)組件來寫,缺點(diǎn)是當(dāng)?shù)卿洏I(yè)務(wù)升級的時候炸渡,所有有關(guān)的項目都需要重新構(gòu)建娜亿、發(fā)布。
CDN SDK
上一篇的初級前端進(jìn)階里面有談到過蚌堵,sdk 的統(tǒng)一登錄方案买决,這里就拿出來詳細(xì)說下,順便附帶部分代碼講解吼畏。
其實總的來說督赤,沒啥難度,就是將整個登錄業(yè)務(wù)封裝一下泻蚊,做的更為通用罷了躲舌。
首先,分析一下性雄,登錄業(yè)務(wù)需要拆分成如下 4 個部分:
- 登錄 DOM 渲染
- 請求模塊
- 登錄使用到的事件模塊
- 登錄事件之后的回調(diào)(成功没卸、失敗等)
登錄 DOM 渲染模塊
預(yù)先將登錄的靜態(tài) html 寫好。然后將寫好的模板以模板字符串保存秒旋,樣式以內(nèi)聯(lián)樣式寫入约计。
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="請輸入手機(jī)號碼"
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;">《用戶服務(wù)協(xié)議》</a>和<a href=${this.agreement.privacyUrl} style="color: #2A70FE;text-decoration:none;">《隱私政策》</a></p>
</div>
</div>` : ''}
</div>`;
統(tǒng)一的登錄界面,可以預(yù)先添加一些模塊定制化迁筛,比如登錄 logo病蛉,背景圖片等,會更加通用一些瑰煎。
另外為了保證 sdk 的體積與加載速度铺然,盡可能的少用大圖素材,小的素材直接 base64 引入酒甸,背景大圖這種比較大的資源魄健,采用 cdn 引入。
請求模塊
為了保證較高的兼容性插勤,以及 sdk 的大小沽瘦,所以直接采用原生的 xhr 請求,不使用額外的 ajax 請求庫與 fetch农尖。
// 發(fā)送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); // 根據(jù)配置緩存方法析恋,將緩存存到制定的位置
if (this.success) this.success(data.data.token); // 直接成功回調(diào),把 token 傳給調(diào)用者
}
return data;
}
};
}
登錄使用到的事件模塊
需要內(nèi)置的事件如下:
- 驗證碼發(fā)送
- 手機(jī)盛卡、賬號助隧、驗證碼校驗
- 登錄請求
- 頁面關(guān)閉
- 提示交互
- 一些可選的額外功能(例如:是否需要勾選協(xié)議驗證等)
// 登陸相關(guān)事件
bindAction() {
// 手機(jī)號正則
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';
}
};
// 驗證碼相關(guān)
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('請輸入正確的手機(jī)號碼');
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('請輸入正確的手機(jī)號碼和驗證碼');
}
if (this.agreement.start && document.getElementById('regulations').style.backgroundImage !== `url("${this.regulationsStart}")`) {
return tipModel.show('請閱讀用戶相關(guān)條例');
}
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)
}
}
登錄事件之后的回調(diào)(成功、失敗等)
在初始化的時候滑沧,可以將需要的回調(diào)方法傳入并村,再在對應(yīng)的場景下巍实,執(zhí)行對應(yīng)的回調(diào)事件。
如上哩牍,已經(jīng)完成了一個簡單棚潦、通用的登錄 sdk,在項目中膝昆,直接引入即可:
<!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 開發(fā)完畢,總體壓縮之后的大小為 9kb 左右荚孵。如果感覺還不夠的話规婆,可以使用 es5 語法開發(fā)酿炸,體積可以再壓縮一些粹懒。
可優(yōu)化點(diǎn)
- 可以設(shè)置初始化 sdk 之后增热,自動玄组、手動判斷登錄態(tài)滔驾,根據(jù)本身需進(jìn)行登錄業(yè)務(wù)處理
- 根據(jù)自身的項目需求,對通用的 sdk 進(jìn)一步定制化
寫在最后
上述是將登錄業(yè)務(wù)剝離之后俄讹,獨(dú)立開發(fā)哆致、部署的一些簡單的方案,如果有更好的方案或優(yōu)化點(diǎn)患膛,歡迎探討摊阀。
項目示例代碼明天會上傳到Github, 有興趣可以下載玩玩踪蹬,自己定制一個胞此。