14 使用脚本 API
14.1 目标
Sa2web 页面脚本和普通接口拦截脚本可使用的 window.api 能力,便于在站点配置、内部站点配置、账号配置和插件脚本中编写更稳定的脚本。
Script API分为五类:
| API | 用途 |
|---|---|
api.config |
读取自定义配置,目前只针对协作链接下有效 |
api.http |
发起 HTTP 请求,且绕过页面 CORS 限制 |
api.user |
读写用户、站点、账号或设备维度的数据 |
api.dom |
查询 DOM、判断可见性、监听元素变化、创建覆盖层 |
api.utils |
等待条件、在页面中执行 JavaScript |
api.header() |
读取已记录的请求头或响应头 |
页面脚本和普通接口拦截脚本可以使用 window.api。SSE 脚本只保证传入 data,通常不依赖 window.api。
14.2 总体结构
在脚本中可以直接使用 api,也可以通过 window.api 访问:
interface Window {
api: {
user: UserApi;
config: Record<string, unknown>;
http: HttpApi;
dom: DomApi;
utils: UtilsApi;
header(headerName: string, isRequestHeader: boolean): Promise<string | string[] | undefined>;
};
}示例:
const apiBase = api.config.apiBase;
const userToken = await api.user.get('token');
const submit = api.dom.querySelector(document, 'button[type="submit"]');
const profile = await api.http.ajax({ url: `http://192.168.1.111:8080/profile` });脚本应优先使用 Sa2web 提供的 api.dom 查询工具。它同时支持 CSS selector 和 XPath selector,比只使用 document.querySelector() 更适合处理复杂业务页面。
14.3 api.config
api.config 用于读取站点脚本环境中的自定义配置。
config: Record<string, unknown>示例:
const apiBase = api.config.apiBase;
const featureEnabled = api.config.featureEnabled === true;14.4 api.http
api.http 为用户脚本提供 HTTP 请求工具。api.http.ajax 会在 浏览器主进程中执行实际请求,因此不受页面 CORS 策略限制。
14.4.1 api.http.ajax(options)
发送 HTTP 请求。
ajax(options: {
url: string;
method?: string;
data?: any;
headers?: Record<string, string>;
timeout?: number;
dataType?: 'json' | 'text' | 'html' | 'arrayBuffer';
contentType?: string;
processData?: boolean;
}): Promise<{
ok: boolean;
status: number;
statusText: string;
data?: any;
error?: string;
timeout?: boolean;
}>参数说明:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
url |
string |
- | 请求 URL。 |
method |
string |
GET |
HTTP 方法。 |
data |
any |
- | 请求数据。GET 和 HEAD 请求会序列化到查询字符串;其他方法会写入请求体。 |
headers |
Record<string, string> |
{} |
请求头。 |
timeout |
number |
- | 超时时间,单位为毫秒。仅大于 0 时生效。 |
dataType |
'json' \| 'text' \| 'html' \| 'arrayBuffer' |
json |
响应解析方式。 |
contentType |
string |
application/x-www-form-urlencoded; charset=UTF-8 |
请求体的 Content-Type。 |
processData |
boolean |
true |
是否自动序列化 data。设为 false 时,data 会直接作为请求体传入。 |
返回值:
| 字段 | 类型 | 说明 |
|---|---|---|
ok |
boolean |
HTTP 2xx 响应为 true;解析错误、HTTP 错误、超时、中止或网络错误为 false。 |
status |
number |
HTTP 状态码。0 表示超时、中止或网络层失败。 |
statusText |
string |
HTTP 状态文本;非 HTTP 失败时为 timeout、abort 或 error。 |
data |
any |
解析成功后的响应数据。 |
error |
string |
解析失败或非 HTTP 失败时的错误消息。 |
timeout |
boolean |
请求因配置的超时时间被中止时为 true。 |
GET 请求示例:
const ret = await api.http.ajax({
url: 'https://example.com/api/profile',
method: 'GET',
dataType: 'json',
timeout: 10000
});
if (ret.ok) {
console.log(ret.data);
}POST 请求示例:
const ret = await api.http.ajax({
url: 'https://example.com/api/items',
method: 'POST',
contentType: 'application/json',
data: { name: 'demo' }
});
if (!ret.ok) {
console.warn(ret.status, ret.error || ret.statusText);
}14.5 api.user
api.user 用于读写脚本运行过程中的用户关联数据。它可以按不同维度隔离存储范围。
通用参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
site |
boolean |
false |
是否按站点隔离 |
account |
boolean |
false |
是否按站点账号或者工作空间名称隔离 |
did |
boolean |
false |
是否按设备隔离 |
维度选择建议:
| 场景 | 建议 |
|---|---|
| 同一用户跨站点共享 | site=false |
| 每个站点独立保存 | site=true |
| 同一站点的不同账号隔离 | account=true |
| 同一用户在不同设备上隔离 | did=true |
14.5.1 api.user.put()
保存指定键值。
put(
name: string,
value: string,
site?: boolean,
account?: boolean,
did?: boolean
): Promise<{ status: boolean }>示例:
await api.user.put('token', 'abc123');
await api.user.put('draft:lastOrderId', 'A-1001', true, true);14.5.2 api.user.get()
读取指定键值。
get(
name: string,
site?: boolean,
account?: boolean,
did?: boolean
): Promise<{ value: string | null, status: boolean }>示例:
const ret = await api.user.get('token');
if (ret.status && ret.value) {
console.log(ret.value);
}14.5.3 api.user.remove()
删除指定键值。
remove(
name: string,
site?: boolean,
account?: boolean,
did?: boolean
): Promise<{ status: boolean }>示例:
await api.user.remove('token');
await api.user.remove('draft:lastOrderId', true, true);14.5.4 api.user.incr()
按步长增加指定键的数值。键不存在时会创建该键。
incr(
name: string,
step?: number,
site?: boolean,
account?: boolean,
did?: boolean
): Promise<{ status: boolean, value: number | string }>示例:
const ret = await api.user.incr('submitCount', 1, true);
console.log(ret.value);14.5.5 api.user.decr()
按步长减少指定键的数值。键不存在时会按负数初始值创建。
decr(
name: string,
step?: number,
site?: boolean,
account?: boolean,
did?: boolean
): Promise<{ status: boolean, value: number | string }>示例:
const ret = await api.user.decr('remainingQuota', 1, true, true);
console.log(ret.value);14.5.6 api.user.startsWith()
按键名前缀查找数据。
startsWith(
prefix: string,
site?: boolean,
account?: boolean,
did?: boolean
): Promise<Array<{ name: string, value: string }>>示例:
const items = await api.user.startsWith('cache:', true);
for (const item of items) {
console.log(item.name, item.value);
}14.5.7 api.user.countAll()
统计指定键名的记录数量。
countAll(
name: string,
site?: boolean,
account?: boolean
): Promise<{ value: number, status: boolean }>示例:
const ret = await api.user.countAll('token', true);
console.log(ret.value);14.5.8 api.user.sumAll()
统计指定键名对应值的数值总和。
sumAll(
name: string,
site?: boolean,
account?: boolean
): Promise<{ value: number, status: boolean }>示例:
const ret = await api.user.sumAll('score', true);
console.log(ret.value);14.6 api.dom
api.dom 提供 DOM 查询、可见性判断、连接监听、尺寸监听和覆盖层创建能力。
选择器支持:
| 写法 | 说明 |
|---|---|
.button.primary |
CSS selector |
xpath://div[@id="app"] |
XPath selector |
.dialog:p |
返回匹配元素的父元素 |
.dialog:p2 |
返回匹配元素向上两级的父元素 |
.header:bottom |
在覆盖层边界方法中使用元素下边界 |
.sidebar:right |
在覆盖层边界方法中使用元素右边界 |
具体也可见 小节 12.15
14.6.1 api.dom.createMutationObserver()
创建并缓存 MutationObserver。如果目标元素上已经绑定同名 observer,会直接返回已有 observer。
createMutationObserver(
ele: Element,
bindStr: string,
childList: boolean,
subtree: boolean,
attributes: boolean,
characterData: boolean,
fn: (mutations: MutationRecord[]) => void
): MutationObserver示例:
api.dom.createMutationObserver(
document.body,
'__bodyObserver__',
true,
true,
false,
false,
(mutations) => console.log(mutations)
);14.6.2 api.dom.querySelector()
查询第一个匹配元素。
querySelector(doc: Document, cssOrXPathSelector: string): HTMLElement | null示例:
const el = api.dom.querySelector(
document,
'xpath://button[contains(.,"提交")]'
);14.6.3 api.dom.querySelectorAll()
查询所有匹配元素。
querySelectorAll(doc: Document, cssOrXPathSelector: string): HTMLElement[]示例:
const buttons = api.dom.querySelectorAll(document, 'button.primary');
buttons.forEach((button) => console.log(button.textContent));14.6.4 api.dom.isVisible()
判断元素是否处于可见交叉区域。
isVisible(ele: HTMLElement): Promise<boolean>示例:
const el = api.dom.querySelector(document, '.submit');
if (el && await api.dom.isVisible(el)) {
console.log('visible');
}14.6.5 api.dom.getVisibleRect()
获取元素当前可见区域矩形。
getVisibleRect(ele: HTMLElement): Promise<DOMRectReadOnly>示例:
const el = api.dom.querySelector(document, '.panel');
if (el) {
const rect = await api.dom.getVisibleRect(el);
console.log(rect.left, rect.top, rect.width, rect.height);
}14.6.6 api.dom.getConnectListeners()
获取当前连接监听器列表。
getConnectListeners(): Array<{
querySelector: string;
callback: (isConnected: boolean) => void;
isConnected?: boolean;
}>示例:
api.dom.addConnectListener('.modal', () => {});
console.log(api.dom.getConnectListeners());14.6.7 api.dom.addConnectListener()
监听元素在文档中的出现和消失。
addConnectListener(
cssOrXPathSelector: string,
callback: (isConnected: boolean) => void
): void示例:
api.dom.addConnectListener('.dialog', (isConnected) => {
console.log('dialog:', isConnected);
});14.6.8 api.dom.removeConnectListener()
移除指定选择器对应的连接监听器。
removeConnectListener(cssOrXPathSelectors: string[]): void示例:
api.dom.removeConnectListener(['.dialog', '.toast']);14.6.9 api.dom.addResizeListener()
监听目标元素尺寸和位置变化。元素不存在时,回调会收到空矩形。
addResizeListener(
cssOrXPathSelector: string,
bindWindowStr: string,
callback: (rect: DOMRect) => void,
createObserver?: boolean,
delayTime?: number
): ResizeObserver | (() => void)示例:
api.dom.addResizeListener('.target', '__targetResize__', (rect) => {
console.log(rect.width, rect.height);
});14.6.10 api.dom.createOverlayBy()
创建一个跟随目标元素可见区域的固定定位覆盖层。
createOverlayBy(
cssOrXPathSelector: string,
bindWindowStr: string,
createObserver?: boolean,
delayTime?: number,
fn?: (rect: DOMRectReadOnly) => void
): HTMLElement示例:
const overlay = api.dom.createOverlayBy('.target', '__overlay__');
overlay.style.border = '2px solid #f00';
overlay.style.pointerEvents = 'none';
overlay.style.zIndex = '999999';14.6.11 api.dom.createOverlayByBorder()
通过上、右、下、左四条边创建固定定位覆盖层。边界值可以是像素数字,也可以是选择器。
createOverlayByBorder(
bindWindowStr: string,
top: string | number,
right: string | number,
bottom: string | number,
left: string | number,
createObserver?: boolean,
delayTime?: number
): HTMLElement示例:
const panel = api.dom.createOverlayByBorder(
'__centerPanel__',
'header:bottom',
20,
'footer:top',
'.sidebar:right'
);
panel.style.background = 'rgba(0,0,0,.08)';14.7 api.utils
api.utils 提供常用辅助能力,适合处理页面等待和页面上下文执行。
14.7.1 api.utils.wait()
轮询等待条件函数返回真值。超时会抛出错误。
wait(
fn: () => boolean,
timeoutMs: number,
intervalMs?: number
): Promise<void>示例:
await api.utils.wait(
() => !!api.dom.querySelector(document, '.ready'),
10000,
200
);14.7.2 api.utils.runScript()
在当前页面执行 JavaScript 代码,并返回执行结果。
runScript(
code: string,
callback?: (result: any, error: Error) => void
): Promise<any>示例:
const title = await api.utils.runScript('document.title');
console.log(title);带回调的写法:
await api.utils.runScript('document.body.innerText.slice(0, 100)', (result, error) => {
if (error) {
console.error(error);
return;
}
console.log(result);
});14.8 api.header()
api.header(headerName, isRequestHeader) 用于读取远程浏览器记录的请求头或响应头。
header(
headerName: string,
isRequestHeader: boolean
): Promise<string | string[] | undefined>参数:
| 参数 | 类型 | 说明 |
|---|---|---|
headerName |
string |
请求头或响应头名称,读取时会转成小写 |
isRequestHeader |
boolean |
true 读取请求头,false 读取响应头 |
示例:
const cookie = await api.header('cookie', true);
const setCookie = await api.header('set-cookie', false);注意事项:
- 只有在站点配置中添加过的请求头或响应头才会被记录;
- 请求头来自远程浏览器发送请求时的 Header;
- 响应头来自远程浏览器收到响应时的 Header;
- 响应头可能返回字符串数组。
14.9 常见脚本组合
14.9.1 等待元素出现后点击
await api.utils.wait(
() => !!api.dom.querySelector(document, '.login-button'),
10000,
200
);
const button = api.dom.querySelector(document, '.login-button');
button?.click();14.9.2 用 XPath 查找中文按钮
const submit = api.dom.querySelector(
document,
'xpath://button[contains(.,"提交")]'
);
if (submit && await api.dom.isVisible(submit)) {
submit.click();
}14.9.3 保存页面状态
const count = await api.user.incr('visitCount', 1, true, true);
await api.user.put('lastTitle', document.title, true, true);
console.log(count.value);14.9.4 给有兴趣区域加覆盖层
const overlay = api.dom.createOverlayBy('.customer-phone', '__phoneMask__');
overlay.style.background = '#fff';
overlay.style.pointerEvents = 'none';
overlay.style.zIndex = '999999';14.10 使用原则
脚本应像业务代码一样管理:
- 限制匹配 URL,避免脚本在不相关页面执行;
- 给脚本设置清晰名称,方便回滚和排查;
- 先在测试站点或测试账号验证;
- 对依赖 DOM selector 的脚本做回归测试;
- 对存储数据设置明确前缀,例如
cache:、draft:、state:; - 不把密钥、长期 Token 或账号密码硬编码进脚本;
- 记录脚本修改原因,必要时做 Code Review。
14.11 排错清单
| 问题 | 检查方向 |
|---|---|
api 不存在 |
确认脚本类型是否支持 window.api,SSE 脚本不要依赖它 |
| 元素找不到 | 检查 URL 是否匹配、页面是否加载完成、selector 是否变化 |
| XPath 不生效 | 确认前缀使用 xpath:// |
| Header 读不到 | 先在站点配置中加入对应请求头或响应头名称 |
| 用户数据串号 | 检查 site、account、did 维度参数 |
| 覆盖层位置异常 | 检查目标元素是否可见,以及边界 selector 是否正确 |
| 脚本影响其它页面 | 缩小匹配 URL,或在脚本开头增加页面条件判断 |