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:
Родитель
b939ca2db9
Коммит
d2fd736ac0
|
@ -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"
|
||||
}
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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.
|
173
functional-samples/libraries-xhr-in-sw/third_party/xhr-shim/xhr-shim.js
поставляемый
Normal file
173
functional-samples/libraries-xhr-in-sw/third_party/xhr-shim/xhr-shim.js
поставляемый
Normal file
|
@ -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;
|
||||
}
|
Загрузка…
Ссылка в новой задаче