Fuse.js:打造极致模糊搜索体验

Fuse.js 完全学习指南:JavaScript模糊搜索库

🎯 什么是 Fuse.js?

Fuse.js 是一个轻量、强大且无依赖的JavaScript模糊搜索库。它提供了简单而强大的模糊搜索功能,可以在任何 JavaScript 环境中使用,包括浏览器和 Node.js。

🌟 核心特点

  • 轻量级:压缩后仅 ~12KB,无外部依赖
  • 模糊搜索:支持拼写错误、部分匹配等容错搜索
  • 高度可配置:提供丰富的配置选项控制搜索行为
  • 多字段搜索:支持在对象的多个字段中搜索
  • 权重系统:不同字段可以设置不同的搜索权重
  • 高亮显示:支持搜索结果高亮显示
  • 跨平台:支持浏览器、Node.js、Deno等环境
  • TypeScript支持:提供完整的TypeScript类型定义

📦 安装与引入

NPM 安装

bash 复制代码
# 使用 npm
npm install fuse.js

# 使用 yarn
yarn add fuse.js

引入方式

ES6 模块语法
javascript 复制代码
import Fuse from 'fuse.js'
CommonJS
javascript 复制代码
const Fuse = require('fuse.js')
直接 <script> 引入
html 复制代码
<!-- 开发版本 -->
<script src="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.js"></script>

<!-- 生产版本(推荐) -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/fuse.min.js"></script>

<!-- ES 模块版本 -->
<script type="module">
  import Fuse from 'https://cdn.jsdelivr.net/npm/[email protected]/dist/fuse.mjs'
</script>
Deno
typescript 复制代码
// @deno-types="https://deno.land/x/[email protected]/dist/fuse.d.ts"
import Fuse from 'https://deno.land/x/[email protected]/dist/fuse.min.mjs'

🚀 基础使用

1. 简单数组搜索

javascript 复制代码
// 创建简单的字符串数组
const books = [
  "老人与海",
  "百年孤独", 
  "哈利·波特",
  "三体",
  "1984",
  "了不起的盖茨比"
]

// 创建 Fuse 实例
const fuse = new Fuse(books)

// 执行搜索
const result = fuse.search('盖茨比')
console.log(result)
/*
输出:
[
  {
    item: "了不起的盖茨比",
    refIndex: 5
  }
]
*/

2. 对象数组搜索

javascript 复制代码
// 创建对象数组
const books = [
  {
    title: "老人与海",
    author: "海明威",
    year: 1952,
    genre: "小说"
  },
  {
    title: "百年孤独",
    author: "马尔克斯", 
    year: 1967,
    genre: "魔幻现实主义"
  },
  {
    title: "三体",
    author: "刘慈欣",
    year: 2006,
    genre: "科幻"
  }
]

// 指定搜索字段
const options = {
  keys: ['title', 'author']
}

const fuse = new Fuse(books, options)

// 搜索示例
const result = fuse.search('刘慈')
console.log(result)
/*
输出:
[
  {
    item: {
      title: "三体",
      author: "刘慈欣",
      year: 2006,
      genre: "科幻"
    },
    refIndex: 2
  }
]
*/

⚙️ 配置选项详解

基础搜索配置

javascript 复制代码
const options = {
  // 是否按分数排序结果
  shouldSort: true,
  
  // 包含匹配结果的元数据
  includeMatches: true,
  
  // 包含分数信息
  includeScore: true,
  
  // 分数阈值(0.0 = 完全匹配,1.0 = 匹配任何内容)
  threshold: 0.3,
  
  // 搜索位置
  location: 0,
  
  // 搜索距离
  distance: 100,
  
  // 最小匹配字符长度
  minMatchCharLength: 1,
  
  // 是否查找所有匹配
  findAllMatches: false
}

搜索字段配置

javascript 复制代码
const options = {
  keys: [
    // 简单字段
    'title',
    'author',
    
    // 带权重的字段(权重范围 0-1)
    {
      name: 'title',
      weight: 0.8  // 标题权重更高
    },
    {
      name: 'author', 
      weight: 0.2  // 作者权重较低
    },
    
    // 嵌套字段
    'author.firstName',
    'author.lastName',
    
    // 带权重的嵌套字段
    {
      name: 'tags',
      weight: 0.5
    }
  ]
}

高级配置选项

javascript 复制代码
const options = {
  // 忽略大小写
  isCaseSensitive: false,
  
  // 忽略变音符号
  ignoreLocation: false,
  
  // 忽略字段长度规范
  ignoreFieldNorm: false,
  
  // 字段长度规范影响因子
  fieldNormWeight: 1,
  
  // 搜索算法选择
  useExtendedSearch: false
}

🔍 搜索功能详解

1. 基础模糊搜索

javascript 复制代码
const books = [
  { title: "JavaScript高级程序设计" },
  { title: "Vue.js实战" },
  { title: "React技术栈开发" },
  { title: "Node.js权威指南" }
]

const fuse = new Fuse(books, {
  keys: ['title'],
  threshold: 0.4
})

// 模糊搜索(允许拼写错误)
console.log(fuse.search('javscript'))  // 找到 "JavaScript高级程序设计"
console.log(fuse.search('vue'))        // 找到 "Vue.js实战"
console.log(fuse.search('react'))      // 找到 "React技术栈开发"

2. 扩展搜索语法

javascript 复制代码
const options = {
  keys: ['title', 'author'],
  useExtendedSearch: true
}

const fuse = new Fuse(books, options)

// 精确匹配
console.log(fuse.search('="Node.js"'))

// 包含搜索
console.log(fuse.search("'js"))

// 排除搜索
console.log(fuse.search('!vue'))

// 前缀搜索
console.log(fuse.search('^java'))

// 后缀搜索
console.log(fuse.search('.js$'))

// 逻辑运算符
console.log(fuse.search('javascript | vue'))  // OR
console.log(fuse.search('node !express'))     // AND NOT

3. 获取搜索匹配详情

javascript 复制代码
const options = {
  keys: ['title'],
  includeMatches: true,
  includeScore: true,
  threshold: 0.3
}

const fuse = new Fuse(books, options)
const result = fuse.search('javascript')

console.log(result)
/*
输出:
[
  {
    item: { title: "JavaScript高级程序设计" },
    refIndex: 0,
    score: 0.001,
    matches: [
      {
        indices: [[0, 9]],
        value: "JavaScript高级程序设计",
        key: "title"
      }
    ]
  }
]
*/

🎨 实际应用案例

