最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to create typescript library with webworkers using worker-loader - Stack Overflow

programmeradmin8浏览0评论

I try to create typescript library with web workers. When I test my code with webpack-dev-server everything looks good, all files are found, but when I make npm run build and try to use lib in another local project (npm install /local/path), I see GET http://localhost:8080/X.worker.js in browser console.

webpack.config.js:

const path = require('path');

module.exports = {
    devtool: 'inline-source-map',
    entry: {
        'mylib': './src/index.ts',
        'mylib.min': './src/index.ts',
    },
    output: {
        path: path.resolve(__dirname, '_bundles'),
        filename: '[name].js',
        libraryTarget: 'umd',
        library: 'mylib',
        umdNamedDefine: true
    },
    resolve: {
        extensions: ['.ts', '.tsx', '.js']
    },
    optimization: {
        minimize: true
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                loader: 'awesome-typescript-loader',
                exclude: /node_modules/,
                query: {
                    declaration: false,
                }
            },
            {
                test: /\.worker\.js$/,
                use: {
                    loader: "worker-loader"
                }
            },
        ]
    }
};

tsconfig.json

{
    "pilerOptions": {
        "target": "es5",
        "module": "es6",
        "lib": [
            "webworker",
            "es2015",
            "dom"
        ],
        "moduleResolution": "node",
        "sourceMap": true,
        "strict": true,
        "alwaysStrict": true,
        "outDir": "lib",
        "resolveJsonModule": true,
        "declaration": true,
        "skipLibCheck": true,
        "allowJs": true
    },
    "include": [
        "**/*.ts",
        "**/*.tsx"
    ],
    "exclude": [
        "node_modules",
        "lib",
    ]
}

package.json

{
  "name": "mylib",
  "version": "1.0.0",
  "description": "",
  "main": "_bundles/mylib.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "webpack": "webpack",
    "build": "rm -rf ./lib && tsc",
    "serve": "webpack-dev-server",
    "clean": "rm -rf _bundles lib lib-esm",
    "newbuild": "npm run clean && tsc && tsc -m es6 --outDir lib-esm && webpack"
  },
  "repository": {
    "type": "git",
    "url": "..."
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "..."
  },
  "homepage": "...e",
  "devDependencies": {
    "prettier": "^2.1.2",
    "tslint": "^6.1.3",
    "tslint-config-prettier": "^1.18.0",
    "worker-loader": "^3.0.5"
  },
  "dependencies": {
     ...
  }
}

Example on how I import workers:

import X from "worker-loader!./X";

I try to create typescript library with web workers. When I test my code with webpack-dev-server everything looks good, all files are found, but when I make npm run build and try to use lib in another local project (npm install /local/path), I see GET http://localhost:8080/X.worker.js in browser console.

webpack.config.js:

const path = require('path');

module.exports = {
    devtool: 'inline-source-map',
    entry: {
        'mylib': './src/index.ts',
        'mylib.min': './src/index.ts',
    },
    output: {
        path: path.resolve(__dirname, '_bundles'),
        filename: '[name].js',
        libraryTarget: 'umd',
        library: 'mylib',
        umdNamedDefine: true
    },
    resolve: {
        extensions: ['.ts', '.tsx', '.js']
    },
    optimization: {
        minimize: true
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                loader: 'awesome-typescript-loader',
                exclude: /node_modules/,
                query: {
                    declaration: false,
                }
            },
            {
                test: /\.worker\.js$/,
                use: {
                    loader: "worker-loader"
                }
            },
        ]
    }
};

tsconfig.json

{
    "pilerOptions": {
        "target": "es5",
        "module": "es6",
        "lib": [
            "webworker",
            "es2015",
            "dom"
        ],
        "moduleResolution": "node",
        "sourceMap": true,
        "strict": true,
        "alwaysStrict": true,
        "outDir": "lib",
        "resolveJsonModule": true,
        "declaration": true,
        "skipLibCheck": true,
        "allowJs": true
    },
    "include": [
        "**/*.ts",
        "**/*.tsx"
    ],
    "exclude": [
        "node_modules",
        "lib",
    ]
}

package.json

{
  "name": "mylib",
  "version": "1.0.0",
  "description": "",
  "main": "_bundles/mylib.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "webpack": "webpack",
    "build": "rm -rf ./lib && tsc",
    "serve": "webpack-dev-server",
    "clean": "rm -rf _bundles lib lib-esm",
    "newbuild": "npm run clean && tsc && tsc -m es6 --outDir lib-esm && webpack"
  },
  "repository": {
    "type": "git",
    "url": "..."
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "..."
  },
  "homepage": "...e",
  "devDependencies": {
    "prettier": "^2.1.2",
    "tslint": "^6.1.3",
    "tslint-config-prettier": "^1.18.0",
    "worker-loader": "^3.0.5"
  },
  "dependencies": {
     ...
  }
}

Example on how I import workers:

import X from "worker-loader!./X";
Share Improve this question asked Dec 12, 2020 at 20:05 wwilkowskiwwilkowski 3615 silver badges15 bronze badges 1
  • I think there may be a better way than what I had initially answered. I edited my answer - check it out. – Seth Lutske Commented Dec 14, 2020 at 15:47
Add a ment  | 

2 Answers 2

Reset to default 5

I found myself in the exact same situation a few months back. I found a solution that worked for me, but first lets discuss why this is happening.

The problem:

There are 3 layers here.

  1. The development layer of your library
  2. The build layer of your library
  3. The application that consumes the build of your library

Layer 1 is simple. In whatever file you want to create a new worker, say its index.ts, you do your import X from "worker-loader!./X". Your index.ts knows exactly where to find your worker file. That's why things work on your webpack-dev-server.

Layer 2 is where things get weird. When you process the worker file with worker-loader, webpack outputs several files. Your config says filename: '[name].js', which would output a file for every file in your source folder, all on the same level in your _bundles folder. Webpack sees your import X from "worker-loader!./X", and it also sees your target name and location for the imported file, and the file doing the importing. It rewrites the location of the web worker file within the index.js output bundle to an absolute path relative to the rest of the bundle. You can control this more carefully by using the publicPath option in the worker-loader. But this doesn't really solve the issue, as you are only setting the publicPath as an absolute path, which leads us to step 3...

Layer 3, where you try to consume your package, is where things go wrong. You could never anticipate where one might try to import { makeAWorker } from 'your-library' in their code. Regardless of where they import it, the build file (in the consumer app's node_modules) will be using the path that webpack wrote into the build of index.js to look for the worker file, but now the absolute path is relative to your consumer project (usually the home path, like where index.html lives), not to the node_modules folder of the build. So your consumer app has no idea where to find the worker file.

My solution: a bit of a hack

In my scenario, I decided that the content of my worker files was simple enough to create a worker from a string, and import it that way. For example, a worker file looked like this:

// worker.ts

// Build a worker from an anonymous function body
export default URL.createObjectURL(
  new Blob([
    '(',
    function () {

      // actual worker content here

    }.toString(),
    ')()', ],
    { type: 'application/javascript' }
  )
);

Then in the place where I want to spawn the worker:

// index.ts

import workerScript from './worker';

const myWorker = new Worker(workerScript, {worker_options});

This works because now you are no longer asking webpack to create a file and write the correct import locations for you. In the end, you won't even have separate files in your bundle for your worker scripts. You can ditch worker-loader altogether. Your index.ts will create the Blob and its URL, and your consumer application will find the worker script at that URL which is dynamically generated at runtime.

A hack indeed

This method es with some serious drawbacks, which led me to ask the question bundle web workers as integral part of npm package with single file webpack output. The issue is that inside your worker script, you really don't have the option to import anything. I was lucky in that my worker was relatively simple. It depended on a single node_module, which itself had no dependencies. I started out by simply including the source of that module in the script, but as I had multiple scripts that needed that same external module, I ended up passing it as a piece of data to the worker when it gets spawned:

import { Rainbow } from './utils';

var data = {
  id,
  data              
  RainbowAsString: Rainbow.toString(),
};

myWorker.postMessage(data);

Then within the worker I simply convert RainbowAsString back to a function and use it. If you are curious to see more detail, you can check out the library I built with this method: leaflet-topography. Look into the src/TopoLayer.ts file to see how the workers are used, and the src//workers folder to see how I set up the worker blobs.

Conclusion

I think there must be a better way. One possible quick fix would be to write a copy script that would copy the worker files from node_modules/yourLibrary to the build folder of your consumer app. But this doesn't make for great portability, and other peple are going to have to do the same thing to get your library working with their app. My solution isn't perfect, but it works for simple-ish worker scripts. I am still thinking about a more robust solution that allows workers to do their own imports.

Edit: A proper solution:

So after writing this answer, I was inspired to join the conversation How can I use this to bundle a library? in the webpack-loader repo. Apparently, when webpack 5 was released about 2 months ago, they added support for this syntax:

new Worker(new URL('./path/to/worker.js', import.meta.url))

I have not tried this, but it looks like the solution you need (and the solution I needed 2 months ago when I was ing up with my hack). Try this out - it may be exactly what you need to tell webpack to bundle your library while stil maintaining the relationship between worker script and the script that imports it.

Finally got this to work with a small hack. The 3 layers explanation that Seth provided is accurate.

There are different hacks to get the worker to load in the project that installs the library, but the simplest and cleanest approach is to ask the developer to add an entry in the webpack.config.js or rspack.config.js file. For some reason, webpack hangs when I tried it but rspack works perfectly.

Here's what I have in the rspack.config.js file.

Before:

entry: "./src/index"

After:

entry: {
  "index": "./src/index",
  "workers/MathWorker": "./node_modules/@wvary/test-babel/workers/MathWorker.js"
}

Needless to say, my library is @wvary/test-babel and the worker is MathWorker.js that lives in the workers directory.

Note, this approach does not need worker-loader at all.

Here's the entry in the library project:

entry: {
  "index": "./src/index",
  "workers/MathWorker": "./src/mineral/workers/MathWorker.js"
}

The same ponent that loads the worker runs in both environments fine.

I'm happy to provide a github sample if someone wants one.

发布评论

评论列表(0)

  1. 暂无评论