粥里有勺糖

vuePress-theme-reco 粥里有勺糖    2018 - 2023
粥里有勺糖 粥里有勺糖

Choose mode

  • dark
  • auto
  • light
关于我
备战春秋
  • 心得总结
  • 校招考点汇总
  • 面经汇总
  • 复习自查
技术笔记
  • 技术教程
  • 模板工程
  • 源码学习
  • 技术概念
  • 个人作品
  • 学习笔记
计算机基础
  • 算法与数据结构
  • 操作系统
  • 计算机网络
  • 设计模式
  • 剑指offer
大前端
  • javascript
  • vue
  • html
  • css
  • 🌏浏览器专题
  • Web性能优化
  • regexp
  • node
面试
  • 问解
  • javascript
  • css
  • 手撕代码
  • 性能优化
  • 综合问题
  • 面经汇总
  • 小程序
手撕代码
  • 数据结构与算法
  • javascript
  • css
个人站点
  • GitHub (opens new window)
  • 博客园 (opens new window)
  • 掘金 (opens new window)
线上作品
  • 轻取(文件收集) (opens new window)
  • 个人图床 (opens new window)
  • 考勤小程序 (opens new window)
  • 时光恋人 (opens new window)
  • 在线简历生成 (opens new window)
留言板
Github (opens new window)
author-avatar

粥里有勺糖

285

文章

40

标签

关于我
备战春秋
  • 心得总结
  • 校招考点汇总
  • 面经汇总
  • 复习自查
技术笔记
  • 技术教程
  • 模板工程
  • 源码学习
  • 技术概念
  • 个人作品
  • 学习笔记
计算机基础
  • 算法与数据结构
  • 操作系统
  • 计算机网络
  • 设计模式
  • 剑指offer
大前端
  • javascript
  • vue
  • html
  • css
  • 🌏浏览器专题
  • Web性能优化
  • regexp
  • node
面试
  • 问解
  • javascript
  • css
  • 手撕代码
  • 性能优化
  • 综合问题
  • 面经汇总
  • 小程序
手撕代码
  • 数据结构与算法
  • javascript
  • css
个人站点
  • GitHub (opens new window)
  • 博客园 (opens new window)
  • 掘金 (opens new window)
线上作品
  • 轻取(文件收集) (opens new window)
  • 个人图床 (opens new window)
  • 考勤小程序 (opens new window)
  • 时光恋人 (opens new window)
  • 在线简历生成 (opens new window)
留言板
Github (opens new window)
  • devlearn

    • 开发教程
    • 实践:利用ArrayBuffer实现预览指定目录下的所有文件的内容
    • 在linux-deepin上使用deepin-wine5完美运行腾讯会议/QQ/微信等此类应用
    • eslint插件开发教程
    • ServerLess之云函数实践-天气API
    • 移动端阻止弹窗下层页面被滑动
    • 小技巧:for of中获取index
    • Git常用的一些基本操作
    • 向页面注入js实现为图片和文字元素添加透明蒙层
    • 实践:使用jsencrypt配合axios实现非对称加密传输的数据
    • 封装dotenv库实现类似Vite加载环境变量的行为
    • 30行代码实现合并指定目录下的所有文件的内容
    • 马上中秋了,把鼠标指针变为小玉兔
    • Node中require与fs.readFile读取JSON文件的对比
    • 使用免费的七牛云OSS(10G)搭建个人的在线图床
    • 分享封装的一些七牛云OSS操作方法
    • 本地配置SSH免密远程登录服务器
    • 工具方法汇总
    • 腾讯云Serverless实践-Node.js服务部署
    • 腾讯云Serverless实践-静态网站托管
    • 为什么'\x1B'.length === 1?\x与\u知识延伸
    • Vite插件开发纪实:vite-plugin-monitor(上)
    • Vite插件开发纪实:vite-plugin-monitor(中)
    • Vite插件开发纪实:vite-plugin-monitor(下)
    • 解决Vite-React项目中js使用jsx语法报错的问题
    • webpack 项目接入Vite的通用方案介绍
    • webpack 项目接入Vite的通用方案介绍-草稿
    • 优雅的处理挂载window上的函数可能不存在的情况
    • Mac上抓包秒通关羊了个羊

优雅的处理挂载window上的函数可能不存在的情况

vuePress-theme-reco 粥里有勺糖    2018 - 2023

优雅的处理挂载window上的函数可能不存在的情况

粥里有勺糖 2021-09-01 技术笔记技术教程

# 优雅的处理挂载window上的函数可能不存在的情况

# 背景

在做一个Web JS SDK(A)时,内部会用到另一个Web JS SDK(B)的方法。(文中后续用A/B代替两者)

B通常会提供Script和NPM包两种使用方式

使用npm引入的缺点

  • 增加包体积
  • 如果这个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)
1

# 解决方案

编写一个通用的工具函数,处理上述的衍生需求

方法定义如下

function patchWindowFun(
  key: string,
  value: string | Function,
  options?: {
    afterScriptLoad?: Function
    beforeAppendScript?: Function
    alreadyExistCB?: Function
    async?: boolean
    defer?: boolean
  },
)
1
2
3
4
5
6
7
8
9
10
11

总共支持传入3个参数:

  1. key:带判断的方法在window上的属性名
  2. value:不存在时的取值(function 表明直接使用此方法代替,string类型表明方法来源外部加载的js资源)
  3. options:是一些可选的配置项,主要用于处理使用过外部js资源加载方法的场景
    1. afterScriptLoad:资源加载完成后的回掉
    2. beforeAppendScript:资源加载前的回掉
    3. alreadyExistCB:方法如果已经存在执行的回掉
    4. async:控制script的async属性
    5. defer:控制script的defer属性

由于大多数web sdk都会存在需要调用特定函数或者方法进行初始化的情况,固提供了afterScriptLoad,beforeAppendScript,alreadyExistCB三个钩子函数处理不同时机初始化的情况

# 方法实现

如果目标属性存在则直接执行相应的回掉,不做进一步处理

  if (window[key]) {
    alreadyExistCB && alreadyExistCB()
    console.log(key, 'already exist')
    return
  }
1
2
3
4
5

目标属性不存在,传入的方法存在时直接进行赋值

  // 函数直接赋值
  if (typeof value === 'function') {
    window[key] = value
    return
  }
1
2
3
4
5

剩余逻辑则是处理方法从外部js资源加载的情况

由于加载script大部分情况是异步的,业务代码中可能已经调用了相关方法,为此临时创建一个方法收集传入的参数

let params = []
window[key] = function () {
  params.push(arguments)
}
1
2
3
4

下面的逻辑就是处理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)
1
2
3
4
5
6
7
8
9
10
11
12
13

# 完整源码如下

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)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# 小结

目前的方法实现仅适用于,调用的方法相对独立不影响正常的交互

如果业务代码依赖方法的返回值,那么异步通过script加载的方法方式将不太适用

Edit this page (opens new window)
Last Updated: 2022/5/15 12:46:34