WebWorker(NWJS)中不能要求节点模块

时间:2020-12-26 19:36:29

I'm trying to do something I thought would be simple. I'm using nwjs (Formerly called Node-Webkit) which if you don't know basically means I'm developing a desktop app using Chromium & Node where the DOM is in the same scope as Node. I want to offload work to a webworker so that the GUI doesn't hang when I send some text off to Ivona Cloud (using ivona-node) which is a text to speech API. The audio comes back in chunks as it's generated and gets written to an MP3. ivona-node uses fs to write the mp3 to the drive. I got it working in the dom but webworkers are needed to not hang the UI. So I have two node modules I need to use in the webworker, ivona-node and fs.

我正在尝试做一些我认为很简单的事情。我正在使用nwjs(以前称为Node-Webkit),如果你不知道基本上意味着我正在使用Chromium和Node开发一个桌面应用程序,其中DOM与Node在同一范围内。我想将工作卸载到webworker,以便当我将一些文本发送到Ivona Cloud(使用ivona-node)这是一个文本到语音API时,GUI不会挂起。音频在生成时会以块的形式返回并写入MP3。 ivona-node使用fs将mp3写入驱动器。我让它在dom中工作但是webworkers需要不挂起UI。所以我需要在webworker中使用两个节点模块,ivona-node和fs。

The problem is that in a webworker you can't use require. So I tried packaging ivona-node and fs with browserify (There's a package called browserify-fs for this which I used) and replacing require with importScripts(). Now I'm getting var errors in the node modules.

问题是在网络工作者中你不能使用require。所以我尝试使用browserify打包ivona-node和fs(我使用了一个名为browserify-fs的软件包)并用importScripts()替换require。现在我在节点模块中遇到var错误。

Note: I don't think the method of native_fs_ will work for writing the mp3 to disk in chunks (The stream) as it should be and I'm getting errors in the Ivona package as well (Actually first and foremost) that I don't know how to fix. I'm including all information to reproduce this.

注意:我不认为native_fs_的方法可以用于将mp3写入磁盘(流),因为它应该是,我在Ivona包中也遇到错误(实际上首先是我)不知道如何解决。我将所有信息都包括在内以重现这一点。

Here's an error I'm getting in the console: Uncaught SyntaxError: Unexpected token var VM39 ivonabundle.js:23132

这是我在控制台中遇到的错误:Uncaught SyntaxError:Unexpected token var VM39 ivonabundle.js:23132

  • Steps to reproduce in NWJS:
  • 在NWJS中重现的步骤:

npm install ivona-node

npm安装ivona-node

npm install browserify-fs

npm install browserify-fs

npm install -g browserify

npm install -g browserify

  • Now I browserified main.js for ivona-node and index.js for browserify-fs:
  • 现在我浏览了用于ivona-node的main.js和用于browserify-fs的index.js:

browserify main.js > ivonabundle.js

browserify main.js> ivonabundle.js

browserify index.js > fsbundle.js

browserify index.js> fsbundle.js


package.json...

{
  "name": "appname",
  "description": "appdescr",
  "title": "apptitle",
  "main": "index.html",
  "window":
  {
    "toolbar": true,
    "resizable": false,
    "width": 800,
    "height": 500
  },
  "webkit":
  {
    "plugin": true
  }
}

index.html...

<html>
<head>
    <title>apptitle</title>
</head>
<body>

<p><output id="result"></output></p>
<button onclick="startWorker()">Start Worker</button>
<button onclick="stopWorker()">Stop Worker</button>
<br><br>

<script>
    var w;

    function startWorker() {
        if(typeof(Worker) !== "undefined") {
            if(typeof(w) == "undefined") {
                w = new Worker("TTMP3.worker.js");
                w.postMessage(['This is some text to speak.']);
            }
            w.onmessage = function(event) {
                document.getElementById("result").innerHTML = event.data;
            };
        } else {
            document.getElementById("result").innerHTML = "Sorry! No Web Worker support.";
        }
    }

    function stopWorker() {
        w.terminate();
        w = undefined;
    }
</script>
</body>
</html>

TTMP3.worker.js...

importScripts('node_modules/browserify-fs/fsbundle.js','node_modules/ivona-node/src/ivonabundle.js');
onmessage = function T2MP3(Text2Speak)
{
postMessage(Text2Speak.data[0]);
//var fs = require('fs'),

//    Ivona = require('ivona-node');

var ivona = new Ivona({
    accessKey: 'xxxxxxxxxxx',
    secretKey: 'xxxxxxxxxxx'
});

//ivona.listVoices()
//.on('end', function(voices) {
//console.log(voices);
//});

//  ivona.createVoice(text, config)
//  [string] text - the text to be spoken
//  [object] config (optional) - override Ivona request via 'body' value
ivona.createVoice(Text2Speak.data[0], {
    body: {
        voice: {
            name: 'Salli',
            language: 'en-US',
            gender: 'Female'
        }
    }
}).pipe(fs.createWriteStream('text.mp3'));
postMessage("Done");
}

1 个解决方案

#1


3  

There are two things that I wan to point out first:

我要先指出两件事:

  1. Including node modules in a web worker
  2. 在Web worker中包含节点模块

In order to include the module ivona-node I had to change a little its code. When I try to browserify it I get an error: Uncaught Error: Cannot find module '/node_modules/ivona-node/src/proxy'. Checking the bundle.js generated I notice that it doesn't include the code of the module proxy which is in the file proxy.js in the src folder of ivona-node. I can load the proxy module changing this line HttpsPA = require(__dirname + '/proxy'); by this: HttpsPA = require('./proxy');. After that ivona-node can be loaded in the client side through browserify. Then I was facing another error when trying to follow the example. Turn out that this code:

为了包含ivona-node模块,我不得不改变它的代码。当我尝试浏览它时,我收到一个错误:未捕获错误:找不到模块'/ node_modules / ivona-node / src / proxy'。检查生成的bundle.js我注意到它不包含ivona-node的src文件夹中的文件proxy.js中的模块代理的代码。我可以加载代理模块更改此行HttpsPA = require(__ dirname +'/ proxy');由此:HttpsPA = require('./ proxy');.之后,可以通过browserify在客户端加载ivona节点。然后,当我尝试按照示例时,我遇到了另一个错误。原来这个代码:

ivona.createVoice(Text2Speak.data[0], {
    body: {
        voice: {
            name: 'Salli',
            language: 'en-US',
            gender: 'Female'
        }
    }
}).pipe(fs.createWriteStream('text.mp3'));

is no longer correct, it cause the error: Uncaught Error: Cannot pipe. Not readable. The problem here is in the module http. the module browserify has wrapped many built-in modules of npm, which mean that they are available when you use require() or use their functionality. http is one of them, but as you can reference here: strem-http, It tries to match node's api and behavior as closely as possible, but some features aren't available, since browsers don't give nearly as much control over requests. Very significant is the fact of the class http.ClientRequest, this class in nodejs environment create an OutgoingMessage that produce this statement Stream.call(this) allowing the use of the method pipe in the request, but in the browserify version when you call https.request the result is a Writable Stream, this is the call inside the ClientRequest: stream.Writable.call(self). So we have explicitly a WritableStream even with this method:

不再正确,它会导致错误:未捕获错误:无法管道。不可读。这里的问题是在模块http中。 browserify模块包含了许多npm的内置模块,这意味着当你使用require()或使用它们的功能时它们是可用的。 http就是其中之一,但正如你可以在这里引用的那样:strem-http,它试图尽可能地匹配节点的api和行为,但是某些功能不可用,因为浏览器对请求的控制几乎没有。非常重要的是http.ClientRequest类的事实,nodejs环境中的这个类创建一个OutgoingMessage来生成这个语句Stream.call(this)允许在请求中使用方法管道,但是当你调用https时在browserify版本中.request结果是一个Writable Stream,这是ClientRequest中的调用:stream.Writable.call(self)。因此,即使使用此方法,我们也明确地使用了WritableStream:

Writable.prototype.pipe = function() {
  this.emit('error', new Error('Cannot pipe. Not readable.'));
}; 

The responsible of the above error. Now we have to use a different approach to save the data from ivona-node, which leave me to the second issue.

负责以上错误。现在我们必须使用不同的方法来保存ivona-node的数据,这让我想到了第二个问题。

  1. Create a file from a web worker
  2. 从Web worker创建文件

Is well know that having access to the FileSystem from a web application have many security issues, so the problem is how we can have access to the FileSystem from the web worker. One first approach is using the HTML5 FileSystem API. This approach has the inconvenient that it operate in a sandbox, so if we have in a desktop app we want to have access to the OS FileSystem. To accomplish this goal we can pass the data from the web worker to the main thread where we can use all the nodejs FileSystem functionalities. Web worker provide a functionality called Transferable Objects, you can get more info here and here that we can use to pass the data received from the module ivona-node in the web worker to the main thread and then use require('fs') in the same way that node-webkit provide us. These are the step you can follow:

众所周知,从Web应用程序访问FileSystem有许多安全问题,因此问题是我们如何从Web worker访问FileSystem。第一种方法是使用HTML5 FileSystem API。这种方法在沙箱中操作很不方便,因此如果我们在桌面应用程序中有操作,我们希望能够访问操作系统文件系统。为了实现这一目标,我们可以将数据从Web worker传递到主线程,在那里我们可以使用所有nodejs FileSystem功能。 Web worker提供了一个名为Transferable Objects的功能,您可以在这里获得更多信息,我们可以使用这些信息将从web worker中的模块ivona-node接收的数据传递给主线程,然后使用require('fs')与node-webkit提供给我们的方式相同。这些是您可以遵循的步骤:

  1. install browserify

    npm install -g browserify
    
  2. 安装browserify npm install -g browserify

  3. install ivona-node

    npm install ivona-node --save
    
  4. 安装ivona-node npm install ivona-node --save

  5. go to node_modules/ivona-node/src/main.js and change this line:

    转到node_modules / ivona-node / src / main.js并更改此行:

    HttpsPA = require(__dirname + '/proxy');

    HttpsPA = require(__ dirname +'/ proxy');

    by this:

    HttpsPA = require('./proxy');

    HttpsPA = require('./ proxy');

  6. create your bundle.js.

    创建你的bundle.js。

    Here you have some alternatives, create a bundle.js to allow a require() or put some code in a file with some logic of what you want (you can actually include all the code of the web worker) and then create the bundle.js. In this example I will create the bundle.js only for have access to require() and use importScripts() in the web worker file

    在这里你有一些选择,创建一个bundle.js来允许require()或者将一些代码放在一个文件中,你需要一些逻辑(你可以实际包含web worker的所有代码),然后创建bundle。 JS。在这个例子中,我将创建bundle.js,仅用于访问require()并在web worker文件中使用importScripts()

    browserify -r ivona-node > ibundle.js

    browserify -r ivona-node> ibundle.js

  7. Put all together

    全部放在一起

    Modify the code of the web worker and index.html in order to receive the data in the web worker and send it to the main thread (in index.html)

    修改web worker和index.html的代码,以便在Web worker中接收数据并将其发送到主线程(在index.html中)

this is the code of web worker (MyWorker.js)

这是web worker的代码(MyWorker.js)

importScripts('ibundle.js');
var Ivona = require('ivona-node');

onmessage = function T2MP3(Text2Speak)
{
    var ivona = new Ivona({
        accessKey: 'xxxxxxxxxxxx',
        secretKey: 'xxxxxxxxxxxx'
    });

    var req = ivona.createVoice(Text2Speak.data[0], {
        body: {
            voice: {
                name: 'Salli',
                language: 'en-US',
                gender: 'Female'
            }
        }
    });

    req.on('data', function(chunk){
        var arr = new Uint8Array(chunk);
        postMessage({event: 'data', data: arr}, [arr.buffer]);
    });

    req.on('end', function(){
        postMessage(Text2Speak.data[0]);
    });

}

and index.html:

<html>
<head>
    <title>apptitle</title>
</head>
<body>

<p><output id="result"></output></p>
<button onclick="startWorker()">Start Worker</button>
<button onclick="stopWorker()">Stop Worker</button>
<br><br>

<script>
    var w;
    var fs = require('fs');

    function startWorker() {
        var writer = fs.createWriteStream('text.mp3');
        if(typeof(Worker) !== "undefined") {
            if(typeof(w) == "undefined") {
                w = new Worker("MyWorker.js");

                w.postMessage(['This is some text to speak.']);
            }
            w.onmessage = function(event) {
                var data = event.data;
                if(data.event !== undefined && data.event == 'data'){
                     var buffer = new Buffer(data.data);
                     writer.write(buffer);
                }
                else{
                    writer.end();
                    document.getElementById("result").innerHTML = data;
                }

            };
        } else {
            document.getElementById("result").innerHTML = "Sorry! No Web Worker support.";
        }
    }

    function stopWorker() {
        w.terminate();
        w = undefined;
    }
</script>
</body>
</html>

#1


3  

There are two things that I wan to point out first:

我要先指出两件事:

  1. Including node modules in a web worker
  2. 在Web worker中包含节点模块

In order to include the module ivona-node I had to change a little its code. When I try to browserify it I get an error: Uncaught Error: Cannot find module '/node_modules/ivona-node/src/proxy'. Checking the bundle.js generated I notice that it doesn't include the code of the module proxy which is in the file proxy.js in the src folder of ivona-node. I can load the proxy module changing this line HttpsPA = require(__dirname + '/proxy'); by this: HttpsPA = require('./proxy');. After that ivona-node can be loaded in the client side through browserify. Then I was facing another error when trying to follow the example. Turn out that this code:

为了包含ivona-node模块,我不得不改变它的代码。当我尝试浏览它时,我收到一个错误:未捕获错误:找不到模块'/ node_modules / ivona-node / src / proxy'。检查生成的bundle.js我注意到它不包含ivona-node的src文件夹中的文件proxy.js中的模块代理的代码。我可以加载代理模块更改此行HttpsPA = require(__ dirname +'/ proxy');由此:HttpsPA = require('./ proxy');.之后,可以通过browserify在客户端加载ivona节点。然后,当我尝试按照示例时,我遇到了另一个错误。原来这个代码:

ivona.createVoice(Text2Speak.data[0], {
    body: {
        voice: {
            name: 'Salli',
            language: 'en-US',
            gender: 'Female'
        }
    }
}).pipe(fs.createWriteStream('text.mp3'));

is no longer correct, it cause the error: Uncaught Error: Cannot pipe. Not readable. The problem here is in the module http. the module browserify has wrapped many built-in modules of npm, which mean that they are available when you use require() or use their functionality. http is one of them, but as you can reference here: strem-http, It tries to match node's api and behavior as closely as possible, but some features aren't available, since browsers don't give nearly as much control over requests. Very significant is the fact of the class http.ClientRequest, this class in nodejs environment create an OutgoingMessage that produce this statement Stream.call(this) allowing the use of the method pipe in the request, but in the browserify version when you call https.request the result is a Writable Stream, this is the call inside the ClientRequest: stream.Writable.call(self). So we have explicitly a WritableStream even with this method:

不再正确,它会导致错误:未捕获错误:无法管道。不可读。这里的问题是在模块http中。 browserify模块包含了许多npm的内置模块,这意味着当你使用require()或使用它们的功能时它们是可用的。 http就是其中之一,但正如你可以在这里引用的那样:strem-http,它试图尽可能地匹配节点的api和行为,但是某些功能不可用,因为浏览器对请求的控制几乎没有。非常重要的是http.ClientRequest类的事实,nodejs环境中的这个类创建一个OutgoingMessage来生成这个语句Stream.call(this)允许在请求中使用方法管道,但是当你调用https时在browserify版本中.request结果是一个Writable Stream,这是ClientRequest中的调用:stream.Writable.call(self)。因此,即使使用此方法,我们也明确地使用了WritableStream:

Writable.prototype.pipe = function() {
  this.emit('error', new Error('Cannot pipe. Not readable.'));
}; 

The responsible of the above error. Now we have to use a different approach to save the data from ivona-node, which leave me to the second issue.

负责以上错误。现在我们必须使用不同的方法来保存ivona-node的数据,这让我想到了第二个问题。

  1. Create a file from a web worker
  2. 从Web worker创建文件

Is well know that having access to the FileSystem from a web application have many security issues, so the problem is how we can have access to the FileSystem from the web worker. One first approach is using the HTML5 FileSystem API. This approach has the inconvenient that it operate in a sandbox, so if we have in a desktop app we want to have access to the OS FileSystem. To accomplish this goal we can pass the data from the web worker to the main thread where we can use all the nodejs FileSystem functionalities. Web worker provide a functionality called Transferable Objects, you can get more info here and here that we can use to pass the data received from the module ivona-node in the web worker to the main thread and then use require('fs') in the same way that node-webkit provide us. These are the step you can follow:

