Site icon Blog ARC Optimizer

Recherche de votre site sans base de données

Recherche de votre site sans base de données


Apprenez à travailler avec la recherche floue, la notation et plusieurs champs et permettez à votre application Nuxt de rechercher des données statiques.

Avez-vous déjà vu des sites Web de documentation qui ont une fonction de recherche et c’est super rapide? Eh bien, les novices décident d’utiliser une vraie base de données comme des algolies et potentiellement le payer. Les pros utilisent Pure JavaScript.

TL; Dr

Apprenez à créer une application Nuxt qui peut rechercher des données statiques. Définissez les données en tant qu’objet, puis filtrez les résultats en temps réel pour obtenir la page souhaitée. Cette démo ajoute une recherche floue, utilise la notation et gère plusieurs champs.

Nuxt

Cette démo utilise Nuxt et suppose que vous avez une compréhension de base des composiables, des pages et des dispositions.

Mise en page

Créez la disposition par défaut.

// layouts/default.vue

<template>
  <main class="min-h-screen bg-gray-100 text-gray-800">
    <div class="flex flex-col items-center py-8">
      <img src="https://www.telerik.com/bttf.webp" alt="Back to the Future man!" />
    </div>
    <div class="max-w-4xl mx-auto px-6">
      <slot />
    </div>
    <nav class="bg-white border-t border-gray-200 mt-12 py-8">
      <div class="max-w-4xl mx-auto px-6">
        <h2 class="text-xl font-bold text-blue-700 mb-4">
          Back to the Future Archive
        </h2>
        <ul class="grid grid-cols-1 sm:grid-cols-2 gap-3">
          <li>
            <NuxtLink to="https://www.telerik.com/" class="hover:underline text-blue-600">
              Home
            </NuxtLink>
          </li>
          <li>
            <NuxtLink
              to="/vehicles/delorean"
              class="hover:underline text-blue-600"
            >
              The DeLorean Time Machine
            </NuxtLink>
          </li>
          <li>
            <NuxtLink
              to="/characters/marty"
              class="hover:underline text-blue-600"
            >
              Marty McFly
            </NuxtLink>
          </li>
          <li>
            <NuxtLink
              to="/characters/doc-brown"
              class="hover:underline text-blue-600"
            >
              Doc Emmett Brown
            </NuxtLink>
          </li>
          <li>
            <NuxtLink
              to="/timeline/hill-valley"
              class="hover:underline text-blue-600"
            >
              Hill Valley Timeline
            </NuxtLink>
          </li>
          <li>
            <NuxtLink
              to="/tech/hoverboard"
              class="hover:underline text-blue-600"
            >
              Hoverboard Technology
            </NuxtLink>
          </li>
          <li>
            <NuxtLink
              to="/characters/biff"
              class="hover:underline text-blue-600"
            >
              Biff Tannen's Antagonism
            </NuxtLink>
          </li>
          <li>
            <NuxtLink
              to="/tech/flux-capacitor"
              class="hover:underline text-blue-600"
            >
              Flux Capacitor
            </NuxtLink>
          </li>
          <li>
            <NuxtLink
              to="/timeline/1985-vs-2015"
              class="hover:underline text-blue-600"
            >
              1985 vs. 2015
            </NuxtLink>
          </li>
          <li>
            <NuxtLink
              to="/events/enchantment-dance"
              class="hover:underline text-blue-600"
            >
              Enchantment Under the Sea Dance
            </NuxtLink>
          </li>
          <li>
            <NuxtLink
              to="/shop/merchandise"
              class="hover:underline text-blue-600"
            >
              Back to the Future Merchandise
            </NuxtLink>
          </li>
        </ul>
      </div>
    </nav>
  </main>
</template>

Ce ne sont que quelques liens vers les pages futures.

Créer une route fourre-tout

Je n’avais pas envie de créer manuellement un tas de pages, donc je les ai juste générés. Dans une application réelle, vous aurez probablement généré des fichiers statiques créés dans Markdown. Si vous générez des pages à partir d’une base de données, vous utiliseriez probablement la base de données elle-même pour rechercher.

// pages/[...slug].vue

<script setup lang="ts">
import { useRoute } from "vue-router";

const route = useRoute();

const fullPath = "https://www.telerik.com/" + (route.params.slug as string[]).join("https://www.telerik.com/");
const page = items.find((item) => item.url === fullPath);
</script>

<template>
  <div v-if="page">
    <h1>{{ page.title }}</h1>
    <p>{{ page.description }}</p>
  </div>
  <div v-else>
    <h1>404 - Page Not Found</h1>
    <p>The requested page does not exist.</p>
  </div>
</template>

Encore une fois, c’est à des fins de démonstration uniquement.

Recherche: les données réelles

Nous voulons stocker nos données dans un grand objet.

// composables/useSearch.ts

type BTTFItem = {
    title: string;
    description: string;
    url: string;
};

export const items: BTTFItem[] = [
    {
        title: "The DeLorean Time Machine",
        description: "Explore the iconic DeLorean, the time-traveling car built by Doc Brown.",
        url: "/vehicles/delorean"
    },
    {
        title: "Marty McFly",
        description: "Learn all about the skateboarding teenager who travels through time.",
        url: "/characters/marty"
    },
    {
        title: "Doc Emmett Brown",
        description: "Meet the eccentric inventor behind the time machine.",
        url: "/characters/doc-brown"
    },
    {
        title: "Hill Valley Timeline",
        description: "A deep dive into the changing history of Hill Valley across the trilogy.",
        url: "/timeline/hill-valley"
    },
    {
        title: "Hoverboard Technology",
        description: "Discover the future of personal transportation with hoverboards.",
        url: "/tech/hoverboard"
    },
    {
        title: "Biff Tannen's Antagonism",
        description: "Explore the many timelines where Biff makes life difficult for Marty.",
        url: "/characters/biff"
    },
    {
        title: "Flux Capacitor",
        description: "The core component that makes time travel possible.",
        url: "/tech/flux-capacitor"
    },
    {
        title: "1985 vs. 2015",
        description: "Compare the original 1985 to the future version envisioned in Part II.",
        url: "/timeline/1985-vs-2015"
    },
    {
        title: "Enchantment Under the Sea Dance",
        description: "The pivotal high school dance that almost erased Marty from existence.",
        url: "/events/enchantment-dance"
    },
    {
        title: "Back to the Future Merchandise",
        description: "Browse collectibles, clothes, and posters from the BTTF universe.",
        url: "/shop/merchandise"
    }
];

Comment fonctionne la recherche

Il n’y a pas de secret à rechercher dans la vanille JS. Nous utilisons un filtre.

items.filter(item => item.title.includes(q));

Maintenant, nous devons vérifier les minuscules, vérifier à la fois le titre et la description, gérer l’espace blanc avant et après et gérer les signaux.

utilisation

Pour notre première version la plus simple, nous créons un composable.

export function useSearch() {

    const query = ref('');
    const results = ref<BTTFItem[]>([]);

    function search() {
        const q = query.value.trim().toLowerCase();
        results.value = q === ''
            ? []
            : items.filter(item =>
                item.title.toLowerCase().includes(q) ||
                item.description.toLowerCase().includes(q)
            );
    }
    return {
        query,
        results,
        search
    };
}

Ce composable recherche à travers notre items Arraie et filtre les résultats.

📝 Remarquez le items Le tableau est déclaré à l’extérieur du crochet lui-même, car nous n’avons besoin de déclarer les données statiques qu’une seule fois.

Usage

Nous devons importer le composable et l’utiliser dans notre composant de recherche. Assurez-vous d’utiliser une séparation appropriée des préoccupations et le principe de responsabilité unique.

🙏🏼 Gardez la fonctionnalité dans le composable.

<script setup lang="ts">
const { query, results, search } = useSearch();
</script>

