Scoping av avhengigheter i JavaScript-fil som skal brukes av eksterne webapper

Beklager JavaScript-spørsmål her på irb.no. Appen er Ruby-backed :)

Jeg trenger å lære litt om scoping i JavaScript. Kjenner dere til intercom.io? Du legger inn en minimal javascript-kodesnutt i din egen frontend, og voila, intercom viser en boks (modal) hvor man kan send inn forespørsler og se tidligere samtaler. Ganske genialt system!

Hva om intercom brukte Backbone til dette, og vår app også brukte backbone? Hva måtte intercom gjort for å unngå å ødelegge for meg, og for å unngå at jeg med min hypotetisk sett eldre eller nyere Backbone, ødelegger for intercom ?

For å gjenta meg selv.
webapp A inkluderer ekstern javascript som er laget for å kommunisere og presentere innhold fra webapp B. Hva er viktig for team B å tenke på?

Fyi, de bruker ikke backbone, dette er hypotetisk, det kunne like gjerne vært jQuery, Zepto eller annet.

:)

Ole Morten

Vist 343 ganger. Følges av 5 personer.

Kommentarer

Vi bruker Browserify til dependencies.

Da spesifiserer man avhengighetene i hver app/bibliotek med en package.json som følger npm-standarden. Det betyr riktignok at man er nødt til å bruke pakker som er publisert via npm, men det virker ikke veldig uproblematisk; i verste fall kan man lage sin egen pakke som man linker til via Git.

Browserify følger så alle require() i filene dine, og lager en sammenpakket fil som inneholder alle dependencies, og som initialiserer dem i riktig rekkefølge. Dersom du ønsker å lenke til noe manuelt — f.eks. lenke til jQuery hos Google sin CDN — kan du fortelle Browserify at gitte pakker er eksterne.

Om spørsmålet ditt er om generell konflikthåndtering: Alle biblioteker er selv ansvarlige for å ikke forurense det globale namespacet. Du kan ofte unngå forurensning ved å wrappe selve JavaScript-filen i en funksjon, slik:

window._myIntercom = (function() {
  // ... paste intercom.js her ...

  return intercom;  // Eller noe sånt
})();

Browserify gjør dette for deg — den pakker hver modul inn i en funksjon slik at ingenting blir globalt. En Browserify-pakke eksporterer et bestemt objekt, ved å deklarere et eller annet sted:

// mymodule.js
var Foo = {
  hello: function() { return "foo"; }
};
module.exports = Foo;

Du bruker denne pakken slik:

var mymodule = require('mymodule');
mymodule.hello();

Ditto jQuery, Backbone osv.

Så lenge det ikke opprettes eller endres globale variabler (dvs attributter på den globale variabelen window) så skal det ikke være noe problem for to JS-biblioteker å leve sammen. Problemet med de fleste JS-biblioteker er at de oppretter en global variabel (f.eks. Backbone).

JS som skal settes inn på andres sider, widgets o.l., bør aldri opprette globale variabler. Dette gjelder også eksterne biblioteker som brukes, som Backbone i ditt hypotetiske tilfelle. En slik fil bør se ut omtrent som dette:

(function(){

var foo = bar;
//Din kode

})();

Dette er en anonym funksjon som kjøres med en gang den er definert. JS har kun lokale variabler, og scope defineres av funksjoner, så alle variabler som defineres inne i denne funksjonen gjelder kun der. Det vi kaller globale variabler i JS er egentlig attributter på objektet window, slik at disse er akkurat det samme:

//Antar at dette kjøres utenfor funksjoner, i "main"
window.foo = bar;
var foo = bar;
foo = bar;

(function(){
foo = bar; //Jepp, samme, se nedenfor
})();

Om du stusser på den siste, så er det slik at variabler som defineres uten “var” foran, defineres som attributter på window, altså som “globale variabler”. Derfor skal “var” alltid være med.

Jeg har ikke svart på det egentlige spørsmålet ennå: Hvordan skal to biblioteker kunne bruke samme eksterne bibliotek uten at det krasjer (to forskjellige versjoner, f.eks.)? Noen biblioteker er laget med dette i tankene, og lar deg definere selv hvor det skal havne:

(function(){
var jq = jQuery(); //Om jeg husker riktig
})();

Ellers kan du bare omdefinere

(function(){
var Backbone = window.Backbone;
})();

På denne måten har du en lokal Backbone som kun gjelder innenfor den anonyme funksjonen. Om et annet bibliotek redefinerer window.Backbone så påvirker ikke det din lokale. Du må bare passe på at ikke det andre biblioteket lastes før du får gjort dette.

Alexander har jo allerede gitt det egentlige svaret ovenfor: Bruk npm-pakker som man kan laste direkte inn i den lokale variabel man måtte ønske (jeg har aldri brukt det selv, men det virker enkelt nok). Jeg ville bare egentlig forklare hvilke problemer som oppstår.

Det er egentlig bare en ting du bør ta med deg fra dette: Du som bibliotekforfatter har lov til å kun opprette én global variabel, og du bør vurdere å gjøre det til en npm-pakke slik at du ikke oppretter noen som helst.

Edit: Jeg skrev en artikkel om scope i JS en gang, men den er ikke oppe nå. Kan prøve å finne den igjen om det er interesse.

Dette var lærerikt. Takk!

Herlig Alexander og Tore. Lenge siden jeg var i Nice og hacket med deg Tore Darell! Hvordan går det?

Jeg er 100% med på global variabel-opplegget, derfor min bekymring om Backbone. Om jeg inkluderer Backbone i min js (som pakkes med sprockets og uglifier), vil denne legge seg på window.Backbone og potensielt forkludre? Inkludere min fil først er en løsning jeg ikke er megagira på, hva om jeg møter meg selv i døra? Hvem vinnner? :)

Jeg skal undersøke Browserify nærmere! Takk for gode og lærerike svar!

Bruk noConflict: http://backbonejs.org/#Utility-Backbone-noConflict. Den vil sette window.Backbone tilbake til det den var satt til før backbone.min.js ble lastet.

(function() {
var Backbone = window.Backbone.noConflict()
})();

Slik implementerer du noConflict på dine egne moduler:

(function() {
var oldLib = window.Lib;
var Lib = window.Lib = …;
Lib.noConflict = function() {
window.Lib = oldLib;
};
})();

Oops. Du må såklart legge til en `return Lib` i noConflict.

noConflict var det det het ja :) Det er potensielt litt vanskelig å bruke når man ikke har kontroll på hva som lastes når, etter som noe annet kan komme i mellomtiden og redefinere f.eks. window.Backbone før du har fått hentet den inn i ditt scope. Den eneste måten å forsikre seg om dette på er vel å laste din fil like etter at backbone.js lastes.. Jeg må si jeg liker tanken på den require-greia hvor man angi en variabel for å ta vare på det som lastes, altså at filen lastes helt uten bieffekter.

@OleMorten: Ja, det var tider.. Jeg regner med dere tar neste IRB-treff i Nice slik at jeg kan være med? ;)

En lite kjent JS-oppførsel: Dersom du deklarerer ting med var blir det lokale variable. Uten var blir de globale dersom variabelen ikke tidligere er deklarert. Mer informasjon her.

Nye bilder