案例1:书籍搜索系统

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>图书搜索系统</title>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/fuse.min.js"></script>
    <style>
        .search-container {
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }
        
        .search-box {
            width: 100%;
            padding: 12px;
            font-size: 16px;
            border: 2px solid #ddd;
            border-radius: 8px;
            margin-bottom: 20px;
        }
        
        .book-item {
            border: 1px solid #eee;
            border-radius: 8px;
            padding: 15px;
            margin-bottom: 10px;
            background: white;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        
        .book-title {
            font-size: 18px;
            font-weight: bold;
            color: #333;
            margin-bottom: 5px;
        }
        
        .book-meta {
            color: #666;
            font-size: 14px;
        }
        
        .highlight {
            background-color: yellow;
            font-weight: bold;
        }
        
        .no-results {
            text-align: center;
            color: #999;
            padding: 50px;
        }
    </style>
</head>
<body>
    <div class="search-container">
        <h1>📚 图书搜索系统</h1>
        <input type="text" class="search-box" placeholder="搜索书名、作者或分类..." id="searchInput">
        <div id="results"></div>
    </div>

    <script>
        // 书籍数据
        const books = [
            {
                title: "JavaScript高级程序设计",
                author: "Nicholas C. Zakas",
                year: 2020,
                category: "编程",
                rating: 4.8,
                description: "JavaScript开发者必读经典,深入解析语言特性"
            },
            {
                title: "Vue.js实战",
                author: "梁灏",
                year: 2019,
                category: "前端",
                rating: 4.6,
                description: "Vue.js框架实战指南,从入门到精通"
            },
            {
                title: "三体",
                author: "刘慈欣",
                year: 2006,
                category: "科幻",
                rating: 4.9,
                description: "获得雨果奖的中国科幻小说代表作"
            },
            {
                title: "百年孤独",
                author: "加西亚·马尔克斯",
                year: 1967,
                category: "文学",
                rating: 4.7,
                description: "魔幻现实主义文学的巅峰之作"
            },
            {
                title: "设计模式",
                author: "Gang of Four",
                year: 1994,
                category: "编程",
                rating: 4.5,
                description: "软件工程中的设计模式经典教材"
            }
        ]

        // 配置 Fuse.js
        const options = {
            keys: [
                { name: 'title', weight: 0.4 },
                { name: 'author', weight: 0.3 },
                { name: 'category', weight: 0.2 },
                { name: 'description', weight: 0.1 }
            ],
            threshold: 0.3,
            includeMatches: true,
            includeScore: true
        }

        const fuse = new Fuse(books, options)

        // 高亮显示匹配文本
        function highlightMatches(text, matches) {
            if (!matches || matches.length === 0) return text
            
            let highlighted = text
            const indices = matches[0].indices
            
            // 从后往前替换,避免索引偏移
            for (let i = indices.length - 1; i >= 0; i--) {
                const [start, end] = indices[i]
                highlighted = highlighted.slice(0, start) + 
                            '<span class="highlight">' + 
                            highlighted.slice(start, end + 1) + 
                            '</span>' + 
                            highlighted.slice(end + 1)
            }
            
            return highlighted
        }

        // 渲染搜索结果
        function renderResults(results) {
            const resultsContainer = document.getElementById('results')
            
            if (results.length === 0) {
                resultsContainer.innerHTML = '<div class="no-results">没有找到相关书籍</div>'
                return
            }

            const html = results.map(result => {
                const book = result.item
                const matches = result.matches || []
                
                // 获取标题和作者的匹配高亮
                const titleMatch = matches.find(m => m.key === 'title')
                const authorMatch = matches.find(m => m.key === 'author')
                
                const highlightedTitle = titleMatch ? 
                    highlightMatches(book.title, [titleMatch]) : book.title
                const highlightedAuthor = authorMatch ? 
                    highlightMatches(book.author, [authorMatch]) : book.author

                return `
                    <div class="book-item">
                        <div class="book-title">${highlightedTitle}</div>
                        <div class="book-meta">
                            作者:${highlightedAuthor} | 
                            年份:${book.year} | 
                            分类:${book.category} | 
                            评分:${book.rating}⭐
                        </div>
                        <div style="margin-top: 8px; color: #666; font-size: 14px;">
                            ${book.description}
                        </div>
                    </div>
                `
            }).join('')

            resultsContainer.innerHTML = html
        }

        // 搜索处理
        function handleSearch() {
            const query = document.getElementById('searchInput').value.trim()
            
            if (query === '') {
                renderResults(books.map((book, index) => ({ item: book, refIndex: index })))
                return
            }

            const results = fuse.search(query)
            renderResults(results)
        }

        // 绑定事件
        document.getElementById('searchInput').addEventListener('input', handleSearch)

        // 初始显示所有书籍
        renderResults(books.map((book, index) => ({ item: book, refIndex: index })))
    </script>
</body>
</html>

案例2:Vue.js 集成搜索组件

vue 复制代码
<template>
  <div class="search-component">
    <div class="search-header">
      <input
        v-model="searchQuery"
        @input="handleSearch"
        class="search-input"
        placeholder="搜索员工姓名、部门或技能..."
        autocomplete="off"
      >
      <div class="search-stats" v-if="searchQuery">
        找到 {{ filteredEmployees.length }} 个结果
      </div>
    </div>

    <div class="filters">
      <button 
        v-for="dept in departments" 
        :key="dept"
        @click="filterByDepartment(dept)"
        :class="['filter-btn', { active: selectedDepartment === dept }]"
      >
        {{ dept }}
      </button>
    </div>

    <div class="results-container">
      <div 
        v-for="employee in filteredEmployees" 
        :key="employee.item.id"
        class="employee-card"
      >
        <div class="employee-avatar">
          {{ employee.item.name.charAt(0) }}
        </div>
        <div class="employee-info">
          <h3 v-html="highlightText(employee.item.name, employee.matches, 'name')"></h3>
          <p class="department">{{ employee.item.department }}</p>
          <div class="skills">
            <span 
              v-for="skill in employee.item.skills" 
              :key="skill"
              class="skill-tag"
              v-html="highlightText(skill, employee.matches, 'skills')"
            >
            </span>
          </div>
          <div class="contact">
            📧 {{ employee.item.email }} | 📞 {{ employee.item.phone }}
          </div>
        </div>
        <div class="score" v-if="employee.score">
          匹配度: {{ Math.round((1 - employee.score) * 100) }}%
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import Fuse from 'fuse.js'

export default {
  name: 'EmployeeSearch',
  data() {
    return {
      searchQuery: '',
      selectedDepartment: 'all',
      employees: [
        {
          id: 1,
          name: '张三',
          department: '技术部',
          email: '[email protected]',
          phone: '138****1234',
          skills: ['JavaScript', 'Vue.js', 'Node.js', 'Python']
        },
        {
          id: 2,
          name: '李四',
          department: '设计部',
          email: '[email protected]',
          phone: '139****5678',
          skills: ['Photoshop', 'Figma', 'UI设计', '用户体验']
        },
        {
          id: 3,
          name: '王五',
          department: '产品部',
          email: '[email protected]',
          phone: '137****9012',
          skills: ['产品规划', '数据分析', 'Axure', 'SQL']
        },
        {
          id: 4,
          name: '赵六',
          department: '技术部',
          email: '[email protected]',
          phone: '136****3456',
          skills: ['React', 'TypeScript', 'GraphQL', 'MongoDB']
        }
      ],
      filteredEmployees: [],
      fuse: null
    }
  },
  computed: {
    departments() {
      const depts = ['all', ...new Set(this.employees.map(emp => emp.department))]
      return depts
    }
  },
  mounted() {
    this.initializeFuse()
    this.filteredEmployees = this.employees.map((emp, index) => ({
      item: emp,
      refIndex: index
    }))
  },
  methods: {
    initializeFuse() {
      const options = {
        keys: [
          { name: 'name', weight: 0.4 },
          { name: 'department', weight: 0.2 },
          { name: 'skills', weight: 0.3 },
          { name: 'email', weight: 0.1 }
        ],
        threshold: 0.3,
        includeMatches: true,
        includeScore: true,
        minMatchCharLength: 1
      }
      
      this.fuse = new Fuse(this.employees, options)
    },
    
    handleSearch() {
      if (!this.searchQuery.trim()) {
        this.filteredEmployees = this.employees.map((emp, index) => ({
          item: emp,
          refIndex: index
        }))
        this.applyDepartmentFilter()
        return
      }

      const results = this.fuse.search(this.searchQuery)
      this.filteredEmployees = results
      this.applyDepartmentFilter()
    },
    
    filterByDepartment(department) {
      this.selectedDepartment = department
      this.applyDepartmentFilter()
    },
    
    applyDepartmentFilter() {
      if (this.selectedDepartment === 'all') return
      
      this.filteredEmployees = this.filteredEmployees.filter(
        emp => emp.item.department === this.selectedDepartment
      )
    },
    
    highlightText(text, matches, key) {
      if (!matches || !Array.isArray(text)) {
        const match = matches?.find(m => m.key === key)
        if (!match) return text
        
        let highlighted = text
        const indices = match.indices
        
        for (let i = indices.length - 1; i >= 0; i--) {
          const [start, end] = indices[i]
          highlighted = highlighted.slice(0, start) + 
                      '<mark>' + 
                      highlighted.slice(start, end + 1) + 
                      '</mark>' + 
                      highlighted.slice(end + 1)
        }
        
        return highlighted
      }
      
      return text
    }
  }
}
</script>

<style scoped>
.search-component {
  max-width: 1000px;
  margin: 0 auto;
  padding: 20px;
}

.search-header {
  margin-bottom: 20px;
}

.search-input {
  width: 100%;
  padding: 12px 16px;
  font-size: 16px;
  border: 2px solid #e1e5e9;
  border-radius: 8px;
  outline: none;
  transition: border-color 0.3s;
}

.search-input:focus {
  border-color: #007bff;
}

.search-stats {
  margin-top: 8px;
  color: #666;
  font-size: 14px;
}

.filters {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.filter-btn {
  padding: 8px 16px;
  border: 1px solid #ddd;
  background: white;
  border-radius: 20px;
  cursor: pointer;
  transition: all 0.3s;
}

.filter-btn:hover,
.filter-btn.active {
  background: #007bff;
  color: white;
  border-color: #007bff;
}

.results-container {
  display: grid;
  gap: 15px;
}

.employee-card {
  display: flex;
  align-items: center;
  padding: 20px;
  border: 1px solid #e1e5e9;
  border-radius: 8px;
  background: white;
  transition: box-shadow 0.3s;
}

.employee-card:hover {
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}

.employee-avatar {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  background: #007bff;
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 18px;
  font-weight: bold;
  margin-right: 15px;
}

.employee-info {
  flex: 1;
}

.employee-info h3 {
  margin: 0 0 5px 0;
  font-size: 18px;
}

.department {
  color: #666;
  margin: 0 0 10px 0;
}

.skills {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-bottom: 10px;
}

.skill-tag {
  background: #f8f9fa;
  padding: 4px 8px;
  border-radius: 12px;
  font-size: 12px;
  color: #495057;
}

.contact {
  font-size: 14px;
  color: #666;
}

.score {
  text-align: right;
  color: #28a745;
  font-weight: bold;
}

/* 高亮样式 */
:deep(mark) {
  background-color: #ffeb3b;
  color: #333;
  padding: 1px 2px;
  border-radius: 2px;
}
</style>

案例3:React Hooks 集成

jsx 复制代码
import React, { useState, useMemo, useCallback } from 'react'
import Fuse from 'fuse.js'

// 自定义 Hook:useFuseSearch
function useFuseSearch(data, options) {
  const [query, setQuery] = useState('')
  
  const fuse = useMemo(() => {
    return new Fuse(data, options)
  }, [data, options])
  
  const results = useMemo(() => {
    if (!query.trim()) {
      return data.map((item, index) => ({ item, refIndex: index }))
    }
    return fuse.search(query)
  }, [fuse, query, data])
  
  return {
    query,
    setQuery,
    results,
    search: useCallback((searchQuery) => {
      return fuse.search(searchQuery)
    }, [fuse])
  }
}

// 主搜索组件
function ProductSearch() {
  const products = [
    {
      id: 1,
      name: 'MacBook Pro 16寸',
      brand: 'Apple',
      category: '笔记本电脑',
      price: 16999,
      tags: ['高性能', '创作', '专业']
    },
    {
      id: 2,
      name: 'iPhone 14 Pro',
      brand: 'Apple', 
      category: '智能手机',
      price: 7999,
      tags: ['摄影', '5G', '高端']
    },
    {
      id: 3,
      name: 'Surface Laptop 5',
      brand: 'Microsoft',
      category: '笔记本电脑',
      price: 8888,
      tags: ['轻薄', '办公', '便携']
    },
    {
      id: 4,
      name: 'Galaxy S23 Ultra',
      brand: 'Samsung',
      category: '智能手机',
      price: 9999,
      tags: ['大屏', 'S Pen', '摄影']
    }
  ]

  const searchOptions = {
    keys: [
      { name: 'name', weight: 0.4 },
      { name: 'brand', weight: 0.3 },
      { name: 'category', weight: 0.2 },
      { name: 'tags', weight: 0.1 }
    ],
    threshold: 0.3,
    includeMatches: true,
    includeScore: true
  }

  const { query, setQuery, results } = useFuseSearch(products, searchOptions)
  const [sortBy, setSortBy] = useState('relevance')

  // 排序结果
  const sortedResults = useMemo(() => {
    const sorted = [...results]
    
    switch (sortBy) {
      case 'price-asc':
        return sorted.sort((a, b) => a.item.price - b.item.price)
      case 'price-desc':
        return sorted.sort((a, b) => b.item.price - a.item.price)
      case 'name':
        return sorted.sort((a, b) => a.item.name.localeCompare(b.item.name))
      default:
        return sorted // 保持相关性排序
    }
  }, [results, sortBy])

  // 高亮匹配文本
  const highlightMatches = (text, matches) => {
    if (!matches || matches.length === 0) return text
    
    const match = matches[0]
    if (!match) return text
    
    let highlighted = text
    const indices = match.indices
    
    for (let i = indices.length - 1; i >= 0; i--) {
      const [start, end] = indices[i]
      highlighted = highlighted.slice(0, start) + 
                  '<mark style="background: #ffeb3b; padding: 1px 2px; border-radius: 2px;">' + 
                  highlighted.slice(start, end + 1) + 
                  '</mark>' + 
                  highlighted.slice(end + 1)
    }
    
    return highlighted
  }

  return (
    <div style={{ maxWidth: '800px', margin: '50px auto', padding: '20px' }}>
      <h1>🛍️ 产品搜索</h1>
      
      {/* 搜索栏 */}
      <div style={{ marginBottom: '20px' }}>
        <input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="搜索产品名称、品牌或分类..."
          style={{
            width: '100%',
            padding: '12px',
            fontSize: '16px',
            border: '2px solid #ddd',
            borderRadius: '8px',
            outline: 'none'
          }}
        />
      </div>

      {/* 排序选项 */}
      <div style={{ marginBottom: '20px', display: 'flex', gap: '10px', alignItems: 'center' }}>
        <span>排序:</span>
        <select 
          value={sortBy} 
          onChange={(e) => setSortBy(e.target.value)}
          style={{ padding: '5px 10px', borderRadius: '4px', border: '1px solid #ddd' }}
        >
          <option value="relevance">相关性</option>
          <option value="price-asc">价格从低到高</option>
          <option value="price-desc">价格从高到低</option>
          <option value="name">名称</option>
        </select>
        <span style={{ marginLeft: 'auto', color: '#666' }}>
          找到 {sortedResults.length} 个结果
        </span>
      </div>

      {/* 搜索结果 */}
      <div style={{ display: 'grid', gap: '15px' }}>
        {sortedResults.map((result) => {
          const product = result.item
          const matches = result.matches || []
          
          const nameMatch = matches.find(m => m.key === 'name')
          const brandMatch = matches.find(m => m.key === 'brand')
          
          return (
            <div
              key={product.id}
              style={{
                border: '1px solid #eee',
                borderRadius: '8px',
                padding: '20px',
                background: 'white',
                boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
              }}
            >
              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
                <div style={{ flex: 1 }}>
                  <h3 
                    style={{ margin: '0 0 5px 0', fontSize: '18px' }}
                    dangerouslySetInnerHTML={{
                      __html: nameMatch ? highlightMatches(product.name, [nameMatch]) : product.name
                    }}
                  />
                  <p style={{ margin: '0 0 10px 0', color: '#666' }}>
                    品牌:
                    <span dangerouslySetInnerHTML={{
                      __html: brandMatch ? highlightMatches(product.brand, [brandMatch]) : product.brand
                    }} />
                    {' | '}
                    分类:{product.category}
                  </p>
                  <div style={{ display: 'flex', gap: '5px', marginBottom: '10px' }}>
                    {product.tags.map(tag => (
                      <span
                        key={tag}
                        style={{
                          background: '#f0f0f0',
                          padding: '2px 8px',
                          borderRadius: '12px',
                          fontSize: '12px',
                          color: '#666'
                        }}
                      >
                        {tag}
                      </span>
                    ))}
                  </div>
                </div>
                <div style={{ textAlign: 'right' }}>
                  <div style={{ fontSize: '20px', fontWeight: 'bold', color: '#e74c3c' }}>
                    ¥{product.price.toLocaleString()}
                  </div>
                  {result.score && (
                    <div style={{ fontSize: '12px', color: '#999', marginTop: '5px' }}>
                      匹配度: {Math.round((1 - result.score) * 100)}%
                    </div>
                  )}
                </div>
              </div>
            </div>
          )
        })}
      </div>

      {sortedResults.length === 0 && query && (
        <div style={{ textAlign: 'center', padding: '50px', color: '#999' }}>
          没有找到匹配的产品
        </div>
      )}
    </div>
  )
}

export default ProductSearch

🔧 性能优化建议

1. 大数据集处理

javascript 复制代码
// 对于大数据集,考虑使用 Web Workers
class FuseWorker {
  constructor(data, options) {
    this.worker = new Worker('/fuse-worker.js')
    this.worker.postMessage({ type: 'init', data, options })
  }
  
  search(query) {
    return new Promise((resolve) => {
      this.worker.onmessage = (e) => {
        if (e.data.type === 'search-result') {
          resolve(e.data.results)
        }
      }
      this.worker.postMessage({ type: 'search', query })
    })
  }
}

// fuse-worker.js
let fuse

self.onmessage = function(e) {
  const { type, data, options, query } = e.data
  
  if (type === 'init') {
    importScripts('https://cdn.jsdelivr.net/npm/[email protected]/dist/fuse.min.js')
    fuse = new Fuse(data, options)
  }
  
  if (type === 'search' && fuse) {
    const results = fuse.search(query)
    self.postMessage({ type: 'search-result', results })
  }
}

2. 防抖搜索

javascript 复制代码
// 使用防抖避免频繁搜索
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value)
  
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value)
    }, delay)
    
    return () => {
      clearTimeout(handler)
    }
  }, [value, delay])
  
  return debouncedValue
}

