简体   繁体   English

类链式方法中的链式等待

[英]chained await in class chained methods

context: Two javascript classes in separate files, each integrating a different external service and being called in a express.js router.上下文:两个单独文件中的 javascript 类,每个都集成不同的外部服务并在 express.js 路由器中调用。

See "problematic code" below:请参阅下面的“有问题的代码”:

  • route路线
routes.post('/aws', upload.single('file'), async (req, res) => {
    const transcribeParams = JSON.parse(req.body.options)
    const bucket = 'bucket-name'

    const data = await ( await ( await awsTranscribe.Upload(req.file, bucket)).CreateJob(transcribeParams)).GetJob()

    res.send(data) 
})  

  • S3 class S3级
class AmazonS3 {
    constructor() {
        this.Upload = this.Upload
    }

    async Upload(file, bucket) {
        const uploadParams = {
            Bucket: bucket,
            Body: fs.createReadStream(file.path),
            Key: file.filename,
        }

        this.data = await s3.upload(uploadParams).promise()

        return this
    }
}  

  • Transcribe class转录类
class Transcribe extends AwsS3 {
    constructor() {
        super()
        this.CreateJob = this.CreateJob
        this.GetJob = this.GetJob
    }

    async CreateJob(params) {
        if(this.data?.Location) {
            params.Media = { ...params.Media, MediaFileUri: this.data.Location }
        }

        this.data = await transcribeService.startTranscriptionJob(params).promise()

        return this
    }

    async GetJob(jobName) {
        if(this.data?.TranscriptionJob?.TranscriptionJobName) {
            jobName = this.data.TranscriptionJob.TranscriptionJobName
        }

        this.data = await transcribeService.getTranscriptionJob({TranscriptionJobName: jobName}).promise()

        return this
    }
}

problem : the problem is with the chained awaits in the router file:问题:问题在于路由器文件中的链式等待:
await ( await ( await awsTranscribe.Upload...
Yes, it does work, but it would be horrible for another person to maintain this code in the future.是的,它确实有效,但是将来另一个人维​​护此代码将是可怕的。
How can i make so it would be just我该怎么做才能做到
awsTranscribe.Upload(req.file, bucket).CreateJob(transcribeParams).GetJob() without the .then? awsTranscribe.Upload(req.file, bucket).CreateJob(transcribeParams).GetJob()没有 .then?

The problem is with the chained awaits in the router file: await ( await ( await awsTranscribe.Upload...问题在于路由器文件中的链式等待: await ( await ( await awsTranscribe.Upload...

No, that's fine.不,没关系。 In particular it would be trivial to refactor it to separate lines:特别是,将其重构为单独的行将是微不足道的:

routes.post('/aws', upload.single('file'), async (req, res) => {
    const transcribeParams = JSON.parse(req.body.options)
    const bucket = 'bucket-name'

    const a = await awsTranscribe.Upload(req.file, bucket);
    const b = await b.CreateJob(transcribeParams);
    const c = await b.GetJob();

    res.send(c);
});

Your actual problem is that a , b , and c all refer to the same object awsTranscribe .您的实际问题是abc都引用同一个对象awsTranscribe Your code would also "work" if it was written如果编写了您的代码也将“工作”

routes.post('/aws', upload.single('file'), async (req, res) => {
    const transcribeParams = JSON.parse(req.body.options)
    const bucket = 'bucket-name'

    await awsTranscribe.Upload(req.file, bucket);
    await awsTranscribe.CreateJob(transcribeParams);
    await awsTranscribe.GetJob();

    res.send(awsTranscribe);
});

The horrible thing is that you are passing your data between these methods through the mutable awsTranscribe.data property - even storing different kinds of data in it at different times!可怕的是,您正在通过可变的awsTranscribe.data属性在这些方法之间传递数据——甚至在不同时间在其中存储不同类型的数据! One could change the order of method calls and it would completely break in non-obvious and hard-to-debug ways.可以更改方法调用的顺序,它会以不明显且难以调试的方式完全中断。

Also it seems that multiple requests share the same awsTranscribe instance.此外,多个请求似乎共享同一个awsTranscribe实例。 This will not work with concurrent requests.这不适用于并发请求。 Anything is possible from just "not working" to responding with the job data from a different user (request)!从“不工作”到使用来自不同用户(请求)的工作数据进行响应,一切皆有可能! You absolutely need to fix that , then look at ugly syntax later.你绝对需要修复,然后再看看丑陋的语法。


What you really should do is get rid of the class es.你真正应该做的是摆脱class es。 There's no reason to use stateful objects here, this is plain procedural code.这里没有理由使用有状态对象,这是简单的程序代码。 Write simple functions, taking parameters and returning values:编写简单的函数,接受参数和返回值:

export async function uploadFile(file, bucket) {
    const uploadParams = {
        Bucket: bucket,
        Body: fs.createReadStream(file.path),
        Key: file.filename,
    };
    const data = s3.upload(uploadParams).promise();
    return data.Location;
}

export async function createTranscriptionJob(location, params) {
    params = {
        ...params,
        Media: {
            ...params.Media,
            MediaFileUri: location,
        },
    };
    const data = await transcribeService.startTranscriptionJob(params).promise();
    return data.TranscriptionJob;
}

async function getTranscriptionJob(job) {
    const jobName = job.TranscriptionJobName;
    return transcribeService.getTranscriptionJob({TranscriptionJobName: jobName}).promise();
}

Then you can import and call them as然后您可以导入并将它们称为

routes.post('/aws', upload.single('file'), async (req, res) => {
    const transcribeParams = JSON.parse(req.body.options)
    const bucket = 'bucket-name'

    const location = await uploadFile(req.file, bucket);
    const job = await createTranscriptionJob(location, transcribeParams);
    const data = await getTranscriptionJob(job);

    res.send(c);
});

I got interested in whether it was possible to take an object with several async methods and somehow make them automatically chainable.我对是否可以使用多个async方法获取一个对象并以某种方式使它们自动链接起来很感兴趣。 Well, you can:好吧,你可以:

 function chain(obj, methodsArray) { if (!methodsArray || !methodsArray.length) { throw new Error("methodsArray argument must be array of chainable method names"); } const methods = new Set(methodsArray); let lastPromise = Promise.resolve(); const proxy = new Proxy(obj, { get(target, prop, receiver) { if (prop === "_promise") { return function() { return lastPromise; } } const val = Reflect.get(target, prop, receiver); if (typeof val !== "function" || !methods.has(prop)) { // no chaining if it's not a function // or it's not listed as a chainable method return val; } else { // return a stub function return function(...args) { // chain a function call lastPromise = lastPromise.then(() => { return val.apply(obj, args); //return Reflect.apply(val, obj, ...args); }); return proxy; } } } }); return proxy; } function delay(t) { return new Promise(resolve => { setTimeout(resolve, t); }); } function log(...args) { if (!log.start) { log.start = Date.now(); } const delta = Date.now() - log.start; const deltaPad = (delta + "").padStart(6, "0"); console.log(`${deltaPad}: `, ...args) } class Transcribe { constructor() { this.greeting = "Hello"; } async createJob(params) { log(`createJob: ${this.greeting}`); return delay(200); } async getJob(jobName) { log(`getJob: ${this.greeting}`); return delay(100); } } const t = new Transcribe(); const obj = chain(t, ["getJob", "createJob"]); log("begin"); obj.createJob().getJob()._promise().then(() => { log("end"); });

There's a placeholder for your Transcribe class that has two asynchronous methods that return a promise. Transcribe类有一个占位符,它有两个返回承诺的异步方法。

Then, there's a chain() function that returns a proxy to an object that makes a set of passed in method names be chainable which allows you to then do something like this:然后,有一个chain()函数返回一个对象的代理,使一组传入的方法名称可链接,这允许您执行以下操作:

const t = new Transcribe();
// make chainable proxy
const obj = chain(t, ["getJob", "createJob"]);

obj.createJob().getJob()

or或者

await obj.createJob().getJob()._promise()

I wouldn't necessarily say this is production-ready code, but it is an interesting feasibility demonstration and (for me) a chance to learn more about a Javascript proxy object.我不一定会说这是生产就绪代码,但这是一个有趣的可行性演示,并且(对我而言)是一个了解更多关于 Javascript 代理对象的机会。


Here's a different approach that (instead of the proxy object) adds method stubs to a promise to make things chainable:这是一种不同的方法,它(而不是代理对象)将方法存根添加到 promise 以使事物可链接:

 function chain(orig, methodsArray) { let masterP = Promise.resolve(); function addMethods(dest) { for (const m of methodsArray) { dest[m] = function(...args) { // chain onto master promise to force sequencing masterP = masterP.then(result => { return orig[m].apply(orig, ...args); }); // add methods to the latest promise befor returning it addMethods(masterP); return masterP; } } } // add method to our returned promise addMethods(masterP); return masterP; } function delay(t) { return new Promise(resolve => { setTimeout(resolve, t); }); } function log(...args) { if (!log.start) { log.start = Date.now(); } const delta = Date.now() - log.start; const deltaPad = (delta + "").padStart(6, "0"); console.log(`${deltaPad}: `, ...args) } class Transcribe { constructor() { this.greeting = "Hello"; this.cntr = 0; } async createJob(params) { log(`createJob: ${this.greeting}`); ++this.cntr; return delay(200); } async getJob(jobName) { log(`getJob: ${this.greeting}`); ++this.cntr; return delay(100); } } const t = new Transcribe(); log("begin"); chain(t, ["getJob", "createJob"]).createJob().getJob().then(() => { log(`cntr = ${t.cntr}`); log("end"); });

Since this returns an actual promise (with additional methods attached), you can directly use .then() or await with it without the separate ._promise() that the first implementation required.由于这会返回一个实际的 Promise(附加了其他方法),因此您可以直接使用.then()await它,而无需第一个实现所需的单独._promise()

So, you can now do something like this:因此,您现在可以执行以下操作:

const t = new Transcribe();

chain(t, ["getJob", "createJob"]).createJob().getJob().then(() => {
    log(`cntr = ${t.cntr}`);
});

or:或者:

const t = new Transcribe();

await chain(t, ["getJob", "createJob"]).createJob().getJob();
log(`cntr = ${t.cntr}`);

And, here's a third version where it creates a thenable object (a pseudo-promise) with the added methods on it (if it bothers you to add methods to an existing promise):而且,这是第三个版本,它创建了一个 thenable 对象(一个伪承诺),并在其上添加了方法(如果它打扰您向现有承诺添加方法):

 function chain(orig, methodsArray) { if (!methodsArray || !methodsArray.length) { throw new Error("methodsArray argument must be array of chainable method names"); } let masterP = Promise.resolve(); function makeThenable() { let obj = {}; for (const m of methodsArray) { obj[m] = function(...args) { // chain onto master promise to force sequencing masterP = masterP.then(result => { return orig[m].apply(orig, ...args); }); return makeThenable(); } } obj.then = function(onFulfill, onReject) { return masterP.then(onFulfill, onReject); } obj.catch = function(onReject) { return masterP.catch(onReject); } obj.finally = function(onFinally) { return masterP.finally(onFinally); } return obj; } return makeThenable(); } function delay(t) { return new Promise(resolve => { setTimeout(resolve, t); }); } function log(...args) { if (!log.start) { log.start = Date.now(); } const delta = Date.now() - log.start; const deltaPad = (delta + "").padStart(6, "0"); console.log(`${deltaPad}: `, ...args) } class Transcribe { constructor() { this.greeting = "Hello"; this.cntr = 0; } async createJob(params) { log(`createJob: ${this.greeting}`); ++this.cntr; return delay(200); } async getJob(jobName) { log(`getJob: ${this.greeting}`); ++this.cntr; return delay(100); } } const t = new Transcribe(); log("begin"); chain(t, ["getJob", "createJob"]).createJob().getJob().then(() => { log(`cntr = ${t.cntr}`); log("end"); });

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM