Migrating Web Components from Vue 2 to Vue 3 at Open Library

Ray BergerRay Berger
Jan 20, 2025·
7 min read

Update Feb, 2025: Vue Migration Part 2 - A Simple Solution Emerges

TL;DR: Migrating Vue 2 Web Components to Vue 3 isn't as straightforward as the docs suggest. We tackled three main challenges: setting up Vite builds, managing multiple components, and handling plugins.

You can see the final pull request here.

This is the story of how a seemingly simple migration turned into a 20-hour adventure through the world of Vite and Vue 3. If you're considering a similar migration or just curious about the state of Vue 3 Web Components, this is for you.

Understanding Vue and Web Components

In contrast to typical Single Page Applications (SPAs), where Vue renders the entire application, Open Library leverages Vue for specific interactive elements within traditional HTML pages. This is accomplished by integrating custom HTML tags, or Web Components, which are then transformed into Vue components.

For example, when you look at the source of an Open Library page, you might see an <ol-barcode-scanner> tag. This custom tag, along with its JavaScript, allows Vue to render just that specific element rather than managing the entire page. It's like having mini Vue apps scattered throughout traditional HTML pages.

Vue 3 Migration Challenges

While Vue 3 is largely compatible with Vue 2, and there are many resources available for migrating from Vue 2 to Vue 3, our challenge lay in building web components in Vue 3. The Vue 3 documentation covers Web Components, but the approach was not directly applicable to our use case, which involves building one component per page with isolated scripts.

Vue 2 Build Setup

The process of building Web Components for Vue 2 was very straightforward. We would simply run vue-cli-service build BarcodeScanner.vue, and a JS file would be generated that we could use on any page.

Challenges with Vue 3 Web Components

Let me break down our journey into three main challenges. While each challenge has a straightforward solution in hindsight, finding these solutions took considerable exploration.

Building one Web Component with Vite

The deprecated Vue CLI we used to build Vue 2 Web Components does not support Vue 3 Web Components and has not been updated in years. As a result, I investigated the latest and greatest build tool: Vite. There's one big difference with Vite: it doesn't accept .vue files (Single File Components) like Vue CLI did. Instead, Vite requires a configuration file as its input.

This posed a challenge because I was 1) disbelieving that it couldn't take .vue files and instead required two new files for every component, 2) trying to get it to create all the components without so many new files, and 3) attempting to get a Vue plugin working (more on that later). That being said, the docs are fairly straightforward on how to do this for just one component. I just needed two new files:

// vite.config.js
export default defineConfig({
    plugins: [vue({ customElement: true })], // Because all of our Vue components are customElements
    build: {
        outDir: 'PRODUCTION_DIR',
        emptyOutDir: false, // Preserve existing files since we build components individually
        target: 'es2015', // The oldest browsers Vite supports out of the box
        rollupOptions: {
            input: join(BUILD_DIR, `BarcodeScanner.js`),
            output: {
                entryFileNames: `ol-barcode-scanner.js`,
                inlineDynamicImports: true, // needed for components to work with just one js file
                format: 'iife' // use iife to support old browsers without type="module"
            },
        },
    },
});
// BarcodeScanner.js
import { defineCustomElement } from 'vue';
import ele from './BarcodeScanner.vue';
customElements.define('ol-barcode-scanner', defineCustomElement(ele));

Building multiple Web Components

At this point, we have a Vite config file that can only have one input, which is a JavaScript file, and it produces a single output, the desired JavaScript file. This limitation poses a challenge since we have numerous components to build. While Vite config files support multiple input and output setups, this functionality is not available when using the inlineDynamicImports option, which we need for the components to work in isolation.

The problem we face is that we need a single configuration file to be mapped to one JavaScript file, which in turn is mapped to one Vue file. This setup seems overly complicated. Three files now for each component instead of just one .vue file. The two new files are basically identical for each component. There has to be a way around this, right?

The left half is the old Vue CLI setup, the right half is the new vite setup.

Trial and Error

Through reading docs, trial and error, and ChatGPT, we painfully accepted that we indeed need a Vue file, a JavaScript file that points to the Vue file, and a config file that points to the JavaScript file (please someone come and show me how I'm wrong). If we were to approach this in a traditional manner, we would require a Vue file, a JavaScript file, and a Vite config file for each component. Currently, we have five components, which means we would end up adding ten new files that are essentially identical. There has to be a way around this, right?

Solution

It seems too hacky for some of the best webdev tools (Vue and Vite), but I found a solution that actually worked without adding 10 new files to the codebase. It comes in two parts.

First, set an environment variable COMPONENT_NAME that vite.config.js reads so that we don't need one config file for every component:

const COMPONENT_NAME = process.env.COMPONENT;
export default defineConfig({
    build: {
        /// shorted to show the change
        rollupOptions: {
            input: join(BUILD_DIR, `vue-tmp-${COMPONENT_NAME}.js`),
            output: { entryFileNames: `ol-${COMPONENT_NAME}.js`, },
        },
    },
});

Second, we need to deal with these pesky input files. So we just generate them from a string... in the config file itself.


generateViteEntryFile(COMPONENT_NAME);

export default defineConfig({...})

function generateViteEntryFile(componentName) {
    const template = `
import { defineCustomElement } from 'vue';
import ele from './${componentName}.vue';
customElements.define('${kebabCase(componentName)}', defineCustomElement(ele));
`;

    try {
        writeFileSync(join(BUILD_DIR, `vue-tmp-${componentName}.js`), template);
    } catch (error) {
        // eslint-disable-next-line no-console
        console.error(`Failed to generate Vite entry file: ${error.message}`);
        process.exit(1);
    }
}

All things considered, this solution is simple and keeps us from having many nearly identical files to keep in sync. That being said, I really can't believe these hacks (environment variables and generated input files) are the best way.

Plugin Support

Everything was functioning smoothly for all of our components, except for one that utilizes a Vue plugin: vue-async-computed. We previously considered removing this plugin since it didn't support Vue 3, but we ultimately decided against it because it helps maintain a clean structure in our code. Drini actually made the PR to add Vue 3 support years ago. Given the lack of updates to that plugin, maybe it's worth reconsidering switching to newer alternatives like computedAsync, but dang it, I'm on a mission to upgrade to Vue 3. Besides, Drini made a good point that there are nice plugins we should consider using after the upgrade.

I explored various approaches to integrate the plugin into our Vue component. The guides I found often went into Vue's internals, particularly regarding how to incorporate the plugin for the component. Our team does not consist of Vue experts; we simply want a little more interactivity on parts of the site.

Ultimately, I decided to use the vue-web-component-wrapper library. To be fair, I saw it earlier, but resisted adding another dependency for something that should be easy. Anyway, the library simplifies the process by allowing us to pass in the desired plugin along with our component. Short and simple, and I even added a conditional so the plugin is only added for the one component that needs it.

Summary

I faced many challenges while migrating from Vue 2 Web Components to Vue 3 Web Components. However, they don't seem too hard now that we have solutions.

I am certain that there are technical reasons for the difficulties encountered during this migration. However, if I had a magic wand, here are the changes I would like:

  1. Vite (rollup under the hood) should allow for multiple inputs and outputs while utilizing inlineDynamicImports. This feature alone would have greatly simplified the entire process. If this cannot be implemented, then the Web Components guide should be updated to include a reasonable workaround. Related issue here.

  2. The Web Component guide should be updated to include information about plugins. It would be helpful to explain how to add them without introducing another dependency, or at the very least, to point to vue-web-component-wrapper as a viable solution. Issue opened here.

  3. Vite should be able to accept .vue files in the vite.config.js.

Overall, this migration process required approximately 20 hours of my effort (not counting the attempts by other folks in previous years). Throughout this process, I did not find a single example that addressed all of these challenges. I hope that this account will be helpful to those poor souls who find themselves migrating from Vue 2 to Vue 3 in 2025 and beyond. Perhaps the Vue and Vite teams will take note of this and improve the official guide and possibly even the tooling. Happy open sourcing!

Found this helpful? Consider joining our Open Library community as a dev, librarian, designer, or any other way you want to help.

PS: Huge thanks to Drini for reviewing and merging this pull request!

12
Subscribe to my newsletter

Read articles from Ray Berger directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ray Berger
Ray Berger

MSc Candidate in Urban Studies, Software Engineer