跳到主要內容
版本:23.11.1

請求攔截

一旦啟用請求攔截,每個請求都會暫停,除非它被繼續、回應或中止。

一個簡單的請求攔截器範例,它會中止所有圖片請求

import puppeteer from 'puppeteer';

(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setRequestInterception(true);
page.on('request', interceptedRequest => {
if (interceptedRequest.isInterceptResolutionHandled()) return;
if (
interceptedRequest.url().endsWith('.png') ||
interceptedRequest.url().endsWith('.jpg')
)
interceptedRequest.abort();
else interceptedRequest.continue();
});
await page.goto('https://example.com');
await browser.close();
})();

多個攔截處理器和非同步解析

預設情況下,如果 request.abortrequest.continuerequest.respond 在任何一個被呼叫後又被呼叫,Puppeteer 會引發 Request is already handled! 例外。

始終假設未知的處理器可能已經呼叫了 abort/continue/respond。即使您的處理器是您註冊的唯一一個,第三方套件也可能會註冊它們自己的處理器。因此,務必在使用 abort/continue/respond 之前,使用 request.isInterceptResolutionHandled 來檢查解析狀態。

重要的是,在您的處理器等待非同步操作時,攔截解析可能會被另一個監聽器處理。因此,request.isInterceptResolutionHandled 的返回值僅在同步程式碼區塊中是安全的。始終**同步**執行 request.isInterceptResolutionHandledabort/continue/respond

此範例示範兩個同步處理器如何協同工作

/*
This first handler will succeed in calling request.continue because the request interception has never been resolved.
*/
page.on('request', interceptedRequest => {
if (interceptedRequest.isInterceptResolutionHandled()) return;
interceptedRequest.continue();
});

/*
This second handler will return before calling request.abort because request.continue was already
called by the first handler.
*/
page.on('request', interceptedRequest => {
if (interceptedRequest.isInterceptResolutionHandled()) return;
interceptedRequest.abort();
});

此範例示範非同步處理器如何協同工作

/*
This first handler will succeed in calling request.continue because the request interception has never been resolved.
*/
page.on('request', interceptedRequest => {
// The interception has not been handled yet. Control will pass through this guard.
if (interceptedRequest.isInterceptResolutionHandled()) return;

// It is not strictly necessary to return a promise, but doing so will allow Puppeteer to await this handler.
return new Promise(resolve => {
// Continue after 500ms
setTimeout(() => {
// Inside, check synchronously to verify that the intercept wasn't handled already.
// It might have been handled during the 500ms while the other handler awaited an async op of its own.
if (interceptedRequest.isInterceptResolutionHandled()) {
resolve();
return;
}
interceptedRequest.continue();
resolve();
}, 500);
});
});
page.on('request', async interceptedRequest => {
// The interception has not been handled yet. Control will pass through this guard.
if (interceptedRequest.isInterceptResolutionHandled()) return;

await someLongAsyncOperation();
// The interception *MIGHT* have been handled by the first handler, we can't be sure.
// Therefore, we must check again before calling continue() or we risk Puppeteer raising an exception.
if (interceptedRequest.isInterceptResolutionHandled()) return;
interceptedRequest.continue();
});

為了更精細的內省 (請參閱下方的協同攔截模式),您也可以在使用 abort/continue/respond 之前同步呼叫 request.interceptResolutionState

以下是使用 request.interceptResolutionState 重寫的上述範例

