ai
  • index
  • cursor
  • vector
  • crawl
  • crawl-front
  • DrissionPage
  • logging
  • mysql
  • pprint
  • sqlalchemy
  • contextmanager
  • dotenv
  • Flask
  • python
  • job
  • pdfplumber
  • python-docx
  • redbook
  • douyin
  • ffmpeg
  • json
  • numpy
  • opencv-python
  • pypinyin
  • re
  • requests
  • subprocess
  • time
  • uuid
  • watermark
  • milvus
  • pymilvus
  • search
  • Blueprint
  • flash
  • Jinja2
  • secure_filename
  • url_for
  • Werkzeug
  • chroma
  • HNSW
  • pillow
  • pandas
  • beautifulsoup4
  • langchain-community
  • langchain-core
  • langchain
  • langchain_unstructured
  • libreoffice
  • lxml
  • openpyxl
  • pymupdf
  • python-pptx
  • RAGFlow
  • tabulate
  • sentence_transformers
  • jsonl
  • collections
  • jieba
  • rag_optimize
  • rag
  • rank_bm25
  • Hugging_Face
  • modelscope
  • all-MiniLM-L6-v2
  • ollama
  • rag_measure
  • ragas
  • ASGI
  • FastAPI
  • FastChat
  • Jupyter
  • PyTorch
  • serper
  • uvicorn
  • markdownify
  • NormalizedLevenshtein
  • raq-action
  • CrossEncoder
  • Bi-Encoder
  • neo4j
  • neo4j4python
  • matplotlib
  • Plotly
  • Streamlit
  • py2neo
  • abc
  • read_csv
  • neo4jinstall
  • APOC
  • neo4jproject
  • uv
  • GDS
  • heapq
  • 掘金文章爬虫前端项目实现教程
    • 目录
    • 1. 项目概述与技术栈
      • 项目介绍
      • 技术栈
    • 2. 环境准备
      • 安装Node.js和npm
      • 安装Milvus
      • 获取OpenAI API密钥
    • 3. 项目初始化
      • 创建Next.js项目
      • 安装依赖
      • 配置Tailwind CSS
    • 4. 基础架构搭建
      • 创建项目结构
      • 实现基础布局
      • 配置全局样式
    • 5. Milvus服务集成
      • 配置Milvus连接
      • 创建错误处理工具
      • 实现Milvus服务类
    • 6. 界面组件开发
      • 实现搜索栏组件
      • 实现文章列表组件
    • 7. API接口实现
      • 实现文章API路由
      • 实现主页组件
    • 8. 集成测试
      • 创建启动脚本
      • 测试流程
    • 9. 项目优化
      • 性能优化
      • 用户体验优化
    • 10. 部署上线
      • 构建生产版本
      • 部署选项
    • 总结

掘金文章爬虫前端项目实现教程 #

本教程将带你循序渐进地实现一个掘金文章爬虫的前端项目。该项目使用Next.js框架构建,结合了Milvus向量数据库进行文章的语义搜索功能。

目录 #

  1. 项目概述与技术栈
  2. 环境准备
  3. 项目初始化
  4. 基础架构搭建
  5. Milvus服务集成
  6. 界面组件开发
  7. API接口实现
  8. 集成测试
  9. 项目优化
  10. 部署上线

1. 项目概述与技术栈 #

项目介绍 #

本项目是一个掘金文章浏览和搜索系统的前端部分,能够展示从掘金网站爬取的文章,并提供基于语义的搜索功能。通过向量化文章标题,我们可以实现比传统关键词匹配更智能的文章搜索体验。

技术栈 #

  • 前端框架:Next.js 15
  • UI库:Tailwind CSS 4
  • 向量数据库:Milvus
  • 向量化服务:OpenAI Embedding API
  • HTTP客户端:Axios
  • UI组件:Headless UI, Heroicons

2. 环境准备 #

安装Node.js和npm #

确保你的开发环境已安装最新版本的Node.js和npm。

安装Milvus #

本项目依赖Milvus向量数据库,你可以通过Docker快速安装:

docker run -d --name milvus -p 19530:19530 -p 19531:19531 -p 9091:9091 milvusdb/milvus:v2.3.3

获取OpenAI API密钥 #

向量化服务需要OpenAI API密钥,请在OpenAI官网注册并获取API密钥。

3. 项目初始化 #

创建Next.js项目 #

首先,我们使用Create Next App创建一个新的Next.js项目:

npx create-next-app@latest my-juejin-crawler
cd my-juejin-crawler

安装依赖 #

接下来,安装所需的依赖包:

npm install @zilliz/milvus2-sdk-node axios @headlessui/react @heroicons/react classnames
npm install tailwindcss @tailwindcss/vite

配置Tailwind CSS #

在 Next.js 项目中配置 Tailwind CSS v4.0:

/** @type {import('next').NextConfig} */
import tailwindcss from '@tailwindcss/vite';

const nextConfig = {
  webpack: (config) => {
    return config;
  },

  experimental: {
    plugins: [
      tailwindcss(),
    ],
  },
};

export default nextConfig;

在 src/app/globals.css 中添加 Tailwind CSS 导入:

@import "tailwindcss";

/* 您的其他全局样式 */

4. 基础架构搭建 #

创建项目结构 #

建立基本的项目目录结构:

src/
├── app/
│   ├── api/
│   │   └── articles/
│   │       └── route.js
│   ├── components/
│   │   ├── ArticleList.js
│   │   └── SearchBar.js
│   ├── utils/
│   │   ├── error-handler.js
│   │   ├── milvus-config.js
│   │   └── milvus-service.js
│   ├── favicon.ico
│   ├── globals.css
│   ├── layout.js
│   └── page.js
├── public/

实现基础布局 #

创建src/app/layout.js文件,设置全局布局:

import './globals.css'

export const metadata = {
  title: '掘金文章浏览器',
  description: '基于语义搜索的掘金文章浏览工具',
}

export default function RootLayout({ children }) {
  return (
    <html lang="zh">
      <body>
        {children}
      </body>
    </html>
  )
}

配置全局样式 #

在src/app/globals.css中添加Tailwind指令和全局样式:

@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --foreground-rgb: 0, 0, 0;
  --background-rgb: 255, 255, 255;
}

body {
  color: rgb(var(--foreground-rgb));
  background: rgb(var(--background-rgb));
}

5. Milvus服务集成 #

配置Milvus连接 #

创建src/app/utils/milvus-config.js文件,存储Milvus和OpenAI的配置:

export const milvusConfig = {
  milvus: {
    address: 'localhost',
    port: '19530',
    database: 'default',
    collection: 'juejin_articles'
  },
  embedding: {
    url: 'https://api.openai.com/v1/embeddings',
    model: 'text-embedding-3-small',
    apiKey: process.env.OPENAI_API_KEY || 'your-api-key'  // 实际部署时应使用环境变量
  }
};

创建错误处理工具 #

创建src/app/utils/error-handler.js文件,用于统一错误处理:

/**
 * 格式化错误消息
 */
export function formatErrorMessage(error) {
  if (typeof error === 'string') {
    return error;
  }

  if (error.message) {
    return error.message;
  }

  return '发生未知错误';
}

/**
 * 记录服务器错误
 */
export function logServerError(source, error) {
  console.error(`[${source}] 错误:`, error);

  // 提取和记录关键错误信息
  const errorInfo = {
    message: error.message || '未知错误',
    stack: error.stack,
    timestamp: new Date().toISOString()
  };

  console.error(`详细错误信息:`, JSON.stringify(errorInfo, null, 2));
}

实现Milvus服务类 #

创建src/app/utils/milvus-service.js文件,封装与Milvus数据库的交互:

import { MilvusClient, DataType } from '@zilliz/milvus2-sdk-node';
import { milvusConfig } from './milvus-config';
import axios from 'axios';

/**
 * 文本转向量函数
 * @param {string} text - 需要转换为向量的文本
 * @returns {Promise<Array<number>>} - 返回向量数组
 */