众所周知,从Web应用程序访问FileSystem有许多安全问题,因此问题是我们如何从Web worker访问FileSystem。第一种方法是使用HTML5 FileSystem API。这种方法在沙箱中操作很不方便,因此如果我们在桌面应用程序中有操作,我们希望能够访问操作系统文件系统。为了实现这一目标,我们可以将数据从Web worker传递到主线程,在那里我们可以使用所有nodejs FileSystem功能。 Web worker提供了一个名为Transferable Objects的功能,您可以在这里获得更多信息,我们可以使用这些信息将从web worker中的模块ivona-node接收的数据传递给主线程,然后使用require('fs')与node-webkit提供给我们的方式相同。这些是您可以遵循的步骤:

  1. install browserify

    npm install -g browserify
    
  2. 安装browserify npm install -g browserify

  3. install ivona-node

    npm install ivona-node --save
    
  4. 安装ivona-node npm install ivona-node --save

  5. go to node_modules/ivona-node/src/main.js and change this line:

    转到node_modules / ivona-node / src / main.js并更改此行:

    HttpsPA = require(__dirname + '/proxy');

    HttpsPA = require(__ dirname +'/ proxy');

    by this:

    HttpsPA = require('./proxy');

    HttpsPA = require('./ proxy');

  6. create your bundle.js.

    创建你的bundle.js。

    Here you have some alternatives, create a bundle.js to allow a require() or put some code in a file with some logic of what you want (you can actually include all the code of the web worker) and then create the bundle.js. In this example I will create the bundle.js only for have access to require() and use importScripts() in the web worker file

    在这里你有一些选择,创建一个bundle.js来允许require()或者将一些代码放在一个文件中,你需要一些逻辑(你可以实际包含web worker的所有代码),然后创建bundle。 JS。在这个例子中,我将创建bundle.js,仅用于访问require()并在web worker文件中使用importScripts()

    browserify -r ivona-node > ibundle.js

    browserify -r ivona-node> ibundle.js

  7. Put all together

    全部放在一起

    Modify the code of the web worker and index.html in order to receive the data in the web worker and send it to the main thread (in index.html)

    修改web worker和index.html的代码,以便在Web worker中接收数据并将其发送到主线程(在index.html中)

this is the code of web worker (MyWorker.js)

这是web worker的代码(MyWorker.js)

importScripts('ibundle.js');
var Ivona = require('ivona-node');

onmessage = function T2MP3(Text2Speak)
{
    var ivona = new Ivona({
        accessKey: 'xxxxxxxxxxxx',
        secretKey: 'xxxxxxxxxxxx'
    });

    var req = ivona.createVoice(Text2Speak.data[0], {
        body: {
            voice: {
                name: 'Salli',
                language: 'en-US',
                gender: 'Female'
            }
        }
    });

    req.on('data', function(chunk){
        var arr = new Uint8Array(chunk);
        postMessage({event: 'data', data: arr}, [arr.buffer]);
    });

    req.on('end', function(){
        postMessage(Text2Speak.data[0]);
    });

}

and index.html:

<html>
<head>
    <title>apptitle</title>
</head>
<body>

<p><output id="result"></output></p>
<button onclick="startWorker()">Start Worker</button>
<button onclick="stopWorker()">Stop Worker</button>
<br><br>

<script>
    var w;
    var fs = require('fs');

    function startWorker() {
        var writer = fs.createWriteStream('text.mp3');
        if(typeof(Worker) !== "undefined") {
            if(typeof(w) == "undefined") {
                w = new Worker("MyWorker.js");

                w.postMessage(['This is some text to speak.']);
            }
            w.onmessage = function(event) {
                var data = event.data;
                if(data.event !== undefined && data.event == 'data'){
                     var buffer = new Buffer(data.data);
                     writer.write(buffer);
                }
                else{
                    writer.end();
                    document.getElementById("result").innerHTML = data;
                }

            };
        } else {
            document.getElementById("result").innerHTML = "Sorry! No Web Worker support.";
        }
    }

    function stopWorker() {
        w.terminate();
        w = undefined;
    }
</script>
</body>
</html>