基于 Nexty 服务端的浏览器插件开发指南
概述
在开发浏览器插件的时候,我们通常需要一个服务端,而 Nexty 具备了 Supabase 认证、Stripe 支付等等 SaaS 产品必备的服务端能力,你完全可以基于 Nexty 开发你的服务端功能,让浏览器插件开发也变得快起来。
本文将详细介绍如何基于 Nexty 模板的服务端能力,实现浏览器插件登录和业务数据同步。
以下内容主要解决3个需求:
- 浏览器插件完成公开数据请求
- 浏览器插件完成登录
- 浏览器插件完成需要鉴权的非公开数据请求
测试方式
本文主要讲解思路,代码不是完整的,你可以 clone 以下完整源码,在本地进行测试:
- 服务端代码请查看Nexty 源码 extension 分支
- 浏览器插件端代码请查看开源仓库
git clone https://github.com/WeNextDev/nexty.dev.git
cd nexty.dev
git checkout extension-request-demo
pnpm install
pnpm dev
git clone https://github.com/WeNextDev/nexty-extension-request-demo.git
cd nexty-extension-request-demo
pnpm install
pnpm dev
需求1:公开数据请求
假设我们的服务端有一个接口,返回的是公开数据,请求路径是这样的:
我们先在 Nexty 代码里编写一个公开的、不需要鉴权的接口:
import { apiResponse } from "@/lib/api-response";
export async function GET() {
const publicData = {
latestAnnouncement:
"🎉 Welcome to Nexty.dev! This is a public endpoint.",
};
try {
const response = apiResponse.success(publicData);
response.headers.set(
"Cache-Control",
"public, s-maxage=86400, stale-while-revalidate=43200", // 添加缓存头,避免频繁请求,减轻服务端压力
);
} catch (error) {
console.error("Error fetching public data:", error);
return apiResponse.serverError();
}
}
在浏览器插件前端发起请求:
useEffect(() => {
chrome.runtime.sendMessage({ type: "GET_PUBLIC_DATA" }, (response) => {
if (response.success) {
setPublicDataState({ data: response.data, error: null, isLoading: false });
} else {
setPublicDataState({ data: null, error: response.error, isLoading: false });
}
});
}, []);
在浏览器插件 background 转发请求:
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === "GET_PUBLIC_DATA") {
apiFetch("/api/extension-demo/public-data", false, "public_data_cache").then(sendResponse);
return true;
}
return false;
});
现在浏览器插件前端就能拿到公开接口返回的数据了。
需求2:插件登录认证与登录态同步
认证流程图
先看一下认证流程图
看起来有点复杂,核心原则就是 3 点:
- Nexty 网站是唯一的认证中心:浏览器插件不处理登录凭据,只“借用”网站的登录态
- 服务端验证:所有权限检查都在 Nexty 后端进行,确保数据安全
- 实时同步:浏览器插件每次打开都会验证最新的用户状态,保证信息准确性
实现步骤
在 Nexty 项目中创建获取用户登录态的 API app/api/user-status/route.ts
,并且这里有一步很重要的是,要把 Cookie 完整地返回,确保插件端的登录态能够持续:
// ... other code ...
// 未登录的返回信息:
if (!user) {
return apiResponse.success({ isLoggedIn: false })
}
// 已登录的返回信息
const responseData = {
isLoggedIn: true, // 已登录的标识
id: user.id,
email: user.email,
plan: profile?.role || 'user',
}
const finalResponse = apiResponse.success(responseData)
// 重要!返回 Cookie
response.cookies.getAll().forEach((cookie) => {
finalResponse.cookies.set(cookie)
})
return finalResponse
// ... other code ...
在浏览器插件前端发起请求:
useEffect(() => {
chrome.runtime.sendMessage({ type: "GET_USER_STATUS" }, (response) => {
if (response.success) {
setUserStatusState({ data: response.data, error: null, isLoading: false });
} else {
if (!cachedStatus) setUserStatusState({ data: null, error: response.error, isLoading: false });
}
});
}, []);
在浏览器插件 background 转发请求:
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === "GET_USER_STATUS") {
apiFetch("/api/extension-demo/user-status", true, "user_status_cache").then(sendResponse);
return true;
}
return false;
});
如果用户已登录,则展示用户信息;如果用户未登录,则展示登录按钮,按钮点击会调用 chrome.tabs.create
方法,在新标签页打开登录页面:
const handleLoginClick = () => {
chrome.tabs.create({ url: process.env.PLASMO_PUBLIC_SITE_LOGIN_URL });
};
return (
<div>
<h2>User Info</h2>
{userStatusState.data && (
userStatusState.data.isLoggedIn ? (
<div>
<p>Welcome, <strong>{userStatusState.data.email}</strong>!</p>
<p>Your user role: <strong>{userStatusState.data.plan}</strong></p>
</div>
) : (
<div>
<h4>Please login to use</h4>
<button onClick={handleLoginClick}>Go to login</button>
</div>
)
)}
</div>
);
在本文档提供的浏览器插件端代码里,还应用了乐观更新的理念,即先展示旧的缓存数据,请求完成后使用新数据覆盖掉旧数据:
useEffect(() => {
// 先尝试读取缓存
const cachedStatus = await storage.get<UserStatus>("user_status_cache");
if (cachedStatus) {
setUserStatusState({ data: cachedStatus, error: null, isLoading: false });
}
chrome.runtime.sendMessage({ type: "GET_USER_STATUS" }, (response) => {
// ... other code ...
});
}, []);
除此之外,为了让用户刚打开浏览器插件就能看到用户状态,我们还可以在插件安装和浏览器启动的时候就发起一次请求:
// When the extension installed or the browser starts
// get the status once as the initial cache
chrome.runtime.onInstalled.addListener(() => {
apiFetch("/api/extension-demo/user-status", true, "user_status_cache");
});
chrome.runtime.onStartup.addListener(() => {
apiFetch("/api/extension-demo/user-status", true, "user_status_cache");
});
需求3:非公开数据请求
实际产品开发过程中,肯定有一部分数据需要登录用户才能访问,例如用户的订阅状态。
我们创建一个获取用户订阅状态的接口作为示例:
export async function GET(request: NextRequest) {
try {
// 1. 判断登录状态
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
return apiResponse.unauthorized();
}
// 2. 调用 Server Actions,不需要单独写处理逻辑
const benefits = await getUserBenefits(user.id);
// 3. 返回
const finalResponse = apiResponse.success(benefits);
response.cookies.getAll().forEach((cookie) => {
finalResponse.cookies.set(cookie);
});
return finalResponse;
} catch (error) {
console.error("Error fetching user benefits:", error);
return apiResponse.serverError();
}
}
浏览器前端请求前需要先判断登录状态,仅已登录用户才进行请求:
if (!userStatusState.isLoading && userStatusState.data?.isLoggedIn) {
const cachedBenefits = await storage.get<UserBenefits>("user_benefits_cache");
if (cachedBenefits) {
setUserBenefitsState({ data: cachedBenefits, error: null, isLoading: false });
}
chrome.runtime.sendMessage({ type: "GET_USER_BENEFITS" }, (response) => {
if (response.success) {
setUserBenefitsState({ data: response.data, error: null, isLoading: false });
} else {
if (!cachedBenefits) setUserBenefitsState({ data: null, error: response.error, isLoading: false });
}
});
}
浏览器插件 background 仍然进行请求转发:
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === "GET_USER_BENEFITS") {
apiFetch("/api/extension-demo/user-benefits", true, "user_benefits_cache").then(sendResponse);
return true;
}
return false;
});
其他最佳实践建议
-
使用独立 API 前缀:为浏览器插件提供的 API 使用独立前缀,方便处理公共逻辑
-
利用缓存减轻服务端压力:对于实时性要求不高的数据,要利用 Next.js 的缓存能力或者 http 请求返回添加缓存头的方式,减轻服务器的压力
提示
参考 Nexty 源码 extenison 分支的
app/api/extension-demo/public-data/route.ts
-
加密请求:浏览器插件请求应该添加加密签名头,这样即使是公开的接口也能多一层防护
提示
Demo 源码位置:
- Nexty 源码 extension 分支的
middleware.ts
和lib/crypto-utils.ts
- 浏览器插件源码的
background/index.ts
和background/crypto-utils.ts
crypto-utils.ts
的加密方法仅作为示例,如果需要更复杂的加密方法,可以让 AI 为你编写,它们很擅长加密算法。 - Nexty 源码 extension 分支的