export async function textToVector(text) {
    const response = await axios.post(milvusConfig.embedding.url, {
        model: milvusConfig.embedding.model,
        input: text,
        encoding_format: 'float'
    }, {
        headers: {
            'Authorization': `Bearer ${milvusConfig.embedding.apiKey}`,
            'Content-Type': 'application/json'
        }
    });

    return response.data.data[0].embedding;
}

/**
 * Milvus服务类 - 处理与Milvus的所有交互
 */
export class MilvusService {
    constructor() {
        this.client = null;
        this.isConnected = false;
    }

    /**
     * 连接到Milvus数据库
     */
    async connect() {
        if (this.isConnected) return;

        try {
            this.client = new MilvusClient({
                address: `${milvusConfig.milvus.address}:${milvusConfig.milvus.port}`,
                username: '',
                password: '',
                ssl: false,
                loadProto: true,
                useIdentity: false
            });

            await this.client.use({
                db_name: milvusConfig.milvus.database
            });

            // 检查集合是否存在
            const hasCollection = await this.client.hasCollection({
                collection_name: milvusConfig.milvus.collection
            });

            if (!hasCollection.value) {
                throw new Error(`集合 ${milvusConfig.milvus.collection} 不存在`);
            }

            // 加载集合,确保可以搜索
            await this.client.loadCollection({
                collection_name: milvusConfig.milvus.collection
            });

            this.isConnected = true;
        } catch (error) {
            console.error('连接Milvus失败:', error);
            throw new Error('无法连接到Milvus数据库');
        }
    }

    /**
     * 搜索文章
     * @param {string} searchTerm - 搜索关键词
     * @param {number} limit - 结果数量限制
     * @returns {Promise<Array>} - 搜索结果
     */
    async searchArticles(searchTerm, limit = 10) {
        await this.connect();

        try {
            // 将搜索词转换为向量
            const searchVector = await textToVector(searchTerm);

            // 搜索相似向量
            const searchResults = await this.client.search({
                collection_name: milvusConfig.milvus.collection,
                data: [searchVector],
                anns_field: 'title_vector',
                param: {
                    metric_type: 'COSINE',
                    params: { ef: 64 }
                },
                limit: limit,
                output_fields: ['rank', 'title', 'url', 'likes', 'views']
            });

            // 处理搜索结果
            let processedResults = [];

            if (searchResults.results && Array.isArray(searchResults.results)) {
                processedResults = searchResults.results.map(result => {
                    return {
                        id: result.id || '',
                        score: typeof result.score === 'number' ? result.score : 0,
                        rank: result.entity.rank || 0,
                        title: result.entity.title || '',
                        url: result.entity.url || '',
                        likes: result.entity.likes || 0,
                        views: result.entity.views || 0
                    };
                });
            }

            return processedResults;
        } catch (error) {
            console.error('搜索文章失败:', error);
            throw error;
        }
    }

    /**
     * 获取所有文章
     * @param {number} limit - 结果数量限制
     * @returns {Promise<Array>} - 文章列表
     */
    async getAllArticles(limit = 100) {
        await this.connect();

        try {
            const result = await this.client.query({
                collection_name: milvusConfig.milvus.collection,
                output_fields: ['rank', 'title', 'url', 'likes', 'views'],
                limit: limit
            });

            if (!result.data) {
                return [];
            }

            return result.data.map(item => ({
                id: item.id || '',
                rank: item.rank || 0,
                title: item.title || '',
                url: item.url || '',
                likes: item.likes || 0,
                views: item.views || 0
            }));
        } catch (error) {
            console.error('获取所有文章失败:', error);
            throw error;
        }
    }

    /**
     * 关闭Milvus连接
     */
    async close() {
        if (this.client) {
            try {
                // Milvus SDK没有显式的close方法,但我们可以将client设为null
                this.client = null;
                this.isConnected = false;
            } catch (error) {
                console.error('关闭Milvus连接失败:', error);
            }
        }
    }
}

6. 界面组件开发 #

实现搜索栏组件 #

创建src/app/components/SearchBar.js文件:

'use client';

import { useState } from 'react';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';

