API nhắn tin cho phép bạn giao tiếp giữa các tập lệnh khác nhau đang chạy trong các ngữ cảnh được liên kết với tiện ích của bạn. Điều này bao gồm cả hoạt động giao tiếp giữa trình chạy dịch vụ, chrome-extension://pages và tập lệnh nội dung. Ví dụ: tiện ích trình đọc RSS có thể sử dụng tập lệnh nội dung để phát hiện sự hiện diện của nguồn cấp dữ liệu RSS trên một trang, sau đó thông báo cho worker dịch vụ để cập nhật biểu tượng thao tác cho trang đó.
Có hai API truyền thông báo: một cho các yêu cầu một lần và một API phức tạp hơn cho các kết nối duy trì lâu dài cho phép gửi nhiều thông báo.
Để biết thông tin về cách gửi thông báo giữa các tiện ích, hãy xem phần thông báo giữa các tiện ích.
Yêu cầu một lần
Để gửi một thông báo duy nhất đến một phần khác của tiện ích và nhận phản hồi (không bắt buộc), hãy gọi runtime.sendMessage() hoặc tabs.sendMessage().
Các phương thức này cho phép bạn gửi một thông báo có thể chuyển đổi tuần tự thành JSON một lần từ một tập lệnh nội dung đến tiện ích, hoặc từ tiện ích đến một tập lệnh nội dung. Cả hai API đều trả về một Promise phân giải thành phản hồi do người nhận cung cấp.
Việc gửi yêu cầu từ một tập lệnh nội dung sẽ diễn ra như sau:
content-script.js:
(async () => {
const response = await chrome.runtime.sendMessage({greeting: "hello"});
// do something with response here, not outside the function
console.log(response);
})();
Phản hồi
Để theo dõi một thông báo, hãy sử dụng sự kiện chrome.runtime.onMessage:
// Event listener
function handleMessages(message, sender, sendResponse) {
if (message !== 'get-status') return;
fetch('https://example.com')
.then((response) => sendResponse({statusCode: response.status}))
// Since `fetch` is asynchronous, must return an explicit `true`
return true;
}
chrome.runtime.onMessage.addListener(handleMessages);
// From the sender's context...
const {statusCode} = await chrome.runtime.sendMessage('get-status');
Khi trình nghe sự kiện được gọi, một hàm sendResponse sẽ được truyền dưới dạng tham số thứ ba. Đây là một hàm có thể được gọi để đưa ra phản hồi. Theo mặc định, lệnh gọi lại sendResponse phải được gọi một cách đồng bộ.
Nếu bạn gọi sendResponse mà không có tham số nào, thì null sẽ được gửi dưới dạng phản hồi.
Để gửi phản hồi không đồng bộ, bạn có 2 lựa chọn: trả về true hoặc trả về một promise.
Quay lại true
Để phản hồi không đồng bộ bằng cách sử dụng sendResponse(), hãy trả về một giá trị true theo nghĩa đen (không chỉ là một giá trị đúng) từ trình nghe sự kiện. Thao tác này sẽ giữ cho kênh thông báo mở cho đến khi sendResponse được gọi, cho phép bạn gọi kênh này sau.
Trả về một promise
Kể từ Chrome 144, bạn có thể trả về một lời hứa từ trình nghe thông báo để phản hồi không đồng bộ. Nếu promise phân giải, giá trị đã phân giải của promise sẽ được gửi dưới dạng phản hồi.
Nếu lời hứa bị từ chối, lệnh gọi sendMessage() của người gửi sẽ bị từ chối kèm theo thông báo lỗi. Hãy xem phần xử lý lỗi để biết thêm thông tin chi tiết và ví dụ.
Ví dụ cho thấy việc trả về một lời hứa có thể phân giải hoặc từ chối:
// Event listener
function handleMessages(message, sender, sendResponse) {
// Return a promise that wraps fetch
// If the response is OK, resolve with the status. If it's not OK then reject
// with the network error that prevents the fetch from completing.
return new Promise((resolve, reject) => {
fetch('https://example.com')
.then(response => {
if (!response.ok) {
reject(response);
} else {
resolve(response.status);
}
})
.catch(error => {
reject(error);
});
});
}
chrome.runtime.onMessage.addListener(handleMessages);
Bạn cũng có thể khai báo một trình nghe dưới dạng async để trả về một promise:
chrome.runtime.onMessage.addListener(async function(message, sender) {
const response = await fetch('https://example.com');
if (!response.ok) {
// rejects the promise returned by `async function`.
throw new Error(`Fetch failed: ${response.status}`);
}
// resolves the promise returned by `async function`.
return {statusCode: response.status};
});
Trả về một promise: Các lỗi thường gặp về hàm async
Xin lưu ý rằng hàm async dưới dạng một trình nghe sẽ luôn trả về một lời hứa, ngay cả khi không có câu lệnh return. Nếu trình nghe async không trả về giá trị, thì lời hứa của trình nghe sẽ ngầm giải quyết thành undefined và null được gửi dưới dạng phản hồi cho người gửi. Điều này có thể gây ra hành vi không mong muốn khi có nhiều trình nghe:
// content_script.js
function handleResponse(message) {
// The first listener promise resolves to `undefined` before the second
// listener can respond. When a listener responds with `undefined`, Chrome
// sends null as the response.
console.assert(message === null);
}
function notifyBackgroundPage() {
const sending = chrome.runtime.sendMessage('test');
sending.then(handleResponse);
}
notifyBackgroundPage();
// background.js
chrome.runtime.onMessage.addListener(async (message) => {
// This just causes the function to pause for a millisecond, but the promise
// is *not* returned from the listener so it doesn't act as a response.
await new Promise(resolve => {
setTimeout(resolve, 1, 'OK');
});
// `async` functions always return promises. So once we
// reach here there is an implicit `return undefined;`. Chrome translates
// `undefined` responses to `null`.
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
return new Promise((resolve) => {
setTimeout(resolve, 1000, 'response');
});
});
Xử lý lỗi
Kể từ Chrome 144, nếu trình nghe onMessage gặp lỗi (đồng bộ hoặc không đồng bộ bằng cách trả về một lời hứa từ chối), thì lời hứa do sendMessage() trả về trong người gửi sẽ từ chối bằng thông báo lỗi.
Điều này cũng có thể xảy ra nếu một trình nghe cố gắng trả về một phản hồi không thể JSON-serialized mà không bắt được TypeError kết quả.
Nếu một trình nghe trả về một lời hứa từ chối, thì trình nghe đó phải từ chối bằng một thực thể Error để người gửi nhận được thông báo lỗi đó. Nếu lời hứa bị từ chối với bất kỳ giá trị nào khác (chẳng hạn như null hoặc undefined), sendMessage() sẽ bị từ chối bằng một thông báo lỗi chung.
Nếu nhiều trình nghe được đăng ký cho onMessage, thì chỉ trình nghe đầu tiên phản hồi, từ chối hoặc gửi lỗi mới ảnh hưởng đến người gửi; tất cả các trình nghe khác sẽ chạy, nhưng kết quả của chúng sẽ bị bỏ qua.
Ví dụ
Nếu một trình nghe trả về một lời hứa từ chối, thì sendMessage() sẽ bị từ chối:
// sender.js
try {
await chrome.runtime.sendMessage('test');
} catch (e) {
console.log(e.message); // "some error"
}
// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
return Promise.reject(new Error('some error'));
});
Nếu một trình nghe phản hồi bằng một giá trị không thể được chuyển đổi tuần tự, thì sendMessage() sẽ bị từ chối:
// sender.js
try {
await chrome.runtime.sendMessage('test');
} catch (e) {
console.log(e.message); // "Error: Could not serialize message."
}
// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse(() => {}); // Functions are not serializable
return true; // Keep channel open for async sendResponse
});
Nếu một trình nghe đồng bộ hoá lỗi trước khi bất kỳ trình nghe nào khác phản hồi, thì lời hứa sendMessage() của trình nghe sẽ bị từ chối:
// sender.js
try {
await chrome.runtime.sendMessage('test');
} catch (e) {
console.log(e.message); // "error!"
}
// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
throw new Error('error!');
});
Tuy nhiên, nếu một trình nghe phản hồi trước khi một trình nghe khác báo lỗi, thì sendMessage() sẽ thành công:
// sender.js
const response = await chrome.runtime.sendMessage('test');
console.log(response); // "OK"
// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse('OK');
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
throw new Error('error!');
});
Kết nối lâu dài
Để tạo một kênh truyền thông điệp có thể sử dụng lại trong thời gian dài, hãy gọi:
runtime.connect()để truyền thông báo từ một tập lệnh nội dung đến một trang tiện íchtabs.connect()để truyền thông báo từ trang tiện ích đến tập lệnh nội dung.
Bạn có thể đặt tên cho kênh bằng cách truyền một tham số lựa chọn có khoá name để phân biệt giữa các loại kết nối:
const port = chrome.runtime.connect({name: "example"});
Một trường hợp sử dụng tiềm năng cho kết nối duy trì lâu dài là tiện ích tự động điền biểu mẫu. Tập lệnh nội dung có thể mở một kênh đến trang tiện ích cho một lần đăng nhập cụ thể và gửi một thông báo đến tiện ích cho từng phần tử đầu vào trên trang để yêu cầu dữ liệu biểu mẫu cần điền. Kết nối dùng chung cho phép tiện ích chia sẻ trạng thái giữa các thành phần của tiện ích.
Khi thiết lập một kết nối, mỗi đầu sẽ được chỉ định một đối tượng runtime.Port để gửi và nhận thông báo thông qua kết nối đó.
Sử dụng mã sau đây để mở một kênh từ tập lệnh nội dung, đồng thời gửi và lắng nghe các thông báo:
content-script.js:
const port = chrome.runtime.connect({name: "knockknock"});
port.onMessage.addListener(function(msg) {
if (msg.question === "Who's there?") {
port.postMessage({answer: "Madame"});
} else if (msg.question === "Madame who?") {
port.postMessage({answer: "Madame... Bovary"});
}
});
port.postMessage({joke: "Knock knock"});
Để gửi yêu cầu từ tiện ích đến một tập lệnh nội dung, hãy thay thế lệnh gọi đến runtime.connect() trong ví dụ trước bằng tabs.connect().
Để xử lý các kết nối đến cho tập lệnh nội dung hoặc trang tiện ích, hãy thiết lập trình nghe sự kiện runtime.onConnect. Khi một phần khác của tiện ích gọi connect(), phần đó sẽ kích hoạt sự kiện này và đối tượng runtime.Port. Mã phản hồi các kết nối đến sẽ có dạng như sau:
service-worker.js:
chrome.runtime.onConnect.addListener(function(port) {
if (port.name !== "knockknock") {
return;
}
port.onMessage.addListener(function(msg) {
if (msg.joke === "Knock knock") {
port.postMessage({question: "Who's there?"});
} else if (msg.answer === "Madame") {
port.postMessage({question: "Madame who?"});
} else if (msg.answer === "Madame... Bovary") {
port.postMessage({question: "I don't get it."});
}
});
});
Chuyển đổi tuần tự
Trong Chrome, các API truyền thông báo sử dụng phương thức chuyển đổi tuần tự JSON. Đáng chú ý là điều này khác với các trình duyệt khác triển khai cùng một API bằng thuật toán sao chép có cấu trúc.
Điều này có nghĩa là một thông báo (và các phản hồi do người nhận cung cấp) có thể chứa bất kỳ giá trị JSON.stringify() hợp lệ nào. Các giá trị khác sẽ được chuyển đổi thành các giá trị có thể chuyển đổi tuần tự (đáng chú ý là undefined sẽ được chuyển đổi tuần tự thành null);
Giới hạn kích thước thư
Kích thước tối đa của một thư là 64 MiB.
Thời gian tồn tại của cổng
Cổng được thiết kế như một cơ chế giao tiếp hai chiều giữa các phần khác nhau của một tiện ích. Khi một phần của tiện ích gọi tabs.connect(), runtime.connect() hoặc runtime.connectNative(), tiện ích đó sẽ tạo một Port có thể gửi tin nhắn ngay lập tức bằng postMessage().
Nếu có nhiều khung trong một thẻ, việc gọi tabs.connect() sẽ gọi sự kiện runtime.onConnect một lần cho mỗi khung trong thẻ. Tương tự, nếu runtime.connect() được gọi, thì sự kiện onConnect có thể kích hoạt một lần cho mỗi khung hình trong quy trình tiện ích.
Bạn có thể muốn tìm hiểu thời điểm đóng một kết nối, chẳng hạn như nếu bạn đang duy trì các trạng thái riêng biệt cho từng cổng mở. Để thực hiện việc này, hãy theo dõi sự kiện runtime.Port.onDisconnect. Sự kiện này sẽ kích hoạt khi không có cổng hợp lệ ở đầu kia của kênh. Điều này có thể do bất kỳ nguyên nhân nào sau đây:
- Không có trình nghe nào cho
runtime.onConnectở đầu kia. - Thẻ chứa cổng này sẽ được huỷ tải (ví dụ: nếu thẻ được điều hướng).
- Khung nơi
connect()được gọi đã được huỷ tải. - Tất cả các khung nhận được cổng (thông qua
runtime.onConnect) đều đã được huỷ tải. runtime.Port.disconnect()được gọi bởi đầu kia. Nếu lệnh gọiconnect()dẫn đến nhiều cổng ở phía đầu thu vàdisconnect()được gọi trên bất kỳ cổng nào trong số này, thì sự kiệnonDisconnectchỉ kích hoạt ở cổng gửi, chứ không phải ở các cổng khác.
Nhắn tin trên nhiều tiện ích
Ngoài việc gửi thông báo giữa các thành phần khác nhau trong tiện ích, bạn có thể dùng API nhắn tin để giao tiếp với các tiện ích khác. Thao tác này cho phép bạn hiển thị một API công khai để các tiện ích khác sử dụng.
Để theo dõi các yêu cầu và kết nối đến từ các tiện ích khác, hãy sử dụng phương thức runtime.onMessageExternal hoặc runtime.onConnectExternal. Sau đây là ví dụ về từng loại:
service-worker.js
// For a single request:
chrome.runtime.onMessageExternal.addListener(
function(request, sender, sendResponse) {
if (sender.id !== allowlistedExtension) {
return; // don't allow this extension access
}
if (request.getTargetData) {
sendResponse({ targetData: targetData });
} else if (request.activateLasers) {
const success = activateLasers();
sendResponse({ activateLasers: success });
}
}
);
// For long-lived connections:
chrome.runtime.onConnectExternal.addListener(function(port) {
port.onMessage.addListener(function(msg) {
// See other examples for sample onMessage handlers.
});
});
Để gửi một thông báo đến một tiện ích khác, hãy truyền mã nhận dạng của tiện ích mà bạn muốn giao tiếp như sau:
service-worker.js
// The ID of the extension we want to talk to.
const laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";
// For a minimal request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
function(response) {
if (targetInRange(response.targetData))
chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
}
);
// For a long-lived connection:
const port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);
Gửi tin nhắn từ trang web
Tiện ích cũng có thể nhận và phản hồi tin nhắn từ các trang web. Để gửi thông báo từ một trang web đến một tiện ích, hãy chỉ định trong manifest.json những trang web mà bạn muốn cho phép gửi thông báo bằng khoá tệp kê khai "externally_connectable". Ví dụ:
manifest.json
"externally_connectable": {
"matches": ["https://*.example.com/*"]
}
Thao tác này sẽ hiển thị API nhắn tin cho mọi trang khớp với các mẫu URL mà bạn chỉ định. Mẫu URL phải chứa ít nhất một miền cấp hai; tức là các mẫu tên máy chủ như "*", "*.com", "*.co.uk" và "*.appspot.com" không được hỗ trợ. Bạn có thể sử dụng <all_urls> để truy cập vào tất cả các miền.
Dùng API runtime.sendMessage() hoặc runtime.connect() để gửi thông báo đến một tiện ích cụ thể. Ví dụ:
webpage.js
// The ID of the extension we want to talk to.
const editorExtensionId = 'abcdefghijklmnoabcdefhijklmnoabc';
// Check if extension is installed
if (chrome && chrome.runtime) {
// Make a request:
chrome.runtime.sendMessage(
editorExtensionId,
{
openUrlInEditor: url
},
(response) => {
if (!response.success) handleError(url);
}
);
}
Từ tiện ích của bạn, hãy theo dõi thông báo từ các trang web bằng cách sử dụng API runtime.onMessageExternal hoặc runtime.onConnectExternal như trong nhắn tin giữa các tiện ích. Ví dụ:
service-worker.js
chrome.runtime.onMessageExternal.addListener(
function(request, sender, sendResponse) {
if (sender.url === blocklistedWebsite)
return; // don't allow this web page access
if (request.openUrlInEditor)
openUrl(request.openUrlInEditor);
});
Bạn không thể gửi thông báo từ một tiện ích đến một trang web.
Nhắn tin bằng ứng dụng gốc
Các tiện ích có thể trao đổi thông báo với các ứng dụng gốc được đăng ký làm máy chủ lưu trữ thông báo gốc. Để tìm hiểu thêm về tính năng này, hãy xem phần Nhắn tin gốc.
Lưu ý về bảo mật
Sau đây là một số yếu tố bảo mật liên quan đến việc nhắn tin.
Tập lệnh nội dung ít đáng tin cậy hơn
Tập lệnh nội dung ít đáng tin cậy hơn so với worker dịch vụ của tiện ích. Ví dụ: một trang web độc hại có thể xâm nhập vào quy trình hiển thị chạy các tập lệnh nội dung. Giả sử rằng các thông báo từ một tập lệnh nội dung có thể do kẻ tấn công tạo ra và đảm bảo xác thực và dọn dẹp tất cả dữ liệu đầu vào. Giả sử mọi dữ liệu được gửi đến tập lệnh nội dung có thể bị rò rỉ sang trang web. Giới hạn phạm vi của các thao tác đặc quyền có thể được kích hoạt bằng các thông báo nhận được từ tập lệnh nội dung.
Tập lệnh trên nhiều trang web
Hãy nhớ bảo vệ các tập lệnh của bạn khỏi tấn công tập lệnh trên nhiều trang web. Khi nhận dữ liệu từ một nguồn không đáng tin cậy, chẳng hạn như dữ liệu đầu vào của người dùng, các trang web khác thông qua một tập lệnh nội dung hoặc một API, hãy cẩn thận để tránh diễn giải dữ liệu này dưới dạng HTML hoặc sử dụng dữ liệu này theo cách có thể cho phép mã không mong muốn chạy.
Sử dụng các API không chạy tập lệnh bất cứ khi nào có thể:
service-worker.js
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) { // JSON.parse doesn't evaluate the attacker's scripts. const resp = JSON.parse(response.farewell); });
service-worker.js
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) { // innerText does not let the attacker inject HTML elements. document.getElementById("resp").innerText = response.farewell; });
Tránh sử dụng các phương thức sau đây khiến tiện ích của bạn dễ bị tấn công:
service-worker.js
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) { // WARNING! Might be evaluating a malicious script! const resp = eval(`(${response.farewell})`); });
service-worker.js
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) { // WARNING! Might be injecting a malicious script! document.getElementById("resp").innerHTML = response.farewell; });