<template>

  <div class="p-6 max-w-xl mx-auto relative">
    <input
      type="text"
      v-model="query"
      @input="search"
      placeholder="Search Back to the Future..."
      class="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
    />

    <transition
      enter-active-class="transition duration-200 ease-out"
      enter-from-class="opacity-0"
      enter-to-class="opacity-100"
      leave-active-class="transition duration-150 ease-in"
      leave-from-class="opacity-100"
      leave-to-class="opacity-0"
    >
      <ul
        v-if="query && results.length"
        class="absolute left-0 right-0 mt-2 z-50 bg-white border border-gray-200 rounded-lg shadow-lg max-h-80 overflow-y-auto"
      >
        <li
          v-for="(item, index) in results"
          :key="index"
          class="hover:bg-gray-50 border-b border-gray-100 last:border-0"
        >
          <NuxtLink
            :to="item.url"
            class="block px-4 py-3 text-blue-600 font-medium"
          >
            {{ item.title }}
          </NuxtLink>
        </li>
      </ul>
    </transition>

    <transition
      enter-active-class="transition duration-200 ease-out"
      enter-from-class="opacity-0"
      enter-to-class="opacity-100"
      leave-active-class="transition duration-150 ease-in"
      leave-from-class="opacity-100"
      leave-to-class="opacity-0"
    >
      <p
        v-if="query && !results.length"
        class="absolute left-0 right-0 mt-2 z-50 bg-white border border-gray-200 rounded-lg shadow-lg px-4 py-3 text-center text-gray-500 italic"
      >
        No results found.
      </p>
    </transition>
  </div>
</template>

📝 Vue a de beaux effets de transition qui fonctionnent très bien avec le vent arrière.

Pour les exemples de filtrage de base:

Je sais, je sais, tu en veux plus! Vous souhaitez également implémenter une recherche floue de base!

Version 1

function fuzzyMatch(source: string, target: string): boolean {
  source = source.toLowerCase();
  target = target.toLowerCase();

  let sIndex = 0;
  for (let i = 0; i < target.length; i++) {
    if (source[sIndex] === target[i]) {
      sIndex++;
    }
    if (sIndex === source.length) {
      return true;
    }
  }
  return false;
}

export function useSearch() {

  const query = ref('');
  const results = ref<BTTFItem[]>([]);

  function search() {
    const q = query.value.trim().toLowerCase();
    if (q === '') {
      results.value = [];
      return;
    }

    results.value = items.filter(item =>
      fuzzyMatch(q, item.title) || fuzzyMatch(q, item.description)
    );
  }

  return {
    query,
    results,
    search
  };
}

Le fuzzyMatch L’algorithme vérifie si tous les caractères correspondent ou non.

Exemple 1

fuzzyMatch("doc", "Doc Emmett Brown") // true
  • Matchs: d → trouvé à l’index 0, o → Index 1, c → Index 2 (tous dans l’ordre ✅)

Exemple 2

fuzzyMatch("mcf", "Marty McFly") // true
  • Matchs: m (Marty), c (Mcfly), f (Mcfly) – tout dans l’ordre ✅

Exemple 3

fuzzyMatch("dcm", "Doc Emmett Brown") // false
  • d matchs, c matchs – mais il n’y a pas m après c Dans la chaîne ❌

Si vous souhaitez ignorer les espaces, vous pouvez ajouter:

source = source.replace(/\s+/g, '').toLowerCase();
target = target.replace(/\s+/g, '').toLowerCase();

Notation

Maintenant, marquons les résultats afin que nous puissions trier la commande.

function fuzzyScore(query: string, text: string) {

    query = query.replace(/\s+/g, '').toLowerCase();
    text = text.replace(/\s+/g, '').toLowerCase();

    let score = 0;
    let lastIndex = -1;

    for (const char of query) {
        const index = text.indexOf(char, lastIndex + 1);
        if (index === -1) return 0;
        score += 1 / (index - lastIndex);
        lastIndex = index;
    }

    return score;
}

export function useSearch() {

    const query = ref('');
    const results = ref<BTTFItem[]>([]);

    function search() {
        const q = query.value.trim().toLowerCase();
        if (!q) {
            results.value = [];
            return;
        }

        results.value = items
            .map(item => {
                const scoreTitle = fuzzyScore(q, item.title);
                const scoreDesc = fuzzyScore(q, item.description);
                const totalScore = scoreTitle * 2 + scoreDesc;
                return { item, score: totalScore };
            })
            .filter(entry => entry.score > 0)
            .sort((a, b) => b.score - a.score)
            .map(entry => entry.item);
    }

    return {
        query,
        results,
        search
    };
}

Au lieu de retourner un booleannous retournons un score et utiliser .sort() à trier par ce score. Cela inclut le description Et le title.

Beau!

Ce n’est que le début de ce que vous pouvez faire, mais maintenant vous avez les fondamentaux!

L’avenir n’est pas encore écrit.

Repo: Girub
Démo: Vercel




Source link
Quitter la version mobile