ByteNoteByteNote

字节笔记本

2026年7月20日

Node.js + Docker 实现 Google 图片搜索工具

API中转
¥120

Google Custom Search API 支持按关键词搜索图片,但官方没有专门的图片搜索 SDK,需要通过 REST 接口调用。本文实现一个 Node.js 后端代理 + React 前端的完整图片搜索工具,用 Docker 部署。

前置准备

在开始之前,需要在 Google Cloud 完成两件事:

  1. 创建 API Key:在 Google Cloud Console 创建项目,启用 Custom Search API,生成 API 密钥。
  2. 创建自定义搜索引擎:在 Programmable Search Engine 创建搜索引擎,在设置中启用"图片搜索"。

免费版每天 100 次查询,个人使用足够。

Node.js 后端 API

用 Express 做一个代理接口,前端把 API Key 和 Search Engine ID 通过请求体传过来,后端转发给 Google API。这样避免在前端暴露 Key(虽然这个场景下用户自己提供 Key,但如果做公开服务可以改成后端统一管理)。

javascript
// app.js
const express = require('express');
const axios = require('axios');

const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());

// 搜索图片
app.post('/search', async (req, res) => {
  const { query, num = 10, apiKey, searchEngineId } = req.body;

  if (!query || !apiKey || !searchEngineId) {
    return res.status(400).json({ error: '缺少必要参数: query, apiKey, searchEngineId' });
  }

  if (num < 1 || num > 10) {
    return res.status(400).json({ error: 'num 参数范围: 1-10' });
  }

  const url = `https://www.googleapis.com/customsearch/v1?key=${apiKey}&cx=${searchEngineId}&q=${encodeURIComponent(query)}&searchType=image&num=${num}`;

  try {
    const response = await axios.get(url);

    if (!response.data.items) {
      return res.json([]);
    }

    const results = response.data.items.map(item => ({
      title: item.title,
      link: item.link,
      thumbnail: item.image.thumbnailLink,
      contextUrl: item.image.contextLink,
      width: item.image.width,
      height: item.image.height,
    }));

    res.json(results);
  } catch (error) {
    const status = error.response?.status;
    if (status === 400) {
      res.status(400).json({ error: 'API Key 或 Search Engine ID 无效' });
    } else if (status === 403) {
      res.status(403).json({ error: 'API 配额已用尽或权限不足' });
    } else {
      res.status(500).json({ error: '搜索服务暂时不可用' });
    }
  }
});

// 健康检查
app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

app.listen(PORT, () => {
  console.log(`Image search API running on port ${PORT}`);
});

注意 searchType=image 参数是关键——没有它,API 返回的是网页搜索结果而不是图片。

Docker 部署

dockerfile
# Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install --production

COPY . .

EXPOSE 3000
CMD ["node", "app.js"]
yaml
# docker-compose.yml
version: '3'
services:
  image-search:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
    restart: unless-stopped
json
// package.json
{
  "name": "google-image-search-api",
  "version": "1.0.0",
  "main": "app.js",
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "axios": "^1.6.0"
  }
}

启动:

bash
docker-compose up -d --build

React 前端

前端用一个搜索框 + 图片网格,用户输入关键词后展示搜索结果。API Key 和 Search Engine ID 保存到 localStorage,这样不用每次都重新输入。

核心搜索组件:

jsx
import { useState, useEffect } from 'react';

const API_URL = 'http://localhost:3000';

function App() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  const [settings, setSettings] = useState(() => {
    const saved = localStorage.getItem('image-search-settings');
    return saved ? JSON.parse(saved) : { apiKey: '', engineId: '' };
  });

  useEffect(() => {
    localStorage.setItem('image-search-settings', JSON.stringify(settings));
  }, [settings]);

  const handleSearch = async () => {
    if (!query.trim()) return;
    if (!settings.apiKey || !settings.engineId) {
      setError('请先在设置中填写 API Key 和 Search Engine ID');
      return;
    }

    setLoading(true);
    setError('');
    setResults([]);

    try {
      const resp = await fetch(`${API_URL}/search`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          query: query,
          num: 10,
          apiKey: settings.apiKey,
          searchEngineId: settings.engineId,
        }),
      });

      const data = await resp.json();

      if (!resp.ok) {
        setError(data.error || '搜索失败');
        return;
      }

      setResults(data);
    } catch (e) {
      setError('网络错误,请检查后端服务是否启动');
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="max-w-4xl mx-auto p-6">
      <div className="flex items-center justify-between mb-6">
        <h1 className="text-2xl font-bold">Google 图片搜索</h1>
        <SettingsButton settings={settings} onSave={setSettings} />
      </div>

      <div className="flex gap-2 mb-6">
        <input
          className="flex-1 border rounded-lg px-4 py-2"
          placeholder="输入搜索关键词..."
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
        />
        <button
          className="bg-blue-600 text-white px-6 py-2 rounded-lg"
          onClick={handleSearch}
          disabled={loading}
        >
          {loading ? '搜索中...' : '搜索'}
        </button>
      </div>

      {error && (
        <div className="bg-red-50 text-red-600 p-3 rounded-lg mb-4">{error}</div>
      )}

      <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
        {results.map((item, i) => (
          <a
            key={i}
            href={item.link}
            target="_blank"
            className="block rounded-lg overflow-hidden border hover:shadow-lg transition-shadow"
          >
            <img src={item.thumbnail} alt={item.title} className="w-full h-32 object-cover" />
            <div className="p-2 text-xs text-gray-600 truncate">{item.title}</div>
          </a>
        ))}
      </div>

      {results.length === 0 && !loading && !error && query && (
        <p className="text-gray-400 text-center mt-8">没有找到相关图片</p>
      )}

      <div className="mt-8 p-4 bg-gray-50 rounded-lg text-xs text-gray-500">
        <p>使用前需要在 Google Cloud Console 创建 API Key 并启用 Custom Search API,
        然后在 Programmable Search Engine 创建自定义搜索引擎(启用图片搜索)。</p>
      </div>
    </div>
  );
}

Google Custom Search API 返回结构

API 返回的 items 中,每条结果的结构:

json
{
  "title": "图片标题",
  "link": "原图完整 URL",
  "image": {
    "thumbnailLink": "缩略图 URL",
    "contextLink": "图片来源页面 URL",
    "width": 1920,
    "height": 1080
  }
}

前端显示用 thumbnailLink 作为网格缩略图,点击跳转 link 查看原图。如果需要下载原图,直接请求 link 即可。

注意事项

  • API Key 安全:这个方案中 Key 由用户自行输入并保存在本地 localStorage。如果做公开服务,应该在后端统一管理 Key,前端不接触。
  • 免费配额:100 次/天,超出后返回 403。可以在 Google Cloud Console 查看用量。
  • CORS:如果前端和后端不在同一域名,需要在后端加 CORS 中间件,或者用 Nginx 反代把前后端统一到一个域名下。
  • searchType=image:这个参数是必须的,没有它返回的是网页结果。很多初次使用这个 API 的人会漏掉这个参数。
分享: