解决 无界wujie 下 xgplayer 部分图标无法显示的问题

0 阅读3分钟

最近在一个 wujie 项目中,使用 xgplayer 封装播放器,结果发现部分 icon 无法显示,同时,控制台出现如下报错

TypeError: Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'.

排查

浏览器debug定位到如下代码

{
  key: "initIcons",
  value: function initIcons() {
    var icons = this.icons;
    var contentIcon = this.find(".xgplayer-icon");
    contentIcon.appendChild(icons.cssFullscreen);
    contentIcon.appendChild(icons.exitCssFullscreen);
  }
}

这里的 icons.cssFullscreen 本来应该是一个 dom 节点,结果却是一个 null
在xgplayer源码中,定位到对应源码

initIcons () {
  const { icons } = this
  const contentIcon = this.find('.xgplayer-icon')
  contentIcon.appendChild(icons.cssFullscreen)
  contentIcon.appendChild(icons.exitCssFullscreen)
}

再一路定位到这个icons被设置值的地方: github.com/bytedance/x…

function registerIconsObj (iconsConfig, plugin) {
  const _icons = plugin.config.icons || plugin.playerConfig.icons
  Object.keys(iconsConfig).map(key => {
    const orgIcon = iconsConfig[key]
    let classname = orgIcon && orgIcon.class ? orgIcon.class : ''
    let attr = orgIcon && orgIcon.attr ? orgIcon.attr : {}
    let newIcon = null
    if (_icons && _icons[key]) {
      classname = mergeIconClass(_icons[key], classname)
      attr = mergeIconAttr(_icons[key], attr)
      newIcon = createIcon(_icons[key], key, classname, attr, plugin.pluginName)
    }
    if (!newIcon && orgIcon) {
      newIcon = createIcon((orgIcon.icon ? orgIcon.icon : orgIcon), attr, classname, {}, plugin.pluginName)
    }
    plugin.icons[key] = newIcon
  })
}

可以看到赋值语句:

plugin.icons[key] = newIcon

以及newIcon由createIcon创建

if (_icons && _icons[key]) {
  classname = mergeIconClass(_icons[key], classname)
  attr = mergeIconAttr(_icons[key], attr)
  newIcon = createIcon(_icons[key], key, classname, attr, plugin.pluginName)
}
if (!newIcon && orgIcon) {
  newIcon = createIcon((orgIcon.icon ? orgIcon.icon : orgIcon), attr, classname, {}, plugin.pluginName)
}

在控制台调试后发现,这里的createIcon返回值就已经是null了,再定位到createIcon


function createIcon (icon, key, classname = '', attr = {}, pluginName = '') {
  let newIcon = null
  if (icon instanceof window.Element) {
    Util.addClass(icon, classname)
    Object.keys(attr).map(key => {
      icon.setAttribute(key, attr[key])
    })
    return icon
  }

  if (isUrl(icon) || isUrl(icon.url)) {
    attr.src = isUrl(icon) ? icon : (icon.url || '')
    newIcon = Util.createDom(icon.tag || 'img', '', attr, `xg-img ${classname}`)
    return newIcon
  }

  if (typeof icon === 'function') {
    try {
      newIcon = icon()
      if (newIcon instanceof window.Element) {
        Util.addClass(newIcon, classname)
        Object.keys(attr).map(key => {
          newIcon.setAttribute(key, attr[key])
        })
        return newIcon
      } else {
        XG_DEBUG.logWarn(`warn>>icons.${key} in config of plugin named [${pluginName}] is a function mast return an Element Object`)
      }
      return null
    } catch (e) {
      XG_DEBUG.logError(`Plugin named [${pluginName}]:createIcon`, e)
      return null
    }
  }

  if (typeof icon === 'string') {
    return Util.createDomFromHtml(icon, attr, classname)
  }
  XG_DEBUG.logWarn(`warn>>icons.${key} in config of plugin named [${pluginName}] is invalid`)
  return null
}

经过debug,可以确定代码走了以下逻辑

if (typeof icon === 'function') {
  try {
    newIcon = icon()
    if (newIcon instanceof window.Element) {
      Util.addClass(newIcon, classname)
      Object.keys(attr).map(key => {
        newIcon.setAttribute(key, attr[key])
      })
      return newIcon
    } else {
      XG_DEBUG.logWarn(`warn>>icons.${key} in config of plugin named [${pluginName}] is a function mast return an Element Object`)
    }
    return null
  } catch (e) {
    XG_DEBUG.logError(`Plugin named [${pluginName}]:createIcon`, e)
    return null
  }
}

问题就出在这个if (newIcon instanceof window.Element)
这里的newIcon是一个svg节点,它本应该继承于window.Element,但是这里instanceof的结果却是false
基于这个情况,可以推断 window.Element 和 newIcon这个svg节点 不是来自于同一个window对象
这个window.Element是子应用的window下的Element,这没有问题,那么这个newIcon是哪里来的?
可以看到,代码中出现了一个icon变量,而在这里的if逻辑中,最终判断它是一个函数。
源码中,这个icon是导入一个svg文件得到的
而经过打包后,这个icon是一个函数(见node_modules下的xgplayer:node_modules/xgplayer/es/plugins/assets/requestCssFull.js)

function CssFullSceenSvg() {
  return new DOMParser().parseFromString(`<svg xmlns="http://www.w3.org/2000/svg" width="31" height="40" viewBox="0 -5 31 40">
  <path fill="#fff" transform="scale(1.3, 1.3)" class='path_full' d="M9,10v1a.9.9,0,0,1-1,1,.9.9,0,0,1-1-1V9A.9.9,0,0,1,8,8h2a.9.9,0,0,1,1,1,.9.9,0,0,1-1,1Zm6,4V13a1,1,0,0,1,2,0v2a.9.9,0,0,1-1,1H14a1,1,0,0,1,0-2Zm3-7H6V17H18Zm2,0V17a2,2,0,0,1-2,2H6a2,2,0,0,1-2-2V7A2,2,0,0,1,6,5H18A2,2,0,0,1,20,7Z"></path>
</svg>
`, "image/svg+xml").firstChild;
}
export { CssFullSceenSvg as default };

可以看到svg节点由DOMParser创建,这个DOMParser如果也是属于子应用,那就没有问题,其创建的dom节点一定继承于window.Element,可是实际上并不是这样的,在wujie中,对子应用的部分window属性做了处理:
见:github.com/Tencent/wuj…

export const windowProxyProperties = ["getComputedStyle", "visualViewport", "matchMedia", "DOMParser"];

可以看到这个windowProxyProperties中有一个DOMParser,查找这个windowProxyProperties的引用,定位到:github.com/Tencent/wuj…

Object.getOwnPropertyNames(iframeWindow).forEach((key) => {
  // 特殊处理
  if (key === "getSelection") {
    Object.defineProperty(iframeWindow, key, {
      get: () => iframeWindow.document[key],
    });
    return;
  }
  // 单独属性
  if (windowProxyProperties.includes(key)) {
    processWindowProperty(key);
    return;
  }
  // 正则匹配,可以一次处理多个
  windowRegWhiteList.some((reg) => {
    if (reg.test(key) && key in iframeWindow.parent) {
      return processWindowProperty(key);
    }
    return false;
  });
});

定位到处理windowProxyProperties的processWindowProperty方法:github.com/Tencent/wuj…

function processWindowProperty(key: string): boolean {
  const value = iframeWindow[key];
  try {
    if (typeof value === "function" && !isConstructable(value)) {
      iframeWindow[key] = window[key].bind(window);
    } else {
      iframeWindow[key] = window[key];
    }
    return true;
  } catch (e) {
    warn(e.message);
    return false;
  }
}

可以看到,它将子应用的,存在于windowProxyProperties中的属性,改成使用主应用的对应属性 此时,因为DOMParser是主应用的,那么它创建出来的dom节点也只能继承于主应用的Element,所以这里判断会失败

解决

这里不讨论为什么wujie要做这种处理,但是既然显式地做了处理,肯定有它的道理,那么就不应该纠正它。但是如何修复xgplayer缺少的图标?
我的解决方案如下:

// 从xgplayer找到需要的图标
import createPlayIcon from "xgplayer/es/plugins/assets/play"
import createFullScreenSvg from "xgplayer/es/plugins/assets/requestFull"
import createReplaySvg from "xgplayer/es/plugins/assets/replay"
// 这里的player是xgplayer的实例
// 在xgplayer初始化完成后插入图标
player.once(Events.READY, () => {
  if (window !== window.top && window.DOMParser === (window.top as any)?.DOMParser) {
    // 这里的mountEl就是xgplayer的挂载节点
    // 这里直接找到要插入图标的位置插入图标
    const playBtnBox = mountEl.querySelector(".xgplayer-start")?.children?.[0]
    playBtnBox && playBtnBox.appendChild(createPlayIcon())
    const fullscreenBtnBox = mountEl.querySelector(".xgplayer-fullscreen")?.querySelector(".xgplayer-icon")
    fullscreenBtnBox && fullscreenBtnBox.appendChild(createFullScreenSvg())
    const replayBtnBox = mountEl.querySelector(".xgplayer-replay")
    // replay图标为空时,xgplayer会插入默认图标,这里将默认图标清空
    while (replayBtnBox?.firstChild) {
      replayBtnBox.removeChild(replayBtnBox.firstChild)
    }
    replayBtnBox && replayBtnBox.appendChild(createReplaySvg())
  }
})