// 在组件中使用
function SearchComponent() {
  const [query, setQuery] = useState('')
  const debouncedQuery = useDebounce(query, 300)
  
  const results = useMemo(() => {
    if (!debouncedQuery) return []
    return fuse.search(debouncedQuery)
  }, [debouncedQuery])
  
  // ...
}

3. 结果缓存

javascript 复制代码
// 简单的 LRU 缓存实现
class LRUCache {
  constructor(capacity) {
    this.capacity = capacity
    this.cache = new Map()
  }
  
  get(key) {
    if (this.cache.has(key)) {
      const value = this.cache.get(key)
      this.cache.delete(key)
      this.cache.set(key, value)
      return value
    }
    return null
  }
  
  set(key, value) {
    if (this.cache.has(key)) {
      this.cache.delete(key)
    } else if (this.cache.size >= this.capacity) {
      const firstKey = this.cache.keys().next().value
      this.cache.delete(firstKey)
    }
    this.cache.set(key, value)
  }
}

// 带缓存的搜索函数
const searchCache = new LRUCache(100)

function cachedSearch(fuse, query) {
  const cached = searchCache.get(query)
  if (cached) return cached
  
  const results = fuse.search(query)
  searchCache.set(query, results)
  return results
}

🛠️ 高级功能

1. 自定义评分函数

