web worker_Web Worker比您想象的要容易

web worker

Web Worker easier than you thought

In this article, a DIRTY, unsafe, unstable and scary <em>eval</em> method will be described. So, if you are uncomfortable with that, stop reading right now.

在本文中,将介绍一种肮脏,不安全,不稳定且令人恐惧的<em>eval</em>方法。 因此,如果您对此感到不舒服,请立即停止阅读。

First off, some issues with convenience remained unresolved: in code sent to web web workers, closure can't be used.

首先,一些便利性问题尚未解决:在发送给Web Web Worker的代码中,不能使用闭包。



All of us like new technologies, and all of us like new technologies to be convenient to use. However, it's not exactly the case with web workers. web workers accept files or links to files, which isn't convenient. It would be good to be able to put any task into web workers, not just specifically planned code.

我们所有人都喜欢新技术,而我们所有人都喜欢易于使用的新技术。 但是,Web Worker并非完全如此。 网络工作者会接受文件或文件链接,这很不方便。 能够将任何任务放入Web Worker中,而不仅仅是专门计划的代码,这将是很好的。

What do we need to make web workers more convenient to operate? I believe, it's the following:

我们需要什么来使网络工作者更方便地操作? 我相信是这样的:

  • A possibility to launch in web workers any code at any moment

    随时可以在Web Worker中启动任何代码的可能性
  • A possibility to send to web workers complicated data (class instances, functions)

    向网络工作者发送复杂数据(类实例,函数)的可能性
  • A possibility to receive a promise with a reply from a web worker.

    可能会收到来自网络工作者的答复并作出承诺。

Let's try to write it. For starters, we'll need a communication protocol between a web worker and the main window. In general, a protocol is just a structure and types of data used for communication between a browser window and a web worker. It's pretty straightforward. You can use this or write your own version. Every message will have an ID and data typical of a specific message type. Initially, we'll have two types of messages for web workers:

让我们尝试编写它。 首先,我们需要Web Worker和主窗口之间的通信协议。 通常,协议只是用于浏览器窗口和Web Worker之间通信的数据的结构和类型。 这很简单。 您可以使用版本或编写自己的版本。 每个消息将具有特定消息类型的典型ID和数据。 最初,我们将为Web Worker提供两种类型的消息:

  • Adding libraries/files to a web worker

    将库/文件添加到Web Worker
  • Launch.

    发射。

将在Web Worker中的文件 (A file that will be inside a web worker)

Before writing a web worker, we need to describe a file that will be inside of it, supporting the protocol described above. I like object-oriented programming (OOP), so this will be a class named workerBody. This class has to subscribe to an event from the parent window.

在编写Web Worker之前,我们需要描述一个包含在其中的文件,以支持上述协议。 我喜欢面向对象的编程(OOP),因此这将是一个名为workerBody的类。 此类必须从父窗口预订事件。

self.onmessage = (message) => {
    this.onMessage(message.data);
};

Now we can listen to events from the parent window. We have two types of events: those which imply a reply and all the rest. Let's do events: \ Libraries and files are added to a web worker using importScripts API.

现在我们可以从父窗口监听事件。 我们有两种类型的事件:暗示事件的事件和所有其他事件。 让我们做事件:\使用importScripts API将库和文件添加到Web Worker。

And now the scariest part: for launching a random function, we'll use eval.

现在最可怕的部分是:为了启动随机函数,我们将使用eval

...

onMessage(message) {
  switch (message.type) {
      case MESSAGE_TYPE.ADD_LIBS:
          this.addLibs(message.libs);
          break;
      case MESSAGE_TYPE.WORK:
          this.doWork(message);
          break;
  }
}

doWork(message) {
    try {
        const processor = eval(message.job);
        const params = this._parser.parse(message.params);
        const result = processor(params);
        if (result && result.then && typeof result.then === 'function') {
             result.then((data) => {
                 this.send({ id: message.id, state: true, body: data });
             }, (error) => {
                 if (error instanceof Error) {
                      error = String(error);
                 }
                 this.send({ id: message.id, state: false, body: error });
             });
        } else {
           this.send({ id: message.id, state: true, body: result });
        }
    } catch (e) {
       this.send({ id: message.id, state: false, body: String(e) });
    }
}

send(data) {
    data.body = this._serializer.serialize(data.body);
    try {
            self.postMessage(data);
    } catch (e) {
        const toSet = {
          id: data.id,
          state: false,
          body: String(e)
        };
        self.postMessage(toSet);
    }
}

The method onMessage is responsible for receiving a message and choosing a handler, doWork launches a sent function and send sends a reply to the parent window.

onMessage方法负责接收消息并选择处理程序, doWork启动已发送函数,然后send发送答复到父窗口。

解析器和序列化器 (Parser and serializer)

Now that we have the web worker's content, we need to learn to serialize and parse any data, so they could be sent to the web worker. Let's start with a serializer. We want to be able to send to the web worker any data, including class instances, classes and functions, while the web worker's native capacity enables sending only JSON-like data. To go around that, we'll need _eval_. We'll wrap all data that JSON can't accept into corresponding sting structures and launch them on the other side. To preserve immutability, received data will be cloned on the fly, replacing whatever cannot be serialized by ordinary methods with service objects, which will be replaced back on the other side by a parser. At first sight, this task isn't difficult, but there are many pitfalls. The scariest limitation of this approach is the inability to use closure, which leads to a slightly different code writing style. Let's start with the easiest part, the function. First, we need to learn to distinguish a function from a class constructor. Let's do that:

现在我们有了Web Worker的内容,我们需要学习序列化和解析任何数据,以便将它们发送给Web Worker。 让我们从序列化器开始。 我们希望能够将任何数据(包括类实例,类和函数)发送给Web Worker,而Web Worker的本机容量仅允许发送类似JSON的数据。 要解决此问题,我们需要_ eval _。 我们会将JSON无法接受的所有数据包装到相应的字符串结构中,然后在另一侧启动它们。 为了保持不变性,接收到的数据将被即时克隆,用服务对象替换普通方法无法序列化的内容,而服务对象将在另一端由解析器替换。 乍一看,这个任务并不困难,但是有很多陷阱。 这种方法最可怕的局限性是无法使用闭包,这导致代码编写风格略有不同。 让我们从最简单的部分开始,即函数。 首先,我们需要学习将函数与类构造函数区分开。 让我们这样做:

static isFunction(Factory){
        if (!Factory.prototype) {
            // Arrow function has no prototype
            return true;
        }

        const prototypePropsLength = Object.getOwnPropertyNames(Factory.prototype)
            .filter(item => item !== 'constructor')
            .length;

        return prototypePropsLength === 0 && Serializer.getClassParents(Factory).length === 1;
}

static getClassParents(Factory) {
    const result = [Factory];
    let tmp = Factory;
    let item = Object.getPrototypeOf(tmp);

    while (item.prototype) {
      result.push(item);
      tmp = item;
      item = Object.getPrototypeOf(tmp);
    }

    return result.reverse();
}

First, we'll check if the function has a prototype. If it doesn't, this is certainly a function. Then, we look at the number of the prototype's features. If it only has a constructor and the function isn't a successor of another class, we consider it a function.

首先,我们将检查该函数是否具有原型。 如果没有,那肯定是一个功能。 然后,我们看一下原型功能的数量。 如果它只有一个构造函数,而该函数不是另一个类的后继者,则我们将其视为一个函数。

When we discover a function, we just replace it with a service object with the fields __type = “serialized-function“ and template corresponds to the template of this function (func.toString()).

当我们发现一个函数时,我们将其替换为具有__type =“ serialized-function”字段的服务对象,并且模板对应于该函数的模板(func.toString())。

For now, we'll skip class and look at class instance. Later, we'll need to distinguish between regular objects and class instances.

现在,我们将跳过类,并查看类实例。 稍后,我们将需要区分常规对象和类实例。

static isInstance(some) {
        const constructor = some.constructor;
        if (!constructor) {
            return false;
        }

        return !Serializer.isNative(constructor);
    }

static isNative(data) {
        return /function .*?\(\) \{ \[native code\] \}/.test(data.toString());
}

We believe that an object is regular if it doesn’t have a constructor or its constructor is a native function. Once we've discovered a class instance, we'll replace it with a service object with the following fields:

我们认为,如果对象没有构造函数或其构造函数是本机函数,则该对象是常规的。 发现类实例后,我们将其替换为具有以下字段的服务对象:

  • __type: 'serialized-instance'

    __type:“序列化实例”
  • data is data contained in the instance

    数据是实例中包含的数据
  • index is the class index of this instance on the service class list.

    index是该实例在服务类列表上的类索引。

