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