javascript 复制代码
const options = {
  // 自定义字段权重
  getFn: (obj, path) => {
    // 自定义字段获取逻辑
    if (path === 'fullName') {
      return `${obj.firstName} ${obj.lastName}`
    }
    return obj[path]
  },
  
  // 自定义排序函数
  sortFn: (a, b) => {
    // 优先显示完全匹配
    if (a.score === 0 && b.score !== 0) return -1
    if (a.score !== 0 && b.score === 0) return 1
    
    // 按分数排序
    return a.score - b.score
  }
}

2. 动态更新索引

javascript 复制代码
class DynamicFuse {
  constructor(initialData, options) {
    this.options = options
    this.data = [...initialData]
    this.fuse = new Fuse(this.data, options)
  }
  
  add(item) {
    this.data.push(item)
    this.rebuildIndex()
  }
  
  remove(predicate) {
    this.data = this.data.filter(item => !predicate(item))
    this.rebuildIndex()
  }
  
  update(predicate, updater) {
    this.data = this.data.map(item => 
      predicate(item) ? updater(item) : item
    )
    this.rebuildIndex()
  }
  
  rebuildIndex() {
    this.fuse = new Fuse(this.data, this.options)
  }
  
  search(query) {
    return this.fuse.search(query)
  }
}

3. 多语言支持

