richardhulbert's avatar

Vue defineAsyncComponent in build

Hi I have a single page (Intertiajs) application that uses defineAsyncComponent to load the appropriate Vue components for each page.

<script setup>
import {useBlock} from "@/blocks/composables/useBlock";
import {inject} from "vue";


const props=defineProps({
  template:String,
  blocks:Array,
  template_id:{type:Number,default:-1}
})


   function loadComponent(component) {
        return defineAsyncComponent({
            loader: () => import( /* @vite-ignore */component),
            loadingComponent:LoadingComponent,
            delay: 200,
            errorComponent: MissingComponent,
            // The error component will be displayed if a timeout is
            // provided and exceeded. Default: Infinity.
            timeout: 3000
        })
    }


</script>
<template>
  <div class="flex">
  <div id="editor-sidebar" class="bg-primary-100 min-w-fit relative dark:bg-primary-900 dark:text-primary-100">
  </div>
  <main id="main" class="relative w-full min-h-screen">
    <div>
      <component :is="loadComponent(props.template)" :blocks="props.blocks" :layout_id="props.template_id">
        <slot/>
      </component>
    </div>

  </main>
    <div id="revisions-sidebar" class="min-w-fit">
    </div>
  </div>
</template>

props.template is a string '/resources/js/Templates/dynamic/TimerLayout.vue'

When I run vite dev Everything works. However when I build the project with vite build and stop Vite's dev server then of course I get a 404 for '/resources/js/Templates/dynamic/TimerLayout.vue' and even if I symlink the resources folder into the public server it still won't work as the .vue file is treated as bit of text.

What do i need to do to make this work in production? Bare in mind that the path to the Vue file is stored as a property in the database so could be anything.

Any help appreciated

0 likes
3 replies
LaryAI's avatar
Level 58

To address the issue of dynamically loading Vue components in a production build with Vite, you need to ensure that the components are bundled correctly during the build process. The problem arises because Vite's build process doesn't know about the dynamic imports and thus doesn't include them in the final bundle.

Here's a solution to handle this scenario:

  1. Use a dynamic import with a known path: Instead of directly using the path from the database, you can map the paths to the actual components. This way, Vite can include these components in the build.

  2. Create a mapping of components: Create a mapping of your component paths to the actual component imports. This can be done in a separate file.

  3. Modify the loadComponent function: Use the mapping to dynamically import the components.

Here's how you can implement this:

Step 1: Create a Component Mapping

Create a file componentMapping.js in your project:

// componentMapping.js
export const componentMapping = {
  'TimerLayout': () => import('@/Templates/dynamic/TimerLayout.vue'),
  // Add other components here
  // 'AnotherComponent': () => import('@/Templates/dynamic/AnotherComponent.vue'),
};

Step 2: Modify the loadComponent Function

Update your loadComponent function to use the mapping:

// Your Vue component script
import { defineAsyncComponent } from 'vue';
import { componentMapping } from '@/componentMapping';
import LoadingComponent from '@/components/LoadingComponent.vue';
import MissingComponent from '@/components/MissingComponent.vue';

function loadComponent(componentName) {
  const loader = componentMapping[componentName];
  if (!loader) {
    return MissingComponent;
  }

  return defineAsyncComponent({
    loader,
    loadingComponent: LoadingComponent,
    delay: 200,
    errorComponent: MissingComponent,
    timeout: 3000,
  });
}

Step 3: Use the Component Name in Props

Ensure that the props.template contains the component name (e.g., 'TimerLayout') instead of the full path:

const props = defineProps({
  template: String, // e.g., 'TimerLayout'
  blocks: Array,
  template_id: { type: Number, default: -1 },
});

Step 4: Update the Template

Use the loadComponent function in your template:

<template>
  <div class="flex">
    <div id="editor-sidebar" class="bg-primary-100 min-w-fit relative dark:bg-primary-900 dark:text-primary-100"></div>
    <main id="main" class="relative w-full min-h-screen">
      <div>
        <component :is="loadComponent(props.template)" :blocks="props.blocks" :layout_id="props.template_id">
          <slot />
        </component>
      </div>
    </main>
    <div id="revisions-sidebar" class="min-w-fit"></div>
  </div>
</template>

Summary

By creating a mapping of component names to their respective import functions, you ensure that Vite can include these components in the build. This approach allows you to dynamically load components based on a name stored in the database while still benefiting from Vite's bundling and optimization.

This solution should work both in development and production environments.

richardhulbert's avatar

@LaryAI

I don't want to have to manually add an entry to componentMapping.js each time I add a component - there may be many

richardhulbert's avatar
richardhulbert
OP
Best Answer
Level 6

So to round this off... As @laryai has set out we need to declare the dynamic components (so Vite can do its magic mapping). I wanted this to be dynamically created so that I don't have to manually an entry for each Vue file I am going to to dynamically import using defineAsyncComponent

So instead of creating a constant with the component mapping I create a function that returns the mapping

// componentMapping.js
export function componentMapping() {
    const modules = import.meta.glob('./components/dynamic/*.vue')
    let dynamicComponents = {}
    for (const path in modules) {
        let arr = path.split('/')
        let name = arr[arr.length - 1]
        dynamicComponents[name] = modules[path]
    }
    return dynamicComponents
}

Then I need to adjust my loadComponent function (in Loader.vue)

function loadComponent(component) {
  const loader = componentMapping()[component];
  if (!loader) {
    return Missing;
  }
  return defineAsyncComponent({
    loader,
    loadingComponent:Loading,
    delay:2000,
    errorComponent: Missing,
    timeout: 3000
  })
}

I have made a Git project as a reference for any last souls like me who are easily confused by Vite: https://github.com/richardhulbert/vite-async.git

Please or to participate in this conversation.