最近在逛一些技术社区和设计论坛,发现一个叫Nano-Banana的AI图像生成模型讨论度特别高。很多设计师和开发者都在用它做各种创意项目,从电商海报到产品拆解图,效果确实挺惊艳的。但每次都要打开网页、上传图片、输入提示词,这个流程对于需要频繁使用的开发者来说,效率有点低。
作为一个VSCode的重度用户,我就在想:能不能把这个功能直接做到编辑器里?这样写代码的时候,突然需要一张示意图或者UI概念图,直接在侧边栏点几下就能生成,那该多方便。
今天我就来分享一下,怎么从零开始为VSCode开发一个Nano-Banana图像生成插件。整个过程其实没有想象中那么复杂,跟着步骤走,大概一两个小时就能做出一个可用的版本。
开发VSCode插件,首先得把开发环境搭好。别担心,步骤都很简单。
1.1 安装必要工具
你需要安装两个核心工具:
- Node.js:这是开发JavaScript/TypeScript项目的基础,建议安装LTS版本(比如18.x或20.x)
- VSCode:这个不用说,你肯定已经有了
安装完Node.js后,打开终端验证一下:
node --version npm --version
能看到版本号就说明安装成功了。
1.2 创建插件项目
VSCode官方提供了一个非常方便的命令行工具,可以快速生成插件模板:
# 安装Yeoman和VSCode扩展生成器 npm install -g yo generator-code # 创建新项目 yo code
运行yo code后,会有一个交互式的命令行界面,按照下面的选项来选:
? What type of extension do you want to create? New Extension (TypeScript) ? What's the name of your extension? nano-banana-image-generator ? What's the identifier of your extension? nano-banana-image-generator ? What's the description of your extension? AI image generation with Nano-Banana in VSCode ? Initialize a git repository? Yes ? Which package manager to use? npm
选完之后,工具会自动创建一个完整的项目结构。用VSCode打开这个文件夹,你会看到这样的目录:
nano-banana-image-generator/ ├── src/ │ └── extension.ts # 插件主入口文件 ├── package.json # 插件配置和依赖 ├── tsconfig.json # TypeScript配置 └── .vscode/ # VSCode调试配置
1.3 安装依赖
进入项目目录,安装一些我们需要的额外依赖:
cd nano-banana-image-generator npm install axios form-data fs-extra path
这些包的作用分别是:
axios:用来发送HTTP请求到Nano-Banana的APIform-data:处理文件上传的表单数据fs-extra:增强的文件系统操作path:处理文件路径
在开始写代码之前,我们先简单了解一下VSCode插件的几个核心概念,这样后面写起来会更清楚。
2.1 package.json - 插件的身份证
这个文件定义了插件的基本信息、命令、视图等。我们主要关注这几个部分:
{ "activationEvents": [ "onCommand:nano-banana.generateImage" ], "contributes": { "commands": [ { "command": "nano-banana.generateImage", "title": "Generate Image with Nano-Banana" } ], "viewsContainers": { "activitybar": [ { "id": "nano-banana", "title": "Nano Banana", "icon": "media/banana.svg" } ] }, "views": { "nano-banana": [ { "id": "nano-banana.view", "name": "Image Generator" } ] } } }
简单解释一下:
activationEvents:什么时候激活插件(比如执行某个命令时)commands:定义插件提供的命令viewsContainers和views:定义在VSCode侧边栏显示的视图
2.2 extension.ts - 插件的大脑
这是插件的主文件,所有逻辑都在这里。一个最简单的插件结构是这样的:
import * as vscode from 'vscode'; // 插件激活时调用 export function activate(context: vscode.ExtensionContext) { console.log('Nano-Banana插件已激活'); // 注册命令 const command = vscode.commands.registerCommand('nano-banana.generateImage', () => { vscode.window.showInformationMessage('Hello from Nano-Banana!'); }); context.subscriptions.push(command); } // 插件停用时调用 export function deactivate() {}
现在你可以按F5运行这个插件,会打开一个新的VSCode窗口(扩展开发主机)。在命令面板(Ctrl+Shift+P)里输入"Generate Image with Nano-Banana",就能看到弹出的消息了。
一个好的插件界面应该简洁易用。我们设计一个侧边栏面板,包含以下几个部分:
3.1 创建Webview视图
VSCode插件可以通过Webview显示自定义的HTML界面。我们先创建一个管理Webview的类:
// src/WebviewProvider.ts import * as vscode from ‘vscode’; import * as path from ‘path’;
export class NanoBananaViewProvider implements vscode.WebviewViewProvider { private _view?: vscode.WebviewView;
constructor(private readonly _extensionUri: vscode.Uri) {}
resolveWebviewView(webviewView: vscode.WebviewView) {
this._view = webviewView;
webviewView.webview.options = {
enableScripts: true,
localResourceRoots: [this._extensionUri]
};
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
// 处理从Webview发来的消息
webviewView.webview.onDidReceiveMessage(async (data) => {
switch (data.type) {
case 'generate':
await this._handleGenerate(data);
break;
case 'save':
await this._handleSave(data);
break;
}
});
}
private _getHtmlForWebview(webview: vscode.Webview): string
private async _handleGenerate(data: any) {
// 处理生成图片的逻辑
}
private async _handleSave(data: any) {
// 处理保存图片的逻辑
} }
3.2 设计HTML界面
现在我们来完善HTML界面,让它看起来更专业一些:
private _getHtmlForWebview(webview: vscode.Webview): string “>
Nano Banana Image Generator
提示词设置
提示:描述越详细,生成的图片越符合预期
图片设置
参考图片(可选)
点击上传参考图片
支持JPG、PNG格式,最大5MB
`;
}
3.3 添加CSS样式
在项目根目录创建media/styles.css文件:
body
.container { max-width: 800px; margin: 0 auto; }
h1, h2, h3 { color: var(–vscode-editor-foreground); margin-top: 0; }
.section
textarea { width: 100%; padding: 12px; border: 1px solid var(–vscode-input-border); background-color: var(–vscode-input-background); color: var(–vscode-input-foreground); border-radius: 4px; font-size: 14px; resize: vertical; box-sizing: border-box; }
textarea:focus { outline: none; border-color: var(–vscode-focusBorder); }
.hint { font-size: 12px; color: var(–vscode-descriptionForeground); margin-top: 8px; }
.form-group { margin-bottom: 12px; display: flex; align-items: center; }
.form-group label { width: 100px; margin-right: 12px; }
select { padding: 8px 12px; border: 1px solid var(–vscode-input-border); background-color: var(–vscode-input-background); color: var(–vscode-input-foreground); border-radius: 4px; font-size: 14px; flex: 1; }
.upload-area { border: 2px dashed var(–vscode-input-border); border-radius: 6px; padding: 40px 20px; text-align: center; cursor: pointer; transition: border-color 0.2s; }
.upload-area:hover { border-color: var(–vscode-focusBorder); }
.upload-placeholder span { display: block; margin-bottom: 8px; color: var(–vscode-descriptionForeground); }
.upload-placeholder small { color: var(–vscode-disabledForeground); }
.preview-container { margin-top: 16px; text-align: center; }
.preview-container img { max-width: 100%; max-height: 200px; border-radius: 4px; margin-bottom: 12px; }
.actions { text-align: center; margin: 24px 0; }
.btn-primary, .btn-secondary { padding: 10px 20px; border: none; border-radius: 4px; font-size: 14px; cursor: pointer; transition: background-color 0.2s; margin: 0 4px; }
.btn-primary { background-color: var(–vscode-button-background); color: var(–vscode-button-foreground); }
.btn-primary:hover { background-color: var(–vscode-button-hoverBackground); }
.btn-secondary { background-color: var(–vscode-button-secondaryBackground); color: var(–vscode-button-secondaryForeground); }
.btn-secondary:hover { background-color: var(–vscode-button-secondaryHoverBackground); }
.result-actions
#resultContainer { text-align: center; }
#resultContainer img { max-width: 100%; max-height: 400px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); }
.status { margin-top: 16px; padding: 12px; border-radius: 4px; font-size: 14px; }
.status.success { background-color: rgba(46, 204, 113, 0.1); color: #27ae60; border: 1px solid rgba(46, 204, 113, 0.3); }
.status.error { background-color: rgba(231, 76, 60, 0.1); color: #c0392b; border: 1px solid rgba(231, 76, 60, 0.3); }
.status.info { background-color: rgba(52, 152, 219, 0.1); color: #2980b9; border: 1px solid rgba(52, 152, 219, 0.3); }
.spinner { display: inline-block; margin-left: 8px; }
界面做好了,接下来就是最核心的部分:调用Nano-Banana的API生成图片。
4.1 配置API访问
首先,我们需要一个方式来管理API配置。创建一个配置文件:
// src/config.ts export interface ApiConfig { apiKey: string; host: string; model: string; }
export class ConfigManager );
if (!apiKey) { throw new Error('API Key是必需的'); } const newConfig: ApiConfig = { apiKey, host: 'https://api.grsai.com', // 默认使用海外节点 model: 'nano-banana-pro' }; await this.saveConfig(context, newConfig); return newConfig; } return config;
}
static async saveConfig(context: vscode.ExtensionContext, config: ApiConfig) {
await context.globalState.update(this.CONFIG_KEY, config);
}
static async updateConfig(context: vscode.ExtensionContext, updates: Partial
await this.saveConfig(context, newConfig);
} }
4.2 实现图片生成服务
现在创建主要的服务类来处理API调用:
// src/NanoBananaService.ts import * as vscode from ‘vscode’; import axios from ‘axios’; import FormData from ‘form-data’; import * as fs from ‘fs’; import * as path from ‘path’; import { ApiConfig } from ‘./config’;
export class NanoBananaService { constructor(private config: ApiConfig) {}
async generateImage(
prompt: string, options: { aspectRatio?: string; imageSize?: string; referenceImage?: string; // base64编码的图片 }
): Promise
try { const url = `${this.config.host}/v1/draw/nano-banana`; const payload: any = { model: this.config.model, prompt, aspectRatio: options.aspectRatio || '1:1', imageSize: options.imageSize || '2K', shutProgress: true }; // 如果有参考图片,添加到请求中 if (options.referenceImage) { payload.urls = [options.referenceImage]; } const response = await axios.post(url, payload, { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.config.apiKey}` }, timeout: // 120秒超时 }); if (response.status === 200) else { throw new Error(`生成失败: ${result.error || '未知错误'}`); } } else { throw new Error(`请求失败: ${response.status}`); } } catch (error: any) `); } else if (error.request) { throw new Error('网络错误:请检查网络连接'); } else { throw new Error(`请求异常: ${error.message}`); } }
}
async downloadImage(imageUrl: string, savePath: string): Promise
await fs.promises.writeFile(savePath, response.data); return savePath; } catch (error: any) { throw new Error(`下载图片失败: ${error.message}`); }
}
async imageToBase64(imagePath: string): Promise
try { const imageBuffer = await fs.promises.readFile(imagePath); const base64 = imageBuffer.toString('base64'); return `data:image/jpeg;base64,${base64}`; } catch (error: any) { throw new Error(`读取图片失败: ${error.message}`); }
} }
4.3 完善Webview的JavaScript逻辑
回到WebviewProvider,我们需要添加前端的交互逻辑:
// 在_getHtmlForWebview的
// DOM元素 const promptInput = document.getElementById(‘prompt’); const aspectRatioSelect = document.getElementById(‘aspectRatio’); const imageSizeSelect = document.getElementById(‘imageSize’); const uploadArea = document.getElementById(‘uploadArea’); const imageUpload = document.getElementById(‘imageUpload’); const preview = document.getElementById(‘preview’); const previewImage = document.getElementById(‘previewImage’); const removeImageBtn = document.getElementById(‘removeImage’); const generateBtn = document.getElementById(‘generateBtn’); const resultSection = document.getElementById(‘resultSection’); const resultImage = document.getElementById(‘resultImage’); const saveBtn = document.getElementById(‘saveBtn’); const statusDiv = document.getElementById(‘status’);
let selectedImage = null;
// 上传图片 uploadArea.addEventListener(‘click’, () => { imageUpload.click(); });
imageUpload.addEventListener(‘change’, (e) =>
const reader = new FileReader(); reader.onload = (event) => ; reader.readAsDataURL(file);
} });
// 移除图片 removeImageBtn.addEventListener(‘click’, () => { selectedImage = null; preview.style.display = ‘none’; uploadArea.style.display = ‘block’; imageUpload.value = ‘’; });
// 生成图片 generateBtn.addEventListener(‘click’, async () =>
const aspectRatio = aspectRatioSelect.value; const imageSize = imageSizeSelect.value;
// 显示加载状态 generateBtn.disabled = true; generateBtn.querySelector(‘.btn-text’).style.display = ‘none’; generateBtn.querySelector(‘.spinner’).style.display = ‘inline-block’;
showStatus(‘正在生成图片,请稍候…’, ‘info’);
try {
// 发送消息到扩展 vscode.postMessage({ type: 'generate', prompt, aspectRatio, imageSize, referenceImage: selectedImage });
} catch (error) {
showStatus(`生成失败: ${error.message}`, 'error'); resetGenerateButton();
} });
// 保存图片 saveBtn.addEventListener(‘click’, () => ); } });
// 显示状态消息 function showStatus(message, type = ‘info’) { statusDiv.textContent = message; statusDiv.className = `status ${type}`; statusDiv.style.display = ‘block’;
if (type !== ‘info’) , 5000); } }
// 重置生成按钮状态 function resetGenerateButton() { generateBtn.disabled = false; generateBtn.querySelector(‘.btn-text’).style.display = ‘inline-block’; generateBtn.querySelector(‘.spinner’).style.display = ‘none’; }
// 监听来自扩展的消息 window.addEventListener(‘message’, (event) => );
break; case 'error': showStatus(message.message, 'error'); resetGenerateButton(); break; case 'saved': showStatus(`图片已保存到: ${message.path}`, 'success'); break;
} });
4.4 完善WebviewProvider的消息处理
现在我们需要完善之前留空的消息处理函数:
private async _handleGenerate(data: any)
try );
// 发送结果回Webview this._view.webview.postMessage({ type: 'generated', imageUrl });
} catch (error: any) {
this._view.webview.postMessage({ type: 'error', message: error.message });
} }
private async _handleSave(data: any)
try {
// 让用户选择保存位置 const uri = await vscode.window.showSaveDialog({ filters: { 'Images': ['png', 'jpg', 'jpeg'] }, defaultUri: vscode.Uri.file(`generated-${Date.now()}.png`) }); if (uri) ); vscode.window.showInformationMessage(`图片已保存到: ${uri.fsPath}`); }
} catch (error: any) {
this._view.webview.postMessage({ type: 'error', message: `保存失败: ${error.message}` });
} }
现在我们需要在extension.ts中注册所有的功能:
// src/extension.ts import * as vscode from ‘vscode’; import { NanoBananaViewProvider } from ‘./WebviewProvider’; import { ConfigManager } from ‘./config’;
export function activate(context: vscode.ExtensionContext) { console.log(‘Nano-Banana插件已激活’);
// 注册侧边栏视图 const provider = new NanoBananaViewProvider(context.extensionUri); context.subscriptions.push(
vscode.window.registerWebviewViewProvider('nano-banana.view', provider)
);
// 注册命令:生成图片 const generateCommand = vscode.commands.registerCommand(‘nano-banana.generateImage’, async () => {
// 显示侧边栏 await vscode.commands.executeCommand('workbench.view.extension.nano-banana'); // 可以在这里添加一些初始化逻辑 vscode.window.showInformationMessage('准备好生成图片了吗?在侧边栏输入提示词吧!');
});
// 注册命令:配置API const configCommand = vscode.commands.registerCommand(‘nano-banana.configure’, async () => );
if (apiKey !== undefined) { await ConfigManager.updateConfig(context, { apiKey }); vscode.window.showInformationMessage('API配置已更新'); }
});
// 注册命令:快速生成 const quickGenerateCommand = vscode.commands.registerCommand(‘nano-banana.quickGenerate’, async () => {
const prompt = await vscode.window.showInputBox({
prompt: '输入图片描述',
placeHolder: '例如:一只可爱的猫咪在草地上玩耍'
});
if (prompt) , async (progress) => {
progress.report({ increment: 0 });
const imageUrl = await service.generateImage(prompt, {
aspectRatio: '1:1',
imageSize: '2K'
});
progress.report({ increment: 100 });
// 显示图片
const panel = vscode.window.createWebviewPanel(
'nanoBananaPreview',
'生成结果',
vscode.ViewColumn.Beside,
{ enableScripts: true }
);
panel.webview.html = `
生成结果
`; return new Promise(resolve => }); if (uri) { await service.downloadImage(message.imageUrl, uri.fsPath); vscode.window.showInformationMessage(`已保存到: ${uri.fsPath}`); } } else if (message.type === 'copy') { vscode.env.clipboard.writeText(message.prompt); vscode.window.showInformationMessage('提示词已复制到剪贴板'); } }); }); }); } catch (error: any) { vscode.window.showErrorMessage(`生成失败: ${error.message}`); } }
});
// 将所有命令添加到订阅 context.subscriptions.push(generateCommand, configCommand, quickGenerateCommand);
// 添加快捷键(可选) context.subscriptions.push(
vscode.commands.registerCommand('nano-banana.generateFromSelection', async () => else { vscode.window.showWarningMessage('请先选择一些文本作为提示词'); } } })
); }
export function deactivate() {}
现在插件的主要功能都完成了,我们来测试一下。
6.1 运行调试
按F5启动调试,会打开一个新的VSCode窗口。在这个窗口里:
- 点击左侧活动栏的香蕉图标(如果没有看到,可能需要手动启用)
- 在侧边栏输入提示词,比如”一只可爱的柯基犬在公园里玩耍,阳光明媚“
- 选择图片比例和尺寸
- 点击”生成图片“
如果一切正常,几十秒后就能看到生成的图片了。
6.2 常见问题排查
如果遇到问题,可以检查以下几点:
- API Key是否正确:在命令面板输入”Nano-Banana: Configure“检查配置
- 网络连接:确保可以访问API服务
- 控制台日志:在调试控制台查看错误信息
6.3 添加更多功能
基本的生成功能完成后,你可以考虑添加一些增强功能:
// 示例:添加历史记录功能 class HistoryManager { private static readonly HISTORY_KEY = ‘nano-banana.history’;
static async addToHistory(context: vscode.ExtensionContext, item: {
prompt: string; imageUrl: string; timestamp: number;
})
await context.globalState.update(this.HISTORY_KEY, history);
}
static async getHistory(context: vscode.ExtensionContext) }
// 示例:添加预设提示词 const PRESET_PROMPTS = [ {
name: '电商产品图', prompt: '专业产品摄影,白色背景,自然光线,细节清晰,商业用途', aspectRatio: '1:1'
}, {
name: 'UI概念图', prompt: '现代简约的网页设计,渐变色彩,玻璃拟态效果,未来科技感', aspectRatio: '16:9'
}, {
name: '技术架构图', prompt: '技术架构示意图,干净线条,信息可视化,专业商务风格', aspectRatio: '4:3'
} ];
插件开发完成后,你可能想分享给其他人使用。
7.1 打包插件
# 安装打包工具 npm install -g @vscode/vsce
打包
vsce package
这会生成一个.vsix文件,其他人可以通过”从VSIX安装“来使用你的插件。
7.2 发布到市场
如果你想发布到VSCode扩展市场:
- 注册Azure DevOps账号
- 创建个人访问令牌
- 发布插件:
vsce publish
7.3 更新package.json
确保package.json中有完整的元数据:
{ ”displayName“: ”Nano Banana Image Generator“, ”description“: ”Generate AI images with Nano-Banana directly in VSCode“, ”version“: ”1.0.0“, ”publisher“: ”your-name“, ”engines“: {
"vscode": "^1.60.0"
}, ”categories“: [
"Visualization", "AI", "Other"
], ”keywords“: [
"ai", "image", "generation", "nano-banana", "art", "design"
], ”repository“: {
"type": "git", "url": "https://github.com/your-username/nano-banana-vscode"
}, ”icon“: ”media/icon.png“ }
整个插件开发下来,感觉比想象中要顺利。VSCode的扩展API设计得挺合理的,Webview功能也很强大,可以做出很漂亮的界面。
实际用的时候,这个插件确实能提升工作效率。比如写技术文档需要配图,或者设计原型需要一些概念图,直接在编辑器里就能搞定,不用切换窗口。生成的速度和效果都还不错,特别是有了参考图功能后,可以更好地控制输出风格。
当然,现在这个版本还有很多可以改进的地方。比如可以加个历史记录功能,把之前生成的图片和提示词都保存下来;或者加个批量生成,一次生成多个变体;再或者集成更多的AI模型,让用户有更多选择。
如果你也想试试开发VSCode插件,我觉得可以从这种小工具开始。不用一开始就想做很复杂的功能,先解决一个具体的问题,把核心流程跑通,然后再慢慢添加特性。遇到问题多看看官方文档,VSCode的文档写得挺详细的,社区里也有很多例子可以参考。
开发过程中最深的体会是:好的工具应该让人感觉不到它的存在。这个插件现在还有点”工具感“,理想状态应该是完全融入工作流,就像编辑器自带的功能一样自然。这需要更多的打磨和优化,但第一步已经迈出去了。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/269462.html