首页 > 解决方案 > 在同步函数中使用 javascript `crypto.subtle`

问题描述

在 javascript 中,是否可以在同步函数中使用浏览器内置的 sha256 哈希(https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#Converting_a_digest_to_a_hex_string )?

理想情况下,我想做类似的事情

String.prototype.sha256 = function() {
    // ...
    return hash
}

我已经尝试过类似的东西(async() => {hash = await digestMessage(message); return hash})(),但我只能取回承诺对象。

在我看来,可能无法实现我想要的,但我想在放弃之前我会在这里问。谢谢!

标签: javascripthashcryptography

解决方案


TL;博士

不,不可能在 JavaScript 中将异步函数包装在同步函数中传播结果。请参阅这篇关于各种语言的同步与异步函数的优秀博文。要点是,由于语言运行方式的性质,JavaScript 是(许多语言中的一种)异步函数具有传染性。

异步内置函数是 JS 中的救星

JavaScript 在一个线程上运行。更具体地说,与特定网页相关的所有 JavaScript 都在同一个线程上运行,以保证在任何特定时刻只有一行JS 将运行。这让我们尼安德特人的 web 程序员免去了编写诸如互斥锁和原子操作之类的同步代码的责任,以免多个线程同时写入同一内​​存并导致数据损坏甚至崩溃。

但是,我们只有一个线程来操作网页上的视觉元素运行各种业务逻辑,如加密/解密和数据管理,这有点糟糕。这一切都可能变得有点慢并损害用户体验。但是异步函数如何解决这个问题?采取这个功能:

function syncGenRSAKey() {
    // fancy math stuff...

    return generatedKey;
}

让我们让它异步(基于承诺):

function asyncGenRSAKey() {
    return new Promise((resolve, reject) => {
        resolve(syncGenRSAKey());
    });
}

希望您的直觉不会告诉您基于 Promise 的函数在这里更快。发生的一切是这样的:

  1. 一些代码调用asyncGenRSAKey()
  2. 浏览器运行Promise构造函数
  3. Promise 构造函数立即/同步调用(resolve, reject) => { ... }传递给它的回调函数
  4. 浏览器运行syncGenRSAKey()函数
  5. 承诺同步履行

我们的代码仍然是完全同步的。我们一无所获。请记住,我们的 JavaScript一次只能运行一行。只要我们的底层密钥生成代码 ( ) 是用 JavaScript 编写的,无论从哪里调用它,它总是会占用主线程上的时间。这意味着它将阻止浏览器跳转到其他 JavaScript,即事件处理程序。浏览器还在主线程上渲染页面,因此它会在运行时冻结页面上的几乎所有内容(一些 CSS 动画被特别渲染)。用户可以悬停按钮,按钮背景和鼠标光标都不会更新。syncGenRSAKey()genRSAKey()

现在,请参阅我的回答的这一部分的副标题。关键字是内置的。内置函数,就像下面提供的函数一样,crypto.subtle是用浏览器实现者选择的任何语言编写的:C++、Rust 等。这些函数不是JavaScript 引擎运行的,它们是它的一部分. 它们可以生成尽可能多的操作系统线程,以在您的计算机在给定时刻可以腾出的尽可能多(或尽可能少)的 CPU 内核上运行。这意味着密钥生成代码可以并且通常将与您的一堆 JavaScript 代码和页面呈现选项完全并行运行,然后当密钥准备好并且任何当前正在运行的 JavaScript 完成运行时,浏览器只会回调您的 JavaScript ,触发解决的承诺(如果生成密钥时出错,则拒绝),然后可以启动链接到生成密钥的任何承诺中的代码。

现在,这对于SHA-256校验和真的有必要吗?不。事实上,我自己仍然有一个 GitHub PR,我一直在推迟,因为我厌倦了承诺一切(包括一些非常复杂的 Angular 组件),因为当用户打开一个模式时我计算了一个f**king 哈希。这是给你的,苏珊娜。

以下是两个优秀的视频,任何阅读此 StackOverflow 帖子的人都应该花时间观看。除非您对 JavaScript 的同步/异步特性有足够的了解,能够准确地描绘出您的代码将如何运行,否则您并不真正了解JavaScript,并且您最终会遇到无法理解的错误。

Node.js 事件循环:不是那么单线程

Jake Archibald:在循环中 - JSConf.Asia

JavaScript中async/的说明await

和关键字是纯语法async糖。它们不能让你做任何你以前不能用老式的承诺链做的事情,就像承诺不能让你做任何你用好的 ole 嵌套回调函数做不到的事情一样。/只需让你的代码干净 10 倍。最后,与使用嵌套回调相比,Promise 实际上会产生少量的运行时开销,因为 Promise 具有各种状态以方便它们很好地链接它们并且是堆分配的;/ ,我听说,可以通过让 JS 引擎更容易地查看异步代码的整体上下文以及变量的使用位置等来撤消这个小小的退步,并进行优化。awaitasyncawaitasyncawait

以下是一些正确使用async/的常见示例。await为了清楚返回类型,它们是用 TypeScript 编写的,但如果你去掉: Whatevers ,它就会变成 JavaScript。

在基于 Promise 的 API 中包装一个同步函数

这实际上很少需要,但有时您需要您的代码来适应第三方代码(如库)所需的接口。

function withoutAsyncAwait(): Promise<number> {
    // Note that the reject callback provided to us by the Promise
    // constructor is rarely useful because the promise will
    // automatically be rejected if our callback throws an error,
    // e.g., if the Math.random() throws an error.
    return new Promise((resolve, reject) => resolve(Math.random()));

    // Could be (ignore the reject callback):
    // return new Promise(resolve => resolve(Math.random()));
}

async function withAsyncAwait(): Promise<number> {
    // If any synchronous code inside an async function throws an
    // error, a promise will still be returned by the async function,
    // but it will be rejected (by far the only desirable behavior).
    // The same is true if an await'ed promise rejects.
    return Math.random();
}

Promise如果您将传统的基于回调的异步函数包装为 Promise,则不能(以及为什么要)避免构造函数。

function timeout(milliseconds: number): Promise<void> {
    return new Promise(resolve => window.setTimeout(resolve, milliseconds));
}

条件异步步骤

有时你想在一堆同步代码之前有条件地执行一个异步操作。之前 async/await这意味着您必须复制同步代码或将其全部包装在一个承诺链中,如果条件不成立,则初始承诺将是无操作的。

function doStuffWithoutAsyncAwait1(needToMakeAsyncRequest: boolean): Promise<void> {
    // Might be a no-op promise if we don't need to make a request before sync code
    const promise = needToMakeAsyncRequest ? makeAsyncRequest() : Promise.resolve();

    return promise.then(() => {
        // tons of code omitted here, imagine like 30 lines...
    });
}

function doStuffWithoutAsyncAwait2(needToMakeAsyncRequest: boolean): Promise<void> {
    // Or we can just write the sync code twice, wrapping it in a promise in the branch
    // where we make an async request first. This sucks because our 30 lines of sync
    // code is written twice AND one of the times it is nested/indented inside of both
    // an if-statement and a .then() call
    if (needToMakeAsyncRequest) {
        return makeAsyncRequest().then(() => {
            // tons of code omitted here, imagine like 30 lines...
        });
    }
    
    // tons of code omitted here, imagine like 30 lines...
}

async function cmereAsyncAwaitYouSexyBoiYou(needToMakeAsyncRequest: boolean): Promise<void> {
    if (needToMakeAsyncRequest) {
        // Brings tears to my eyes 
        await makeAsyncRequest();
    }

    // tons of code omitted here, imagine like 30 lines...
}

结合 async/await 和现有的 Promise 机制

async/await不是灵丹妙药。它使编写一系列异步步骤非常干净,但有时我们不只是想要一个序列:我们希望多个异步步骤同时运行。

async function takes12SecondsTotal(): Promise<[string, string]> {
    const result1 = await takes7Seconds();
    const result2 = await takes5Seconds(); // will not get here till 1st result is done

    return [result1, result2];
}

async function takes7SecondsTotal(): Promise<[string, string]> {
    // Both inner functions start doing stuff immediately and we just wait for them
    // both to finish
    const [result1, result2] = await Promise.all([
        takes7Seconds(),
        takes5Seconds()
    ]);

    return [result1, result2];
}

function nottttttActuallyyyyyTheSammeeeeIKnowIKnowScrewErrorHandling(): Promise<[string, string]> {
    // We are almost there! However, we just introduced a potential sh!tstorm by reducing down our
    // code and getting rid of async/await: we now have the assumption that both the takes7Seconds()
    // and takes5Seconds() calls DO return promises... but they might have synchronous code and the
    // beginning of them that could throw an error because the author screwed up and then they will
    // blow up SYNCHRONOUSLY in our face and this function will also blow up SYNCHRONOUSLY and it
    // will continue up the call stack until it hits a try-catch or it reaches all the way out and
    // the JS engine stops it and logs it in the dev tools
    return Promise.all([
        takes7Seconds(),
        takes5Seconds()
    ]);

    // Let me illustrate:
    function takes5Seconds(): Promise<string> {
        const now = new Date; // Trivia: you don't need constructor parenthesis if no parameters

        if (now.getDay() === 6 && now.getHours() === 21) { // 9pm on a Saturday
            // Synchronous error
            throw Error("I ain't workin' right now, ok?")
        }

        // Returns a promise, whose rejection will be handled by the promise chain, so an
        // "asynchronous" error (but this function could also throw a synchronous error, you
        // never know)
        return doSomeWork();
    }
}

function thisIsFunctionallyTheSame(): Promise<[string, string]> {
    try {
        return Promise.all([
            takes7Seconds(),
            takes5Seconds()
        ]);
    } catch (err) {
        // catch any synchronous error and gift-wrap it in a promise to protect whoever calls
        // us from a synchronous error explosion
        return Promise.reject(err);
    }
}

async function justBeSmartAndUseAsync(): Promise<[string, string]> {
    // Even though we don't use await at all, async functions act as a stalwart line of defense,
    // stopping any synchronous errors thrown from continuing up the callstack, implicitly
    // catching them and making sure we return a promise NO MATTER WHAT (implicitly does what
    // I did above but the browser probably does it better since async functions are part of the
    // language spec and lots of work has been and will be put into optimizing them)
    return Promise.all([
        takes7Seconds(),
        takes5Seconds()
    ]);
}

我们甚至可能希望同时运行多个异步步骤序列。

async function youCouldBeForgivenForDoingThis(): Promise<void> {
    // Please edit this answer if I'm wrong, but last time I checked, an await keyword holds up
    // the entire expression it's part of--in our case, that means the entire Promise.all(...)
    // expression. The doSomethingUnrelated() will not even start running until writeCode()
    // finishes
    await Promise.all([
        pushCodeToGitHub(await writeCode()),
        doSomethingUnrelated()
    ]);
}

async function armedWithEsotericJSKnowledge(): Promise<void> {
    // Also please note I just await the Promise.all to discard the array of undefined's and
    // return void from our async function
    await Promise.all([
        writeCode().then(code => pushCodeToGitHub(code)),
        doSomethingUnrelated()
    ]);
}

永远不要害怕将 Promise 存储在变量中,或者根据需要将async箭头函数混合到传统 .then()的 Promise 链中以获得最智能的代码。

在异步函数中返回的深奥的bullsh * t

如果您使用 TypeScript 或通常熟悉 JS 承诺,您可能已经知道在.then()回调内部,您可以返回类型T aPromise<T>并且承诺机制在内部完成工作以确保仅将普通内容T传递给下一个.then() 链上。T可以是number或任何其他类型。async函数做同样的事情。错误处理并不那么简单。

function getNumber(): number {
    return 420;
}

async function getNumberAsync(): Promise<number> {
    return getNumber(); // auto-wrap it in a promise cuz we're an async function
}

async function idkJavaScriptButIWantToMakeSureIGetThatNumber(): Promise<number> {
    return await getNumberAsync(); // this IS fine, really
}

async function iKNOWJavaScript(): Promise<number> {
    return getNumberAsync(); // this will NOT return Promise<Promise<number>> because async unwraps it
}

function iLikeToBlowUpRandomly(): Promise<number> {
    if (Math.random() > 0.5) {
        // This is not an async function so this throw clause will NOT get wrapped in a rejected promise
        // and returned pleasantly to the caller
        throw new Error("boom");
    }

    return getNumberAsync();
}

async function iHandleMyProblemsAndAlwaysFulfillMyPromises(): Promise<number> {
    try {
        return iLikeToBlowUpRandomly();
    } catch (err) {
        // This will always catch the "boom" explosions, BUT, if iLikeToBlowUpRandomly() returns a
        // rejected promise, it will sneakily slip through our try-catch because try-catches only
        // catch THROWN errors, and whoever called us will get a bad promise even though we
        // promised (haha) we would only ever return fulfilled promises containing numbers
        return -1;
    }
}

async function iActuallyHandleMyProblemsAndAlwaysFulfillMyPromises(): Promise<number> {
    try {
        // Bam! The normally extraneous await here brings this promise into our pseudo-synchronous
        // async/await code so if it was rejected, it will also trigger our catch branch just like
        // a synchronous error would
        return await iLikeToBlowUpRandomly();
    } catch (err) {
        return 3522047650; // call me if you have job offers  but I'm kinda busy rn and spent way too much time on this
    }
}

推荐阅读