export default function SearchBar({ onSearch }) {
  const [searchQuery, setSearchQuery] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    onSearch(searchQuery);
  };

  return (
    <div className="w-full">
      <form onSubmit={handleSubmit} className="relative">
        <div className="relative">
          <input
            type="text"
            className="w-full pl-10 pr-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
            placeholder="搜索文章..."
            value={searchQuery}
            onChange={(e) => setSearchQuery(e.target.value)}
          />
          <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
            <MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
          </div>
        </div>
        <button
          type="submit"
          className="absolute right-2.5 bottom-2.5 bg-blue-500 text-white rounded-lg px-4 py-1 hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-400"
        >
          搜索
        </button>
      </form>
    </div>
  );
}

实现文章列表组件 #

创建src/app/components/ArticleList.js文件:

'use client';

import { HeartIcon, EyeIcon } from '@heroicons/react/24/outline';

export default function ArticleList({ articles, loading }) {
  if (loading) {
    return (
      <div className="mt-8 w-full">
        <div className="flex justify-center items-center h-64">
          <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
        </div>
      </div>
    );
  }

  if (!articles || articles.length === 0) {
    return (
      <div className="mt-8 text-center p-8 bg-gray-50 rounded-lg">
        <p className="text-gray-500">没有找到符合条件的文章</p>
      </div>
    );
  }

  return (
    <div className="mt-8 w-full">
      <h2 className="text-xl font-semibold mb-4">文章列表 ({articles.length})</h2>
      <div className="space-y-4">
        {articles.map((article, index) => (
          <div
            key={article.id || index}
            className="p-4 border border-gray-200 rounded-lg hover:shadow-md transition-shadow"
          >
            <a
              href={article.url}
              target="_blank"
              rel="noopener noreferrer"
              className="block"
            >
              <h3 className="text-lg font-medium text-blue-600 hover:underline">
                {article.title}
              </h3>

              <div className="mt-2 flex items-center text-sm text-gray-500">
                <div className="flex items-center mr-4">
                  <HeartIcon className="h-4 w-4 mr-1 text-red-500" />
                  <span>{article.likes || 0} 喜欢</span>
                </div>

                <div className="flex items-center">
                  <EyeIcon className="h-4 w-4 mr-1 text-gray-500" />
                  <span>{article.views || 0} 浏览</span>
                </div>

                {article.score !== undefined && (
                  <div className="ml-auto">
                    <span className="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded">
                      相关度: {(article.score * 100).toFixed(1)}%
                    </span>
                  </div>
                )}
              </div>
            </a>
          </div>
        ))}
      </div>
    </div>
  );
}

7. API接口实现 #

实现文章API路由 #

创建src/app/api/articles/route.js文件:

import { NextResponse } from 'next/server';
import { MilvusService } from '../../utils/milvus-service';
import { formatErrorMessage, logServerError } from '../../utils/error-handler';

/**
 * GET处理函数 - 获取所有文章或搜索文章
 */
export async function GET(request) {
  const milvusService = new MilvusService();
  const { searchParams } = new URL(request.url);
  const query = searchParams.get('query') || '';

  try {
    let articles;

    if (query) {
      // 如果有查询参数,执行搜索
      articles = await milvusService.searchArticles(query);
      // 搜索结果已经在MilvusService中按相关度排序
    } else {
      // 否则获取所有文章
      articles = await milvusService.getAllArticles();
      // 无搜索时按排名排序
      articles.sort((a, b) => a.rank - b.rank);
    }

    // 直接返回文章数组
    return NextResponse.json(articles);
  } catch (error) {
    logServerError('API', error);
    return NextResponse.json({
      error: formatErrorMessage(error)
    }, { status: 500 });
  } finally {
    await milvusService.close();
  }
}

实现主页组件 #

创建src/app/page.js文件,整合各个组件:

'use client';

import { useState, useEffect } from 'react';
import SearchBar from './components/SearchBar';
import ArticleList from './components/ArticleList';

export default function Home() {
  const [query, setQuery] = useState('');
  const [articles, setArticles] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // 获取文章的方法
  const fetchArticles = async (searchQuery = '') => {
    try {
      setLoading(true);
      console.log('开始获取文章, 查询:', searchQuery);

      // 构建API请求URL
      const url = searchQuery
        ? `/api/articles?query=${encodeURIComponent(searchQuery)}`
        : '/api/articles';

      const response = await fetch(url, {
        cache: 'no-store',
        headers: {
          'Cache-Control': 'no-cache'
        }
      });

      if (!response.ok) {
        throw new Error(`获取文章失败: ${response.statusText}`);
      }

      const data = await response.json();

      // 确保articles是数组
      if (Array.isArray(data)) {
        setArticles(data);
      } else if (data.error) {
        // 如果返回了错误信息
        throw new Error(data.error);
      } else {
        // 如果返回了不期望的数据结构
        setArticles([]);
        throw new Error('服务器返回了不正确的数据格式');
      }

      setError(null);
    } catch (err) {
      console.error('获取文章出错:', err);
      setError(err.message);
      setArticles([]);
    } finally {
      setLoading(false);
    }
  };

  // 首次加载时获取所有文章
  useEffect(() => {
    fetchArticles();
  }, []);

  // 处理搜索
  const handleSearch = async (searchQuery) => {
    setQuery(searchQuery);
    await fetchArticles(searchQuery);
  };

  return (
    <main className="flex min-h-screen flex-col items-center p-8">
      <div className="w-full max-w-3xl mx-auto">
        <h1 className="text-3xl font-bold text-center mb-6">掘金文章浏览器</h1>

        <SearchBar onSearch={handleSearch} />

        {error && (
          <div className="mt-4 p-4 bg-red-50 text-red-600 rounded-lg border border-red-200">
            错误: {error}
          </div>
        )}

        <ArticleList
          articles={articles}
          loading={loading}
        />
      </div>
    </main>
  );
}

8. 集成测试 #

创建启动脚本 #

为了方便Windows用户启动项目,创建start.bat文件:

@echo off
echo =============================
echo 掘金文章浏览器启动脚本
echo =============================
echo.

echo 正在安装依赖...
call npm install

echo.
echo 正在启动开发服务器...
echo.
echo 提示: 服务器启动后,请在浏览器访问 http://localhost:3000
echo.
echo 按 Ctrl+C 可停止服务器
echo.

call npm run dev

pause

测试流程 #

  1. 确保Milvus服务已经启动并包含文章数据
  2. 运行start.bat脚本启动开发服务器
  3. 在浏览器中访问http://localhost:3000
  4. 测试搜索功能是否正常工作
  5. 检查文章列表展示是否正确

9. 项目优化 #

性能优化 #

  1. 图片优化:使用Next.js的Image组件优化图片加载
  2. 组件懒加载:对大型组件使用动态导入实现懒加载
  3. 缓存策略:实现API响应的缓存机制

用户体验优化 #

  1. 加载状态:添加骨架屏提升加载体验
  2. 错误处理:优化错误提示,使用户更容易理解问题
  3. 响应式设计:确保在各种设备上都有良好的显示效果

10. 部署上线 #

构建生产版本 #

运行以下命令构建生产版本:

npm run build

部署选项 #

  1. Vercel部署:

    vercel
  2. 服务器部署:

    npm run build
    npm run start
  3. Docker部署: 创建Dockerfile:

    FROM node:18-alpine
    
    WORKDIR /app
    
    COPY package*.json ./
    RUN npm install
    
    COPY . .
    RUN npm run build
    
    EXPOSE 3000
    
    CMD ["npm", "start"]

    构建并运行Docker镜像:

    docker build -t juejin-crawler-front .
    docker run -p 3000:3000 juejin-crawler-front

总结 #

通过本教程,我们已经完成了一个功能完整的掘金文章浏览器前端项目。该项目融合了现代前端技术和向量数据库技术,实现了基于语义的文章搜索功能。

关键技术点包括:

  1. Next.js应用程序的搭建
  2. Milvus向量数据库的集成
  3. 文本向量化与语义搜索的实现
  4. 响应式UI组件的开发
  5. 前后端结合的API设计

希望本教程能帮助你理解如何构建一个现代化的前端应用,并为你的项目提供参考。

访问验证

请输入访问令牌

Token不正确,请重新输入