2016-09-08 1 views
1

Supposons que j'ai ces données dans spanish.json:ES6 cours: Fetch dans la classe parente, reportez-vous à résoudre chercher dans la classe des enfants

[ 
    {"word": "casa", "translation": "house"}, 
    {"word": "coche", "translation": "car"}, 
    {"word": "calle", "translation": "street"} 
] 

Et j'ai une classe Dictionary qui le charge et ajoute une méthode de recherche:

// Dictionary.js 
class Dictionary { 
    constructor(url){ 
    this.url = url; 
    this.entries = []; // we’ll fill this with a dictionary 
    this.initialize(); 
    } 

    initialize(){ 
    fetch(this.url) 
     .then(response => response.json()) 
     .then(entries => this.entries = entries) 
    } 

    find(query){ 
    return this.entries.filter(entry => 
     entry.word == query)[0].translation 
    } 
} 

et je peux instancier que, et l'utiliser pour regarder « calle » avec cette petite application d'une seule page:

<!DOCTYPE html> 
<html lang="en"> 
<head> 
    <meta charset="UTF-8"> 
    <title>spanish dictionary</title> 
</head> 
<body> 

<p><input placeholder="Search for a Spanish word" type=""> 
<p><output></output> 

<script src=Dictionary.js></script> 
<script> 

    let es2en = new Dictionary('spanish.json') 
    console.log(es2en.find('calle')) // 'street' 

    input.addEventListener('submit', ev => { 
    ev.preventDefault(); 
    let translation = dictionary.find(ev.target.value); 
    output.innerHTML = translation; 
    }) 

</script> 


</body> 
</html> 

Jusqu'ici tout va bien. Mais, disons que je veux sous-classer Dictionary et ajouter une méthode qui compte tous les mots et ajoute que compte à la page. (Man, j'ai besoin des investisseurs.)

Alors, je reçois une autre série de financement et mettre en œuvre CountingDictionary:

class CountingDictionary extends Dictionary { 
    constructor(url){ 
    super(url) 
    } 

    countEntries(){ 
    return this.entries.length 
    } 
} 

La nouvelle application d'une seule page:

<!DOCTYPE html> 
<html lang="en"> 
<head> 
    <meta charset="UTF-8"> 
    <title>Counting Spanish Dictionary</title> 
</head> 
<body> 

<p><input placeholder="Search for a Spanish word" type=""> 
<p><output></output> 

<script src=Dictionary.js></script> 
<script> 


    let 
    es2en = new CountingDictionary('spanish.json'), 
    h1 = document.querySelector('h1'), 
    input = document.querySelector('input'), 
    output = document.querySelector('output'); 

    h1.innerHTML = es2en.countEntries(); 

    input.addEventListener('input', ev => { 
    ev.preventDefault(); 
    let translation = es2en.find(ev.target.value); 
    if(translation) 
     output.innerHTML = `${translation}`; 
    }) 

</script> 

</body> 
</html> 

Lorsque cette page se charge, le h1 est rempli avec 0.

Je sais quel est mon problème, je ne sais pas comment le réparer.

Le problème est que le retour appel fetch un Promise, et la propriété .entries est peuplé que les données de l'URL, une fois que la promesse est de retour. Jusque-là, .entries reste vide.

Comment puis-je faire .countEntries attendre que la promesse de récupération à résoudre?

Ou y at-il un meilleur moyen de réaliser ce que je veux ici?

Répondre

1

Vous devez attribuer le résultat de l'appel fetch() à une variable, par exemple:

initialize(){ 
    this.promise = fetch(this.url) 
    .then(response => response.json()) 
    .then(entries => this.entries = entries) 
} 

Ensuite, vous pouvez appeler la méthode then() sur elle:

let es2en = new CountingDictionary('spanish.json'), 
    h1 = document.querySelector('h1'), 
    input = document.querySelector('input'), 
    output = document.querySelector('output'); 

es2en.promise.then(() => h1.innerHTML = es2en.countEntries()) 

input.addEventListener('input', ev => { 
    ev.preventDefault(); 
    let translation = es2en.find(ev.target.value); 
    if(translation) 
    output.innerHTML = `${translation}`; 
}) 
+0

Ceci est assez similaire à la réponse de Frxstrem, et présente donc le même inconvénient. – pat

3

Le problème est que l'appel fetch renvoie un Promise, et que la propriété .entries est uniquement remplie avec les données à partir de l'URL une fois que Promise est retourné. Jusque-là, .entries reste vide.

Vous devriez faire entries une promesse. De cette façon, toutes vos méthodes devaient renvoyer des promesses, mais l'instance Dictionary est immédiatement utilisable.

class Dictionary { 
    constructor(url) { 
    this.entriesPromise = fetch(url) 
     .then(response => response.json()) 
    } 
    find(query) { 
    return this.entriesPromise.then(entries => { 
     var entry = entries.find(e => e.word == query); 
     return entry && entry.translation; 
    }); 
    } 
} 
class CountingDictionary extends Dictionary { 
    countEntries() { 
    return this.entriesPromise.then(entries => entries.length); 
    } 
} 