To send data, we have to create an extra field, in which we will store a list of unique classes that we send. However, there is a challenge: discovering a class, we need to take not only its template, but also the templates of all parent classes and save them as independent classes, so every parent class gets sent only once, also saving instanceof proof. Discovering a class is easy: this is a function that failed our Serializer.isFunction proof. When adding a class, we check the presence of that class on the list of serialized data and add only unique classes. Code that assembles a class into a template is quite large and is available here.

要发送数据,我们必须创建一个额外的字段,在其中存储我们发送的唯一类的列表。 但是,这是一个挑战:发现一个类,我们不仅需要获取其模板,还需要获取所有父类的模板,并将它们保存为独立的类,因此每个父类仅发送一次,还保存了证明实例。 发现一个类很容易:这是一个未能通过Serializer.isFunction证明的函数。 添加类时,我们检查序列化数据列表中该类的存在,并仅添加唯一的类。 将类组合成模板的代码很大,可以在这里找到

In the parser, we initially check all classes sent to us and compile them if they haven’t been sent. Then, we recursively check every data field and replace service objects with compiled data. The most interesting part is class instances. We have a class and data that were in its instance, but we can't just create an instance as a constructor request may require parameters that we don't have. We get that from the nearly forgotten Object.create method, which creates an object with a set prototype. This way, we avoid requesting a constructor, get a class instance and just copy properties into the instance.

在解析器中,我们首先检查所有发送给我们的类,如果尚未发送,则将它们编译。 然后,我们递归检查每个数据字段,并用已编译的数据替换服务对象。 最有趣的部分是类实例。 我们在其实例中有一个类和数据,但是我们不能仅仅创建一个实例,因为构造函数请求可能需要我们没有的参数。 我们从几乎被遗忘的Object.create方法中获得了该方法,该方法使用设置的原型创建一个对象。 这样,我们避免了请求构造函数,获取类实例并将属性复制到实例中的情况。

创建一个网络工作者 (Creating a web worker)

For a web worker to operate successfully, we need a parser and a serializer within the web worker and outside. So we take a serializer and turn it, parser and web worker body into a template. From the template, we make a blob and create a download link over URL.createObjectURL (this method may not work for some “Content-Security-Policy”). This method is also good for launching random code from a string.

为了使Web Worker成功运行,我们需要Web Worker内部和外部的解析器和串行器。 因此,我们采用了一个序列化器,并将其,解析器和Web Worker主体转换为模板。 从模板中,我们创建一个Blob,并通过URL.createObjectURL创建下载链接(此方法可能不适用于某些“ Content-Security-Policy”)。 此方法也适用于从字符串启动随机代码。

_createworker(customworker) {
    const template = `var Myworker = ${this._createTemplate(customworker)};`;
    const blob = new Blob([template], { type: 'application/javascript' });

    return new worker(URL.createObjectURL(blob));
}

_createTemplate(workerBody) {
    const Name = Serializer.getFnName(workerBody);
    if (!Name) {
        throw new Error('Unnamed worker Body class! Please add name to worker Body class!');
    }

    return [
        '(function () {',
        this._getFullClassTemplate(Serializer, 'Serializer'),
        this._getFullClassTemplate(Parser, 'Parser'),
        this._getFullClassTemplate(workerBody, 'workerBody'),
        `return new workerBody(Serializer, Parser)})();`
    ].join('\n');
}

结果 (Outcome)

So, we got a simple-to-use library that can send any code to the web worker. It supports TypeScript classes, for instance:

因此,我们有了一个易于使用的库,可以将任何代码发送给Web Worker。 它支持TypeScript类,例如:

const wrapper = workerWrapper.create();

wrapper.process((params) => {
    // This code in worker. Cannot use closure!
    // do some hard work
    return 100; // or return Promise.resolve(100)
}, params).then((result) => {
    // result = 100;
});

wrapper.terminate() // terminate for kill worker process

未来发展 (Future development)

Unfortunately, this library is far from ideal. We need to add support of setters and getters for classes, objects, prototypes and static features. Also, we need to add caching, an alternative script launch without eval, using URL.createObjectURL instead. Finally, a file with the web worker content needs to be added to the assembly (in case on-the-fly creation is not available) etc. Visit the repository!

不幸的是,这个库远非理想。 我们需要为类,对象,原型和静态功能添加对setter和getter的支持。 另外,我们需要添加缓存,而不使用eval的替代脚本启动,而是使用URL.createObjectURL 。 最后,需要将具有Web Worker内容的文件添加到程序集中(以防无法即时创建)等。请访问存储库

翻译自: https://habr.com/en/company/waves/blog/462325/

web worker

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值