/*
This first handler will succeed in calling request.continue because the request interception has never been resolved.
*/
page.on('request', interceptedRequest => {
// The interception has not been handled yet. Control will pass through this guard.
const {action} = interceptedRequest.interceptResolutionState();
if (action === InterceptResolutionAction.AlreadyHandled) return;

// It is not strictly necessary to return a promise, but doing so will allow Puppeteer to await this handler.
return new Promise(resolve => {
// Continue after 500ms
setTimeout(() => {
// Inside, check synchronously to verify that the intercept wasn't handled already.
// It might have been handled during the 500ms while the other handler awaited an async op of its own.
const {action} = interceptedRequest.interceptResolutionState();
if (action === InterceptResolutionAction.AlreadyHandled) {
resolve();
return;
}
interceptedRequest.continue();
resolve();
}, 500);
});
});
page.on('request', async interceptedRequest => {
// The interception has not been handled yet. Control will pass through this guard.
if (
interceptedRequest.interceptResolutionState().action ===
InterceptResolutionAction.AlreadyHandled
)
return;

await someLongAsyncOperation();
// The interception *MIGHT* have been handled by the first handler, we can't be sure.
// Therefore, we must check again before calling continue() or we risk Puppeteer raising an exception.
if (
interceptedRequest.interceptResolutionState().action ===
InterceptResolutionAction.AlreadyHandled
)
return;
interceptedRequest.continue();
});

協同攔截模式

request.abortrequest.continuerequest.respond 可以接受一個可選的 priority,以在協同攔截模式下工作。當所有處理器都使用協同攔截模式時,Puppeteer 保證所有攔截處理器都會按照註冊順序執行並等待。攔截會解析為最高優先級的解析。以下是協同攔截模式的規則

  • 所有解析都必須向 abort/continue/respond 提供一個數值 priority 引數。
  • 如果任何解析未提供數值 priority,則會啟用傳統模式,並且停用協同攔截模式。
  • 非同步處理器會在攔截解析最終確定之前完成。
  • 最高優先級的攔截解析會「勝出」,也就是說,攔截最終會根據哪個解析被賦予最高優先級而被中止/回應/繼續。
  • 如果發生平局,則 abort > respond > continue

為了標準化,當指定協同攔截模式優先級時,請使用 0DEFAULT_INTERCEPT_RESOLUTION_PRIORITY (從 HTTPRequest 匯出),除非您有明確的理由使用更高的優先級。這會在 continue 之上優雅地偏好 respond,並在 respond 之上優雅地偏好 abort,並允許其他處理器協同工作。如果您確實有意要使用不同的優先級,則較高的優先級會勝過較低的優先級。允許負優先級。例如,continue({}, 4) 將會勝過 continue({}, -2)

為了保持向後相容性,任何在解析攔截時未指定 priority (傳統模式) 的處理器都會立即解析。為了使協同攔截模式正常工作,所有解析都必須使用 priority。實際上,這表示您仍然必須測試 request.isInterceptResolutionHandled,因為超出您控制範圍的處理器可能已在沒有優先級的情況下呼叫了 abort/continue/respond (傳統模式)。

在此範例中,傳統模式佔上風,並且請求會立即中止,因為至少有一個處理器在解析攔截時省略了 priority

// Final outcome: immediate abort()
page.setRequestInterception(true);
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;

// Legacy Mode: interception is aborted immediately.
request.abort('failed');
});
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;
// Control will never reach this point because the request was already aborted in Legacy Mode

// Cooperative Intercept Mode: votes for continue at priority 0.
request.continue({}, 0);
});

在此範例中,傳統模式佔上風,並且請求會繼續,因為至少有一個處理器未指定 priority

// Final outcome: immediate continue()
page.setRequestInterception(true);
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;

// Cooperative Intercept Mode: votes to abort at priority 0.
request.abort('failed', 0);
});
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;

// Control reaches this point because the request was cooperatively aborted which postpones resolution.

// { action: InterceptResolutionAction.Abort, priority: 0 }, because abort @ 0 is the current winning resolution
console.log(request.interceptResolutionState());

// Legacy Mode: intercept continues immediately.
request.continue({});
});
page.on('request', request => {
// { action: InterceptResolutionAction.AlreadyHandled }, because continue in Legacy Mode was called
console.log(request.interceptResolutionState());
});

