14  Sử dụng Script API

14.1 Mục tiêu

Mô tả khả năng window.api có sẵn cho script trang Sa2web và script chặn giao diện tiêu chuẩn, để script trong cấu hình trang, cấu hình trang nội bộ, cấu hình tài khoản và script plugin có thể được viết tin cậy hơn.

Script API có năm danh mục chính:

API Mục đích
api.config Đọc cấu hình tùy chỉnh; hiện chỉ hợp lệ dưới liên kết cộng tác
api.http Gửi yêu cầu HTTP bỏ qua hạn chế CORS trang
api.user Đọc và ghi dữ liệu theo phạm vi user, trang, tài khoản hoặc thiết bị
api.dom Truy vấn DOM, kiểm tra hiển thị, lắng nghe thay đổi phần tử và tạo overlay
api.utils Chờ điều kiện và chạy JavaScript trong trang
api.header() Đọc header yêu cầu hoặc phản hồi đã ghi

Script trang và script chặn giao diện tiêu chuẩn có thể dùng window.api. Script SSE chỉ đảm bảo data được truyền vào và thường không phụ thuộc window.api.

14.2 Cấu trúc Tổng thể

Trong script, bạn có thể dùng api trực tiếp hoặc truy cập qua 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>;
  };
}

Ví dụ:

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` });
Tip

Ưu tiên dùng công cụ truy vấn api.dom của Sa2web trong script. Chúng hỗ trợ cả CSS selector và XPath selector, làm cho chúng phù hợp hơn cho trang kinh doanh phức tạp so với document.querySelector() đơn lẻ.

14.3 api.config

api.config đọc cấu hình tùy chỉnh từ môi trường script trang.

config: Record<string, unknown>

Ví dụ:

const apiBase = api.config.apiBase;
const featureEnabled = api.config.featureEnabled === true;

14.4 api.http

api.http cung cấp công cụ yêu cầu HTTP cho script user. api.http.ajax thực thi yêu cầu thực tế trong tiến trình chính trình duyệt, nên nó không bị hạn chế bởi chính sách CORS của trang.

14.4.1 api.http.ajax(options)

Gửi yêu cầu 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;
}>

Tham số:

Tùy chọn Kiểu Mặc định Mô tả
url string - URL yêu cầu.
method string GET Phương thức HTTP.
data any - Dữ liệu yêu cầu. Yêu cầu GETHEAD được serialize vào query string; phương thức khác ghi vào body yêu cầu.
headers Record<string, string> {} Header yêu cầu.
timeout number - Timeout tính bằng mili-giây. Chỉ có hiệu lực khi lớn hơn 0.
dataType 'json' \| 'text' \| 'html' \| 'arrayBuffer' json Chế độ parse phản hồi.
contentType string application/x-www-form-urlencoded; charset=UTF-8 Content-Type body yêu cầu.
processData boolean true Có tự động serialize data không. Khi đặt false, data truyền trực tiếp làm body yêu cầu.

Giá trị trả về:

Trường Kiểu Mô tả
ok boolean true cho phản hồi HTTP 2xx; false cho lỗi parse, lỗi HTTP, timeout, abort hoặc lỗi mạng.
status number Mã trạng thái HTTP. 0 nghĩa là timeout, abort hoặc thất bại lớp mạng.
statusText string Văn bản trạng thái HTTP. Cho thất bại không phải HTTP, là timeout, abort hoặc error.
data any Dữ liệu phản hồi đã parse.
error string Thông báo lỗi cho thất bại parse hoặc thất bại không phải HTTP.
timeout boolean true khi yêu cầu bị abort do timeout cấu hình.

Ví dụ 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);
}

Ví dụ 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 đọc và ghi dữ liệu liên quan user khi chạy script. Nó có thể cô lập phạm vi lưu trữ qua các chiều khác nhau.

Tham số phổ biến:

Tham số Kiểu Mặc định Mô tả
site boolean false Có cô lập theo trang không
account boolean false Có cô lập theo tài khoản trang hoặc tên workspace không
did boolean false Có cô lập theo thiết bị không

Khuyến nghị chiều:

Tình huống Khuyến nghị
Chia sẻ dữ liệu cho cùng user trên các trang site=false
Lưu dữ liệu độc lập cho mỗi trang site=true
Cô lập tài khoản khác nhau dưới cùng trang account=true
Cô lập cùng user trên thiết bị khác nhau did=true

14.5.1 api.user.put()

Lưu cặp key-value.

put(
  name: string,
  value: string,
  site?: boolean,
  account?: boolean,
  did?: boolean
): Promise<{ status: boolean }>

Ví dụ:

await api.user.put('token', 'abc123');
await api.user.put('draft:lastOrderId', 'A-1001', true, true);

14.5.2 api.user.get()

Đọc key được chỉ định.

get(
  name: string,
  site?: boolean,
  account?: boolean,
  did?: boolean
): Promise<{ value: string | null, status: boolean }>

Ví dụ:

const ret = await api.user.get('token');
if (ret.status && ret.value) {
  console.log(ret.value);
}

14.5.3 api.user.remove()

Xóa key được chỉ định.

remove(
  name: string,
  site?: boolean,
  account?: boolean,
  did?: boolean
): Promise<{ status: boolean }>

Ví dụ:

await api.user.remove('token');
await api.user.remove('draft:lastOrderId', true, true);

14.5.4 api.user.incr()

Tăng giá trị số của key được chỉ định theo bước. Nếu key không tồn tại, nó được tạo.

incr(
  name: string,
  step?: number,
  site?: boolean,
  account?: boolean,
  did?: boolean
): Promise<{ status: boolean, value: number | string }>

Ví dụ:

const ret = await api.user.incr('submitCount', 1, true);
console.log(ret.value);

14.5.5 api.user.decr()

Giảm giá trị số của key được chỉ định theo bước. Nếu key không tồn tại, nó được tạo với giá trị ban đầu âm.

decr(
  name: string,
  step?: number,
  site?: boolean,
  account?: boolean,
  did?: boolean
): Promise<{ status: boolean, value: number | string }>

Ví dụ:

const ret = await api.user.decr('remainingQuota', 1, true, true);
console.log(ret.value);

14.5.6 api.user.startsWith()

Tìm dữ liệu theo tiền tố tên key.

startsWith(
  prefix: string,
  site?: boolean,
  account?: boolean,
  did?: boolean
): Promise<Array<{ name: string, value: string }>>

Ví dụ:

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()

Đếm bản ghi cho tên key được chỉ định.

countAll(
  name: string,
  site?: boolean,
  account?: boolean
): Promise<{ value: number, status: boolean }>

Ví dụ:

const ret = await api.user.countAll('token', true);
console.log(ret.value);

14.5.8 api.user.sumAll()

Tính tổng số của giá trị cho tên key được chỉ định.

sumAll(
  name: string,
  site?: boolean,
  account?: boolean
): Promise<{ value: number, status: boolean }>

Ví dụ:

const ret = await api.user.sumAll('score', true);
console.log(ret.value);

14.6 api.dom

api.dom cung cấp truy vấn DOM, kiểm tra hiển thị, listener kết nối, listener resize và tạo overlay.

Hỗ trợ Selector:

Cú pháp Mô tả
.button.primary CSS selector
xpath://div[@id="app"] XPath selector
.dialog:p Trả về phần tử cha của phần tử khớp
.dialog:p2 Trả về tổ tiên hai cấp trên phần tử khớp
.header:bottom Dùng ranh giới dưới của phần tử trong phương pháp ranh giới overlay
.sidebar:right Dùng ranh giới phải của phần tử trong phương pháp ranh giới overlay

Xem thêm Section 12.15.

14.6.1 api.dom.createMutationObserver()

Tạo và cache MutationObserver. Nếu observer với cùng tên binding đã tồn tại trên phần tử mục tiêu, observer hiện có được trả về trực tiếp.

createMutationObserver(
  ele: Element,
  bindStr: string,
  childList: boolean,
  subtree: boolean,
  attributes: boolean,
  characterData: boolean,
  fn: (mutations: MutationRecord[]) => void
): MutationObserver

Ví dụ:

api.dom.createMutationObserver(
  document.body,
  '__bodyObserver__',
  true,
  true,
  false,
  false,
  (mutations) => console.log(mutations)
);

14.6.2 api.dom.querySelector()

Truy vấn phần tử khớp đầu tiên.

querySelector(doc: Document, cssOrXPathSelector: string): HTMLElement | null

Ví dụ:

const el = api.dom.querySelector(
  document,
  'xpath://button[contains(.,"Submit")]'
);

14.6.3 api.dom.querySelectorAll()

Truy vấn tất cả phần tử khớp.

querySelectorAll(doc: Document, cssOrXPathSelector: string): HTMLElement[]

Ví dụ:

const buttons = api.dom.querySelectorAll(document, 'button.primary');
buttons.forEach((button) => console.log(button.textContent));

14.6.4 api.dom.isVisible()

Kiểm tra phần tử có ở khu vực giao điểm hiển thị không.

isVisible(ele: HTMLElement): Promise<boolean>

Ví dụ:

const el = api.dom.querySelector(document, '.submit');
if (el && await api.dom.isVisible(el)) {
  console.log('visible');
}

14.6.5 api.dom.getVisibleRect()

Lấy hình chữ nhật hiển thị hiện tại của phần tử.

getVisibleRect(ele: HTMLElement): Promise<DOMRectReadOnly>

Ví dụ:

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()

Lấy danh sách listener kết nối hiện tại.

getConnectListeners(): Array<{
  querySelector: string;
  callback: (isConnected: boolean) => void;
  isConnected?: boolean;
}>

Ví dụ:

api.dom.addConnectListener('.modal', () => {});
console.log(api.dom.getConnectListeners());

14.6.7 api.dom.addConnectListener()

Lắng nghe phần tử xuất hiện trong hoặc biến mất khỏi document.

addConnectListener(
  cssOrXPathSelector: string,
  callback: (isConnected: boolean) => void
): void

Ví dụ:

api.dom.addConnectListener('.dialog', (isConnected) => {
  console.log('dialog:', isConnected);
});

14.6.8 api.dom.removeConnectListener()

Xóa listener kết nối cho selector được chỉ định.

removeConnectListener(cssOrXPathSelectors: string[]): void

Ví dụ:

api.dom.removeConnectListener(['.dialog', '.toast']);

14.6.9 api.dom.addResizeListener()

Lắng nghe thay đổi kích thước và vị trí phần tử mục tiêu. Nếu phần tử không tồn tại, callback nhận hình chữ nhật rỗng.

addResizeListener(
  cssOrXPathSelector: string,
  bindWindowStr: string,
  callback: (rect: DOMRect) => void,
  createObserver?: boolean,
  delayTime?: number
): ResizeObserver | (() => void)

Ví dụ:

api.dom.addResizeListener('.target', '__targetResize__', (rect) => {
  console.log(rect.width, rect.height);
});

14.6.10 api.dom.createOverlayBy()

Tạo overlay vị trí cố định theo dõi khu vực hiển thị của phần tử mục tiêu.

createOverlayBy(
  cssOrXPathSelector: string,
  bindWindowStr: string,
  createObserver?: boolean,
  delayTime?: number,
  fn?: (rect: DOMRectReadOnly) => void
): HTMLElement

Ví dụ:

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()

Tạo overlay vị trí cố định từ ranh giới trên, phải, dưới, trái. Giá trị ranh giới có thể là số pixel hoặc selector.

createOverlayByBorder(
  bindWindowStr: string,
  top: string | number,
  right: string | number,
  bottom: string | number,
  left: string | number,
  createObserver?: boolean,
  delayTime?: number
): HTMLElement

Ví dụ:

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 cung cấp khả năng hỗ trợ phổ biến cho chờ điều kiện trang và thực thi mã trong ngữ cảnh trang.

14.7.1 api.utils.wait()

Poll cho đến khi hàm điều kiện trả về giá trị truthy. Ném lỗi khi timeout.

wait(
  fn: () => boolean,
  timeoutMs: number,
  intervalMs?: number
): Promise<void>

Ví dụ:

await api.utils.wait(
  () => !!api.dom.querySelector(document, '.ready'),
  10000,
  200
);

14.7.2 api.utils.runScript()

Chạy mã JavaScript trong trang hiện tại và trả về kết quả.

runScript(
  code: string,
  callback?: (result: any, error: Error) => void
): Promise<any>

Ví dụ:

const title = await api.utils.runScript('document.title');
console.log(title);

Dạng callback:

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) đọc header yêu cầu hoặc phản hồi được ghi bởi trình duyệt từ xa.

header(
  headerName: string,
  isRequestHeader: boolean
): Promise<string | string[] | undefined>

Tham số:

Tham số Kiểu Mô tả
headerName string Tên header yêu cầu hoặc phản hồi. Được chuyển thành chữ thường khi đọc
isRequestHeader boolean true đọc header yêu cầu; false đọc header phản hồi

Ví dụ:

const cookie = await api.header('cookie', true);
const setCookie = await api.header('set-cookie', false);

Lưu ý:

  • Chỉ header yêu cầu hoặc phản hồi đã được thêm trong cấu hình trang mới được ghi.
  • Header yêu cầu đến từ yêu cầu do trình duyệt từ xa gửi.
  • Header phản hồi đến từ phản hồi do trình duyệt từ xa nhận.
  • Header phản hồi có thể trả về mảng chuỗi.

14.9 Kết hợp Script Phổ biến

14.9.1 Chờ Phần tử và Click Nó

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 Tìm Nút Tiếng Anh Với XPath

const submit = api.dom.querySelector(
  document,
  'xpath://button[contains(.,"Submit")]'
);

if (submit && await api.dom.isVisible(submit)) {
  submit.click();
}

14.9.3 Lưu Trạng thái Trang

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 Thêm Overlay Vào Khu vực Quan Tâm

const overlay = api.dom.createOverlayBy('.customer-phone', '__phoneMask__');
overlay.style.background = '#fff';
overlay.style.pointerEvents = 'none';
overlay.style.zIndex = '999999';

14.10 Nguyên tắc Sử dụng

Quản lý script như mã kinh doanh:

  • Giới hạn URL khớp để script không chạy trên trang không liên quan.
  • Đặt tên script rõ ràng để rollback và khắc phục sự cố.
  • Xác minh trước trên trang test hoặc tài khoản test.
  • Test hồi quy script phụ thuộc selector DOM.
  • Dùng tiền tố rõ ràng cho dữ liệu lưu trữ, như cache:, draft:, state:.
  • Đừng hard-code bí mật, token dài hạn hoặc mật khẩu tài khoản vào script.
  • Ghi lý do thay đổi script và code review khi cần.

14.11 Danh sách Kiểm tra Khắc phục Sự cố

Vấn đề Kiểm tra gì
api không tồn tại Xác nhận loại script có hỗ trợ window.api không; script SSE không nên phụ thuộc nó
Phần tử không tìm thấy Kiểm tra URL khớp, trang đã load xong, selector có thay đổi không
XPath không hoạt động Xác nhận tiền tố xpath:// được dùng
Header không đọc được Thêm tên header yêu cầu hoặc phản hồi tương ứng trong cấu hình trang trước
Dữ liệu user trộn lẫn giữa phạm vi Kiểm tra tham số chiều site, account, did
Vị trí overlay bất thường Kiểm tra phần tử mục tiêu có hiển thị không và selector ranh giới có đúng không
Script ảnh hưởng trang khác Thu hẹp URL khớp hoặc thêm kiểm tra điều kiện trang ở đầu script