Faire une construction moderne

Bonjour, Habr!

Chaque navigateur moderne vous permet désormais de travailler avec les modules ES6 .

À première vue, il semble que ce soit une chose complètement inutile - après tout, nous utilisons tous des collectionneurs qui remplacent les importations par leurs défis internes. Mais si vous vous plongez dans les spécifications, il s'avère que grâce à elles, vous pouvez donner un assemblage séparé pour les navigateurs modernes.

Under the cat est une histoire sur la façon dont j'ai pu réduire la taille de l'application de 11% sans préjudice pour les anciens navigateurs et mes nerfs.



Caractéristiques des modules ES6


ES6 Modules est un système modulaire bien connu et largement utilisé pour tout le monde:

/* someFile.js */ import { someFunc } from 'path/to/helpers.js' 

 /* helpers.js */ export function someFunc() { /* ... */ } 

Pour utiliser ce système modulaire dans les navigateurs, vous devez ajouter le type de module à chaque balise de script. Les navigateurs plus anciens verront que le type est différent de text / javascript et n'exécutera pas le fichier en JavaScript.

 <!--        ES6 Modules --> <script type="module" src="/path/to/someFile.js"></script> 

La spécification possède également un attribut nomodule pour les balises de script. Les navigateurs qui prennent en charge les modules ES6 ignoreront ce script, et les navigateurs plus anciens le téléchargeront et l'exécuteront.

 <!--       --> <script nomodule src="/path/to/someFileFallback.js"></script> 

Il s'avère que vous pouvez simplement créer deux assemblys: le premier avec le type de module pour les navigateurs modernes (Modern Build), et l'autre avec le nomodule pour les anciens navigateurs (Fallback build):

 <script type="module" src="/path/to/someFile.js"></script> <script nomodule src="/path/to/someFileFallback.js"></script> 

Pourquoi est-ce nécessaire


Avant d'envoyer un projet en production, nous devons:

  • Ajoutez des polyphiles.
  • Transposez le code moderne en un code plus ancien.

Dans mes projets, j'essaye de supporter le nombre maximum de navigateurs, parfois même IE 10 . Par conséquent, ma liste de fichiers polyfichiers comprend des éléments de base tels que es6.promise, es6.object.values, etc. Mais les navigateurs avec les modules ES6 prennent en charge toutes les méthodes ES6, et ils n'ont pas besoin de kilo-octets supplémentaires de polyfills.

La transpilation laisse également une marque notable sur la taille du fichier: babel / preset-env utilise 25 transformateurs pour couvrir la plupart des navigateurs, chacun augmentant la taille du code. Dans le même temps, pour les navigateurs prenant en charge les modules ES6 , le nombre de transformateurs est réduit à 9.

Ainsi, dans l'assemblage pour les navigateurs modernes, nous pouvons supprimer les polyfichiers inutiles et réduire le nombre de transformateurs, ce qui affectera considérablement la taille des fichiers résultants!

Comment ajouter des polyfichiers


Avant de préparer Modern Build pour les navigateurs modernes, il convient de mentionner comment j'ajoute des polyfills au projet.



En règle générale, les projets utilisent core-js pour ajouter tous les polyfills possibles.

Bien sûr, vous ne voulez pas tous les 88 Ko de fichiers polyfiles de cette bibliothèque, mais seulement ceux qui sont nécessaires pour votre liste de navigateurs. Cette fonctionnalité est disponible en utilisant babel / preset-env et son option useBuiltIns. Si vous le définissez sur entry, l'importation de core-js sera remplacée par l'importation des modules individuels dont vos navigateurs ont besoin:

 /* .babelrc.js */ module.exports = { presets: [ ['@babel/preset-env', { useBuiltIns: 'entry', /* ... */ }] ], /* ... */ }; 

 /*   */ import 'core-js'; 

 /*   */ import "core-js/modules/es6.array.copy-within"; import "core-js/modules/es6.array.fill"; import "core-js/modules/es6.array.find"; /*   -  */ 

Mais par une telle transformation, nous ne nous sommes débarrassés que d'une partie des très vieux polyphiles inutiles. Nous avons encore des polyphiles pour TypedArray, WeakMap et d'autres choses étranges qui ne sont jamais utilisées dans le projet.

