Making Modern Build

Hola Habr!

Cada navegador moderno ahora le permite trabajar con módulos ES6 .

A primera vista, parece que esto es algo completamente inútil: después de todo, todos utilizamos coleccionistas que reemplazan las importaciones con sus desafíos internos. Pero si profundiza en las especificaciones, resulta que gracias a ellas puede proporcionar un ensamblaje separado para los navegadores modernos.

Under the cat es una historia sobre cómo pude reducir el tamaño de la aplicación en un 11% sin perjuicio de los navegadores antiguos y mis nervios.



Características de los módulos ES6


ES6 Modules es un sistema modular bien conocido y ampliamente utilizado para todos:

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

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

Para usar este sistema modular en los navegadores, debe agregar el tipo de módulo a cada etiqueta de script. Los navegadores más antiguos verán que el tipo es diferente de text / javascript y no ejecutarán el archivo como JavaScript.

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

La especificación también tiene un atributo nomodule para etiquetas de script. Los navegadores que admiten módulos ES6 ignorarán este script, y los navegadores más antiguos lo descargarán y ejecutarán.

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

Resulta que simplemente puede hacer dos ensamblajes: el primero con el tipo de módulo para navegadores modernos (Modern Build), y el otro con el nomodule para navegadores antiguos (Fallback build):

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

Porque es necesario


Antes de enviar un proyecto a producción, debemos:

  • Añadir polifilos.
  • Transponga el código moderno a uno más antiguo.

En mis proyectos, trato de admitir el número máximo de navegadores, a veces incluso IE 10 . Por lo tanto, mi lista de polyfiles consta de elementos básicos como es6.promise, es6.object.values, etc. Pero los navegadores con módulos ES6 son compatibles con todos los métodos ES6, y no necesitan kilobytes adicionales de polyfills.

La transpilación también deja una marca notable en el tamaño del archivo: babel / preset-env usa 25 transformadores para cubrir la mayoría de los navegadores, cada uno de los cuales aumenta el tamaño del código. Al mismo tiempo, para los navegadores con soporte para módulos ES6 , el número de transformadores se reduce a 9.

Por lo tanto, en el ensamblaje de los navegadores modernos, podemos eliminar archivos innecesarios y reducir la cantidad de transformadores, lo que afectará en gran medida el tamaño de los archivos resultantes.

Cómo agregar polyfiles


Antes de preparar Modern Build para navegadores modernos, vale la pena mencionar cómo agrego polyfills al proyecto.



Por lo general, los proyectos usan core-js para agregar todos los polyfills posibles.

Por supuesto, no desea todos los 88 Kbytes de archivos de polígonos de esta biblioteca, sino solo aquellos que son necesarios para su lista de navegadores. Esta característica está disponible usando babel / preset-env y su opción useBuiltIns. Si lo configura como entrada, la importación de core-js se reemplazará con las importaciones de los módulos individuales que necesita su navegador:

 /* .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"; /*   -  */ 

Pero con tal transformación nos libramos de solo una parte de los innecesarios polifilos muy antiguos. Todavía tenemos polífilos para TypedArray, WeakMap y otras cosas extrañas que nunca se usan en el proyecto.

Para superar completamente este problema, para la opción useBuiltIns configuré el valor de uso. En la etapa de compilación, babel / preset-env analizará los archivos para el uso de funciones que no están disponibles en los navegadores seleccionados, y les agregará archivos polifónicos:

 /* .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(); } 

En el ejemplo anterior, babel / preset-env agregó un polífilo a la función de clasificación. No puede encontrar en JavaScript qué tipo de objeto se pasará a la función: será una matriz o un objeto de clase con la función de clasificación, pero babel / preset-env selecciona el peor de los casos para sí mismo e inserta un archivo polivinílico.

Las situaciones en que babel / preset-env están mal suceden todo el tiempo. Para eliminar los polífilos innecesarios, verifique de vez en cuando cuáles importa y elimine los innecesarios con la opción de exclusión:

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

No considero el módulo regenerator-runtime, ya que uso fast-async ( y aconsejo a todos ).

Crea una construcción moderna


Configuremos Modern Build.

Asegúrese de que tengamos un archivo de lista de navegadores en el proyecto que describa todos los navegadores necesarios:

 /* .browserslistrc */ > 0.5% IE 10 

Agregue la variable de entorno BROWSERS_ENV durante la compilación, que puede tomar los valores de respaldo (para Fallback Build) y moderno (para 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" }, /* ... */ } 

Ahora cambie la configuración de babel / preset-env. Para especificar navegadores compatibles en el preajuste hay una opción de objetivos. Ella tiene una abreviatura especial: esmodules. Al usarlo, babel / preset-env sustituirá automáticamente a los navegadores que admitan módulos 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 hará todo el trabajo por nosotros aún más: seleccionará solo los polífilos y transformaciones necesarios.

¡Ahora podemos construir un proyecto para navegadores modernos o antiguos con solo un comando desde la consola!

Vincula la construcción moderna y alternativa


El último paso es combinar Modern y Fallback Builds en uno.

Planeo crear una estructura de proyecto de este tipo:

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

En index.html habrá enlaces a los archivos javascript necesarios de ambos ensamblados:

 /* 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> 

Este paso se puede dividir en tres partes:

  1. Build Modern y Fallback Build en diferentes directorios.
  2. Obteniendo información sobre las rutas a los archivos javascript necesarios.
  3. Crear index.html con enlaces a todos los archivos javascript.

¡Empecemos!

Build Modern y Fallback Build en diferentes directorios


Para comenzar, tomemos el paso más fácil: recopilaremos Modern y Fallback Build en diferentes directorios dentro del directorio dist.

Es simplemente imposible especificar el directorio deseado para output.path, ya que necesitamos que webpack tenga rutas a los archivos relativos al directorio dist (index.html está en este directorio, y todas las demás dependencias se eliminarán en relación con él).

Cree una función especial para generar rutas de archivos:

 /* 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'), }), /* ... */ ], /* ... */ } 

El proyecto comenzó a reunirse en diferentes directorios para Modern y Fallback Build.

Obtener información sobre rutas a los archivos JavaScript necesarios


Para obtener información sobre los archivos recopilados, conecte el plugin webpack-manifest-plugin. Al final del ensamblaje, agregará un archivo manifest.json con datos sobre las rutas a los archivos:

 /* 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'), }), /* ... */ ], /* ... */ } 

Ahora tenemos información sobre los archivos recopilados:

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

Crear index.html con enlaces a todos los archivos javascript


Lo único que queda es agregar index.html e insertar las rutas a los archivos necesarios en él.

Para generar el archivo html, usaré html-webpack-plugin durante Modern Build. El plugin html-webpack insertará las rutas a los archivos modernos en sí, y obtendré las rutas a los archivos de respaldo del archivo creado en el paso anterior y los pegaré en el HTML usando un pequeño 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; 

Actualizar package.json:

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

Usando el comando npm run build: full, crearemos un archivo html con Modern y Fallback Build. Cualquier navegador ahora recibirá el JavaScript que puede ejecutar.

Agregue Modern Build a su aplicación


Para probar mi solución en algo real, la conduje a uno de mis proyectos. Configurar la configuración me llevó menos de una hora, y el tamaño de los archivos JavaScript disminuyó en un 11%. Gran resultado con una implementación simple.

¡Gracias por leer el artículo hasta el final!

Materiales utilizados


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


All Articles