bknd-client-setup
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseClient Setup
客户端设置
Set up the Bknd TypeScript SDK in your frontend application.
在你的前端应用中设置Bknd TypeScript SDK。
Prerequisites
前提条件
- Bknd backend running (local or deployed)
- Frontend project initialized (React, Vue, vanilla JS, etc.)
- package installed:
bkndnpm install bknd
- 运行中的Bknd后端(本地或已部署)
- 已初始化的前端项目(React、Vue、原生JS等)
- 已安装包:
bkndnpm install bknd
When to Use UI Mode
何时使用UI模式
Not applicable - client setup is code-only.
不适用 - 客户端设置仅通过代码完成。
When to Use Code Mode
何时使用代码模式
- Always - SDK setup requires code configuration
- Choose approach based on architecture:
- Standalone API client - Connecting to separate Bknd backend
- Embedded (BkndBrowserApp) - Bknd runs in browser (Vite/React)
- Framework adapter - Next.js, Astro, etc.
- 始终适用 - SDK设置需要代码配置
- 根据架构选择方案:
- 独立API客户端 - 连接到独立的Bknd后端
- 嵌入式(BkndBrowserApp) - Bknd在浏览器中运行(Vite/React)
- 框架适配器 - Next.js、Astro等
Approach 1: Standalone API Client
方案1:独立API客户端
Use when connecting to a separate Bknd backend server.
当连接到独立的Bknd后端服务器时使用。
Step 1: Basic Setup
步骤1:基础设置
typescript
import { Api } from "bknd";
const api = new Api({
host: "https://api.example.com", // Your Bknd backend URL
});
// Make requests
const { ok, data } = await api.data.readMany("posts");typescript
import { Api } from "bknd";
const api = new Api({
host: "https://api.example.com", // 你的Bknd后端URL
});
// 发起请求
const { ok, data } = await api.data.readMany("posts");Step 2: Add Token Persistence
步骤2:添加令牌持久化
Store auth tokens across page refreshes:
typescript
import { Api } from "bknd";
const api = new Api({
host: "https://api.example.com",
storage: localStorage, // Persists token as "auth" key
});Custom storage key:
typescript
const api = new Api({
host: "https://api.example.com",
storage: localStorage,
key: "myapp_auth", // Custom key instead of "auth"
});跨页面刷新存储认证令牌:
typescript
import { Api } from "bknd";
const api = new Api({
host: "https://api.example.com",
storage: localStorage, // 将令牌持久化为"auth"键
});自定义存储键:
typescript
const api = new Api({
host: "https://api.example.com",
storage: localStorage,
key: "myapp_auth", // 自定义键,替代"auth"
});Step 3: Handle Auth State Changes
步骤3:处理认证状态变更
React to login/logout events:
typescript
const api = new Api({
host: "https://api.example.com",
storage: localStorage,
onAuthStateChange: (state) => {
console.log("Auth state:", state);
// state.user - current user or undefined
// state.token - JWT or undefined
// state.verified - whether token was verified with server
},
});响应登录/登出事件:
typescript
const api = new Api({
host: "https://api.example.com",
storage: localStorage,
onAuthStateChange: (state) => {
console.log("认证状态:", state);
// state.user - 当前用户或undefined
// state.token - JWT或undefined
// state.verified - 令牌是否已通过服务器验证
},
});Step 4: Cookie-Based Auth (SSR)
步骤4:基于Cookie的认证(SSR)
For server-side rendering with cookies:
typescript
const api = new Api({
host: "https://api.example.com",
credentials: "include", // Send cookies cross-origin
});适用于使用Cookie的服务器端渲染:
typescript
const api = new Api({
host: "https://api.example.com",
credentials: "include", // 跨域发送Cookie
});Step 5: Full Configuration
步骤5:完整配置
typescript
import { Api } from "bknd";
const api = new Api({
// Required
host: "https://api.example.com",
// Auth persistence
storage: localStorage,
key: "auth",
// Auth events
onAuthStateChange: (state) => {
if (state.user) {
console.log("Logged in:", state.user.email);
} else {
console.log("Logged out");
}
},
// Request options
credentials: "include", // For cookies
verbose: true, // Log requests (dev only)
// Data API defaults
data: {
defaultQuery: { limit: 20 },
},
});
export { api };typescript
import { Api } from "bknd";
const api = new Api({
// 必填项
host: "https://api.example.com",
// 认证持久化
storage: localStorage,
key: "auth",
// 认证事件
onAuthStateChange: (state) => {
if (state.user) {
console.log("已登录:", state.user.email);
} else {
console.log("已登出");
}
},
// 请求选项
credentials: "include", // 用于Cookie
verbose: true, // 记录请求(仅开发环境)
// Data API默认值
data: {
defaultQuery: { limit: 20 },
},
});
export { api };Approach 2: React with BkndBrowserApp (Embedded)
方案2:通过BkndBrowserApp集成React(嵌入式)
Use when Bknd runs entirely in the browser (Vite + React).
当Bknd完全在浏览器中运行时使用(Vite + React)。
Step 1: Define Schema
步骤1:定义Schema
typescript
// bknd.config.ts
import { boolean, em, entity, text } from "bknd";
export const schema = em({
todos: entity("todos", {
title: text(),
done: boolean(),
}),
});
// Type registration for autocomplete
type Database = (typeof schema)["DB"];
declare module "bknd" {
interface DB extends Database {}
}typescript
// bknd.config.ts
import { boolean, em, entity, text } from "bknd";
export const schema = em({
todos: entity("todos", {
title: text(),
done: boolean(),
}),
});
// 注册类型以获得自动补全
type Database = (typeof schema)["DB"];
declare module "bknd" {
interface DB extends Database {}
}Step 2: Configure BkndBrowserApp
步骤2:配置BkndBrowserApp
tsx
// App.tsx
import { BkndBrowserApp, type BrowserBkndConfig } from "bknd/adapter/browser";
import { schema } from "./bknd.config";
const config = {
config: {
data: schema.toJSON(),
auth: {
enabled: true,
jwt: {
secret: "your-secret-key", // Use env var in production
},
},
},
options: {
seed: async (ctx) => {
// Initial data (runs once on empty DB)
await ctx.em.mutator("todos").insertMany([
{ title: "Learn bknd", done: false },
]);
},
},
} satisfies BrowserBkndConfig;
export default function App() {
return (
<BkndBrowserApp {...config}>
<YourRoutes />
</BkndBrowserApp>
);
}tsx
// App.tsx
import { BkndBrowserApp, type BrowserBkndConfig } from "bknd/adapter/browser";
import { schema } from "./bknd.config";
const config = {
config: {
data: schema.toJSON(),
auth: {
enabled: true,
jwt: {
secret: "your-secret-key", // 生产环境使用环境变量
},
},
},
options: {
seed: async (ctx) => {
// 初始数据(空数据库时仅运行一次)
await ctx.em.mutator("todos").insertMany([
{ title: "学习Bknd", done: false },
]);
},
},
} satisfies BrowserBkndConfig;
export default function App() {
return (
<BkndBrowserApp {...config}>
<YourRoutes />
</BkndBrowserApp>
);
}Step 3: Use the useApp Hook
步骤3:使用useApp Hook
tsx
import { useApp } from "bknd/adapter/browser";
function TodoList() {
const { api, app, user, isLoading } = useApp();
if (isLoading) return <div>Loading...</div>;
// api - Api instance for data/auth/media
// app - Full App instance
// user - Current user or null
// isLoading - True while initializing
const [todos, setTodos] = useState([]);
useEffect(() => {
api.data.readMany("todos").then(({ data }) => setTodos(data));
}, [api]);
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}tsx
import { useApp } from "bknd/adapter/browser";
function TodoList() {
const { api, app, user, isLoading } = useApp();
if (isLoading) return <div>加载中...</div>;
// api - 用于数据/认证/媒体的Api实例
// app - 完整的App实例
// user - 当前用户或null
// isLoading - 初始化时为True
const [todos, setTodos] = useState([]);
useEffect(() => {
api.data.readMany("todos").then(({ data }) => setTodos(data));
}, [api]);
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}Approach 3: React Standalone (External Backend)
方案3:独立React集成(连接外部后端)
Use when React connects to a separate Bknd server.
当React连接到独立的Bknd服务器时使用。
Step 1: Create API Instance
步骤1:创建API实例
typescript
// lib/api.ts
import { Api } from "bknd";
export const api = new Api({
host: import.meta.env.VITE_BKND_URL || "http://localhost:7654",
storage: localStorage,
});typescript
// lib/api.ts
import { Api } from "bknd";
export const api = new Api({
host: import.meta.env.VITE_BKND_URL || "http://localhost:7654",
storage: localStorage,
});Step 2: Create React Context
步骤2:创建React Context
tsx
// context/BkndContext.tsx
import { createContext, useContext, useEffect, useState, ReactNode } from "react";
import { Api, type TApiUser } from "bknd";
import { api } from "../lib/api";
type BkndContextType = {
api: Api;
user: TApiUser | null;
isLoading: boolean;
};
const BkndContext = createContext<BkndContextType | null>(null);
export function BkndProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<TApiUser | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Listen for auth changes
api.options.onAuthStateChange = (state) => {
setUser(state.user ?? null);
};
// Verify existing token on mount
api.verifyAuth().finally(() => {
setUser(api.getUser());
setIsLoading(false);
});
return () => {
api.options.onAuthStateChange = undefined;
};
}, []);
return (
<BkndContext.Provider value={{ api, user, isLoading }}>
{children}
</BkndContext.Provider>
);
}
export function useBknd() {
const ctx = useContext(BkndContext);
if (!ctx) throw new Error("useBknd must be used within BkndProvider");
return ctx;
}tsx
// context/BkndContext.tsx
import { createContext, useContext, useEffect, useState, ReactNode } from "react";
import { Api, type TApiUser } from "bknd";
import { api } from "../lib/api";
type BkndContextType = {
api: Api;
user: TApiUser | null;
isLoading: boolean;
};
const BkndContext = createContext<BkndContextType | null>(null);
export function BkndProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<TApiUser | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// 监听认证变更
api.options.onAuthStateChange = (state) => {
setUser(state.user ?? null);
};
// 挂载时验证现有令牌
api.verifyAuth().finally(() => {
setUser(api.getUser());
setIsLoading(false);
});
return () => {
api.options.onAuthStateChange = undefined;
};
}, []);
return (
<BkndContext.Provider value={{ api, user, isLoading }}>
{children}
</BkndContext.Provider>
);
}
export function useBknd() {
const ctx = useContext(BkndContext);
if (!ctx) throw new Error("useBknd必须在BkndProvider内部使用");
return ctx;
}Step 3: Wrap App
步骤3:包裹应用
tsx
// main.tsx
import { BkndProvider } from "./context/BkndContext";
ReactDOM.createRoot(document.getElementById("root")!).render(
<BkndProvider>
<App />
</BkndProvider>
);tsx
// main.tsx
import { BkndProvider } from "./context/BkndContext";
ReactDOM.createRoot(document.getElementById("root")!).render(
<BkndProvider>
<App />
</BkndProvider>
);Step 4: Use in Components
步骤4:在组件中使用
tsx
function Profile() {
const { api, user, isLoading } = useBknd();
if (isLoading) return <div>Loading...</div>;
if (!user) return <div>Not logged in</div>;
return <div>Hello, {user.email}</div>;
}tsx
function Profile() {
const { api, user, isLoading } = useBknd();
if (isLoading) return <div>加载中...</div>;
if (!user) return <div>未登录</div>;
return <div>你好,{user.email}</div>;
}Approach 4: Next.js Integration
方案4:Next.js集成
Step 1: Create API Utility
步骤1:创建API工具
typescript
// lib/bknd.ts
import { Api } from "bknd";
// Client-side singleton
let clientApi: Api | null = null;
export function getClientApi() {
if (typeof window === "undefined") {
throw new Error("getClientApi can only be used client-side");
}
if (!clientApi) {
clientApi = new Api({
host: process.env.NEXT_PUBLIC_BKND_URL!,
storage: localStorage,
});
}
return clientApi;
}
// Server-side (per-request)
export function getServerApi(request?: Request) {
return new Api({
host: process.env.BKND_URL!,
request, // Extracts token from cookies/headers
});
}typescript
// lib/bknd.ts
import { Api } from "bknd";
// 客户端单例
let clientApi: Api | null = null;
export function getClientApi() {
if (typeof window === "undefined") {
throw new Error("getClientApi只能在客户端使用");
}
if (!clientApi) {
clientApi = new Api({
host: process.env.NEXT_PUBLIC_BKND_URL!,
storage: localStorage,
});
}
return clientApi;
}
// 服务端(每个请求实例)
export function getServerApi(request?: Request) {
return new Api({
host: process.env.BKND_URL!,
request, // 从Cookie/Header中提取令牌
});
}Step 2: Client Component
步骤2:客户端组件
tsx
"use client";
import { useEffect, useState } from "react";
import { getClientApi } from "@/lib/bknd";
export function PostList() {
const [posts, setPosts] = useState([]);
const api = getClientApi();
useEffect(() => {
api.data.readMany("posts").then(({ data }) => setPosts(data));
}, []);
return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}tsx
"use client";
import { useEffect, useState } from "react";
import { getClientApi } from "@/lib/bknd";
export function PostList() {
const [posts, setPosts] = useState([]);
const api = getClientApi();
useEffect(() => {
api.data.readMany("posts").then(({ data }) => setPosts(data));
}, []);
return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}Step 3: Server Component
步骤3:服务端组件
tsx
// app/posts/page.tsx
import { getServerApi } from "@/lib/bknd";
export default async function PostsPage() {
const api = getServerApi();
const { data: posts } = await api.data.readMany("posts");
return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}tsx
// app/posts/page.tsx
import { getServerApi } from "@/lib/bknd";
export default async function PostsPage() {
const api = getServerApi();
const { data: posts } = await api.data.readMany("posts");
return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}TypeScript Type Registration
TypeScript类型注册
Get autocomplete for entity names and fields:
typescript
// types/bknd.d.ts
import { em, entity, text, number } from "bknd";
// Define your schema
const schema = em({
posts: entity("posts", {
title: text(),
views: number(),
}),
users: entity("users", {
email: text(),
name: text(),
}),
});
// Register types globally
type Database = (typeof schema)["DB"];
declare module "bknd" {
interface DB extends Database {}
}Now returns typed .
api.data.readMany("posts")Post[]为实体名称和字段获得自动补全:
typescript
// types/bknd.d.ts
import { em, entity, text, number } from "bknd";
// 定义你的Schema
const schema = em({
posts: entity("posts", {
title: text(),
views: number(),
}),
users: entity("users", {
email: text(),
name: text(),
}),
});
// 全局注册类型
type Database = (typeof schema)["DB"];
declare module "bknd" {
interface DB extends Database {}
}现在会返回类型化的。
api.data.readMany("posts")Post[]Api Class Methods Reference
Api类方法参考
typescript
// Auth state
api.getUser() // Current user or null
api.isAuthenticated() // Has valid token
api.isAuthVerified() // Token verified with server
api.verifyAuth() // Verify token (async)
api.getAuthState() // { token, user, verified }
// Module APIs
api.data.readMany(...) // CRUD operations
api.auth.login(...) // Authentication
api.media.upload(...) // File uploads
api.system.health() // System checks
// Token management
api.updateToken(token) // Manually set token
api.token_transport // "header" | "cookie" | "none"typescript
// 认证状态
api.getUser() // 当前用户或null
api.isAuthenticated() // 是否拥有有效令牌
api.isAuthVerified() // 令牌是否已通过服务器验证
api.verifyAuth() // 验证令牌(异步)
api.getAuthState() // { token, user, verified }
// 模块API
api.data.readMany(...) // CRUD操作
api.auth.login(...) // 认证
api.media.upload(...) // 文件上传
api.system.health() // 系统检查
// 令牌管理
api.updateToken(token) // 手动设置令牌
api.token_transport // "header" | "cookie" | "none"Environment Variables
环境变量
bash
undefinedbash
undefined.env.local (Next.js)
.env.local (Next.js)
NEXT_PUBLIC_BKND_URL=http://localhost:7654
BKND_URL=http://localhost:7654
NEXT_PUBLIC_BKND_URL=http://localhost:7654
BKND_URL=http://localhost:7654
.env (Vite)
.env (Vite)
VITE_BKND_URL=http://localhost:7654
undefinedVITE_BKND_URL=http://localhost:7654
undefinedCommon Pitfalls
常见陷阱
Token Not Persisting
令牌未持久化
Problem: User logged out after refresh
Fix: Provide storage option:
typescript
// WRONG - no persistence
const api = new Api({ host: "..." });
// CORRECT
const api = new Api({
host: "...",
storage: localStorage,
});问题: 页面刷新后用户登出
解决方法: 提供storage选项:
typescript
// 错误 - 无持久化
const api = new Api({ host: "..." });
// 正确
const api = new Api({
host: "...",
storage: localStorage,
});CORS Errors
CORS错误
Problem: Browser blocks requests to backend
Fix: Configure CORS on backend:
typescript
// bknd.config.ts (server)
const app = new App({
server: {
cors: {
origin: ["http://localhost:3000"],
credentials: true,
},
},
});问题: 浏览器阻止向后端发起的请求
解决方法: 在后端配置CORS:
typescript
// bknd.config.ts (服务端)
const app = new App({
server: {
cors: {
origin: ["http://localhost:3000"],
credentials: true,
},
},
});Auth State Not Updating
认证状态未更新
Problem: UI doesn't reflect login/logout
Fix: Use :
onAuthStateChangetypescript
const api = new Api({
host: "...",
onAuthStateChange: (state) => {
// Update your UI state here
setUser(state.user ?? null);
},
});问题: UI未反映登录/登出状态
解决方法: 使用:
onAuthStateChangetypescript
const api = new Api({
host: "...",
onAuthStateChange: (state) => {
// 在此处更新UI状态
setUser(state.user ?? null);
},
});SSR Hydration Mismatch
SSR hydration不匹配
Problem: Server/client render different content
Fix: Check auth on client only:
tsx
function AuthStatus() {
const [user, setUser] = useState(null);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
setUser(api.getUser());
}, []);
if (!mounted) return null; // Avoid hydration mismatch
return user ? <span>{user.email}</span> : <span>Guest</span>;
}问题: 服务端和客户端渲染内容不同
解决方法: 仅在客户端检查认证:
tsx
function AuthStatus() {
const [user, setUser] = useState(null);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
setUser(api.getUser());
}, []);
if (!mounted) return null; // 避免hydration不匹配
return user ? <span>{user.email}</span> : <span>访客</span>;
}Using Wrong Import Path
使用错误的导入路径
Problem: Import errors
Fix: Use correct subpath:
typescript
// Standalone API
import { Api } from "bknd";
// React browser adapter
import { BkndBrowserApp, useApp } from "bknd/adapter/browser";
// Next.js adapter
import type { NextjsBkndConfig } from "bknd/adapter/nextjs";问题: 导入错误
解决方法: 使用正确的子路径:
typescript
// 独立API
import { Api } from "bknd";
// React浏览器适配器
import { BkndBrowserApp, useApp } from "bknd/adapter/browser";
// Next.js适配器
import type { NextjsBkndConfig } from "bknd/adapter/nextjs";Multiple Api Instances
多个Api实例
Problem: Auth state inconsistent across app
Fix: Use singleton pattern:
typescript
// lib/api.ts
let api: Api | null = null;
export function getApi() {
if (!api) {
api = new Api({ host: "...", storage: localStorage });
}
return api;
}问题: 应用中认证状态不一致
解决方法: 使用单例模式:
typescript
// lib/api.ts
let api: Api | null = null;
export function getApi() {
if (!api) {
api = new Api({ host: "...", storage: localStorage });
}
return api;
}Verification
验证
Test your setup:
typescript
import { api } from "./lib/api";
async function test() {
// 1. Check connection
const { ok } = await api.data.readMany("posts", { limit: 1 });
console.log("API connected:", ok);
// 2. Check auth
console.log("Authenticated:", api.isAuthenticated());
console.log("User:", api.getUser());
// 3. Test login
const { ok: loginOk } = await api.auth.login("password", {
email: "test@example.com",
password: "password123",
});
console.log("Login:", loginOk);
}测试你的设置:
typescript
import { api } from "./lib/api";
async function test() {
// 1. 检查连接
const { ok } = await api.data.readMany("posts", { limit: 1 });
console.log("API已连接:", ok);
// 2. 检查认证
console.log("已认证:", api.isAuthenticated());
console.log("用户:", api.getUser());
// 3. 测试登录
const { ok: loginOk } = await api.auth.login("password", {
email: "test@example.com",
password: "password123",
});
console.log("登录结果:", loginOk);
}DOs and DON'Ts
注意事项
DO:
- Use for token persistence
storage: localStorage - Handle for reactive UI
onAuthStateChange - Use singleton pattern for Api instance
- Call on app startup
verifyAuth() - Use environment variables for host URL
DON'T:
- Create multiple Api instances
- Forget to configure CORS on backend
- Use Api directly in SSR without request context
- Hardcode backend URLs
- Ignore auth state changes in UI
建议:
- 使用实现令牌持久化
storage: localStorage - 处理以实现响应式UI
onAuthStateChange - 为Api实例使用单例模式
- 应用启动时调用
verifyAuth() - 使用环境变量存储后端URL
禁止:
- 创建多个Api实例
- 忘记在后端配置CORS
- 在SSR中直接使用Api而不传入请求上下文
- 硬编码后端URL
- 忽略UI中的认证状态变更
Related Skills
相关技能
- bknd-api-discovery - Explore available endpoints
- bknd-login-flow - Implement authentication
- bknd-crud-read - Query data with SDK
- bknd-session-handling - Manage user sessions
- bknd-local-setup - Set up local backend
- bknd-api-discovery - 探索可用的端点
- bknd-login-flow - 实现认证流程
- bknd-crud-read - 使用SDK查询数据
- bknd-session-handling - 管理用户会话
- bknd-local-setup - 设置本地后端