JS i AJAX – wyniki wyszukiwania dynamicznego są niepotrzebnie powielone

0

Dziś opublikowałem na swoim "blogu" kontrolkę wyszukiwania dynamicznego. Sam ją napisałem (w Javascripcie) – dlatego też mam z nią pewien problem. Otóż powiela mi wyniki.

Kontrolka działa tak, że po każdej wpisanej literze wysyła asynchronicznie nowe zapytanie GET do serwera, a następnie w odpowiedzi na nie wyświetla listę <ul> z wynikami, usuwając wcześniejszą listę (za pomocą metody jQuery empty).

Ale jak za szybko się wpisze kilka liter, to wyniki są powielone (kilka takich samych elementów <li>). Teoretycznie przecież kod asynchroniczny działa w swoim czasie, a synchroniczny w swoim. Już prędzej oczekiwałbym, że wyników będzie za mało (bo odpowiedź nie zdąży dojść z serwera) niż za dużo.

Być może po jakimś czasie sam doszedłbym do tego, co to powoduje (może to nawet banalne?), ale myślę, że ktoś może szybciej to zrobi, mający więcej doświadczenia z AJAX-em. Nie jest to bardzo denerwująca cecha, ale jednak to nieprofesjonalne.

Załączam linki do kodu funkcji odpowiedzialnych za tę kontrolkę:


PS: Tak, wiem, że jest to napisane dość nieciekawie ;) (na razie). Wszelkie sugestie co do ulepszenia bardzo chętnie przyjmę.


UPDATE: Jeśli ktoś chciałby, to wkleję też cały kod tutaj:

search.js

// onblur
function hideResults(event) {
    if (event.relatedTarget === null ||
        !event.relatedTarget.classList.contains("search-form__results-list__item__link")) {
        $(".search-form__results-list").hide();
    }
}

// onfocus
function showResults() {
    const resultsElement = $(".search-form__results-list");
    if (resultsElement.text() != "") {
        resultsElement.show();
    }
}

// onkeyup
function search(noResultsText) {
    const resultsElement = $(".search-form__results-list");
    resultsElement.empty();
    if ($(".search-form__input").val() != "") {
        displaySearchResults(noResultsText);
    } else {
        resultsElement.hide();
    }
}

display-search-results.js

function displaySearchResults(noResultsText) {
    const input = $(".search-form__input");
    const resultsElement = $(".search-form__results-list");

    var xhttp = new XMLHttpRequest();
    xhttp.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
            const data = this.response;
            const xml = new DOMParser().parseFromString(data, "text/xml");
            const results = [];
            const articles = xml.getElementsByTagName("item");
            const query = new RegExp(input.val(), 'gi');
            for (let article of articles) {
                const titleElement = article.getElementsByTagName("title")[0];
                const title = titleElement.textContent;
                const linkElement = article.getElementsByTagName("link")[0];
                const link = linkElement.textContent;
                if (title.match(query)) {
                    results.push({
                        title,
                        link
                    });
                }
            }
            if (results.length != 0) {
                for (let r of results) {
                    const rLink = document.createElement("a");
                    rLink.setAttribute("href", r.link);
                    rLink.append(r.title);
                    rLink.classList.add("search-form__results-list__item__link");
                    const rElement = document.createElement("li");
                    rElement.append(rLink);
                    rElement.classList.add("search-form__results-list__item");
                    resultsElement.append(rElement);
                }
            } else {
                const errorElement = document.createTextNode(noResultsText);
                const rElement = document.createElement("li");
                rElement.append(errorElement);
                rElement.classList.add("search-form__results-list__item", "search-form__results-list__item--error");
                resultsElement.append(rElement);
            }
            resultsElement.show();
        }

    };
    xhttp.open("GET", "/feed.xml", true);
    xhttp.send();
}
2

resultsElement.empty();

