我有一个更广泛的问题,归结为同样的问题。我的要求是编写一个 JS 类来管理一个或多个(数量可以从 1 到无穷大不等)视频嵌入。后端系统是 ExpressionEngine(但在这里无关紧要)。主要目标是建立一个分析框架,将个人数据推送到我们的 Adobe Analytics 平台。这里显示的只是给出播放次数的部分,它可以从这里扩展很多。
CMS 允许编辑者在页面上创建展示视频的模块。每个模块一个视频。每个模块基本上都是通过 Bootstrap 3 排列的 HTML 部分(与此答案无关)。
相关的 HTML 如下所示:
<div id="js_youTubeContainer_{innov_mod_ytplayer:id}" class="embed-responsive embed-responsive-16by9">
<div id="js_youTubeFrame_{innov_mod_ytplayer:id}" class="embed-responsive-item"></div>
</div>
显示“{innov_mod_ytplayer:id}”的部分是来自我们 CMS 的 YouTube 视频 ID。这允许每个嵌入项目的唯一 ID。这在以后很重要。
在此之下,然后我渲染出来:
var innovYouTube_{innov_mod_ytplayer:id} = new Ariba.Innovations.YouTube.Class({
'innovYouTubeVideoId': '{innov_mod_ytplayer:id}',
'innovYouTubeVideoTitle': '{innov_mod_ytplayer:title}',
'innovYouTubeDivId' : 'js_youTubeFrame_{innov_mod_ytplayer:id}'
});
innovYouTube_{innov_mod_ytplayer:id}.Init(); // And... Go!
var onYouTubeIframeAPIReady = (function() {
try{ //wrap this in try/catch because it actually throws errors when it runs subsequent times - this is expected as it's related to YouTube "rerunning" the function on other videos.
innovYouTube_{innov_mod_ytplayer:id}.config.functionCache = onYouTubeIframeAPIReady; //cache the existing global function
return function() {
try{
innovYouTube_{innov_mod_ytplayer:id}.onYouTubeIframeAPIReady(); //execute this instance's function
var newOnYouTubeIframeAPIReady = innovYouTube_{innov_mod_ytplayer:id}.config.functionCache.apply(this, arguments); //add instances to global function
return newOnYouTubeIframeAPIReady; //update global function
}catch(err){}
};
}catch(err){}
})();
您还会在这里看到一些 ExpressionEngine 模板标签 - 这些只是来自 YouTube 的视频 ID 和视频标题。要复制这一点,您当然需要更改这些内容。
这样做是允许我为每个新嵌入的视频使用新代码动态更新单个全局回调。最后,这个回调将包含对他们自己的类实例的调用。您需要这些 try/catch 块,因为它会为所有“其他”嵌入引发误报错误,除了它“现在”实际执行的嵌入 - 请记住,该脚本为页面上的每个嵌入运行一次。这些错误是预期的,实际上没有问题,因此 try/catch 会抑制它们。
使用 CMS 模板标签,我根据 YouTube 视频 ID 创建每个实例。如果有人多次添加相同的视频模块,我会遇到问题,但这是一个很容易处理的业务问题,因为这不应该发生。这使我可以为每个视频一遍又一遍地实例化我的班级的独特实例。
该脚本的关键部分基于这个非常有用的 SO 答案:Adding code to a javascript function programmatically
这是实际的课程。大部分都被评论了……我们使用 jQuery,所以你会在 $.extend() 方法中看到它的一个重要用途。我在类构造方法中使用它作为一种便利,但您也可以使用 vanilla JS(JavaScript 等效于 jQuery 的扩展方法)我只是发现 jQuery 更易于阅读,并且因为它对我可用,所以我使用它。
if (typeof Ariba === "undefined") { var Ariba = {}; }
if (typeof Ariba.Innovations === "undefined") { Ariba.Innovations = {}; }
if (typeof Ariba.Innovations.YouTube === "undefined") { Ariba.Innovations.YouTube = {}; }
if (typeof Ariba.Innovations.YouTube.Class === "undefined") {//this script may be embedded more than once - do this to avoid re-processing it on subsequent loads
Ariba.Innovations.YouTube.Class = function (config) {
this.static = {
'ytScriptId': 'js_youtubeFrameAPI',
'ytScriptUrl': 'https://www.youtube.com/iframe_api'
};//static configuration. Will overwrite any other settings with the same name
this.config = {//optional configuration variables. Will be overridden by instance or static settings with the same name.
'adobeAnalyticsFired': false
};
this.config = $.extend(true, this.config, config);//inserts (destructively!) the instance settings.
this.config = $.extend(true, this.config, this.static);//inserts (destructively!) the static settings.
this.config.this = this;
};
Ariba.Innovations.YouTube.Class.prototype.Init = function () {
//Note: have to allow it to write it over an over because calling the API script is what makes YouTube call onYouTubeIframeAPIReady.
//if (document.getElementById('js_youtubeFrameAPI') === null) { // don't add the script again if it already exists!
this.config.apiScript = document.createElement('script');
this.config.apiScript.src = 'https://www.youtube.com/iframe_api';
this.config.apiScript.id = 'js_youtubeFrameAPI' + this.config.innovYouTubeVideoId;
this.config.firstScriptTag = document.getElementsByTagName('script')[0];
this.config.firstScriptTag.parentNode.insertBefore(this.config.apiScript, this.config.firstScriptTag);
//}
//else { console.log("iframe script already embedded", this.config.innovYouTubeVideoId); }
}
Ariba.Innovations.YouTube.Class.prototype.onYouTubeIframeAPIReady = function (event) {
//console.log("onYouTubeIframeAPIReady", this.config.innovYouTubeVideoId, arguments);
var _this = this;
//console.log(this);
this.config.ytPlayer = new YT.Player(this.config.innovYouTubeDivId, {
videoId: this.config.innovYouTubeVideoId,
events: {
'onReady': _this.onPlayerReady.bind(_this),
'onStateChange': _this.onPlayerStateChange.bind(_this)
}
});
}
Ariba.Innovations.YouTube.Class.prototype.onPlayerReady = function (event) {
//console.log("onPlayerReady", this.config.innovYouTubeVideoId, event);
}
Ariba.Innovations.YouTube.Class.prototype.onPlayerStateChange = function (event) {
//console.log("onPlayerStateChange", this.config.innovYouTubeVideoId, event, this);
if (event.data === YT.PlayerState.PLAYING && !this.config.adobeAnalyticsFired) {
//console.log("YouTube Video is PLAYING!!", this.config.innovYouTubeVideoId);
this.config.adobeAnalyticsFired = true;
if (typeof _satellite !== "undefined") {
window._satellite.data.customVars.adhoc_tracker_val = "Innovations Video: " + this.config.innovYouTubeVideoTitle + " (" + this.config.innovYouTubeVideoId + ")";
_satellite.track('adhoctrack');
}
}
}
}
其他一些注意事项:
一旦解决了主要的全局回调问题,在类实例中保持作用域就很容易了。您只需添加 .bind()。例如:
'onReady': _this.onPlayerReady.bind(_this)
您可能还会看到:
var _this = this;
这样实例的“this”范围就不会意外丢失。也许没有必要,但这是我多年来采用的惯例。
无论如何,我已经为此工作了一周,并且认为我会与 SO 社区分享它,因为从我寻找答案中可以清楚地看出,很多其他人也在寻找解决方案。