Fazendo a Construção Moderna

Olá Habr!

Agora, cada navegador moderno permite que você trabalhe com os Módulos ES6 .

À primeira vista, parece que isso é uma coisa completamente inútil - afinal, todos nós usamos colecionadores que substituem as importações por seus desafios internos. Mas se você se aprofundar nas especificações, acontece que, graças a elas, você pode fornecer um conjunto separado para navegadores modernos.

Sob o gato, há uma história de como consegui reduzir o tamanho do aplicativo em 11%, sem prejuízo dos navegadores antigos e dos meus nervos.



Recursos dos módulos ES6


O ES6 Modules é um sistema modular bem conhecido e amplamente utilizado para todos:

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

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

Para usar esse sistema modular em navegadores, você precisa adicionar o tipo de módulo a cada tag de script. Os navegadores mais antigos verão que o tipo é diferente de texto / javascript e não executará o arquivo como JavaScript.

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

A especificação também possui um atributo nomodule para tags de script. Os navegadores que suportam os Módulos ES6 ignoram esse script e os navegadores mais antigos fazem o download e o executam.

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

Acontece que você pode simplesmente fazer dois assemblies: o primeiro com o tipo de módulo para navegadores modernos (Modern Build) e o outro com o nomodule para navegadores antigos (fallback build):

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

Por que isso é necessário?


Antes de enviar um projeto para produção, devemos:

  • Adicione polyphiles.
  • Transponha o código moderno para um mais antigo.

Nos meus projetos, tento oferecer suporte ao número máximo de navegadores, às vezes até o IE 10 . Portanto, minha lista de polyfiles consiste em itens básicos como es6.promise, es6.object.values, etc. Mas os navegadores com módulos ES6 são compatíveis com todos os métodos ES6 e não precisam de kilobytes extras de polyfills.

A transpilação também deixa uma marca notável no tamanho do arquivo: babel / preset-env usa 25 transformadores para cobrir a maioria dos navegadores, cada um dos quais aumenta o tamanho do código. Ao mesmo tempo, para navegadores com suporte aos Módulos ES6 , o número de transformadores é reduzido para 9.

Portanto, na montagem de navegadores modernos, podemos remover polyfiles desnecessários e reduzir o número de transformadores, o que afetará bastante o tamanho dos arquivos resultantes!

Como adicionar polyfiles


Antes de preparar o Modern Build para navegadores modernos, vale a pena mencionar como adiciono polyfills ao projeto.



Normalmente, os projetos usam core-js para adicionar todos os polyfills possíveis.

Obviamente, você não deseja todos os 88 Kbytes de polyfiles dessa biblioteca, mas apenas os necessários para a sua lista de navegadores. Esse recurso está disponível usando babel / preset-env e sua opção useBuiltIns. Se você configurá-lo como entrada, a importação de core-js será substituída pela importação dos módulos individuais de que seu navegador precisa:

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

Mas, com essa transformação, nos livramos apenas de uma parte dos polifilos desnecessários e muito antigos. Ainda temos polyphiles para TypedArray, WeakMap e outras coisas estranhas que nunca são usadas no projeto.

Para superar completamente esse problema, para a opção useBuiltIns, defino o valor como use. No estágio de compilação, o babel / preset-env analisará os arquivos para usar os recursos que não estão disponíveis nos navegadores selecionados e adicionará polyfiles a eles:

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

No exemplo acima, babel / preset-env adicionou um polyphile à função de classificação. Não é possível descobrir em JavaScript que tipo de objeto será passado para a função - será uma matriz ou um objeto de classe com a função de classificação, mas babel / preset-env seleciona o pior cenário para si e insere um polyfile.

Situações em que babel / preset-env está errado acontecem o tempo todo. Para remover polyphiles desnecessários, verifique periodicamente quais deles você importa e exclua os desnecessários usando a opção de exclusão:

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

Não considero o módulo regenerator-runtime, pois uso o fast-async ( e aconselho a todos ).

Criar versão moderna


Vamos configurar o Modern Build.

Verifique se temos um arquivo de lista de navegadores no projeto que descreve todos os navegadores necessários:

 /* .browserslistrc */ > 0.5% IE 10 

Adicione a variável de ambiente BROWSERS_ENV durante a construção, que pode assumir os valores fallback (para Fallback Build) e modern (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" }, /* ... */ } 

Agora mude a configuração do babel / preset-env. Para especificar navegadores suportados na predefinição, há uma opção de destino. Ela tem uma abreviação especial - esmodules. Ao usá-lo, o babel / preset-env substituirá automaticamente os navegadores que suportam os 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 fará todo o trabalho para nós: selecionará apenas os polifiles e transformações necessários.

Agora podemos criar um projeto para navegadores modernos ou antigos apenas um comando do console!

Vincular Construção Moderna e Fallback


O último passo é combinar o Modern e o Fallback Builds em um.

Eu pretendo criar essa estrutura de projeto:

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

No index.html, haverá links para os arquivos javascript necessários de ambos os assemblies:

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

Esta etapa pode ser dividida em três partes:

  1. Crie Modern e Fallback Build em diferentes diretórios.
  2. Obtendo informações sobre os caminhos para os arquivos javascript necessários.
  3. Criando index.html com links para todos os arquivos javascript.

Vamos começar!

Construa Modern e Fallback Build em diferentes diretórios


Para começar, vamos dar o passo mais fácil - coletaremos o Modern e o Fallback Build em diferentes diretórios dentro do diretório dist.

É simplesmente impossível especificar o diretório desejado para output.path, pois precisamos que o webpack possua caminhos para os arquivos relativos ao diretório dist (o index.html está nesse diretório e todas as outras dependências serão bombeadas em relação a ele).

Crie uma função especial para gerar caminhos de arquivo:

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

O projeto começou a se reunir em diferentes diretórios do Modern e Fallback Build.

Obtendo informações sobre os caminhos para os arquivos javascript necessários


Para obter informações sobre os arquivos coletados, conecte o webpack-manifest-plugin. No final da montagem, ele adicionará um arquivo manifest.json com dados nos caminhos para os arquivos:

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

Agora temos informações sobre os arquivos coletados:

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

Criando index.html com links para todos os arquivos javascript


A única coisa que resta é adicionar index.html e inserir os caminhos para os arquivos necessários.

Para gerar o arquivo html, usarei o plugin html-webpack-plugin durante o Modern Build. O html-webpack-plugin inserirá os caminhos para os arquivos modernos em si, e eu pegarei os caminhos para os arquivos de fallback do arquivo criado na etapa anterior e os colarei no HTML usando um pequeno plugin de 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; 

Atualize o package.json:

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

Usando o comando npm run build: full, criaremos um arquivo html com o Modern e o Fallback Build. Qualquer navegador agora receberá o JavaScript que é capaz de executar.

Adicione a versão moderna ao seu aplicativo


Para testar minha solução em algo real, direcionei-a para um de meus projetos. A instalação da configuração levou menos de uma hora e o tamanho dos arquivos JavaScript diminuiu 11%. Ótimo resultado com uma implementação simples.

Obrigado por ler o artigo até o fim!

Materiais utilizados


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


All Articles