字
字节笔记本
2026年7月20日
Node.js + Docker 实现 Google 图片搜索工具
API中转
¥120
Google Custom Search API 支持按关键词搜索图片,但官方没有专门的图片搜索 SDK,需要通过 REST 接口调用。本文实现一个 Node.js 后端代理 + React 前端的完整图片搜索工具,用 Docker 部署。
前置准备
在开始之前,需要在 Google Cloud 完成两件事:
- 创建 API Key:在 Google Cloud Console 创建项目,启用 Custom Search API,生成 API 密钥。
- 创建自定义搜索引擎:在 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-stoppedjson
// 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 --buildReact 前端
前端用一个搜索框 + 图片网格,用户输入关键词后展示搜索结果。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 的人会漏掉这个参数。
分享: