外观
基础示例
以下 TypeScript 代码块可独立复制。存储和 DB 示例需要浏览器环境;HTTP 示例需要业务接口,Bridge 和 Logger 示例使用内存适配以便本地验证。
保存用户选择
ts
import { createStorage } from '@freenotes/web-runtime';
const storage = createStorage('local');
storage.set('scope', {
subjectId: 'english',
courseId: 'cet4'
});
const scope = storage.get('scope');类型化 key-value
ts
import { createStorage } from '@freenotes/web-runtime';
interface AppStorageSchema {
scope: {
courseId: string;
subjectId: string;
};
}
const storage = createStorage<AppStorageSchema>('local');
storage.set('scope', {
courseId: 'cet4',
subjectId: 'english'
});
const typedScope = storage.get('scope');
typedScope?.courseId;保存会话状态
ts
import { createStorage } from '@freenotes/web-runtime';
const sessionStorage = createStorage('session');
sessionStorage.set('last-path', location.pathname);保存带过期时间的数据
ts
import { createStorage } from '@freenotes/web-runtime';
const storage = createStorage('local');
storage.set('lease', { id: 'startup-ad' }, { ttlMs: 30 * 1000 });
const lease = storage.get('lease');使用 IndexedDB
ts
import { createDb } from '@freenotes/web-runtime';
const db = await createDb().open({
name: 'question-bank',
stores: {
books: {
keyPath: 'id'
}
}
});
try {
await db.store('books').put({
id: 'book-1',
title: 'English'
});
const book = await db.store('books').get('book-1');
console.info(book);
} finally {
db.close();
}复制文本
ts
import { ClipboardError, createClipboard } from '@freenotes/web-runtime';
// 将 copyShareLink 绑定到复制按钮的点击事件。
export async function copyShareLink(): Promise<string> {
try {
await createClipboard().writeText('https://example.com/share');
return '已复制';
} catch (error) {
if (error instanceof ClipboardError) return '复制失败,请稍后重试';
throw error;
}
}调用方根据返回文案展示提示。复制通常需要安全上下文和用户交互,具体限制见 Clipboard 指南。
声明 HTTP 接口
假设 GET /api/books/:id 返回 { code: 0, data: { id, title } },显式开启业务解包。这里用 parser 验证解包后的外部数据,contract.type() 本身只声明类型。
ts
import { createHttp, HttpError, unwrapBusinessEnvelope } from '@freenotes/web-runtime';
interface Book {
id: string;
title: string;
}
const http = createHttp({
baseURL: '/api',
decodeResponse: unwrapBusinessEnvelope
});
const api = http.createApi((contract) => ({
getBook: {
method: 'get',
url: '/books/:id',
path: contract.type<{ id: string }>(),
response: contract.type<unknown>()
}
}));
function parseBook(value: unknown): Book {
if (
typeof value === 'object' &&
value !== null &&
'id' in value &&
typeof value.id === 'string' &&
'title' in value &&
typeof value.title === 'string'
) {
return { id: value.id, title: value.title };
}
throw new TypeError('Invalid book response');
}
export async function loadBook(id: string): Promise<Book> {
try {
return parseBook(await api.getBook({ path: { id } }));
} catch (error) {
if (error instanceof HttpError) {
console.error(error.kind, error.businessCode, error.transportCode);
}
throw error;
}
}如果服务端直接返回业务对象,省略 decodeResponse。需要 Standard Schema 校验、拦截器或重试时,继续阅读 HTTP 指南。
浏览器中模拟 Bridge
mock 返回已解码数据,并提供真实的订阅清理函数。示例结束后不会保留事件监听。
ts
import {
createBridge,
type BridgeAdapter,
type BridgeHandler
} from '@freenotes/web-runtime';
interface Methods {
openDocument: { input: { documentId: string }; output: void };
}
interface Events {
resume: { source: string };
}
const handlers = new Map<string, Set<BridgeHandler>>();
const adapter: BridgeAdapter = {
async call(method, payload) {
if (method === 'configAction') {
return { app_version: 'mock-1.0', platform: 'browser' };
}
if (method === 'openDocument') {
console.info('模拟打开文档', payload);
return undefined;
}
throw new Error(`Unsupported mock method: ${method}`);
},
on(event, handler) {
const listeners = handlers.get(event) ?? new Set<BridgeHandler>();
handlers.set(event, listeners);
listeners.add(handler);
return () => {
listeners.delete(handler);
if (listeners.size === 0) handlers.delete(event);
};
}
};
const bridge = createBridge<Methods, Events>({ adapter });
const subscription = bridge.on('resume', (payload) => {
console.info('页面恢复来源', payload.source);
});
try {
const config = await bridge.getConfig();
console.info(config?.appVersion); // mock-1.0
await bridge.call('openDocument', { documentId: 'doc-1' });
const event: Events['resume'] = { source: 'mock' };
for (const handler of handlers.get('resume') ?? []) handler(event);
} finally {
subscription.dispose();
}App 内使用默认适配时省略 adapter;仍需宿主实现对应方法和事件。协议边界见 Bridge 进阶。
使用自定义 Logger transport
用内存数组观察实际日志记录。生产接入时,把 transport 替换为已有日志 SDK 或上报服务;transport 失败应抛错或 reject。
ts
import { createLogger, type LoggerRecord } from '@freenotes/web-runtime';
interface Context {
documentId: string;
}
const records: LoggerRecord<Context>[] = [];
const logger = createLogger<Context>({
source: 'document-page',
transport(record) {
records.push(record);
}
});
const result = await logger.info('document_opened', { documentId: 'doc-1' });
if (result.status === 'dispatched') {
console.info(records[0]?.context?.documentId); // doc-1
} else if (result.status === 'skipped') {
console.info(result.reason);
} else {
console.error(result.error);
}
await logger.error('document_failed', {
error: new Error('Document unavailable'),
context: { documentId: 'doc-1' }
});dispatched 表示 transport 调用完成。需要 App 日志协议时使用 createBridgeLogTransport(),详见 Logger 指南。