前端進(jìn)階:如何設(shè)計統(tǒng)一登錄業(yè)務(wù)


主題列表: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 個部分:

  1. 登錄 DOM 渲染
  2. 請求模塊
  3. 登錄使用到的事件模塊
  4. 登錄事件之后的回調(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)置的事件如下:

  1. 驗證碼發(fā)送
  2. 手機(jī)盛卡、賬號助隧、驗證碼校驗
  3. 登錄請求
  4. 頁面關(guān)閉
  5. 提示交互
  6. 一些可選的額外功能(例如:是否需要勾選協(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>

效果如下:

image

如上丸边,一個通用的登錄 sdk 開發(fā)完畢,總體壓縮之后的大小為 9kb 左右荚孵。如果感覺還不夠的話规婆,可以使用 es5 語法開發(fā)酿炸,體積可以再壓縮一些粹懒。

image

可優(yōu)化點(diǎn)

  1. 可以設(shè)置初始化 sdk 之后增热,自動玄组、手動判斷登錄態(tài)滔驾,根據(jù)本身需進(jìn)行登錄業(yè)務(wù)處理
  2. 根據(jù)自身的項目需求,對通用的 sdk 進(jìn)一步定制化

寫在最后

上述是將登錄業(yè)務(wù)剝離之后俄讹,獨(dú)立開發(fā)哆致、部署的一些簡單的方案,如果有更好的方案或優(yōu)化點(diǎn)患膛,歡迎探討摊阀。

項目示例代碼明天會上傳到Github, 有興趣可以下載玩玩踪蹬,自己定制一個胞此。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市跃捣,隨后出現(xiàn)的幾起案子漱牵,更是在濱河造成了極大的恐慌,老刑警劉巖疚漆,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件酣胀,死亡現(xiàn)場離奇詭異,居然都是意外死亡娶聘,警方通過查閱死者的電腦和手機(jī)闻镶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來丸升,“玉大人铆农,你說我怎么就攤上這事〗瞥埽” “怎么了顿涣?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵波闹,是天一觀的道長。 經(jīng)常有香客問我涛碑,道長精堕,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任蒲障,我火速辦了婚禮歹篓,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘揉阎。我一直安慰自己庄撮,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布毙籽。 她就那樣靜靜地躺著洞斯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪坑赡。 梳的紋絲不亂的頭發(fā)上烙如,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天,我揣著相機(jī)與錄音毅否,去河邊找鬼亚铁。 笑死,一個胖子當(dāng)著我的面吹牛螟加,可吹牛的內(nèi)容都是我干的徘溢。 我是一名探鬼主播,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼捆探,長吁一口氣:“原來是場噩夢啊……” “哼然爆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起黍图,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤曾雕,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后雌隅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體翻默,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年恰起,在試婚紗的時候發(fā)現(xiàn)自己被綠了修械。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,013評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡检盼,死狀恐怖肯污,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤蹦渣,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布哄芜,位于F島的核電站,受9級特大地震影響柬唯,放射性物質(zhì)發(fā)生泄漏认臊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一锄奢、第九天 我趴在偏房一處隱蔽的房頂上張望失晴。 院中可真熱鬧,春花似錦拘央、人聲如沸涂屁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拆又。三九已至,卻和暖如春栏账,著一層夾襖步出監(jiān)牢的瞬間帖族,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工发笔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留盟萨,地道東北人凉翻。 一個月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓了讨,卻偏偏與公主長得像,于是被迫代替她去往敵國和親制轰。 傳聞我的和親對象是個殘疾皇子前计,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評論 2 355

推薦閱讀更多精彩內(nèi)容