在此範例中,協同攔截模式處於活動狀態,因為所有處理器都指定了 prioritycontinue() 勝出,因為它具有比 abort() 更高的優先級。

// Final outcome: cooperative continue() @ 5
page.setRequestInterception(true);
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;

// Cooperative Intercept Mode: votes to abort at priority 10
request.abort('failed', 0);
});
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;

// Cooperative Intercept Mode: votes to continue at priority 5
request.continue(request.continueRequestOverrides(), 5);
});
page.on('request', request => {
// { action: InterceptResolutionAction.Continue, priority: 5 }, because continue @ 5 > abort @ 0
console.log(request.interceptResolutionState());
});

在此範例中,協同攔截模式處於活動狀態,因為所有處理器都指定了 priorityrespond() 勝出,因為其優先級與 continue() 並列,但 respond() 勝過 continue()

// Final outcome: cooperative respond() @ 15
page.setRequestInterception(true);
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;

// Cooperative Intercept Mode: votes to abort at priority 10
request.abort('failed', 10);
});
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;

// Cooperative Intercept Mode: votes to continue at priority 15
request.continue(request.continueRequestOverrides(), 15);
});
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;

// Cooperative Intercept Mode: votes to respond at priority 15
request.respond(request.responseForRequest(), 15);
});
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;

// Cooperative Intercept Mode: votes to respond at priority 12
request.respond(request.responseForRequest(), 12);
});
page.on('request', request => {
// { action: InterceptResolutionAction.Respond, priority: 15 }, because respond @ 15 > continue @ 15 > respond @ 12 > abort @ 10
console.log(request.interceptResolutionState());
});

協同請求繼續

Puppeteer 要求明確呼叫 request.continue(),否則請求將會掛起。即使您的處理器打算不採取任何特殊動作或「選擇退出」,仍然必須呼叫 request.continue()

隨著協同攔截模式的引入,出現了兩種用於協同請求繼續的用例:無意見和有意見。

第一種情況 (常見) 是您的處理器打算選擇退出而不對請求執行任何特殊動作。它對進一步的動作沒有任何意見,並且只是打算預設繼續,和/或延遲到可能持有意見的其他處理器。但是,如果沒有其他處理器,我們必須呼叫 request.continue() 以確保請求不會掛起。

我們稱此為**無意見繼續**,因為其目的是在沒有其他人有更好主意時繼續請求。針對此類繼續,請使用 request.continue({...}, DEFAULT_INTERCEPT_RESOLUTION_PRIORITY) (或 0)。

第二種情況 (不常見) 是您的處理器實際上確實持有意見,並且打算透過覆寫其他地方發出的較低優先級 abort()respond() 來強制繼續。我們稱此為**有意見繼續**。在您打算指定覆寫繼續優先級的這些罕見情況下,請使用自訂優先級。

總而言之,請考慮您使用 request.continue 的目的只是預設/繞過行為,還是屬於您處理器的預期用例。考慮在範圍內的用例中使用自訂優先級,否則請使用預設優先級。請注意,您的處理器可能同時具有有意見和無意見的情況。

針對套件維護者升級到協同攔截模式

如果您是套件維護者,並且您的套件使用攔截處理器,您可以更新您的攔截處理器以使用協同攔截模式。假設您有以下現有的處理器

page.on('request', interceptedRequest => {
if (request.isInterceptResolutionHandled()) return;
if (
interceptedRequest.url().endsWith('.png') ||
interceptedRequest.url().endsWith('.jpg')
)
interceptedRequest.abort();
else interceptedRequest.continue();
});

若要使用協同攔截模式,請升級 continue()abort()

page.on('request', interceptedRequest => {
if (request.isInterceptResolutionHandled()) return;
if (
interceptedRequest.url().endsWith('.png') ||
interceptedRequest.url().endsWith('.jpg')
)
interceptedRequest.abort('failed', 0);
else
interceptedRequest.continue(
interceptedRequest.continueRequestOverrides(),
0,
);
});

透過這些簡單的升級,您的處理器現在改用協同攔截模式。

但是,我們建議稍微更強大的解決方案,因為上述方法會引入幾個細微的問題

  1. 向後相容性。 如果任何處理器仍然使用傳統模式解析 (也就是說,未指定優先級),即使您的處理器首先執行,該處理器也會立即解析攔截。這可能會對您的使用者造成困擾的行為,因為突然間您的處理器沒有解析攔截,而另一個處理器正在優先處理,而使用者所做的只是升級您的套件。
  2. 硬編碼優先級。 您的套件使用者無法指定您的處理器的預設解析優先級。當使用者希望根據使用案例操作優先級時,這可能會變得重要。例如,一個使用者可能希望您的套件採用高優先級,而另一個使用者可能希望它採用低優先級。

為了解決這兩個問題,我們建議的方法是從您的套件匯出 setInterceptResolutionConfig()。然後,使用者可以呼叫 setInterceptResolutionConfig() 來明確地在您的套件中啟用協同攔截模式,這樣他們就不會對攔截的解析方式的變更感到驚訝。他們也可以選擇使用 setInterceptResolutionConfig(priority) 指定適用於他們用例的自訂優先級

// Defaults to undefined which preserves Legacy Mode behavior
let _priority = undefined;

// Export a module configuration function
export const setInterceptResolutionConfig = (priority = 0) =>
(_priority = priority);

/**
* Note that this handler uses `DEFAULT_INTERCEPT_RESOLUTION_PRIORITY` to "pass" on this request. It is important to use
* the default priority when your handler has no opinion on the request and the intent is to continue() by default.
*/
page.on('request', interceptedRequest => {
if (request.isInterceptResolutionHandled()) return;
if (
interceptedRequest.url().endsWith('.png') ||
interceptedRequest.url().endsWith('.jpg')
)
interceptedRequest.abort('failed', _priority);
else
interceptedRequest.continue(
interceptedRequest.continueRequestOverrides(),
DEFAULT_INTERCEPT_RESOLUTION_PRIORITY, // Unopinionated continuation
);
});

如果您的套件需要對解析優先級進行更精細的控制,請使用像這樣的組態模式

interface InterceptResolutionConfig {
abortPriority?: number;
continuePriority?: number;
}

// This approach supports multiple priorities based on situational
// differences. You could, for example, create a config that
// allowed separate priorities for PNG vs JPG.
const DEFAULT_CONFIG: InterceptResolutionConfig = {
abortPriority: undefined, // Default to Legacy Mode
continuePriority: undefined, // Default to Legacy Mode
};

// Defaults to undefined which preserves Legacy Mode behavior
let _config: Partial<InterceptResolutionConfig> = {};

export const setInterceptResolutionConfig = (
config: InterceptResolutionConfig,
) => (_config = {...DEFAULT_CONFIG, ...config});

page.on('request', interceptedRequest => {
if (request.isInterceptResolutionHandled()) return;
if (
interceptedRequest.url().endsWith('.png') ||
interceptedRequest.url().endsWith('.jpg')
) {
interceptedRequest.abort('failed', _config.abortPriority);
} else {
// Here we use a custom-configured priority to allow for Opinionated
// continuation.
// We would only want to allow this if we had a very clear reason why
// some use cases required Opinionated continuation.
interceptedRequest.continue(
interceptedRequest.continueRequestOverrides(),
_config.continuePriority, // Why would we ever want priority!==0 here?
);
}
});

上述解決方案可確保向後相容性,同時也允許使用者在使用協同攔截模式時調整您的套件在解析鏈中的重要性。您的套件會繼續如預期般運作,直到使用者完全升級其程式碼和所有第三方套件以使用協同攔截模式。如果任何處理器或套件仍然使用傳統模式,您的套件仍然可以在傳統模式下運作。