Automatic Vue Component Registration with Vite

Developing a large web application is a constant struggle to not keep writing the same things over and and over again. Part of this can be the need to continually import your components within a Vue application.

While there are some great tools to help automatically register all of your components globally (like unplugin-vue-components), the downside is that you can’t register them using async components. This means that every single component has to be loaded before your app can start, even ones which aren’t on the page being accessed.

For a public-facing website like southcoastweb this was a non-starter, so we had to come up with a better way.

Written By

Craig Riley


Craig is a full-stack developer who mainly deals in JavaScript and PHP. Frameworks include VueJS Laravel, Wordpress, Nuxt and Quasar. He also likes sysadmin stuff until it goes horribly wrong.

Vite to the rescue

Fortunately, Vite comes with an extremely handy function called import.meta.glob (more information here) which allows you to prepare entire directories for importing either synchronously or asynchronously. Simply feed it a glob and Vite will prepare an array of import functions you can call on at any time. Pair that with a Vue app plugin and you have yourself an automatically-registered component library without any need for dependencies (well, maybe a few lodash utilities).

Creating a Vue 3 app plugin

Plugins for Vue 3 are tremendously easy to create. You’ve probably used a fair few. They’re the things that you see in your entry file with app.use(pinia) which you import from a package. So let’s create a new file in a plugins directory in our app and call it component-library.js. In that file, paste this:

export default {
install: (app, options) => {
// code will go here shortly
}
}

When you import this from your entry file and pass it to app.use(), the install function will execute with your app instance as the first argument (app) and any options passed to the the use function will come through as the options argument. With that app argument, we can register components globally via calling app.component().

Namespacing our components

Registering components with just their filename would be fine for a small app, but what about if we’ve got loads of nested directories with pretty generic filenames like List.vue or Button.vue? It’d quickly become a tangled nightmare of ExtremelyComplicatedAndVerboseFilenamesInAList.vue. Fortunately, we can do the following:

1export const resolveName = (component) => {
2 let name = component.match(/(?:^.*?(?:a?sync|web)\/)(.*?)(?:\.vue$)/i);
3 if (!name || !name[1]) return null;
4 name = name[1]
5 .replace(/(^[a-z]|\/[a-z])/g, (value) => {
6 return value.toUpperCase();
7 })
8 .replaceAll("/", "");
9
10 return name;
11};

We’ll call this function on each component in our glob. The regex might look pretty scary, but all it’s doing is taking a file path like ../../components/sync/path/to/your/component.vue and extracting out path/to/your/component for us to use. Next we:

  1. Check if the glob has given us a valid component
  2. Replace the first letter of the path with a capital letter
  3. Replace any letters after a / with a capital letter
  4. Remove all slashes

The end result will hopefully be PathToYourComponent, which we can use as the component’s name when registering it. So let’s go back to our Vue plugin and implement our new function.

Registering synchronous components

1export default {
2 install: (app, options) => {
3 const components = import.meta.glob("../../components/sync/**/*.vue", { eager: true });
4 for (const component in components) {
5 const name = resolveName(component);
6 if (!name) continue;
7
8 app.component(`Scw${name}`, components[component].default);
9 }
10});

Using our import.meta.glob function, we fetch every .vue file in our components/sync directory. Note that the glob path is relative to the directory of this plugin file.

Next we loop through the components and get a component name for them.

Finally, we’ll register them with our app. We chose to prepend Scw onto the start of all of our components so we know where they’re coming from. Now we can use it anywhere in our app with:

<template>
<ScwPathToOurComponent />
</template>

So far, we’ve just replicated what unplugin-vue-components could have done. But now let’s try to register asynchronous components. These are the ones that will only load when they’re required from the template.

Registering asynchronous components

Thanks to Vue’s defineAsyncComponent function, this step is pretty easy. We only really need to change two things from our previous code.

1export default {
2 install: (app, options) => {
3 const components = import.meta.glob("../../components/async/**/*.vue", { eager: false });
4 for (const component in components) {
5 const name = resolveName(component);
6 if (!name) continue;
7
8 app.component(`Scw${name}`, defineAsyncComponent(components[component]));
9 }
10});

Now we’re globbing the async directory inside components and have changed eager to false. This means that Vite will create a separate .js file for each component and only load it when the function is called.

Secondly, instead of passing the function straight to app.component(), we’re going to pass it via defineAsyncComponent instead. Your component is now turned into a Promise that calls a script loader and returns your component only when needed.

Full code

1import { defineAsyncComponent } from "vue";
2
3export const resolveName = (component) => {
4 let name = component.match(/(?:^.*?(?:a?sync|web)\/)(.*?)(?:\.vue$)/i);
5 if (!name || !name[1]) return null;
6 name = name[1]
7 .replace(/(^[a-z]|\/[a-z])/g, (value) => {
8 return value.toUpperCase();
9 })
10 .replaceAll("/", "");
11
12 return name;
13};
14
15export default {
16 install: (app, options) => {
17 // Sync
18 const syncComponents = import.meta.glob("../../components/sync/**/*.vue", { eager: true });
19 for (const component in syncComponents) {
20 const name = resolveName(component);
21 if (!name) continue;
22
23 app.component(`Scw${name}`, components[component].default);
24 }
25
26 // Async
27 const asyncComponents = import.meta.glob("../../components/async/**/*.vue", { eager: false });
28 for (const component in asyncComponents) {
29 const name = resolveName(component);
30 if (!name) continue;
31
32 app.component(`Scw${name}`, defineAsyncComponent(components[component]));
33 }
34});

Where next?

There are quite a few other opportunities we could pursue here. One we later implemented was to have our asynchronous components also register a loadingComponent which can be shown while the main async component is being downloaded and initialised.

Another option is to also register a directory of web components as well using Vue’s defineCustomElement function.

What you won’t need to do is write endless component imports inside every single page and component of your app.

More Tutorials

Here are some more tutorials that we think might be helpful for you. Feel free to contact us if there's anything you might need

Passing through slots in Vue

If you're keeping your Vue component sizes small, there's a good chance you'll need to implement a wrapper component at some point. If you're only utilising the default slot for these, it can be as simple as putting a &lt;slot /&gt; tag inside your wrapper component. However, there's a bit more effort involved if you need to pass through named slots to your base component

Composable Design Patterns in Vue 3

Back in the days of Vue 2, the mixin was king. Had some code you wanted to reuse in separate components? Create a mixin, import and register it in the component and its provided props, data and methods would magically be available. The problem with mixins was that magic. You'd often end up calling methods or referring to props that you had no way of knowing existed without visiting the mixin file and double-checking what it provided. If you actually wanted unit testing or type checking for that component, you faced an even tougher battle. Enter: the composable.

Using Vue's class function in your own composables

Inside your &lt;template&gt;, you may be used to stacking lots of conditionally applied classes onto elements. You can add dynamic classes (e.g. .btn--${props.type}), arrays of conditional if/else classes (e.g. props.error ? 'has-error' : 'is-valid') or even whole objects of conditionals (e.g. { 'is-disabled': props.disabled }. But what do we do if we want to shift that logic to our &lt;script&gt; block? While undocumented (and thus not a guaranteed API, so tread carefully), Vue internally uses a pretty simple function to make all this magic happen, and you can import it too.

Automating your service and repository patterns in Laravel with command generators

"Why spend 20 seconds doing something when you can spent 4 hours automating it," goes the proverb. See any professional proverbists around any more? No, because they've all been automated. Also it's fun. Whatever your programming pattern in Laravel, chances are that the php artisan make:x command is going to leave you high and dry on occasion. That's why it can be useful to have your own commands available to cover those gaps. We're going to go with the example of the "Service" pattern below.

How to Create a Simple Button Component in Figma

In this tutorial, we’ll create a simple button using Figma’s built-in component system.

Copyright 2007 - 2024 southcoastweb is a brand of DSM Design.