A jakbyś to wrzucał nie w funkcji search, tylko bezpośrednio po tym jak dostaniesz nowe dane w handlerze xhttp.onreadystatechange = function() {?

Poza tym, to pewnie nie pomoże ci w głównym problemie, ale taka poboczna uwaga:

  • jeśli i tak już korzystasz z jQuery to przecież w jQuery masz już metodę ajax / get / post do Ajaxa. Nie musisz tworzyć new XMLHttpRequest(); W jQuery masz również coś takiego jak attr, więc nie musisz setAttribute pisać. jQuery pomaga.

Albo odwrotnie: wywalić jQuery całkowicie, czyste przeglądarkowe API też jest mocne, do ajaxa używać można fetch https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API, albo querySelector do robienia zapytań obiektów DOM.

0

A jakbyś to wrzucał nie w funkcji search, tylko bezpośrednio po tym jak dostaniesz nowe dane w handlerze xhttp.onreadystatechange = function() {?

Hm, pomysł wydaje się w porządku, ale nie bardzo rozumiem, dlaczego miałoby to zadziałać?

jeśli i tak już korzystasz z jQuery to przecież w jQuery masz już metodę ajax / get / post do Ajaxa. Nie musisz tworzyć new XMLHttpRequest();

A no właśnie muszę, tzn. nie widziałem innej drogi, tzn. metody $.get i $.ajax źle obsługiwały mi pobranie pliku z rozszerzeniem .xml (bez rozszerzenia dobrze działały). A że plik rozszerzenie musi mieć, to uznałem, że nie mam czasu na w szukanie drogi akurat w jQuery, tylko zrobię to najszybciej, jak się da. Być może masz pomysł, czemu to mogło mi nie działać? Już nie pamiętam dokładnie, ale zwracany był jakiś dziwny obiekt z dwiema metodami (może to tylko Firefox źle wyświetlał w konsoli, jak się teraz zastanawiam; ale błędu nie było, to skąd taki obiekt). Jeśli chcesz, mogę zmienić kod i wkleić tutaj, jak ten obiekt wygląda.

W jQuery masz również coś takiego jak attr, więc nie musisz setAttribute pisać. jQuery pomaga.

Jeśli to tylko zmiana w nazwie, nie wiem, czy robi mi to jakąś różnicę.

Albo odwrotnie: wywalić jQuery całkowicie, czyste przeglądarkowe API też jest mocne, do ajaxa używać można fetch https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API, albo querySelector do robienia zapytań obiektów DOM.

O, to, to, ale na razie nie mam czasu na naukę nowych rzeczy. Nie wszystko naraz, ale właśnie spróbuję to później, gdybym miał czas.

3

Hm, pomysł wydaje się w porządku, ale nie bardzo rozumiem, dlaczego miałoby to zadziałać?

nie wiem czy nie ma jakiegoś race condition w stylu

  • użytkownik wcisnął literkę
  • czyszczenie okienka
  • odpalenie AJAX po raz pierwszy
  • użytkownik wcisnął drugą literkę
  • czyszczenie okienka
  • odpalenie AJAX po raz drugi
  • dane z pierwszego zapytania AJAX (zapychamy okienko)
  • dane z drugiego zapytania AJAX (zapychamy jeszcze bardziej okienko)

(tzn. nie wiem, czy tak jest, tylko tak sobie wyobraziłem po tym, w jaki sposób o tym napisałeś).

$.get i $.ajax źle obsługiwały mi pobranie pliku z rozszerzeniem .xml (bez rozszerzenia dobrze działały

Tzn. co się działo? I próbowałeś użyć parametru dataType http://api.jquery.com/jquery.ajax/
albo sprawdzić nagłówki na serwerze? I czy generowany XML jest do końca poprawny?

No i czemu akurat xml, a nie JSON, który jest lżejszy i który się parsuje do zwykłych obiektów JS?

0

Ten race condition nie jest taki głupi! Choć jest już 3:32 na zegarku, to nawet go rozumiem. Będę musiał zobaczyć w ten deseń jutro.

Tzn. co się działo? I próbowałeś użyć parametru dataType http://api.jquery.com/jquery.ajax/
albo sprawdzić nagłówki na serwerze? I czy generowany XML jest do końca poprawny?

Nie pamiętam, czy używałem dataType. Nagłówków nie sprawdzałem. Generowanego XML-a też nie pamiętam. Zobaczę jeszcze raz te funkcje, jak będę mieć czas, ale dopiero po rozwiązaniu sprawy powielanych wpisów.

No i czemu akurat xml, a nie JSON, który jest lżejszy i który się parsuje do zwykłych obiektów JS?

Dlatego, ponieważ używam bezpośrednio pliku feed.xml, czyli mojego RSS – uznałem, że zawiera te dane, o które mi chodzi, a więc dodanie pliku np. JSON powieliłoby mi dane (przynajmniej część), a tego chciałem uniknąć. Choć z drugiej strony, jak tak się zastanowić – czy powielanie danych zawsze jest złe? No mnie to jakoś mierzi… Wprowadza chaos.

0

Przeanalizowałem ten race condition, doszedłem do wniosku, że właśnie coś takiego występuje, zmieniłem kod w ten sposób:

function searchAndDisplay(noResultsText) {
    const input = $(".search-form__input");
    const resultsListElement = $(".search-form__results-list");

    resultsListElement.empty(); // <-- to przeniosłem z tamtej funkcji tutaj

    const xhr = new XMLHttpRequest();
    ...
}

Ale wyniki nadal powielają się.

1

Ta zmiana nic nie dała i ciągle masz race condition, elementy powinieneś czyścić zaraz przed ich dodaniem w funkcji podpiętej pod xhttp.onreadystatechange :

var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200)
{
    resultsListElement.empty(); // <-- to przeniosłem z tamtej funkcji tutaj
0

Rzeczywiście, to miałem na myśli, nie wiem, czemu tak nie zrobiłem. :{ Przeniosłem i poskutkowało. Przynajmniej z tego, co widzę. Dzięki, @neves ! Ech, 15 lat temu on się zarejestrował, a wciąż tu wchodzi. ;)

PS. I dzięki, @LukeJL.


UPDATE: Aha, @LukeJL, jeszcze wątek nie do porzucenia, bo jeszcze tę sprawę z $.get i $.ajax muszę zobaczyć.


UPDATE 2: Tak więc co do $.get, to spróbowałem użyć tego w następujący sposób:

$.get("/feed.xml", function(data) {
    ...
}, "xml");

Lista wyników jest pusta, a Firefox wyświetla mi następujący błąd w konsoli:

XML Parsing Error: syntax error
Location: http://127.0.0.1:4000/
Line Number 1, Column 1:

Użycie data.toSource() pokazuje mi taki wynik:

({get location() {
    [native code]
}, set location() {
    [native code]
}})
0

Hm, już chyba się domyślam… patrzę w konsoli, wszystkie dane są w obiekcie XML… Ten błąd może być jedynie firefoksowy… ale muszę to jeszcze zobaczyć… Może po prostu ma inną strukturę ten obiekt z $.get, niż zwykłe dane.


UPDATE: No a teraz, po zmianach, Firefox się uparł, żeby mi mówić, że ReferenceError: searchAndDisplay is not defined w pliku search.js:21:13. A w tej linijce jak byk jest przecież searchAndDisplay(noResultsText);. Coś się wcześniej ładuje?

0

XML Parsing Error: syntax error
Location: http://127.0.0.1:4000/
Line Number 1, Column 1:

a masz to gówienko, co się wkleja do XMLa na początku? Coś w stylu <?xml version="1.0" encoding="UTF-8"?> ? Ale to już czysty strzał. Bo ciężko mi powiedzieć co to może być innego. Jak wygląda ten plik ogólnie?

Aha, i wejdź z przeglądarki ręcznie na http://127.0.0.1:4000/ i zobacz źródło, czy może serwer coś nie pokręcił.

No a teraz, po zmianach, Firefox się uparł, żeby mi mówić, że ReferenceError: searchAndDisplay is not defined w pliku search.js:21:13.

A jak wygląda ten plik search.js?

0

Plik feed.xml wygląda tak:

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" 
	xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<title>silvuss's thoughts</title>
        <author>silvuss</author>
		<description>hello, i am silvuss, and here you can find my thoughts on programming, and maybe some other topics. english is not my native language.</description>
		<link>http://localhost:4000</link>
		<atom:link href="http://localhost:4000/feed.xml" rel="self" type="application/rss+xml" />
              
            <item>
                <title>the things that every blogging developer should know</title>
                <description><p>this article contains tips for bloggers that are programmers (or testers, or similar) on how to create a good article.</p>

</description>
                <pubDate>Tue, 10 Jul 2018 00:00:00 +0200</pubDate>
                <link>http://localhost:4000/2018/07/10/the-things-that-every-blogging-developer.html</link>
                <guid isPermaLink="true">http://localhost:4000/2018/07/10/the-things-that-every-blogging-developer.html</guid>
            </item>
              
            <item>
                <title>the versioning system of my articles</title>
                <description><p>this article describes what is the versioning system that i apply to my articles.</p>

</description>
                <pubDate>Tue, 10 Jul 2018 00:00:00 +0200</pubDate>
                <link>http://localhost:4000/2018/07/10/my-versioning-system.html</link>
                <guid isPermaLink="true">http://localhost:4000/2018/07/10/my-versioning-system.html</guid>
            </item>
          
	</channel>
</rss>

Plik search.js wygląda teraz, po zmianach, tak:

// when focus is lost
function hideResults(event) {
    if (event.relatedTarget === null ||
        !event.relatedTarget.classList.contains("search-form__results-list__item__link")) {
        $(".search-form__results-list").hide();
    }
}

// when focus is got
function showResults() {
    const resultsElement = $(".search-form__results-list");
    if (resultsElement.val() != "") {
        resultsElement.show();
    }
}

// when key WAS pressed (keyup, not keypressed)
function search(noResultsText) {
    if ($(".search-form__input").val() != "") {
        searchAndDisplay(noResultsText);
    } else {
        $(".search-form__results-list").hide();
    }
}

Plik search-and-display.js wygląda teraz, po zmianach, tak:

function searchAndDisplay(noResultsText) {
    const input = $(".search-form__input");
    const resultsListElement = $(".search-form__results-list");

    // const xhr = new XMLHttpRequest();
    // xhr.onreadystatechange = function() {
    // if (this.readyState == 4 && this.status == 200) {
    $.get("/feed.xml", function(data) {
        console.log(data);
        console.log(data.toSource());
        resultsListElement.empty();

        // const data = this.response;
        const xml = new DOMParser().parseFromString(data, "text/xml");
        const resultsList = [];
        const articlesList = xml.getElementsByTagName("item");
        const query = new RegExp(input.val(), 'gi');

        for (let article of articlesList) {
            const titleElement = article.getElementsByTagName("title")[0];
            const title = titleElement.textContent;
            const linkElement = article.getElementsByTagName("link")[0];
            const link = linkElement.textContent;
            if (title.match(query)) {
                resultsList.push({
                    title,
                    link
                });
            }
        }

        if (resultsList.length != 0) {
            for (let result of resultsList) {
                const resultLinkElement = document.createElement("a");
                resultLinkElement.setAttribute("href", result.link);
                resultLinkElement.append(result.title);
                resultLinkElement.classList.add("search-form__results-list__item__link");
                const resultListItemElement = document.createElement("li");
                resultListItemElement.append(resultLinkElement);
                resultListItemElement.classList.add("search-form__results-list__item");
                resultsListElement.append(resultListItemElement);
            }
        } else {
            const errorText = document.createTextNode(noResultsText);
            const resultListItemElement = document.createElement("li");
            resultListItemElement.append(errorText);
            resultListItemElement.classList.add("search-form__results-list__item", "search-form__results-list__item--error");
            resultsListElement.append(resultListItemElement);
        }

        resultsListElement.show();
    }, "xml");
    // }
    // };
    // xhr.open("GET", "/feed.xml", true);
    // xhr.send();
}

Pliki na stronę dołączam w ten sposób, w pliku default.html:

<script src="{{ site.data.internal-uris.js.search-and-display }}"></script>
<script src="{{ site.data.internal-uris.js.search }}"></script>

Wartości w {{}} to są zmienne Liquida, są one przetwarzane na ciągi znaków pobierane z pliku data/internal-uris.json.

Źródło strony głównej index.html wygląda w ten sposób:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="/js/display-search-results.js"></script>
<script src="/js/search.js"></script>

1 użytkowników online, w tym zalogowanych: 0, gości: 1