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