最近在一个 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())
}
})