javascript - tutorial - module exports es6




Inlining HTML Modules ECMAScript (2)

Hacking Together Notre propre import from '#id'

Les exportations / importations entre les scripts en ligne ne sont pas nativement prises en charge, mais ce fut un exercice amusant de pirater une implémentation pour mes documents. Code-golfed à un petit bloc, je l'utilise comme ceci:

<script type="module" data-info="https://stackoverflow.com/a/43834063">let l,e,t
='script',p=/(from\s+|import\s+)['"](#[\w\-]+)['"]/g,x='textContent',d=document,
s,o;for(o of d.querySelectorAll(t+'[type=inline-module]'))l=d.createElement(t),o
.id?l.id=o.id:0,l.type='module',l[x]=o[x].replace(p,(u,a,z)=>(e=d.querySelector(
t+z+'[type=module][src]'))?a+`/* ${z} */'${e.src}'`:u),l.src=URL.createObjectURL
(new Blob([l[x]],{type:'application/java'+t})),o.replaceWith(l)//inline</script>

<script type="inline-module" id="utils">
  let n = 1;
  
  export const log = message => {
    const output = document.createElement('pre');
    output.textContent = `[${n++}] ${message}`;
    document.body.appendChild(output);
  };
</script>

<script type="inline-module" id="dogs">
  import {log} from '#utils';
  
  log("Exporting dog names.");
  
  export const names = ["Kayla", "Bentley", "Gilligan"];
</script>

<script type="inline-module">
  import {log} from '#utils';
  import {names as dogNames} from '#dogs';
  
  log(`Imported dog names: ${dogNames.join(", ")}.`);
</script>

Au lieu de <script type="module"> , nous devons définir nos éléments de script à l'aide d'un type personnalisé tel que <script type="inline-module"> . Cela empêche le navigateur d’essayer d’exécuter lui-même son contenu, ce qui nous laisse le soin de le gérer. Le script (version complète ci-dessous) trouve tous inline-module éléments de script du inline-module dans le document et les transforme en éléments de module de script standard avec le comportement souhaité.

Les scripts en ligne ne peuvent pas être importés directement les uns des autres, nous devons donc leur donner des URL importables. Nous générons un blob: URL pour chacun d'eux, contenant leur code, et définissons l'attribut src pour qu'il s'exécute à partir de cette URL au lieu de s'exécuter en ligne. Le blob: URL agit comme des URL normales du serveur, elles peuvent donc être importées à partir d'autres modules. Chaque fois que nous voyons un inline-module ultérieur essayer d'importer à partir de '#example' example '#example' , où example est l'ID d'un inline-module nous avons transformé, nous modifions cette importation pour qu'elle soit importée à partir du blob: URL. Cela conserve l'exécution unique et la déduplication de références que les modules sont supposés avoir.

<script type="module" id="dogs" src="blob:https://example.com/9dc17f20-04ab-44cd-906e">
  import {log} from /* #utils */ 'blob:https://example.com/88fd6f1e-fdf4-4920-9a3b';

  log("Exporting dog names.");

  export const names = ["Kayla", "Bentley", "Gilligan"];
</script>

L'exécution des éléments de script de module est toujours différée jusqu'à ce que le document soit analysé. Par conséquent, nous n'avons pas à nous soucier d'essayer de prendre en charge la façon dont les éléments de script traditionnels peuvent modifier le document pendant son analyse.

export {};

for (const original of document.querySelectorAll('script[type=inline-module]')) {
  const replacement = document.createElement('script');

  // Preserve the ID so the element can be selected for import.
  if (original.id) {
    replacement.id = original.id;
  }

  replacement.type = 'module';

  const transformedSource = original.textContent.replace(
    // Find anything that looks like an import from '#some-id'.
    /(from\s+|import\s+)['"](#[\w\-]+)['"]/g,
    (unmodified, action, selector) => {
      // If we can find a suitable script with that id...
      const refEl = document.querySelector('script[type=module][src]' + selector);
      return refEl ?
        // ..then update the import to use that script's src URL instead.
        `${action}/* ${selector} */ '${refEl.src}'` :
        unmodified;
    });

  // Include the updated code in the src attribute as a blob URL that can be re-imported.
  replacement.src = URL.createObjectURL(
    new Blob([transformedSource], {type: 'application/javascript'}));

  // Insert the updated code inline, for debugging (it will be ignored).
  replacement.textContent = transformedSource;

  original.replaceWith(replacement);
}

Avertissements: cette implémentation simple ne gère pas les éléments de script ajoutés après l'analyse du document initial, ni n'autorise les éléments de script à importer à partir d'autres éléments de script qui surviennent après ceux-ci dans le document. Si vous avez les deux éléments de script module et inline-module dans un document, leur ordre d'exécution relatif peut ne pas être correct. La transformation du code source est effectuée à l'aide d'une expression rationnelle brute qui ne gère pas certains cas tels que les périodes dans les ID.

J'ai expérimenté un nouveau support de module natif ECMAScript qui a récemment été ajouté aux navigateurs. C'est agréable de pouvoir enfin importer des scripts directement et proprement depuis JavaScript.

/example.html 🔍
<script type="module">
  import {example} from '/example.js';

  example();
</script>
/example.js
export function example() {
  document.body.appendChild(document.createTextNode("hello"));
};

Cependant, cela ne me permet que d'importer des modules définis par des fichiers JavaScript externes distincts. Je préfère généralement intégrer certains scripts utilisés pour le rendu initial, afin que leurs demandes ne bloquent pas le reste de la page. Avec une bibliothèque traditionnelle structurée de manière informelle, cela pourrait ressembler à ceci:

/inline-traditional.html 🔍
<body>
<script>
  var example = {};

  example.example = function() {
    document.body.appendChild(document.createTextNode("hello"));
  };
</script>
<script>
  example.example();
</script>

Cependant, il est évident que les fichiers de modules en ligne ne fonctionneront pas, car ils supprimeraient le nom de fichier utilisé pour identifier le module avec les autres modules. Le serveur HTTP / 2 peut être le moyen canonique de gérer cette situation, mais ce n’est toujours pas une option dans tous les environnements.

Est-il possible d'effectuer une transformation équivalente avec les modules ECMAScript? Existe-t-il un moyen pour un <script type="module"> d'importer un module exporté par un autre dans le même document?

J'imagine que cela pourrait fonctionner en permettant au script de spécifier un chemin de fichier et de se comporter comme s'il avait déjà été téléchargé ou poussé à partir du chemin.

/inline-name.html 🔍
<script type="module" name="/example.js">
  export function example() {
    document.body.appendChild(document.createTextNode("hello"));
  };
</script>

<script type="module">
  import {example} from '/example.js';

  example();
</script>

Ou peut-être par un schéma de référence totalement différent, tel que celui utilisé pour les références SVG locales:

/inline-id.html 🔍
<script type="module" id="example">
  export function example() {
    document.body.appendChild(document.createTextNode("hello"));
  };
</script>
<script type="module">
  import {example} from '#example';

  example();
</script>

Mais aucun de ces hypothèses ne fonctionne réellement, et je n'ai pas vu d'alternative qui le fasse.


Ceci est possible avec les travailleurs du service.

Puisqu’un ouvrier de service doit être installé avant de pouvoir traiter une page, il faut disposer d’une page distincte pour l’initialiser afin d’éviter les problèmes de poulet / œuf - ou une page peut être rechargée quand un ouvrier est prêt.

Voici un exemple censé fonctionner dans les navigateurs async..await charge les modules ES natifs et async..await (à savoir Chrome):

index.html

<html>
  <head>
    <script>
(async () => {
  try {
    const swInstalled = await navigator.serviceWorker.getRegistration('./');

    await navigator.serviceWorker.register('sw.js', { scope: './' })

    if (!swInstalled) {
      location.reload();
    }
  } catch (err) {
    console.error('Worker not registered', err);
  }
})();
    </script>
  </head>
  <body>
    World,

    <script type="module" data-name="./example.js">
      export function example() {
        document.body.appendChild(document.createTextNode("hello"));
      };
    </script>

    <script type="module">
      import {example} from './example.js';

      example();
    </script>  
  </body>
</html>

sw.js

self.addEventListener('fetch', e => {
  // parsed pages
  if (/^https:\/\/run.plnkr.co\/\w+\/$/.test(e.request.url)) {
    e.respondWith(parseResponse(e.request));
  // module files
  } else if (cachedModules.has(e.request.url)) {
    const moduleBody = cachedModules.get(e.request.url);
    const response = new Response(moduleBody,
      { headers: new Headers({ 'Content-Type' : 'text/javascript' }) }
    );
    e.respondWith(response);
  } else {
    e.respondWith(fetch(e.request));
  }
});

const cachedModules = new Map();

async function parseResponse(request) {
  const response = await fetch(request);
  if (!response.body)
    return response;

  const html = await response.text(); // HTML response can be modified further
  const moduleRegex = /<script type="module" data-name="([\w./]+)">([\s\S]*?)<\/script>/;
  const moduleScripts = html.match(new RegExp(moduleRegex.source, 'g'))
    .map(moduleScript => moduleScript.match(moduleRegex));

  for (const [, moduleName, moduleBody] of moduleScripts) {
    const moduleUrl = new URL(moduleName, request.url).href;
    cachedModules.set(moduleUrl, moduleBody);
  }
  const parsedResponse = new Response(html, response);
  return parsedResponse;
}

Les corps de script sont mis en cache (le cache natif peut également être utilisé) et renvoyés pour les requêtes de module correspondantes.

Préoccupations

  • L’approche est inférieure à l’application construite et encombrée avec un outil de regroupement tel que Webpack ou Rollup en termes de performances, de flexibilité, de solidité et de prise en charge du navigateur - en particulier si le blocage des demandes simultanées est la préoccupation principale.

  • Les scripts en ligne augmentent l'utilisation de la bande passante. Ceci est naturellement évité lorsque les scripts sont chargés une seule fois et mis en cache par le navigateur.

  • Les scripts en ligne ne sont pas modulaires et contredisent le concept de modules ES (à moins qu'ils ne soient générés à partir de modules réels par un modèle côté serveur).

  • L'initialisation de l'opérateur de service doit être effectuée sur une page distincte pour éviter les demandes inutiles.

  • La solution est limitée à une seule page et ne prend pas en compte <base> .

  • L'expression régulière est utilisée à des fins de démonstration uniquement. Utilisé comme dans l'exemple ci-dessus, il permet l'exécution de code JS arbitraire disponible sur la page. Une bibliothèque éprouvée, telle que parse5 devrait être utilisée à la place (cela entraînerait une surcharge de performances, et néanmoins, des problèmes de sécurité pourraient subsister). N'utilisez jamais d'expressions régulières pour analyser DOM .





es6-modules