F(E)utter – Initialisierung von externen JS-APIs „Singleton“-Way

F(E)utter – Denkanstöße zu FE-Themen
Diese Blogreihe soll „Futter“ für das Frontend-Hirn bieten.
Es werden verschiedene Themen wie neue Technologien, Herangehensweisen an Problemstellungen u.ä. angeschnitten. Dabei ist eine Diskussion durchaus gewünscht und wir freuen uns über jeden Input.


 

An dieser Stelle möchte ich meinen Ansatz zum einmaligem Initialisieren von JS-API-Scripts auf einer Webpage vorstellen.

Problemstellung

  • Verschiedene Komponenten eines Webauftritts benötigen die selbe externe JS-API (z.B. die GoogleAPI)
  • Nicht jede Seite enthält eine solche Komponente, die API soll daher nicht fix auf jeder Seite eingebunden sein
  • Die API soll erst bei Verwendung geladen werden (asynchron)
  • Die API darf nicht mehrfach in der Seite eingebunden werden

Lösungsansatz

Benötigt wird ein Helper-Script, welches die API nachläd.

initAPI(){
  $.getScript('http://example.com/api/js.js');
}

Soweit so gut, jetzt wird aber bei jedem Aufruf dieser Funktion das Script geladen.

apiHelper = {
  initialized: false,
  init = function(){
    if(!this.initialized){
      this.initialized = true;
      $.getScript('http://example.com/api/js.js');
    }
  }
}

Okay. Script wird nur einmal geladen :)
Aber wie bekomme ich mit, ob es initialisiert wurde?
Nehmen wir an, das Script bietet ein Event oder ein Callback, über den wir informiert werden, wenn das Script geladen wurde.
Im aufrufenden Script können wir dann auf dieses Event reagieren.
Fängt unser Initialisierungshelper die Initialisierung aber ab, gibt es zwei Möglichkeiten: Entweder, wir haben Glück, und das Event wurde noch nicht ausgelöst (und wir bekommen somit das Event mit), oder aber das Script war bereits fertig und wir bekommen die Intitialisierung nicht mitgeteilt.

Die Lösung hierzu ist, dass wir einen Callback an das Helper-Script übergeben. Dieser wird ausgeführt, wenn das Script initialisiert wurde (ganz egal ob das Script die Initialisierung per Event oder Callback o.ä. mitteilt).
Ist das API-Script beim Aufruf des Helpers bereits geladen, wird der Callback direkt ausgeführt.
Zusätzlich muss dieser Helper ein Callback-Stack aufbauen, da die Möglichkeit besteht, dass die API mehrfach angefragt wird, während die Initialisierung bereits gestartet wurde bzw. noch läuft.

Umsetzung am Beispiel der YouTube API (Google)

(function($){
   'use strict';
   Helper = Helper || {};
   Helper.Google = {
      _youtubeAPIcallbackStack: [],
      _youtubeAPIloading: false,
     
      /**
       * Function loads youtube api if not already loaded
       *
       * @method loadYoutubeAPI
       * @params {Object} callback
       * @return void
       */
      loadYoutubeAPI: function(callback){
         if(typeof YT === 'undefined'){
             //if youtube is not available add callback to callbackStack and request api
            Helper.Google._youtubeAPIcallbackStack.push(callback);
            Helper.Google._requestYoutubeAPI();
         }else{
             //if youtube is already available execute callback
            callback();
         }
      },

      /**
       * Function requests youtube api, if not already begun
       *
       * @method _requestYoutubeAPI
       * @return void
       */
      _requestYoutubeAPI: function () {
         if (!Helper.Google._youtubeAPIloading) {
            Helper.Google._youtubeAPIloading = true;
            var $body,
               script,
               lang;
            $body = $('body');
            $body.on('onYouTubeIframeAPIReady', Helper.Google._youtubeAPILoaded);
            //add init callback function for youtube api to trigger init event
            window.onYouTubeIframeAPIReady = function () {
               $body.trigger('onYouTubeIframeAPIReady');
            };
            $.getScript('https://www.youtube.com/iframe_api');
         }
      },

      /**
       * Function removes youtube load listener and triggers registered callbacks
       *
       * @method _youtubeAPILoaded
       * @return void
       */
      _youtubeAPILoaded: function(){
         $('body').off('onYouTubeIframeAPIReady');
         window.onYouTubeIframeAPIReady = undefined;
         //execute every callback method in callback stack
         for(var i = 0; i < Helper.Google._youtubeAPIcallbackStack.length; i+=1){
            Helper.Google._youtubeAPIcallbackStack[i]();
         }
         Helper.Google._youtubeAPIloading = false;
      }
   }
})(jQuery);

Wann auch immer man nun die Youtube API benötigt, kann man Helper.Google.loadYoutubeAPI(callback) aufrufen und sich im Callback sicher sein, dass die API geladen wurde.

Nun zur Diskussion:

  • Was hälst du von dieser Herangehensweise?
  • Wie initialisierst du externe APIs?
  • Hast du Verbesserungsvorschläge oder andere Ansätze dieses Problem zu lösen?

2 Gedanken zu “F(E)utter – Initialisierung von externen JS-APIs „Singleton“-Way

  1. Klingt für mich nach einem guten Anwendungsfall für SystemJS (https://github.com/systemjs/systemjs) mit Dependency-Injection.

    Zb:

    // wird zuerst ausgeführt:
    SystemJS.import(‚./youtubeApi.js‘).then(function (ytApi) {
    someComponent.init(ytApi);
    });

    // … tonnenweise code …

    //irgendwann später und asynchron
    // SystemJS ist schlau genug, zu wissen dass ‚./youtubeApi.js‘ bereits geladen wurde, und lädt es nicht nochmal
    SystemJS.import(‚./youtubeApi.js‘).then(function (ytApi) {
    otherComponent.init(ytApi);
    });

    Hier ein kleiner POC in jsbin: https://jsbin.com/vurexunaqu/edit?html,output

  2. @Anatol: Danke für den interessanten Link. Zusätzlich müsste man im „then“ dann noch implementieren, dass überprüft wird ob die API auch bereits initialisiert wurde, da dies im Zweifel ja ein paar Millisekunden dauern könnte

Hinterlasse eine Antwort

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind markiert *

*

*

Du kannst folgende HTML-Tags benutzen: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>