我們的志工尚未將此文章翻譯為 正體中文 (繁體) 版本。加入我們,幫忙翻譯!
Promise APIs for Common Asynchronous Operations
Due to the performance and stability costs of synchronous IO, many APIs which rely on it have been deprecated. The following page contains examples of many Promise
-based replacement APIs for common operations. These APIs allow asynchronous operation to be achieved with a coding style similar to synchronous variants.
The following examples make use of the Task
API, which harnesses generator functions to remove some of the syntactic clutter of raw Promise
s, such that asynchronous promise code more closely resembles synchronous, procedural code. A Task
example like the following:
Components.utils.import("resource://gre/modules/Task.jsm"); Task.spawn(function* () { var response = yield Request("login", { username: user, password: password }); if (response.messages) { try { yield Publish({ username: user, messages: response.messages }); } catch (e) { self.reportError("Publication failed", e); } } });
Can be converted to a pure Promise
-based equivalent as such:
Request("login", { username: user, password: password }) .then(response => { if (response.messages) return Publish({ username: user, messages: response.messages }); }) .then(null, (e) => { self.reportError("Publication failed", e); });
File IO
File IO in add-ons should be done via the OS.File
API, which provides a simple, but powerful, interface for reading, writing, and manipulating both text and binary files. It is also available for use off-main-thread in Worker
s as a synchronous API.
This interface replaces the previous, complicated XPCOM nsIFile
and streams APIs, and their related JavaScript helper modules. These older interfaces should be avoided, even in their asynchronous forms, due to their performance penalties and needless complexity.
Representative Example Usage
Components.utils.import("resource://gre/modules/osfile.jsm"); Task.spawn(function* () { // Retrieve file metadata to check modification time. let info = yield OS.File.stat(configPath); if (info.lastModificationDate <= timestamp) return; timestamp = info.lastModificationDate; // Read the file as a UTF-8 string, parse as JSON. let config = JSON.parse( yield OS.File.read(configPath, { encoding: "utf-8" })); let files = []; // Get the directory contents from a list of directories. for (let dir of config.directories) { // Iterate over the contents of the directory. let iter = new OS.File.DirectoryIterator(dir); yield iter.forEach(entry => { if (!entry.isDir) files.push(entry.path); }); iter.close(); } // Read the files as binary blobs and process them. let processor = new FileProcessor(); for (let file of files) { let data = yield OS.File.read(file); processor.add(data); } // Now write the processed files back out, as a binary blob. yield OS.File.writeAtomic(config.processedPath, processor.process(), { tmpPath: config.processedPath + "." + Math.random() }); // And write out a new config file. config.indexStats = processor.stats; yield OS.File.writeAtomic(configPath, JSON.stringify(config), { tmpPath: configPath + "." + Math.random(), encoding: "UTF-8" }) timestamp = new Date; });
HTTP Requests
HTTP requests should, in nearly all circumstances, be made via the standard XMLHttpRequest
API. While this API does not have direct support for promises, its standard usage is very easy to adapt to a promise-based approach. Moreover, many third-party wrappers for the XMLHttpRequest
API now support Promise
s out of the box.
Example Direct Usage
Task.spawn(function* () { // Make the initial request. let resp = yield new Promise((resolve, reject) => { let xhr = new XMLHttpRequest; xhr.onload = resolve; xhr.onerror = reject; xhr.open("GET", dataURL); xhr.responseType = "json"; xhr.send(); }); let data = resp.target.response; // Use the response to construct form data object for the // second request. let form = new FormData; form.append("id", data.id); form.append("content", data.content); // Make the second request. resp = yield new Promise((resolve, reject) => { let xhr = new XMLHttpRequest; xhr.onload = resolve; xhr.onerror = reject; xhr.open("POST", updateURL); xhr.send(form); }); // Use the response of the second request. notifyUser(resp.target.responseText); });
Example Using Promise-Based Helper
The following example relies on the helper function defined below.
Task.spawn(function* () { // Make the initial request. let xhr = yield Request(dataURL, { responseType: "json" }); let data = xhr.response; // Use the response to construct form data object for the // second request. let form = new FormData; form.append("id", data.id); form.append("content", data.content); // Make the second request. xhr = yield Request(updateURL, { data: form }); // Use the response of the second request. notifyUser(xhr.responseText); });
Downloading Remote Files
Nearly all previous methods of downloading remote files have been superseded by the much simpler Downloads.jsm
module. The Downloads
object provides a Promise
-based API for downloading remote files, with full support for progress tracking, pause and resume, and, optionally, integration with the download manager UI.
Representative Example Usage
Components.utils.import("resource://gre/modules/Downloads.jsm"); Task.spawn(function* () { // Fetch a file in the background. let download_1 = Downloads.fetch(URL_1, PATH_1); // Fetch a file visible in the download manager. let download_2 = yield Downloads.createDownload({ source: URL_2, target: PATH_2, }); // Add it to the downloads list used by the download manager UI. let list = yield Downloads.getList(Downloads.ALL); list.add(download_2); // Start the second download, and wait for both // downloads to complete. // This will raise an error if either download fails. yield Promise.all([download_1, download_2.start()]); // Do something with the saved files. doStuffWith(PATH_1, PATH_2); });
SQLite
First, it’s important to note that SQLite should be avoided in favor of simpler solutions, such as flat JSON files, under most circumstances. The IO, memory, and CPU overhead added by SQLite is substantial, and in most cases outweighs the cost of dealing with flat files directly.
For use cases which are not easily served by other options, or for legacy code which cannot easily be upgraded to non-relational models, the Sqlite.jsm
module provides a clean, Promise
-based interface to SQLite databases.
Representative Example Usage
Components.utils.import("resource://gre/modules/Sqlite.jsm"); Task.spawn(function* () { // Open the connection. let db = yield Sqlite.openConnection({ path: DATABASE_PATH }); try { // Start a transaction to insert the data. yield db.executeTransaction(function* () { for (let node of nodes) // Insert the node's data, using an automatically-cached, // pre-compiled statement, and parameter placeholders. yield db.executeCached( "INSERT INTO nodes (id, owner, key, value) \ VALUES (:id, :owner, :key, :value);", { params: { id: node.id, owner: node.owner.id, key: node.key, value: node.value }}); }); // Perform a bulk update. yield db.execute( "UPDATE owners, nodes \ SET owners.name = nodes.name \ WHERE owners.id = nodes.owner AND nodes.key = 'name';"); // Process some results. yield db.execute( "SELECT owners.group AS group, nodes.value AS task \ FROM nodes \ INNER JOIN owners ON owner.id = nodes.owner \ WHERE nodes.key = 'task';", { onRow: row => { runTask(row.getResultByName("task"), row.getResultByName("group")); } }); // And quickly grab a single row value. let [row] = yield db.execute( "SELECT value FROM nodes WHERE key = 'timestamp' \ ORDER BY value DESC LIMIT 1"); latestTimestamp = row.getResultByIndex(0); } finally { // Make sure to close the database when finished. // Failure to do this will prevent Firefox from shutting down // cleanly. yield db.close(); } });
Promise Wrappers and Helpers
The following are some example Promise
-based wrappers for common callback-based asynchronous APIs.
AddonManager
var AOM = { __proto__: AddonManager, Addon: function Addon(addon) { if (!(addon && "getDataDirectory" in addon)) return addon; return { __proto__: addon, getDataDirectory: function getDataDirectory() { return new Promise((accept, reject) => { return addon.getDataDirectory((directory, error) => { if (error) reject(error); else accept(directory); }); }); }, }; }, getInstallForURL: function getInstallForURL(url, mimetype, hash, name, iconURL, version, loadGroup) { return new Promise(accept => this.AddonManager.getInstallForURL(url, accept, mimetype, hash, iconURL, version, loadGroup)); }, getInstallForFile: function getInstallForFile(url, mimetype) { return new Promise(accept => this.AddonManager.getInstallForFile(url, accept, mimetype)); }, getAllInstalls: function getAllInstalls() { return new Promise(accept => this.AddonManager.getAllInstalls(accept)); }, _replaceMethod: function replaceMethod(method, callback) { Object.defineProperty(this, method, { enumerable: true, configurable: true, value: key => { return new Promise(accept => this.AddonManager[method](key, addon => accept(callback(addon)))); } }); }, }; for (let method of ["getAddonByID", "getAddonBySyncGUID"]) AOM._replaceMethod(method, addon => AOM.Addon(addon)); for (let method of ["getAllAddons", "getAddonsByIDs", "getAddonsByTypes", "getAddonsWithOperationsByTypes"]) AOM._replaceMethod(method, addons => addons.map(AOM.Addon)); AOM._replaceMethod("getInstallsByTypes", installs => installs); Components.utils.import("resource://gre/modules/AddonManager.jsm", AOM);
Example usage:
Task.spawn(function* () { // Get an extension instance, and its data directory. let addon = yield AOM.getAddonByID(ADDON_ID); let path = yield addon.getDataDirectory(); writer.writeDataTo(path); // Disable all extensions. for (let extension of yield AOM.getAddonsByTypes(["extension"])) extension.userDisabled = true; });
JSON File Storage
This helper simplifies the use of JSON data storage files with asynchronous IO. The JSONStore
object must be instantiated with the base name of the storage file and, optionally, a JSON-compatible object to be used if the file does not yet exist. The constructor returns a Promise
which resolves when the file’s contents have been loaded.
The contents of the file will be initially loaded into the JSON store’s data
property. After any changes are made to this property, the save()
method must be called to queue the changes to be written to disk. Unless the flush()
method is called, no writes will happen until a full second has elapsed between save()
calls.
The variable ADDON_ID
must be defined to the ID of the add-on the code is being used in.
This code makes use of the Add-on Manager helper defined above, though it can be adapted to work without it.
Components.utils.import("resource://gre/modules/DeferredSave.jsm"); /** * Handles the asynchronous reading and writing of add-on-specific JSON * data files. * * @param {string} The basename of the file. * @param {object} A JSON-compatible object which will be used in place * of the file's data, if the file does not already exist. * @optional * * @return {Promise<JSONStore>} */ function JSONStore(name, default_={}) { return Task.spawn(function* () { // Determine the correct path for the file. let addon = yield AOM.getAddonByID(ADDON_ID); let dir = yield addon.getDataDirectory(); this.path = OS.Path.join(dir, name + ".json"); // Read the file's contents, or fall back to defaults. try { this.data = JSON.parse( yield OS.File.read(this.path, { encoding: "utf-8" })); } catch (e if e.becauseNoSuchFile) { this.data = JSON.parse(JSON.stringify(default_)); } // Create a saver to write our JSON-stringified data to our // path, at 1000ms minimum intervals. this.saver = new DeferredSave(this.path, () => JSON.stringify(this.data), 1000); return this; }.bind(this)); } /** * Immediately save the data to disk. * * @return {Promise} A promise which resolves when the file's contents * have been written. */ JSONStore.prototype.flush = function () { return this.saver.flush(); }; /** * Queue a save operation. The operation will commence after a full * second has passed without further calls to this method. * * @return {Promise} A promise which resolves when the file's contents * have been written. */ JSONStore.prototype.save = function () { return this.saver.saveChanges(); };
Example usage:
var ADDON_ID = "[email protected]"; var CONFIG_DEFAULT = { "foo": "bar", }; new JSONStore("config", CONFIG_DEFAULT).then(store => { console.log(store.data); store.data.baz = "quux"; store.save(); })
The following changes will remove the dependency on AOM
wrappers:
let addon = yield new Promise(accept => AddonManager.getAddonByID(ADDON_ID, accept)); let dir = yield new Promise(accept => addon.getDataDirectory(accept));
XMLHttpRequest
function Request(url, options) { return new Promise((resolve, reject) => { let xhr = new XMLHttpRequest; xhr.onload = event => resolve(event.target); xhr.onerror = reject; let defaultMethod = options.data ? "POST" : "GET"; if (options.mimeType) xhr.overrideMimeType(params.options); xhr.open(options.method || defaultMethod, url); if (options.responseType) xhr.responseType = options.responseType; for (let header of Object.keys(options.headers || {})) xhr.setRequestHeader(header, options.headers[header]); let data = options.data; if (data && Object.getPrototypeOf(data).constructor.name == "Object") { options.data = new FormData; for (let key of Object.keys(data)) options.data.append(key, data[key]); } xhr.send(options.data); }); }
Example usage:
Task.spawn(function* () { let request = yield Request("https://example.com/", { method: "PUT", mimeType: "application/json", headers: { "X-Species": "Hobbit" }, data: { foo: new File(path), thing: "stuff" }, responseType: "json" }); console.log(request.response["json-key"]); });