Michael Antipin
Michael Antipin

Reputation: 3532

How to split code into several bundles with Vue CLI3

I have a Vue project using TypeScript and built by Vue-CLI3.

What I'm trying to achieve is to get Webpack to build separate bundles for my workers. I've read about Webpack Code Splitting and about configureWebpack in vue.config.js, but so far had no luck in putting them together.

The project setup is the standard vue create type. I have a ./src/main.ts as the main entry point and a bunch of TypeScript modules, I want as separate bundles with their own dependency trees (I'm fine with code duplication if it can't be avoided).

I'd like to get

./dist/js/all main stuff
./dist/js/workers/worker1.6e3ebec8.js
./dist/js/workers/worker2.712f2df5.js
./dist/js/workers/worker3.83041b4b.js

So I could do new Worker(worker1.6e3ebec8.js) in the main code.

I could launch workers from the main package by generating javascript code, putting it into a blob and instantiating from that, but it looks rather awkward. Besides, my worker code import other modules, so it doesn't seem to be an option anyway.

I'm quite new to all of this, so maybe I'm not even heading in the right direction.

What is the usual way of doing that on this stack?

Upvotes: 0

Views: 1512

Answers (3)

Michael Antipin
Michael Antipin

Reputation: 3532

Now, to the correct answer.

To achieve the following:

  • Using webworkers
  • Using both dynamic imports and normal imports in webworker code
  • Sharing code between webworkers and main app

I had to add a separate rule for worker-loader in vue.config.js and also to add babel-loader. It took me some time to find the correct solution, but I dropped the previous one (in my other answer) in the end. I still use separate tsconfig.js for main and for webworkers.

What I'm still not happy with, is that vue-cli–or rather fork-ts-checker plugin–doesn't seem to know the webworker-specific types in my worker classes (so I can't use DedicatedWorkerScope, for instance).

Upvotes: 0

Michael Antipin
Michael Antipin

Reputation: 3532

Got there! @aquilesb's answer did help, although I've failed to get getModuleDynamically() from the answer working after plenty of experimenting.

Update: Looks like with this solution I'm not able to use imports of npm modules. I've tried experimenting with worker-loader but haven't got anywhere so far.

Here are a few takeaways:

  1. Create a separate webpack config for packing workers. The target: 'webworker' must be there. Call it with webpack --config ./webpack.config.workers.js, as Vue wouldn't know about that.
  2. Create a separate tsconfig.json for workers TypeScript. The lib setting for workers must be there: "lib": ["esnext","webworker","scripthost"] as well as the proper include:[...]/exclude:[...] settings.
  3. You may need to tell Vue to use the main tsconfig.json that has it's own "lib":["esnext","dom","dom.iterable","scripthost"] and include/exclude. This is done in vue.config.js, you will probably need to create it. I use chainWebpack configuration option of Vue config.
  4. Let Webpack know you have dynamic loading by making calls to import() with static (i.e. not variable) names. I haven't found a way to do so in config file, but it doesn't matter: you can't help hardcoding the names somewhere, how else Webpack would know it has to bundle and split the code?
  5. Somehow get the name(s) of generated files, as you must have at least one of them at runtime to do new Worker(filename). I used the --json option of Webpack CLI for that.

There are many ways all of this can be achieved. This is what this ended up looking like in my project:

  • Folder structure:

    webpack.config.workers.js
    vue.config.js
    tsconfig.base.json
    
    src/main/
    src/main/tsconfig.json -- extends tsconfig.base.json
    
    src/shared/ -- this code may be duplicated by the Vue app bundles and by workers bundle
    
    src/workers/
    src/workers/tsconfig.json -- extends tsconfig.base.json
    
  • webpack.config.workers.js: contains a single entry – the main worker file, that loads the other stuff.

    entry: {
        worker: './src/workers/worker.ts'
    } 
    
  • build.workers.sh: the script calls Webpack CLI and produces a JSON file with the resulting workers names (trivial actions on folders are omitted). The only one I need is called "worker". The rest is to be dynamically loaded by it.

    #!/bin/bash
    
    # Map entry name -> bundle file name
    # "assetsByChunkName":{"entryN":"entryN.[hash].js", ...}
    json=$(webpack --config ./webpack.config.workers.js --json $@|tr -d "\n\r\t "|grep -Eo '"assetsByChunkName":.+?}')
    
    # Remove "assetsByChunkName"
    json=$(echo "${json:20}")
    
    echo $json
    echo $json > "$target/$folder/workers.json"
    
  • Load workers.json at runtime. The other option would be to use it at compile time by providing Vue config with const VUE_APP_MAIN_WORKER = require("path to/workers.json").worker and using this env constant.

  • Now that we have the name of the main worker file, we can do new Worker("main worker file path we've got from webpack").

  • The main worker file contains the function that statically references other modules and dynamically loads them. This way Webpack knows what to bundle and how to split the code.

    enum WorkerName {
        sodium = "sodium",
        socket = "socket"
    }
    
    function importModule(name: WorkerName): Promise<any> {
        switch (name) {
            case WorkerName.sodium:
                return import(
                    /* webpackChunkName: "sodium" */
                    "workers/sodium"
                );
            case WorkerName.socket:
                return import(
                    /* webpackChunkName: "socket" */
                    "workers/socket"
                );
        }
    }
    
  • Use the postMessage/message event API to tell your main worker code what to load.

    const messageHandler = (e: MessageEvent) => {
    
        // here goes app-specific implementation of events
        // that gets you the moduleName in the end
    
        importModule(moduleName).then((work) => {
            // do smth
        });
    };
    

Upvotes: 0

aquilesb
aquilesb

Reputation: 2272

You can use import(), it will return a Promise and will resolve your module. As you are using Vue-CLI 3, webpack is ready and it should split your bundle automatically.

const moduleName = 'coolModuleName'
import (
  /* webpackChunkName: "[moduleName]" */
  `@/my/module/path/${moduleName}.js`
).then(moduleCode => {
  // use your module
})


// load them in parallel
const getModuleDynamically(path, moduleName) => import(
  /* webpackChunkName: "[moduleName]" */
  `@/${path}/${moduleName}.js`
)

Promise.all([
  getModuleDynamically(path, moduleName1),
  getModuleDynamically(path, moduleName2),
  getModuleDynamically(path, moduleName3)
])

Upvotes: 2

Related Questions