ReactNode.jsJavaScript

Gatsby-Ghost Suche: Webworker

Erweiterung der Gatsby-Ghost React Suchkomponente um einen Webworker zum Laden und Filtern der Datensätze.

Ein Suchvorgang in Javascript kann je nach Datenstamm zu einen blockierenden UI führen. Es gibt mehrere Möglichkeiten das zu verhindern. In diesem Artikel gehe ich auf die Verwendung eines Webworkers zum Laden der Daten und dem Filtern ein.

Setup

Wir erweitern schlicht die Komponente aus dem Artikel Gatsby-Ghost Suche: React Komponente. Dabei brauchen wird das Gatsby Plugin gatsby-plugin-workerize-loader, welches wir direkt installieren.

$ npm install --save-dev gatsby-plugin-workerize-loader

Zunächst müssen wir das Plugin in gatsby-config.js aktivieren.

gatsby-config.js
module.exports = {
    siteMetadata: {
        siteUrl: config.siteUrl,
    },
    plugins: [
        /**
         *  Content Plugins
         */
        { resolve: `gatsby-plugin-workerize-loader` },
        /* [...] */
    ]
}

Der Webworker

Im Ordner ./src/utils erstellen wir eine JavaScript-Datei mit dem Namen search.worker.js und folgendem Inhalt.

src/utils/search.worker.js
import GhostContentAPI from '@tryghost/content-api';

export async function search(query, max) {
	const api = new GhostContentAPI({
        url: 'deine-ghost-url',
        key: 'dein-ghost-key',
        version: 'v3',
    });
	
    const posts = await api.posts.browse({
        limit: 'all',
        fields: 'title, slug',
        include: 'tags'
    });

    const tags = await api.tags.browse({
        limit: 'all',
        fields: 'name, slug'
    });

    const pages = await api.pages.browse({
        limit: 'all',
        fields: 'title, slug',
        include: 'tags'
    });

    tags.forEach((tag, index, arr) => {
        arr[index] = {
            title: tag.name,
            slug: `tag/${tag.slug}`,
            tags: [],
            id: `${index}5s73a53051c64106bd408bcc`
        }
    });    
    
    const data = [...posts, ...tags, ...pages];

    const results =
                data.filter((obj) => {
                    if(obj.title.toLowerCase().includes(query))
                        return true;
                    
                    for(let i = 0; i < obj.tags.length; i++) {
                        if(obj.tags[i].name.toLowerCase().includes(query))
                            return true;
                    }
                })
                .slice(0, max);

     return results;
}

Das Laden der Daten aus der useEffect() Funktion und das Filtern des onChange Events der React Komponente sind hier in einer Funktion zusammengeführt. Die Funktion wird später im Thread des Webworkers ausgeführt.

Anpassen der React Komponente

Das Modul @tryghost/content-api wird hier nicht mehr benötigt und daher enfernen wir den import. Dafür importieren wir das Script für den Webworker. Das Laden der Datensätze entfernen wir komplett.

src/components/search/index.js
import React, {useRef, useEffect, useState} from 'react';
import PropTypes from 'prop-types';
import {Link} from 'gatsby';
import {MdSearch} from 'react-icons/md';
-import GhostContentAPI from '@tryghost/content-api';
+import SearchWorker from '../../utils/search.worker.js';
import './search.css';

-const ghostConfig = require('../../../.ghost.json');

const Search = ({maxResults = 10}) => {
    const [result, setResult] = useState([]);
-   const [data, setData] = useState([]);
    const [active, setActive] = useState(false);
    const containerElement = useRef(null);
    const inputElement = useRef(null);
+   const searchWorkerRef = useRef(null);
-   const api = new GhostContentAPI({
-       url: process.env.NODE_ENV === `development` 
-         ? ghostConfig.development.apiUrl 
-         : ghostConfig.production.apiUrl,
-       key: process.env.NODE_ENV === `development` 
-         ? ghostConfig.development.contentApiKey 
-         : ghostConfig.production.apiKey,
-       version: 'v3',
-   });
    
    /* ... */

-	useEffect(() => {
-        const fetchedData = async () => {
-            const posts = await api.posts.browse({
-                limit: 'all',
-                fields: 'title, slug',
-                include: 'tags'
-            });
-
-            const tags = await api.tags.browse({
-                limit: 'all',
-                fields: 'name, slug'
-            });
-
-            const pages = await api.pages.browse({
-                limit: 'all',
-                fields: 'title, slug',
-                include: 'tags'
-            });
-
-            tags.forEach((tag, index, arr) => {
-                arr[index] = {
-                    title: tag.name,
-                    slug: `tag/${tag.slug}`,
-                    tags: [],
-                    id: `${index}5s73a53051c64106bd408bcc`
-                }
-            });            
-
-            setData([...posts, ...tags, ...pages]);
-        }
-
-        fetchedData();
-    
-        return () => {
-            setData([]);
-        }
-	}, [])   

	/* ... */
}

export default Search;

Search.propTypes = {
    maxResults: PropTypes.number
}

Initialisieren des Webworker

Mit einer kleinen Funktion initialisieren wir den Webworker, falls das denn nötig ist.

src/components/search/index.js
function getSearchWorker() {
    if(!searchWorkerRef.current) {
        searchWorkerRef.current = new SearchWorker();
    }
    return searchWorkerRef.current;
}

Anpassen des onChange Events

Die change Funktion wird wie folgend abgeändert. So initialisiert sie bei Bedarf den Webworker und führt die search Funktion aus

src/components/search/index.js
async function change() {
    const query = inputElement.current.value.toLowerCase();

    if(query.length > 1) {
        const results = 
              await getSearchWorker().search(query, maxResults);

        if(results.length > 0) {
            setResult(results);
        }
        else {
            setResult([]);
        }
    }
    else {
        setResult([]);
    }
}

Die Datensätze mittels Ghost-Content-API zu laden birgt einige Nachteile. Wir benötigen alles im Allem 3 Requests und können beim Request auch nicht anständig die Datensätze filtern. Dadurch entsteht ein ordentlicher Overhead, den wir vermeiden können. Mehr erfahren Sie im Artikel Gatsby-Ghost Suche: Content indizieren.


Weitere Blog Posts aus dieser Serie

  1. Gatsby-Ghost Suche: React Komponente
  2. Gatsby-Ghost Suche: Webworker
  3. Gatsby-Ghost Suche: Content indizieren
  4. Gatsby-Ghost Suche: Redis und RediSearch (Teil 1)
  5. Gatsby-Ghost Suche: Redis und RediSearch (Teil 2)
  6. Gatsby-Ghost Suche: Redis und RediSearch (Teil 3)