Асинхронная загрузка JS Map API

Open on CodeSandbox

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
    <script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>

    <script
      data-plugins="transform-modules-umd"
      data-presets="typescript"
      type="text/babel"
      src="./common.ts"
    ></script>
    <script data-plugins="transform-modules-umd" data-presets="typescript" type="text/babel">
      import {LOCATION, loadMapScript} from './common';

      window.map = null;

      // Flag that indicates whether the JS Map API is currently being loaded
      let isLoading = false;

      // Function to create the map after initializing the JS Map API
      async function createMap() {
          // Waiting for all api elements to be loaded
          await ymaps3.ready;
          const {YMap, YMapDefaultSchemeLayer} = ymaps3;

          // Initialize the map
          map = new YMap(
              // Pass the link to the HTMLElement of the container
              document.getElementById('map'),
              // Pass the map initialization parameters
              {location: LOCATION, showScaleInCopyrights: true},
              // Add a map scheme layer
              [new YMapDefaultSchemeLayer({})]
          );
      }

      // Function to destroy the map
      function destroyMap() {
          map.destroy();
          map = null;
      }

      // Function to toggle the visibility of the loading spinner
      function toggleLoader() {
          document.querySelector('.loader-container').classList.toggle('_hide', !isLoading);
      }

      // Function to async loading of JS Map API script.
      async function fetchScript() {
          try {
              // Set loading flag to true and show the loading spinner
              isLoading = true;
              toggleLoader();

              // Load the JS Map API script
              await loadMapScript();
          } catch (error) {
              // Log any errors that occur during script loading
              console.error(error);
              return 'error';
          } finally {
              // Set loading flag to false and hide the loading spinner
              isLoading = false;
              toggleLoader();
          }
      }

      // Event handler for tab change
      function onChangeTab(e: Event) {
          const target = e.target as HTMLInputElement;
          const tabId = target.id;

          if (tabId === 'tab2') {
              // If the JS Map API is currently loading, then do nothing
              if (isLoading) return;

              // If ymaps3 is not defined, fetch the script and create the map
              if (typeof ymaps3 === 'undefined') {
                  fetchScript().then((res) => {
                      if (res === 'error') return;
                      createMap();
                  });

                  return;
              }

              // If the map is not yet created, create it
              !map && createMap();
          } else {
              // If the first tab is selected, and the map is created, destroy it
              map && destroyMap();
          }
      }

      // Add event listeners for tab1 and tab2 to handle tab changes
      document.getElementById('tab1').addEventListener('change', onChangeTab);
      document.getElementById('tab2').addEventListener('change', onChangeTab);
    </script>

    <!-- prettier-ignore -->
    <style> html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; } .toolbar { position: absolute; z-index: 1000; top: 0; left: 0; display: flex; align-items: center; padding: 16px; } .toolbar a { padding: 16px; }  </style>
    <link rel="stylesheet" href="./common.css" />
  </head>
  <body>
    <div class="tabs">
      <input id="tab1" type="radio" name="tabs" class="input" checked />
      <label class="label" for="tab1">Description</label>

      <input id="tab2" type="radio" name="tabs" class="input" />
      <label class="label" for="tab2">Map</label>

      <div class="panel">
        <h1>Description</h1>
        <p>The JS Map API will start loading after you open the Map tab.</p>
      </div>

      <div id="map" class="panel">
        <div class="loader-container _hide"><div class="loader" /></div>
      </div>
    </div>
  </body>