javascript 复制代码
// 多语言搜索配置
const multiLanguageOptions = {
  keys: [
    'title.zh',
    'title.en', 
    'description.zh',
    'description.en'
  ],
  threshold: 0.3,
  
  // 自定义获取函数支持多语言
  getFn: (obj, path) => {
    const locale = getCurrentLocale() // 获取当前语言
    
    if (path.includes('.')) {
      const [field, lang] = path.split('.')
      return obj[field] && obj[field][lang]
    }
    
    return obj[path]
  }
}

// 多语言数据示例
const multiLanguageData = [
  {
    id: 1,
    title: {
      zh: '苹果手机',
      en: 'Apple iPhone'
    },
    description: {
      zh: '高端智能手机',
      en: 'Premium smartphone'
    }
  }
]

📝 最佳实践

1. 合理设置阈值

javascript 复制代码
// 不同场景的阈值建议
const thresholds = {
  exact: 0.0,        // 精确匹配
  strict: 0.2,       // 严格搜索
  moderate: 0.4,     // 中等容错
  loose: 0.6,        // 宽松搜索
  veryLoose: 0.8     // 非常宽松
}

// 根据数据类型选择合适的阈值
const getThreshold = (dataType) => {
  switch (dataType) {
    case 'email':
    case 'id':
      return thresholds.exact
    case 'name':
    case 'title':
      return thresholds.moderate
    case 'description':
    case 'content':
      return thresholds.loose
    default:
      return thresholds.moderate
  }
}

