優(yōu)雅的處理Window.Fun可能不存在的情況
本文轉(zhuǎn)載自微信公眾號(hào)「粥里有勺糖」,作者粥里有勺糖。轉(zhuǎn)載本文請(qǐng)聯(lián)系粥里有勺糖公眾號(hào)。
背景
在做一個(gè)Web JS SDK(A)時(shí),內(nèi)部會(huì)用到另一個(gè)Web JS SDK(B)的方法。(文中后續(xù)用A/B代替兩者)
B通常會(huì)提供Script和NPM包兩種使用方式
使用npm pkg的缺點(diǎn)
- 增加包體積
- 如果這個(gè)SDK被Web應(yīng)用已經(jīng)引入過(guò)頁(yè)面,那么理論上可直接使用,不必要再整一個(gè)
如果SDK B包含script引入的方式,目標(biāo)頁(yè)面也存在可能會(huì)引入B的情況,那么優(yōu)先考慮使用Script引入依賴的SDK的情況:例如
- 目標(biāo)頁(yè)面已經(jīng)引入過(guò)JQuery(符合SDK A的使用需求),那么SDK A就可以直接使用已經(jīng)存在的$進(jìn)行操作即可,不必再創(chuàng)建jQuery的script
- 通常頁(yè)面都會(huì)接入埋點(diǎn)監(jiān)控等基建服務(wù)SDK B,SDK A也需要通過(guò)B進(jìn)行數(shù)據(jù)的上報(bào)
衍生需求
- 掛載在window上的函數(shù)不存在時(shí),自動(dòng)通過(guò)script或者polyfill(墊片方法)補(bǔ)全這個(gè)方法
- 調(diào)用方依舊按照SDK B的文檔進(jìn)行使用
- window.sdkB(options)
解決方案
編寫一個(gè)通用的工具函數(shù),處理上述的衍生需求
方法定義如下
- function patchWindowFun(
- key: string,
- value: string | Function,
- options?: {
- afterScriptLoad?: Function
- beforeAppendScript?: Function
- alreadyExistCB?: Function
- async?: boolean
- defer?: boolean
- },
- )
總共支持傳入3個(gè)參數(shù):
- key:帶判斷的方法在window上的屬性名
- value:不存在時(shí)的取值(function 表明直接使用此方法代替,string類型表明方法來(lái)源外部加載的js資源)
- options:是一些可選的配置項(xiàng),主要用于處理使用過(guò)外部js資源加載方法的場(chǎng)景
- afterScriptLoad:資源加載完成后的回掉
- beforeAppendScript:資源加載前的回掉
- alreadyExistCB:方法如果已經(jīng)存在執(zhí)行的回掉
- async:控制script的async屬性
- defer:控制script的defer屬性
由于大多數(shù)web sdk都會(huì)存在需要調(diào)用特定函數(shù)或者方法進(jìn)行初始化的情況,固提供了afterScriptLoad,beforeAppendScript,alreadyExistCB三個(gè)鉤子函數(shù)處理不同時(shí)機(jī)初始化的情況
方法實(shí)現(xiàn)
如果目標(biāo)屬性存在則直接執(zhí)行相應(yīng)的回調(diào),不做進(jìn)一步處理
- if (window[key]) {
- alreadyExistCB && alreadyExistCB()
- console.log(key, 'already exist')
- return
- }
目標(biāo)屬性不存在,傳入的方法存在時(shí)直接進(jìn)行賦值
- // 函數(shù)直接賦值
- if (typeof value === 'function') {
- window[key] = value
- return
- }
剩余邏輯則是處理方法從外部js資源加載的情況
由于加載script大部分情況是異步的,業(yè)務(wù)代碼中可能已經(jīng)調(diào)用了相關(guān)方法,為此臨時(shí)創(chuàng)建一個(gè)方法收集傳入的參數(shù)
- let params = []
- window[key] = function () {
- params.push(arguments)
- }
下面的邏輯就是處理script加載的邏輯
在js資源加載完成后通過(guò)apply配合forEach將提前調(diào)用方法產(chǎn)生的參數(shù)重新正確的執(zhí)行一次
- const script = document.createElement('script')
- script.src = value
- script.async = !!defer
- script.defer = !!async
- script.onload = function () {
- afterScriptLoad && afterScriptLoad()
- // 處理原來(lái)沒處理的
- params.forEach(param => {
- window[key].apply(this, param)
- })
- }
- beforeAppendScript && beforeAppendScript()
- document.body.append(script)
完整源碼如下
- function patchWindowFun(
- key: string,
- value: string | Function,
- options?: {
- afterScriptLoad?: Function
- beforeAppendScript?: Function
- alreadyExistCB?: Function
- async?: boolean
- defer?: boolean
- },
- ) {
- // 存在不處理
- const { alreadyExistCB, afterScriptLoad, beforeAppendScript, defer, async } = options || {}
- if (window[key]) {
- alreadyExistCB && alreadyExistCB()
- console.log(key, 'already exist')
- return
- }
- // 函數(shù)直接賦值
- if (typeof value === 'function') {
- window[key] = value
- return
- }
- // script url
- if (typeof value === 'string') {
- let params = []
- window[key] = function () {
- params.push(arguments)
- }
- const script = document.createElement('script')
- script.src = value
- script.async = !!defer
- script.defer = !!async
- script.onload = function () {
- afterScriptLoad && afterScriptLoad()
- // 處理原來(lái)沒處理的
- params.forEach(param => {
- window[key].apply(this, param)
- })
- }
- beforeAppendScript && beforeAppendScript()
- document.body.append(script)
- }
- }
小結(jié)
目前的方法實(shí)現(xiàn)僅適用于,調(diào)用的方法相對(duì)獨(dú)立不影響正常的交互
如果業(yè)務(wù)代碼依賴方法的返回值,那么異步通過(guò)script加載的方法方式將不太適用