let es2en = new CountingDictionary('spanish.json'), 
    h1 = document.querySelector('h1'), 
    input = document.querySelector('input'), 
    output = document.querySelector('output'); 

es2en.countEntries().then(len => { 
    fh1.innerHTML = len; 
}); 
input.addEventListener(ev => { 
    ev.preventDefault(); 
    es2en.find(ev.target.value).then(translation => { 
    if (translation) 
     output.innerHTML = translation; 
    }); 
}); 

Ou est-il une meilleure façon d'obtenir tout ce que je veux ici?

Oui. Jetez un oeil à Is it bad practice to have a constructor function return a Promise?.

class Dictionary { 
    constructor(entries) { 
    this.entries = entries; 
    } 
    static load(url) { 
    return fetch(url) 
     .then(response => response.json()) 
     .then(entries => new this(entries)); 
    } 

    find(query) { 
    var entry = this.entries.find(e => e.word == query); 
    return entry && entry.translation; 
    } 
} 
class CountingDictionary extends Dictionary { 
    countEntries() { 
    return this.entries.length; 
    } 
} 

let es2enPromise = CountingDictionary.load('spanish.json'), 
    h1 = document.querySelector('h1'), 
    input = document.querySelector('input'), 
    output = document.querySelector('output'); 

es2enPromise.then(es2en => { 
    fh1.innerHTML = es2en.countEntries(); 
    input.addEventListener(…); 
}); 

Comme vous pouvez le voir, ce appraoch nécessite moins d'imbrication globale par rapport à une instance qui contient des promesses. En outre, une promesse pour l'instance est mieux composable, par ex. Quand vous auriez besoin d'attendre domready avant d'installer les écouteurs et d'afficher la sortie, vous pourrez obtenir une promesse pour le DOM et attendre les deux en utilisant Promise.all.

+0

Merci pour votre commentaire. Le problème avec cette approche comme je le vois est que vous vous retrouvez toujours avec une méthode de chargement qui est essentiellement externe à la classe. Bien qu'il y ait une certaine encapsulation en plaçant le .load dans une méthode statique sur la classe, le modèle pour appeler cette méthode est encore peu différent d'exécuter simplement une extraction (url) dans l'espace de noms global, puis d'instancier la classe avec ces données . – pat

+0

Oui, c'est un peu différent, mais c'est la bonne chose à faire :-) Il faut toujours attendre "extérieurement". – Bergi

+0

Y a-t-il une raison pour que la méthode 'load' soit statique? – pat

1

Une solution simple: tenir la promesse après que vous faites fetch(), puis ajouter une méthode ready() que vous permet d'attendre jusqu'à ce que la classe a initialisé complètement:

class Dictionary { 
    constructor(url){ 
    /* ... */ 

    // store the promise from initialize() [see below] 
    // in an internal variable 
    this.promiseReady = this.initialize(); 
    } 

    ready() { 
    return this.promiseReady; 
    } 

    initialize() { 
    // let initialize return the promise from fetch 
    // so we know when it's completed 
    return fetch(this.url) 
     .then(response => response.json()) 
     .then(entries => this.entries = entries) 
    } 

    find(query) { /* ... */ } 
} 

Ensuite, vous appelez juste .ready() après que vous ve construit votre objet, et vous saurez quand il est chargé:

let es2en = new CountingDictionary('spanish.json') 
es2en.ready() 
    .then(() => { 
    // we're loaded and ready 
    h1.innerHTML = es2en.countEntries(); 
    }) 
    .catch((error) => { 
    // whoops, something went wrong 
    }); 

Comme un petit avantage supplémentaire, vous pouvez simplement utiliser .catch pour détecter des erreurs qui se produisent pendant le chargement, par exemple des erreurs de réseau ou des exceptions non interceptées.

+1

Quel est le but de la méthode 'ready()', quand vous pouvez directement accéder à 'promiseReady'? –

+0

@Gothdo: Honnêtement, c'est plutôt une convention personnelle, mais l'idée est celle exposée uniquement à travers un getter (donc il ne peut pas être modifié en externe) et je pense qu'il est plus logique d'avoir une fonction comme une "action" , (c'est-à-dire, "attendez que cette classe soit prête") que directement accédé. – Frxstrem

+0

Cela me rappelle cette approche: https://quickleft.com/blog/leveraging-deferreds-in-backbonejs/. While est "self-contained" dans le sens où l'appel fetch est à l'intérieur de la classe (et ici le Dictionary utilise par conséquent une url comme paramètre constructeur), l'inconvénient est l'étrangeté de tout client qui utilise une instance de la classe se référer à la méthode .ready() "forever" ... et que, pour moi, il semble que .initialize() n'initialise pas vraiment ... – pat