2. 优化搜索键配置

javascript 复制代码
// 智能权重分配
const getSearchKeys = (dataFields) => {
  return dataFields.map(field => {
    let weight = 0.1 // 默认权重
    
    // 根据字段类型分配权重
    if (field.includes('title') || field.includes('name')) {
      weight = 0.4
    } else if (field.includes('tag') || field.includes('category')) {
      weight = 0.3
    } else if (field.includes('description') || field.includes('content')) {
      weight = 0.2
    }
    
    return { name: field, weight }
  })
}

3. 错误处理

javascript 复制代码
class SafeFuse {
  constructor(data, options) {
    try {
      this.fuse = new Fuse(data, options)
      this.isReady = true
    } catch (error) {
      console.error('Fuse.js 初始化失败:', error)
      this.isReady = false
    }
  }
  
  search(query) {
    if (!this.isReady) {
      console.warn('Fuse.js 未就绪,返回原始数据')
      return []
    }
    
    try {
      return this.fuse.search(query)
    } catch (error) {
      console.error('搜索出错:', error)
      return []
    }
  }
}

🎯 总结

Fuse.js 是一个功能强大且易用的模糊搜索库,适用于各种 JavaScript 应用场景:

简单易用 :API设计简洁,上手快速

功能丰富 :支持模糊搜索、权重配置、高亮显示等

