Verwendung von JavaScript-Modulen in der Produktion: aktueller Stand der Dinge. Teil 1

Vor zwei Jahren schrieb ich über eine Technik , die heute allgemein als Modul- / Nomodenmuster bezeichnet wird. Mit seiner Anwendung können Sie JavaScript-Code mit den Funktionen von ES2015 + schreiben und dann mithilfe von Bundlern und Transpilern zwei Versionen der Codebasis erstellen. Eine davon enthält moderne Syntax (sie wird mit einer Struktur wie <script type="module"> geladen, die zweite mit ES5-Syntax (sie wird mit <script nomodule> geladen). Das Modul / Nomodule-Muster ermöglicht das Senden an Browser, die Module unterstützen. Viel weniger Code als Browser, die diese Funktion nicht unterstützen. Dieses Muster wird jetzt von den meisten Webframeworks und Befehlszeilentools unterstützt.



Bisher habe ich empfohlen, Code in Paketen zu sammeln, obwohl ich die Möglichkeit hatte, modernen JavaScript-Code an die Produktion zu senden, und obwohl die meisten Browser Module unterstützten.

Warum? Vor allem, weil ich das Gefühl hatte, dass das Laden von Modulen in den Browser langsam war. Obwohl neuere Protokolle wie HTTP / 2 theoretisch das effiziente Laden mehrerer Dateien unterstützten, kamen alle damaligen Leistungsstudien zu dem Schluss, dass die Verwendung von Bundlern immer noch effizienter ist als die Verwendung von Modulen.

Es muss jedoch zugegeben werden, dass diese Studien unvollständig waren. Testfälle mit den darin untersuchten Modulen bestanden aus nicht optimierten und nicht minimierten Quellcodedateien, die in der Produktion bereitgestellt wurden. Es gab keine Vergleiche des optimierten Bundles mit Modulen mit dem optimierten klassischen Skript.

Um ehrlich zu sein, gab es zu diesem Zeitpunkt jedoch keine optimale Möglichkeit, die Module bereitzustellen. Dank einiger moderner Verbesserungen der Bundler-Technologien ist es jetzt möglich, Produktionscode in Form von ES2015-Modulen sowohl mit statischen als auch mit dynamischen Importbefehlen bereitzustellen und gleichzeitig eine höhere Leistung zu erzielen, als dies mit den verfügbaren Optionen möglich ist Module werden nicht verwendet.

Es ist zu beachten, dass auf der Website, auf der das Originalmaterial veröffentlicht wird, dessen erster Teil der Übersetzung wir heute veröffentlichen, die Module seit mehreren Monaten in der Produktion verwendet werden.

Missverständnisse über Module


Viele Leute, mit denen ich gesprochen habe, lehnen Module vollständig ab und betrachten sie nicht einmal als eine der Optionen für großtechnische Produktionsanwendungen. Viele von ihnen zitieren genau die Studie , die ich bereits erwähnt habe. Das heißt, der Teil davon, der besagt, dass die Module nicht in der Produktion verwendet werden sollten, es sei denn, es handelt sich um „kleine Webanwendungen, die weniger als 100 Module enthalten, die sich in einem relativ„ kleinen “Abhängigkeitsbaum unterscheiden (d. H. - eine, deren Tiefe 5 Stufen nicht überschreitet). “

Wenn Sie jemals in das Verzeichnis node_modules eines Ihrer Projekte geschaut haben, wissen Sie wahrscheinlich, dass selbst eine kleine Anwendung problemlos mehr als 100 Abhängigkeitsmodule haben kann. Ich möchte Ihnen einen Blick darauf werfen, wie viele Module in einigen der beliebtesten npm-Pakete verfügbar sind.
Paket
Anzahl der Module
date-fns
729
lodash-es
643
rxjs
226

Hier wurzelt das Hauptmissverständnis in Bezug auf Module. Programmierer glauben, dass sie bei der Verwendung von Modulen in der Produktion nur zwei Möglichkeiten haben. Die erste besteht darin, den gesamten Quellcode in seiner vorhandenen Form (einschließlich des Verzeichnisses node_modules ) node_modules . Die zweite besteht darin, überhaupt keine Module zu verwenden.

Wenn Sie sich jedoch die Empfehlungen aus der oben genannten Studie genau ansehen, werden Sie feststellen, dass das Laden von Modulen langsamer ist als das Laden normaler Skripte. Es heißt nicht, dass Module überhaupt nicht verwendet werden sollten. Es geht nur um die Tatsache, dass Chrome, wenn jemand Hunderte nicht infizierter Moduldateien in der Produktion bereitstellt, diese nicht so schnell laden kann wie ein einzelnes minimiertes Bundle. Infolgedessen empfiehlt die Studie, weiterhin Bundler, Compiler und Minifier zu verwenden.

Aber weißt du was? Tatsache ist, dass Sie all dies nutzen und Module in der Produktion verwenden können.

In der Tat sind Module ein Format, in das wir konvertieren sollten, da Browser bereits wissen, wie Module geladen werden (und Browser, die dies nicht können, können eine Ersatzversion des Codes mithilfe des Nomodule-Mechanismus laden). Wenn Sie sich den Code ansehen, den die beliebtesten Bundler generieren, finden Sie viele Vorlagenfragmente, deren Zweck nur darin besteht, anderen Code dynamisch zu laden und Abhängigkeiten zu verwalten. All dies ist jedoch nicht erforderlich, wenn wir nur die Module und Ausdrücke zum import und export .

Glücklicherweise unterstützt mindestens einer der beliebten modernen Bundler ( Rollup ) Module in Form von Ausgabedaten . Dies bedeutet, dass Sie den Code mit einem Bundler verarbeiten und Module in der Produktion bereitstellen können (ohne Vorlagenfragmente zum Laden des Codes zu verwenden). Und da Rollup eine hervorragende Implementierung des Tree-Shaking-Algorithmus hat (der beste, den ich bei Bundlern gesehen habe), können Sie durch das Erstellen von Programmen in Form von Modulen mit Rollup Code erhalten, der kleiner ist als die Größe des gleichen Codes, der beim Anwenden erhalten wurde andere Mechanismen heute verfügbar.

Es sollte beachtet werden, dass sie planen, Unterstützung für Module in der nächsten Version von Parcel hinzuzufügen. Webpack unterstützt noch keine Module als Ausgabeformat, aber hier ist es - Diskussionen, die sich auf dieses Problem konzentrieren.

Ein weiteres Missverständnis in Bezug auf Module besteht darin, dass einige Leute glauben, dass Module nur verwendet werden können, wenn 100% der Projektabhängigkeiten Module verwenden. Leider (ich halte es für sehr bedauerlich) werden die meisten npm-Pakete noch für die Veröffentlichung im CommonJS-Format vorbereitet (einige Module, auch solche, die mit ES2015-Funktionen geschrieben wurden, werden vor der Veröffentlichung in npm in das CommonJS-Format übersetzt)!

Auch hier möchte ich darauf hinweisen, dass Rollup über ein Plugin ( rollup-plugin-commonjs ) verfügt, das mit CommonJS geschriebenen Eingabequellcode in ES2015-Code konvertiert. Auf jeden Fall wäre es besser, wenn das von Anfang an verwendete Abhängigkeitsformat das ES2015-Modulformat verwendet. Wenn einige Abhängigkeiten jedoch nicht vorhanden sind, können Sie Projekte nicht mithilfe von Modulen in der Produktion bereitstellen.

In den folgenden Teilen dieses Artikels werde ich Ihnen zeigen, wie ich Projekte in Bundles sammle, die Module verwenden (einschließlich der Verwendung dynamischer Importe und der Codetrennung). Ich werde darüber sprechen, warum solche Lösungen normalerweise produktiver sind als klassische Skripte, und zeigen, wie sie funktionieren mit Browsern, die keine Module unterstützen.

Optimale Code-Build-Strategie


Das Erstellen von Code für die Produktion ist immer ein Versuch, die Vor- und Nachteile verschiedener Lösungen in Einklang zu bringen. Einerseits möchte der Entwickler, dass sein Code so schnell wie möglich geladen und ausgeführt wird. Andererseits möchte er keinen Code herunterladen, der von den Benutzern des Projekts nicht verwendet wird.

Darüber hinaus müssen Entwickler darauf vertrauen können, dass ihr Code am besten für das Caching geeignet ist. Das große Problem der Codebündelung besteht darin, dass jede Änderung des Codes, selbst eine geänderte Zeile, zur Ungültigmachung des Caches des gesamten Bundles führt. Wenn Sie eine Anwendung bereitstellen, die aus Tausenden kleiner Module besteht (genau in der Form dargestellt, in der sie im Quellcode enthalten sind), können Sie sicher geringfügige Änderungen am Code vornehmen und gleichzeitig wissen, dass der größte Teil des Anwendungscodes zwischengespeichert wird . Wie ich bereits sagte, kann ein solcher Entwicklungsansatz wahrscheinlich bedeuten, dass das Laden des Codes beim ersten Besuch einer Ressource länger dauern kann als die Verwendung herkömmlicherer Ansätze.

Infolgedessen stehen wir vor einer schwierigen Aufgabe, nämlich den richtigen Ansatz zu finden, um die Bündel in Teile zu zerlegen. Wir müssen das richtige Gleichgewicht zwischen der Geschwindigkeit des Ladens von Materialien und ihrer langfristigen Zwischenspeicherung finden.

Die meisten Bundler verwenden standardmäßig Codeaufteilungstechniken, die auf dynamischen Importbefehlen basieren. Aber ich würde sagen, dass das Teilen von Code nur mit einem Fokus auf dynamischen Import es nicht erlaubt, ihn in ausreichend kleine Fragmente zu zerlegen. Dies gilt insbesondere für Websites mit vielen wiederkehrenden Benutzern (dh in Situationen, in denen das Zwischenspeichern wichtig ist).

Ich glaube, dass Code in so kleine Fragmente wie möglich aufgeteilt werden sollte. Es lohnt sich, die Größe der Fragmente zu reduzieren, bis ihre Anzahl so stark zunimmt, dass die Download-Geschwindigkeit des Projekts beeinträchtigt wird. Und obwohl ich definitiv jedem empfehlen kann, seine eigene Analyse der Situation durchzuführen, gibt es beim Laden von weniger als 100 Modulen keine merkliche Verlangsamung beim Laden, wenn Sie den ungefähren Berechnungen in der von mir erwähnten Studie glauben. Eine separate Studie zur HTTP / 2-Leistung ergab keine merkliche Projektverlangsamung beim Herunterladen von weniger als 50 Dateien. Dort wurden jedoch nur Optionen getestet, bei denen die Anzahl der Dateien 1, 6, 50 und 1000 betrug. Daher sind wahrscheinlich 100 Dateien ein Wert, zu dem Sie problemlos navigieren können, ohne befürchten zu müssen, dass die Download-Geschwindigkeit abnimmt.

Was ist der beste Weg, um den Code aggressiv, aber nicht zu aggressiv in Teile aufzuteilen? Zusätzlich zum Aufteilen von Code basierend auf dynamischen Importbefehlen würde ich Ihnen raten, sich das Aufteilen von Code in npm-Pakete genauer anzusehen. Bei diesem Ansatz fällt das, was aus dem Ordner node_modules in das Projekt importiert wird, basierend auf dem Paketnamen in ein separates Fragment des fertigen Codes.

Pakettrennung


Ich habe oben gesagt, dass einige der modernen Funktionen von Bundlern es ermöglichen, ein Hochleistungsschema für die Bereitstellung modulbasierter Projekte zu organisieren. Worüber ich gesprochen habe, sind zwei neue Rollup-Funktionen. Die erste ist die automatische Codetrennung durch dynamische import() -Befehle (hinzugefügt in Version 1.0.0 ). Die zweite Option ist die manuelle manualChunks , die vom Programm basierend auf der Option manualChunks (hinzugefügt in Version 1.11.0 ) durchgeführt wird.

Dank dieser beiden Funktionen ist es jetzt sehr einfach, den Erstellungsprozess zu konfigurieren, bei dem Code auf Paketebene aufgeteilt wird.

Hier ist eine Beispielkonfiguration, die die Option manualChunks , wodurch jedes aus node_modules importierte Modul in ein separates node_modules fällt, dessen Name dem Namen des Pakets entspricht (technisch gesehen der Name des Paketverzeichnisses im Ordner node_modules ):

 export default {  input: {    main: 'src/main.mjs',  },  output: {    dir: 'build',    format: 'esm',    entryFileNames: '[name].[hash].mjs',  },  manualChunks(id) {    if (id.includes('node_modules')) {      //   ,    `node_modules`.      //   - ,       .      const dirs = id.split(path.sep);      return dirs[dirs.lastIndexOf('node_modules') + 1];    }  }, } 

Die Option manualChunk akzeptiert eine Funktion, die als einzelnes Argument den Pfad zur Moduldatei akzeptiert. Diese Funktion kann einen Zeichenfolgennamen zurückgeben. Was zurückgegeben wird, zeigt auf ein Fragment der Assembly, zu der das aktuelle Modul hinzugefügt werden soll. Wenn die Funktion nichts zurückgibt, wird das Modul zum Standardfragment hinzugefügt.

cloneDeep() Sie sich eine Anwendung vor, die die cloneDeep() , debounce() und find() aus dem lodash-es Paket lodash-es . Wenn Sie beim lodash dieser Anwendung die obige Konfiguration anwenden, wird jedes dieser Module (sowie jedes von diesen Modulen importierte lodash Modul) in einer einzelnen Ausgabedatei mit einem Namen wie npm.lodash-es.XXXX.mjs (hier ist XXXX eindeutig Moduldatei-Hash im lodash-es Fragment).

Am Ende der Datei sehen Sie einen Exportausdruck wie den folgenden. Beachten Sie, dass dieser Ausdruck nur Exportbefehle für Module enthält, die dem Fragment hinzugefügt wurden, und nicht alle lodash Module.

 export {cloneDeep, debounce, find}; 

Wenn der Code in einem der anderen Fragmente diese lodash Module verwendet (möglicherweise nur die debounce() -Methode), gibt es in diesen Fragmenten im oberen Teil einen Importausdruck, der folgendermaßen aussieht:

 import {debounce} from './npm.lodash.XXXX.mjs'; 

Hoffentlich hat dieses Beispiel die Frage geklärt, wie die manuelle Codetrennung in Rollup funktioniert. Darüber hinaus denke ich, dass die Ergebnisse der Codetrennung mithilfe der import und export viel einfacher zu lesen und zu verstehen sind als der Code von Fragmenten, deren Bildung nicht standardmäßige Mechanismen verwendet, die nur in einem bestimmten Bündler verwendet werden.

Zum Beispiel ist es sehr schwierig herauszufinden, was in der nächsten Datei vor sich geht. Dies ist die Ausgabe eines meiner alten Projekte, bei denen Webpack zum Teilen von Code verwendet wurde. Fast alles in diesem Code wird in Browsern, die Module unterstützen, nicht benötigt.

 (window["webpackJsonp"] = window["webpackJsonp"] || []).push([["import1"],{ /***/ "tLzr": /*!*********************************!*\  !*** ./app/scripts/import-1.js ***!  \*********************************/ /*! exports provided: import1 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "import1", function() { return import1; }); /* harmony import */ var _dep_1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./dep-1 */ "6xPP"); const import1 = "imported: " + _dep_1__WEBPACK_IMPORTED_MODULE_0__["dep1"]; /***/ }) }]); 

Was ist, wenn es Hunderte von npm-Abhängigkeiten gibt?


Wie gesagt, ich glaube, dass die Trennung auf Codeebene auf Paketebene es dem Entwickler normalerweise ermöglicht, eine gute Position einzunehmen, wenn die Codetrennung aggressiv, aber nicht zu aggressiv ist.

Wenn Ihre Anwendung Module aus Hunderten verschiedener npm-Pakete importiert, kann es natürlich immer noch vorkommen, dass der Browser sie nicht alle effektiv laden kann.

Wenn Sie jedoch wirklich viele npm-Abhängigkeiten haben, sollten Sie diese Strategie vorerst nicht vollständig aufgeben. Denken Sie daran, dass Sie wahrscheinlich nicht alle npm-Abhängigkeiten auf jeder Seite herunterladen werden. Daher ist es wichtig herauszufinden, wie viele Abhängigkeiten tatsächlich geladen werden.

Ich bin mir jedoch sicher, dass es einige echte Anwendungen gibt, die so viele npm-Abhängigkeiten aufweisen, dass diese Abhängigkeiten einfach nicht als separate Fragmente dargestellt werden können. Wenn Ihr Projekt genau das ist - ich würde empfehlen, dass Sie nach einer Möglichkeit suchen, Pakete zu gruppieren, bei denen sich der Code, in dem sich mit hoher Wahrscheinlichkeit gleichzeitig ändern kann (wie " react und " react-dom da die Fragment-Cache- react-dom mit diesen Paketen ausgeführt wird zur gleichen Zeit. Später werde ich ein Beispiel zeigen, in dem alle React-Abhängigkeiten in demselben Fragment gruppiert sind.

Fortsetzung folgt…

Liebe Leser! Wie gehen Sie in Ihren Projekten mit dem Problem der Codetrennung um?

Source: https://habr.com/ru/post/de466537/


All Articles