javascriptvue.jsvue-router

waiting for a page to load resources with vue-router - equivalent to window.onload?


i'm developing a game with vue.js & vue-router. this often involves fairly large image assets! working in vanilla javascript, i've approached this with window.onload, displaying a div that only allows the user to progress and see the page once all assets are loaded. is there an equivalent approach for vue-router or dynamic components?

one approach that occurs to me is to follow something along this line, making a list of resource urls and letting the player through after my image list is all loaded.

my question in the end is pretty similar to that poster's - is this still the way to go in 2025?


Solution

  • This is definitely possible. There are various ways to go about it, but I would choose the route of:

    To show this, I have created a little snippet to show what I mean. It's not the nicest Vue code as it has to work in the confines of the Stack snippet, but I think you can get what I'm trying to get at.

    In the example, I pass the necessary asset URLs as data with my routes. Then there is functionality to load the assets and show a loader before displaying the page. For the Gallery page (with 3 large images), you can either access it normally, or preload it by first hovering over the special link. You'll notice that if you access it this way, there is no loading screen because it's loaded in the background. You'll also find that the image that is shared on bothe the main screen and the gallery screen is not reloaded when switching.

    Further improvements on this could be:

    const { createApp, reactive, computed, defineAsyncComponent } = Vue;
    const { createRouter, createWebHashHistory } = VueRouter;
    
    /* ------------ Asset loader with caching ------------ */
    const cache = new Map();
    
    async function loadAsset(url) {
      if (cache.has(url)) return cache.get(url);
      const promise = new Promise((resolve, reject) => {
        const img = new Image();
        img.onload = () => resolve(img);
        img.onerror = reject;
        img.src = url;
      });
      cache.set(url, promise);
      return promise;
    }
    
    async function loadAssets(urls = []) {
      await Promise.all(urls.map(loadAsset));
    }
    
    function preloadPage(assets) {
      loadAssets(assets);
    }
    
    /* ------------ Simple loading store ------------ */
    const loadingState = reactive({ isLoading: false });
    function useLoadingStore() {
      return {
        get isLoading() { return loadingState.isLoading; },
        start() { loadingState.isLoading = true; },
        stop() { loadingState.isLoading = false; },
      };
    }
    
    /* ------------ Loader component ------------ */
    const Loader = {
      template: `
        <div v-if="isLoading" class="loader">
          <div class="spinner"></div>
        </div>`,
      setup() {
        const store = useLoadingStore();
        return { isLoading: computed(() => store.isLoading) };
      }
    };
    
    /* ------------ Page Components ------------ */
    
    const HomePage = defineAsyncComponent(() =>
      Promise.resolve({
        template: `
          <div class="page">
            <h1>Home</h1>
            <p>Welcome to the demo. Navigate to <strong>Gallery</strong> to see big many images lazy-load.</p>
            <p>
              <a href="#"
                 @mouseenter="preloadGallery"
                 @click.prevent="goGallery">
                Go to Gallery (preloads on hover)
              </a>
            </p>
            <img src="https://floatingworld.com/wp-content/uploads/2023/02/Sample-jpg-image-30mb-16.jpg" alt="Big Image 1" width="100%" />
          </div>`,
        setup() {
          const router = VueRouter.useRouter();
          function preloadGallery() {
            preloadPage(galleryAssets);
          }
          function goGallery() { router.push('/gallery'); }
          return { preloadGallery, goGallery };
        },
      })
    );
    
    const galleryAssets = [
      'https://floatingworld.com/wp-content/uploads/2023/02/Sample-jpg-image-30mb-16.jpg',
      'https://media.gettyimages.com/id/84047672/nl/foto/london-rachel-stevens-launches-the-new-virgin-media-50mb-broadband-service-at-the-hospital-club.jpg?s=2048x2048&w=gi&k=20&c=wa_NSN4Tl-3p4NEtvweN4hkVjEUJJk9_1GiEQQ9efA4=',
      'https://upload.wikimedia.org/wikipedia/commons/e/e6/Clocktower_Panorama_20080622_20mb.jpg'
    ];
    
    const GalleryPage = {
      template: `
        <div class="page">
          <h1>Gallery</h1>
          <p>These high-resolution images are lazy loaded with caching.</p>
          <img v-for="img in images" :key="img" :src="img" width="50%"/>
        </div>`,
      setup() {
        const images = galleryAssets;
        return { images };
      },
    };
    
    const AboutPage = {
      template: `
        <div class="page">
          <h1>About</h1>
          <p>This example demonstrates route-level lazy loading and image caching in Vue.</p>
        </div>`,
    };
    
    /* ------------ Router ------------ */
    const routes = [
      { path: '/', name: 'Home', component: HomePage, meta: { assets: ['https://floatingworld.com/wp-content/uploads/2023/02/Sample-jpg-image-30mb-16.jpg'] } },
      { path: '/gallery', name: 'Gallery', component: GalleryPage, meta: { assets: galleryAssets } },
      { path: '/about', name: 'About', component: AboutPage },
    ];
    
    const router = createRouter({
      history: createWebHashHistory(),
      routes,
    });
    
    router.beforeEach(async (to, from, next) => {
      const store = useLoadingStore();
      store.start();
      try {
        if (to.meta.assets?.length) await loadAssets(to.meta.assets);
        next();
      } catch (e) {
        console.error('Asset load failed', e);
        next(false);
      } finally {
        store.stop();
      }
    });
    
    /* ------------ App ------------ */
    const App = {
      components: { Loader },
      template: `
        <div>
          <Loader />
          <nav>
            <router-link to="/">Home</router-link>
            <router-link to="/gallery">Gallery</router-link>
            <router-link to="/about">About</router-link>
          </nav>
          <router-view />
        </div>`
    };
    
    /* ------------ Mount ------------ */
    createApp(App).use(router).mount('#app');
    body {
      font-family: system-ui, sans-serif;
      margin: 0;
      background: #fafafa;
    }
    
    nav {
      display: flex;
      gap: 1rem;
      padding: 1rem;
      background: #42b983;
    }
    
    nav a {
      color: white;
      text-decoration: none;
      font-weight: bold;
    }
    
    nav a.router-link-active {
      text-decoration: underline;
    }
    
    .loader {
      position: fixed;
      inset: 0;
      background: rgba(255,255,255,0.8);
      display: flex;
      justify-content: center;
      align-items: center;
      z-index: 1000;
    }
    
    .spinner {
      width: 50px;
      height: 50px;
      border: 5px solid #ccc;
      border-top-color: #42b983;
      border-radius: 50%;
      animation: spin 1s linear infinite;
    }
    
    @keyframes spin { 100% { transform: rotate(360deg); } }
    
    .page {
      padding: 1rem;
    }
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <title>Vue Lazy-Loaded SPA Example</title>
      <link rel="stylesheet" href="style.css" />
    </head>
    <body>
      <div id="app"></div>
    
      <!-- Vue + Router (already loaded in your sandbox) -->
      <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
      <script src="https://unpkg.com/vue-router@4/dist/vue-router.global.prod.js"></script>
    
      <!-- Your app -->
      <script src="app.js"></script>
    </body>
    </html>