高度可配置 :提供30+个配置选项满足不同需求

性能优秀 :轻量级设计,适合大数据集处理

跨平台支持 :浏览器、Node.js、Deno 全平台兼容

TypeScript友好:完整的类型定义支持

通过合理配置和优化,Fuse.js 可以为您的应用提供专业级的搜索体验,大大提升用户满意度。


开始您的智能搜索之旅吧! 🔍

💡 开发建议:在实际项目中,建议结合防抖、缓存、虚拟滚动等技术,构建高性能的搜索系统。

相关推荐
哈贝#4 分钟前
vue和uniapp聊天页面右侧滚动条自动到底部
javascript·vue.js·uni-app
苦学编程的谢10 分钟前
Java网络编程API 1
java·开发语言·网络
寒山李白17 分钟前
Java 依赖注入、控制反转与面向切面:面试深度解析
java·开发语言·面试·依赖注入·控制反转·面向切面
Lazy_zheng20 分钟前
🚀 前端开发福音:用 json-server 快速搭建本地 Mock 数据服务
前端·javascript·vue.js
用户25191624271121 分钟前
ES6之块级绑定
javascript
ZzMemory22 分钟前
藏起来的JS(四) - GC(垃圾回收机制)
前端·javascript·面试
梓仁沐白25 分钟前
【Kotlin】数字&字符串&数组&集合
android·开发语言·kotlin
林太白26 分钟前
前端必会之Nuxt.js
前端·javascript·vue.js
晓晓莺歌31 分钟前
vue-router路由问题:可以通过$router.push()跳转,但刷新后又变成空白页面
前端·javascript·vue.js
Java菜鸟、35 分钟前
设计模式(代理设计模式)
java·开发语言·设计模式