Pour surmonter complètement ce problème, pour l'option useBuiltIns, j'ai défini la valeur à usage. Au stade de la compilation, babel / preset-env analysera les fichiers pour utiliser des fonctionnalités qui ne sont pas disponibles dans les navigateurs sélectionnés et leur ajoutera des polyfichiers:

 /* .babelrc.js */ module.exports = { presets: [ ['@babel/preset-env', { useBuiltIns: 'usage', /* ... */ }] ], /* ... */ }; 

 /*   */ function sortStrings(strings) { return strings.sort(); } function createResolvedPromise() { return Promise.resolve(); } 

 /*   */ import "core-js/modules/es6.array.sort"; import "core-js/modules/es6.promise"; function sortStrings(strings) { return strings.sort(); } function createResolvedPromise() { return Promise.resolve(); } 

Dans l'exemple ci-dessus, babel / preset-env a ajouté un polyphile à la fonction de tri. Vous ne pouvez pas découvrir en JavaScript quel type d'objet sera transmis à la fonction - ce sera un tableau ou un objet de classe avec la fonction de tri, mais babel / preset-env sélectionne le pire des cas pour lui-même et insère un polyfichier.

Des situations où babel / preset-env est incorrect se produisent tout le temps. Pour supprimer les polyphiles inutiles, vérifiez de temps à autre ceux que vous importez et supprimez ceux qui sont inutiles à l'aide de l'option d'exclusion:

 /* .babelrc.js */ module.exports = { presets: [ ['@babel/preset-env', { useBuiltIns: 'usage', //   ,  ,     debug: true, //      exclude: ['es6.regexp.to-string', 'es6.number.constructor'], /* ... */ }] ], /* ... */ }; 

Je ne considère pas le module d'exécution du régénérateur, car j'utilise fast-async ( et je conseille à tout le monde ).

Créer une construction moderne


Configurons la construction moderne.

Assurez-vous que nous avons un fichier de liste de navigateurs dans le projet qui décrit tous les navigateurs nécessaires:

 /* .browserslistrc */ > 0.5% IE 10 

Ajoutez la variable d'environnement BROWSERS_ENV lors de la génération, qui peut prendre les valeurs fallback (pour Fallback Build) et modern (pour Modern Build):

 /* package.json */ { "scripts": { /* ... */ "build": "NODE_ENV=production webpack /.../", "build:fallback": "BROWSERS_ENV=fallback npm run build", "build:modern": "BROWSERS_ENV=modern npm run build" }, /* ... */ } 

Modifiez maintenant la configuration de babel / preset-env. Pour spécifier les navigateurs pris en charge dans le préréglage, il existe une option cible. Elle a une abréviation spéciale - esmodules. Lors de son utilisation, babel / preset-env remplacera automatiquement les navigateurs prenant en charge les modules ES6 .

 /* .babelrc.js */ const isModern = process.env.BROWSERS_ENV === 'modern'; module.exports = { presets: [ ['@babel/preset-env', { useBuiltIns: 'usage', //  Modern Build     ES6 modules, //   Fallback Build     .browsersrc targets: isModern ? { esmodules: true } : undefined, /* ... */ }] ], /* ... */ ], }; 

Babel / preset-env fera tout le travail pour nous plus loin: il ne sélectionnera que les polyphiles et transformations nécessaires.

Maintenant, nous pouvons construire un projet pour les navigateurs modernes ou anciens juste une commande à partir de la console!

Lier la construction moderne et de secours


La dernière étape consiste à combiner les versions moderne et de secours en une seule.

Je prévois de créer une telle structure de projet:

 //     dist/ //  html- index.html //   Modern Build' modern/ ... //   Fallback Build' fallback/ ... 

Dans index.html, il y aura des liens vers les fichiers javascript nécessaires des deux assemblys:

 /* index.html */ <html> <head> <!-- ... --> </head> <body> <!-- ... --> <script type="module" src="/modern/js/app.540601d23b6d03413d5b.js"></script> <script nomodule src="/fallback/js/app.4d03e1af64f68111703e.js"></script> </body> </html> 

Cette étape peut être divisée en trois parties:

  1. Build Modern et Fallback Build dans différents répertoires.
  2. Obtenir des informations sur les chemins d'accès aux fichiers javascript nécessaires.
  3. Création d'index.html avec des liens vers tous les fichiers javascript.

Commençons!

Construisez des versions modernes et de secours dans différents répertoires


Pour commencer, prenons l'étape la plus simple - nous collecterons les versions Modern et Fallback Build dans différents répertoires du répertoire dist.

Il est tout simplement impossible de spécifier le répertoire souhaité pour output.path, car nous avons besoin que webpack ait des chemins vers des fichiers relatifs au répertoire dist (index.html est dans ce répertoire, et toutes les autres dépendances seront pompées par rapport à lui).

Créez une fonction spéciale pour générer des chemins de fichiers:

 /* getFilePath.js */ /*   ,       */ const path = require('path'); const isModern = process.env.BROWSERS_ENV === 'modern'; const prefix = isModern ? 'modern' : 'fallback'; module.exports = relativePath => ( path.join(prefix, relativePath) ); 

 /* webpack.prod.config.js */ const getFilePath = require('path/to/getFilePath'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = { mode: 'production', output: { path: 'dist', filename: getFilePath('js/[name].[contenthash].js'), }, plugins: [ new MiniCssExtractPlugin({ filename: getFilePath('css/[name].[contenthash].css'), }), /* ... */ ], /* ... */ } 

Le projet a commencé à se rassembler dans différents répertoires pour Modern et Fallback Build.

Obtenir des informations sur les chemins d'accès aux fichiers javascript nécessaires


Pour obtenir des informations sur les fichiers collectés, connectez le plugin webpack-manifest-plugin. À la fin de l'assemblage, il ajoutera un fichier manifest.json avec des données sur les chemins d'accès aux fichiers:

 /* webpack.prod.config.js */ const getFilePath = require('path/to/getFilePath'); const WebpackManifestPlugin = require('webpack-manifest-plugin'); module.exports = { mode: 'production', plugins: [ new WebpackManifestPlugin({ fileName: getFilePath('manifest.json'), }), /* ... */ ], /* ... */ } 

Maintenant, nous avons des informations sur les fichiers collectés:

 /* manifest.json */ { "app.js": "/fallback/js/app.4d03e1af64f68111703e.js", /* ... */ } 

Création d'index.html avec des liens vers tous les fichiers javascript


Il ne reste plus qu'à ajouter index.html et y insérer les chemins d'accès aux fichiers nécessaires.

Pour générer le fichier html, j'utiliserai le plug-in html-webpack pendant Modern Build. Le plugin html-webpack insérera les chemins d'accès aux fichiers modernes lui-même, et je récupérerai les chemins d'accès aux fichiers de secours à partir du fichier créé à l'étape précédente et les coller dans le HTML à l'aide d'un petit plugin webpack:

 /* webpack.prod.config.js */ const HtmlWebpackPlugin = require('html-webpack-plugin'); const ModernBuildPlugin = require('path/to/ModernBuildPlugin'); module.exports = { mode: 'production', plugins: [ ...(isModern ? [ //  html-  Modern Build new HtmlWebpackPlugin({ filename: 'index.html', }), new ModernBuildPlugin(), ] : []), /* ... */ ], /* ... */ } 

 /* ModernBuildPlugin.js */ // Safari 10.1    nomodule. //      Safari   . //    : // https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc const safariFix = '!function(){var e=document,t=e.createE/* ...   ... */'; class ModernBuildPlugin { apply(compiler) { const pluginName = 'modern-build-plugin'; //    Fallback Build const fallbackManifest = require('path/to/dist/fallback/manifest.json'); compiler.hooks.compilation.tap(pluginName, (compilation) => { //    html-webpack-plugin, //      HTML compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync(pluginName, (data, cb) => { //  type="module"  modern- data.body.forEach((tag) => { if (tag.tagName === 'script' && tag.attributes) { tag.attributes.type = 'module'; } }); //    Safari data.body.push({ tagName: 'script', closeTag: true, innerHTML: safariFix, }); //  fallback-   nomodule const legacyAsset = { tagName: 'script', closeTag: true, attributes: { src: fallbackManifest['app.js'], nomodule: true, defer: true, }, }; data.body.push(legacyAsset); cb(); }); }); } } module.exports = ModernBuildPlugin; 

Mettre à jour package.json:

 /* package.json */ { "scripts": { /* ... */ "build:full": "npm run build:fallback && npm run build:modern" }, /* ... */ } 

En utilisant la commande npm run build: full, nous allons créer un fichier html avec Modern et Fallback Build. Tout navigateur recevra désormais le code JavaScript qu'il est capable d'exécuter.

Ajoutez une construction moderne à votre application


Pour tester ma solution sur quelque chose de réel, je l'ai intégrée à l'un de mes projets. L'installation de la configuration m'a pris moins d'une heure et la taille des fichiers JavaScript a diminué de 11%. Excellent résultat avec une implémentation simple.

Merci d'avoir lu l'article jusqu'au bout!

Matériaux utilisés


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


All Articles