</html>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
    <script crossorigin src="https://cdn.jsdelivr.net/npm/react@17/umd/react.production.min.js"></script>
    <script crossorigin src="https://cdn.jsdelivr.net/npm/react-dom@17/umd/react-dom.production.min.js"></script>
    <script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>

    <script
      data-plugins="transform-modules-umd"
      data-presets="react, typescript"
      type="text/babel"
      src="./common.ts"
    ></script>
    <script data-plugins="transform-modules-umd" data-presets="react, typescript" type="text/babel">
      import {LOCATION, loadMapScript} from './common';

      window.map = null;

      main();
      async function main() {
        // Function for create a map component after initializing the JS Map API
        const createMapComponent = async () => {
          // For each object in the JS API, there is a React counterpart
          // To use the React version of the API, include the module @yandex/ymaps3-reactify
          const [ymaps3React] = await Promise.all([ymaps3.import('@yandex/ymaps3-reactify'), ymaps3.ready]);
          const reactify = ymaps3React.reactify.bindTo(React, ReactDOM);
          const {YMap, YMapDefaultSchemeLayer} = reactify.module(ymaps3);

          // Return a functional React component representing the map
          return () => {
            return (
              // Initialize the map and pass initialization parameters
              <YMap location={LOCATION} showScaleInCopyrights={true} ref={React.useCallback((x) => (map = x), [])}>
                {/* Add a map scheme layer */}
                <YMapDefaultSchemeLayer />
              </YMap>
            );
          };
        };

        // When the JS Map API is initialized, the Map variable will become a react component
        let Map: () => React.JSX.Element = null;

        // Loader component to display during JS Map API loading
        function Loader() {
          return (
            <div className={'loader-container'}>
              <div className={'loader'} />
            </div>
          );
        }

        function App() {
          const [tab, setTab] = React.useState('tab1');

          // Flag that indicates whether the JS Map API is currently being loaded
          const [isLoading, setIsLoading] = React.useState(false);

          // Event handler for tab change
          const onChangeTab = React.useCallback(
            async (e: React.ChangeEvent<HTMLInputElement>) => {
              setTab(e.target.id);

              if (e.target.id === 'tab2') {
                // If ymaps3 is not defined, fetch the script and initialize the Map component
                if (typeof ymaps3 === 'undefined' && !isLoading) {
                  try {
                    // Set loading flag to true
                    setIsLoading(true);
                    // Load the JS Map API script
                    await loadMapScript();
                    // Initialize the Map component
                    Map = await createMapComponent();
                  } catch (error) {
                    // Log any errors that occur during script loading
                    console.error(error);
                  } finally {
                    // Set loading flag to false
                    setIsLoading(false);
                  }
                }
              }
            },
            [isLoading]
          );

          return (
            <div className="tabs">
              <input
                id="tab1"
                type="radio"
                name="tabs"
                className="input"
                checked={tab === 'tab1'}
                onChange={onChangeTab}
              />
              <label className="label" htmlFor="tab1">
                Description
              </label>

              <input
                id="tab2"
                type="radio"
                name="tabs"
                className="input"
                checked={tab === 'tab2'}
                onChange={onChangeTab}
              />
              <label className="label" htmlFor="tab2">
                Map
              </label>

              {tab === 'tab1' && (
                <div className="panel visible">
                  <h1>Description</h1>
                  <p>The JS Map API will start loading after you open the Map tab</p>
                </div>
              )}
              {tab === 'tab2' && (
                <div id="map" className="panel visible">
                  {/* Show loader while loading the JS Map API, or render the map component if available. */}
                  {isLoading ? <Loader /> : Map && <Map />}
                </div>
              )}
            </div>
          );
        }

        ReactDOM.render(
          <React.StrictMode>
            <App />
          </React.StrictMode>,
          document.getElementById('app')
        );
      }
    </script>

    <!-- prettier-ignore -->
    <style> html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; } .toolbar { position: absolute; z-index: 1000; top: 0; left: 0; display: flex; align-items: center; padding: 16px; } .toolbar a { padding: 16px; }  </style>
    <link rel="stylesheet" href="./common.css" />
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
    <script crossorigin src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
    <script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
    <script
      data-plugins="transform-modules-umd"
      data-presets="typescript"
      type="text/babel"
      src="./common.ts"
    ></script>
    <script data-plugins="transform-modules-umd" data-presets="typescript" type="text/babel">
      import {LOCATION, loadMapScript} from './common';

      window.map = null;

      async function main() {
        const LoaderComp = Vue.defineComponent({
          template: `
            <div class='loader-container'>
                <div class='loader' />
            </div>
        `
        });

        const ErrorComp = Vue.defineComponent({
          template: `
            <div class='error-container'>
                <div class='error'>
                  Loading map error!
                </div>
            </div>
        `
        });

        let YMap;
        let YMapDefaultSchemeLayer;

        let Map = Vue.defineAsyncComponent({
          loader: () => {
            return new Promise(async (resolve, reject) => {
              if (typeof ymaps3 == 'undefined') {
                await loadMapScript();
              }

              try {
                const [ymaps3Vue] = await Promise.all([ymaps3.import('@yandex/ymaps3-vuefy'), ymaps3.ready]);

                const vuefy = ymaps3Vue.vuefy.bindTo(Vue);
                let mod = vuefy.module(ymaps3);
                YMap = mod['YMap'];
                YMapDefaultSchemeLayer = mod['YMapDefaultSchemeLayer'];
              } catch (e) {
                reject(e);
              }

              resolve({
                setup() {
                  const refMap = (ref) => {
                    window.map = ref?.entity;
                  };
                  return {LOCATION, refMap};
                },
                components: {
                  YMap,
                  YMapDefaultSchemeLayer
                },
                template: `
                          <YMap :location="LOCATION" :showScaleInCopyrights="true" :ref="refMap">
                              <!-- Add a map scheme layer -->
                              <YMapDefaultSchemeLayer />
                          </YMap>
                    `
              });
            });
          },
          loadingComponent: LoaderComp,
          errorComponent: ErrorComp,
          timeout: 10000
        });

        const app = Vue.createApp({
          data() {
            return {
              activeTab: 'tab1',
              isLoading: false
            };
          },
          components: {
            Map
          },
          template: `
            <div class="tabs">
                <input
                    id="tab1"
                    type="radio"
                    name="tabs"
                    class="input"
                    value="tab1"
                    v-model="activeTab"
                />
                <label class="label" for="tab1">
                    Description
                </label>

                <input
                    id="tab2"
                    type="radio"
                    name="tabs"
                    class="input"
                    value="tab2"
                    v-model="activeTab"
                />
                <label class="label" for="tab2">
                    Map
                </label>

                <div v-if="activeTab === 'tab1'" class="panel visible">
                    <h1>Description</h1>
                    <p>The JS Map API will start loading after you open the Map tab</p>
                </div>

                <div v-else id="map" class="panel visible">
                    <!-- Show loader while loading the JS Map API, or render the map component if available. -->
                    <!-- Loader v-if="isLoading" / -->
                    <Map />
                </div>
            </div>
        `
        });
        app.mount('#app');
      }
      main();
    </script>

    <!-- prettier-ignore -->
    <style> html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; } .toolbar { position: absolute; z-index: 1000; top: 0; left: 0; display: flex; align-items: center; padding: 16px; } .toolbar a { padding: 16px; }  </style>
    <link rel="stylesheet" href="./common.css" />
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
body,
#app {
  display: flex;
  justify-content: center;
  align-items: center;

  background: linear-gradient(180deg, #a5a5a5, #dcdcdc);
}

.tabs {
  width: 100%;
  height: 100%;
  max-width: 1200px;
  max-height: 600px;

  display: flex;
  flex-wrap: wrap;
}
.input {
  display: none;
}
.label {
  padding: 20px;
  flex-grow: 1;

  background: #e5e5e5;
  color: #7f7f7f;
  text-align: center;
  font-weight: bold;
  font-size: 24px;

  cursor: pointer;
  transition: background 0.25s, color 0.25s;
}
.label:hover {
  background: #d8d8d8;
}
.label:active {
  background: #ccc;
}
.input:checked + .label {
  background: #fff;
  color: #000;
}

.panel {
  display: none;

  width: 100%;
  height: 100%;
  padding: 10px 0;

  background: #fff;
  box-shadow: 0 48px 80px -32px rgba(0, 0, 0, 0.3);
}

.visible,
.input:checked:nth-of-type(1) ~ .panel:nth-of-type(1),
.input:checked:nth-of-type(2) ~ .panel:nth-of-type(2) {
  display: block;
}

.panel h1 {
  margin: 0;
  padding-top: 50px;

  text-align: center;
  font-size: 36px;
}
.panel p {
  margin: 0;
  padding-top: 20px;

  text-align: center;
  font-size: 20px;
}

.loader-container {
  height: 100%;

  display: flex;
  justify-content: center;
  align-items: center;
}

.loader-container._hide {
  display: none;
}

.loader {
  width: 30px;
  height: 30px;

  border: 8px solid #cccccc80;
  border-top: 8px solid #949494;
  border-radius: 50%;

  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
import type {YMapLocationRequest} from '@yandex/ymaps3-types';

export const LOCATION: YMapLocationRequest = {
  center: [37.623082, 55.75254], // starting position [lng, lat]
  zoom: 9 // starting zoom
};

// Function to async load the JS Map API script.
export function loadMapScript() {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = 'https://api-maps.yandex.ru/v3/?apikey=c8efdf97-cb56-4062-ac67-2f5c7b9305fe&lang=en_US';
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  });
}