Add an example that shows how to polyfill XHR in an extension (#1343)

* add an example that shows how to polyfill XHR in an extension

* updates from feedback

* update based on third party OSS requirements

* update readme links
This commit is contained in:
patrick kettner 2024-11-07 04:30:06 -05:00 коммит произвёл GitHub
Родитель b939ca2db9
Коммит d2fd736ac0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
12 изменённых файлов: 1384 добавлений и 0 удалений

1
functional-samples/libraries-xhr-in-sw/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1 @@
dist

Просмотреть файл

@ -0,0 +1,17 @@
# Using XHR in Service Workers
This sample demonstrates how to use code that relies on XHR within a extension's background service worker.
## Overview
The default background environment for extensions is the service worker. As a result, it only has direct access to [Fetch](https://developer.mozilla.org/docs/Web/API/Fetch_API/Using_Fetch). If you want to use a library that is built with XHR, this will not work by default. However, you can usually monkeypatch the expected behavior by polyfilling XHR. This sample shows an example of how you can use build tools to automatically inject a polyfill for XHR that covers most common XHR use cases, allowing for seamless integration into your extension.
In this sample, we are using a "library" that exports a function called [`fetchTitle`](./third_party/fetchTitle.js). For the fiction of this sample, this is a dependency we _must_ use, but we are unable to change ourselves. Unfortunately, it uses XHR. In order to make this work, we [import](./background.js#L1) a [shim](./third_party/xhr-shim/xhr-shim.js), and then [set the global `XMLHttpRequest` to it](./background.js#L4).
This is all packaged by a build system, in this case [Rollup](https://rollupjs.org/).
## Running this extension
1. Clone this repository
2. Run `npm install` in this folder to install all dependencies.
3. Run `npm run build` to bundle the extension.

Просмотреть файл

@ -0,0 +1,15 @@
import xhrShim from './third_party/xhr-shim/xhr-shim.js';
import fetchTitle from './third_party/fetchTitle.js';
globalThis.XMLHttpRequest = xhrShim;
chrome.action.onClicked.addListener(({ windowId, url }) => {
chrome.sidePanel.open({ windowId });
fetchTitle(url, (err, title) => {
chrome.sidePanel.setOptions({
enabled: true,
path: `./sidePanel/index.html?title=${title}`
});
});
});

Просмотреть файл

@ -0,0 +1,17 @@
{
"name": "Fetching Titles",
"version": "0.1",
"manifest_version": 3,
"description": "This extension fetches the titles of all the tabs in the current window and displays them in a side panel.",
"background": {
"service_worker": "dist/background.js"
},
"permissions": ["activeTab", "sidePanel"],
"host_permissions": ["<all_urls>"],
"side_panel": {
"default_path": "sidepanel/index.html"
},
"action": {
"default_title": "Fetch the title of the current tab"
}
}

1079
functional-samples/libraries-xhr-in-sw/package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,15 @@
{
"name": "XHR polyfill sample",
"private": true,
"version": "1.0.0",
"main": "background.js",
"scripts": {
"build": "rollup -c rollup.config.mjs"
},
"license": "Apache 2.0",
"devDependencies": {
"@rollup/plugin-commonjs": "26.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"rollup": "^4.22.4"
}
}

Просмотреть файл

@ -0,0 +1,14 @@
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
export default {
input: 'background.js',
output: {
inlineDynamicImports: true,
file: 'dist/background.js',
},
plugins: [
commonjs(),
nodeResolve(),
]
};

Просмотреть файл

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>Title Fetcher</title>
</head>
<body>
<h1>Hello, world!</h1>
<script src="/sidepanel/script.js"></script>
</body>
</html>

Просмотреть файл

@ -0,0 +1,2 @@
const fetchedTitle = new URLSearchParams(location.search).get('title');
document.body.innerText = `This tab has the title "${fetchedTitle}"`;

Просмотреть файл

@ -0,0 +1,20 @@
export default function fetchTitle(url, callback) {
let xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function () {
if (xhr.status === 200) {
let title = xhr.responseText.match(/<title>([^<]+)<\/title>/)[1];
callback(null, title);
} else {
callback(new Error('Failed to load URL: ' + xhr.statusText));
}
};
xhr.onerror = function () {
callback(new Error('Network error'));
};
xhr.send();
}

Просмотреть файл

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 apple502j
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Просмотреть файл

@ -0,0 +1,173 @@
/* global module */
const sHeaders = Symbol('headers');
const sRespHeaders = Symbol('response headers');
const sAbortController = Symbol('AbortController');
const sMethod = Symbol('method');
const sURL = Symbol('URL');
const sMIME = Symbol('MIME');
const sDispatch = Symbol('dispatch');
const sErrored = Symbol('errored');
const sTimeout = Symbol('timeout');
const sTimedOut = Symbol('timedOut');
const sIsResponseText = Symbol('isResponseText');
const XMLHttpRequestShim = class XMLHttpRequest extends EventTarget {
constructor() {
super();
this.readyState = this.constructor.UNSENT;
this.response = null;
this.responseType = '';
this.responseURL = '';
this.status = 0;
this.statusText = '';
this.timeout = 0;
this.withCredentials = false;
this[sHeaders] = Object.create(null);
this[sHeaders].accept = '*/*';
this[sRespHeaders] = Object.create(null);
this[sAbortController] = new AbortController();
this[sMethod] = '';
this[sURL] = '';
this[sMIME] = '';
this[sErrored] = false;
this[sTimeout] = 0;
this[sTimedOut] = false;
this[sIsResponseText] = true;
}
static get UNSENT() {
return 0;
}
static get OPENED() {
return 1;
}
static get HEADERS_RECEIVED() {
return 2;
}
static get LOADING() {
return 3;
}
static get DONE() {
return 4;
}
get responseText() {
if (this[sErrored]) return null;
if (this.readyState < this.constructor.HEADERS_RECEIVED) return '';
if (this[sIsResponseText]) return this.response;
throw new DOMException(
'Response type not set to text',
'InvalidStateError'
);
}
get responseXML() {
throw new Error('XML not supported');
}
[sDispatch](evt) {
const attr = `on${evt.type}`;
if (typeof this[attr] === 'function') {
this.addEventListener(evt.type, this[attr].bind(this), {
once: true
});
}
this.dispatchEvent(evt);
}
abort() {
this[sAbortController].abort();
this.status = 0;
this.readyState = this.constructor.UNSENT;
}
open(method, url) {
this.status = 0;
this[sMethod] = method;
this[sURL] = url;
this.readyState = this.constructor.OPENED;
}
setRequestHeader(header, value) {
header = String(header).toLowerCase();
if (typeof this[sHeaders][header] === 'undefined') {
this[sHeaders][header] = String(value);
} else {
this[sHeaders][header] += `, ${value}`;
}
}
overrideMimeType(mimeType) {
this[sMIME] = String(mimeType);
}
getAllResponseHeaders() {
if (this[sErrored] || this.readyState < this.constructor.HEADERS_RECEIVED)
return '';
return Object.entries(this[sRespHeaders])
.map(([header, value]) => `${header}: ${value}`)
.join('\r\n');
}
getResponseHeader(headerName) {
const value = this[sRespHeaders][String(headerName).toLowerCase()];
return typeof value === 'string' ? value : null;
}
send(body = null) {
if (this.timeout > 0) {
this[sTimeout] = setTimeout(() => {
this[sTimedOut] = true;
this[sAbortController].abort();
}, this.timeout);
}
const responseType = this.responseType || 'text';
this[sIsResponseText] = responseType === 'text';
fetch(this[sURL], {
method: this[sMethod] || 'GET',
signal: this[sAbortController].signal,
headers: this[sHeaders],
credentials: this.withCredentials ? 'include' : 'same-origin',
body
})
.finally(() => {
this.readyState = this.constructor.DONE;
clearTimeout(this[sTimeout]);
this[sDispatch](new CustomEvent('loadstart'));
})
.then(
async (resp) => {
this.responseURL = resp.url;
this.status = resp.status;
this.statusText = resp.statusText;
const finalMIME =
this[sMIME] || this[sRespHeaders]['content-type'] || 'text/plain';
Object.assign(this[sRespHeaders], resp.headers);
switch (responseType) {
case 'text':
this.response = await resp.text();
break;
case 'blob':
this.response = new Blob([await resp.arrayBuffer()], {
type: finalMIME
});
break;
case 'arraybuffer':
this.response = await resp.arrayBuffer();
break;
case 'json':
this.response = await resp.json();
break;
}
this[sDispatch](new CustomEvent('load'));
},
(err) => {
let eventName = 'abort';
if (err.name !== 'AbortError') {
this[sErrored] = true;
eventName = 'error';
} else if (this[sTimedOut]) {
eventName = 'timeout';
}
this[sDispatch](new CustomEvent(eventName));
}
)
.finally(() => this[sDispatch](new CustomEvent('loadend')));
}
};
if (typeof module === 'object' && module.exports) {
module.exports = XMLHttpRequestShim;
} else {
(globalThis || self).XMLHttpRequestShim = XMLHttpRequestShim;
}