Reagir, JSX, importação de módulos ES (incluindo dinâmico) em um navegador sem Webpack

Este artigo é uma tentativa de reunir as ferramentas disponíveis no momento e descobrir se é possível criar aplicativos prontos para produção no React sem compilação preliminar de colecionadores como o Webpack, ou pelo menos minimizar essa compilação.


Tudo o que é descrito é muito experimental e, deliberadamente, corto alguns cantos. Em nenhum caso eu recomendo fazer algo assim na produção real.


A capacidade de usar módulos ECMAScript ( <script type="module"/> com importações do formulário import Foo from './foo'; e import('./Foo') ) diretamente no navegador não é nova há muito tempo, é uma funcionalidade bem suportada: https: //caniuse.com/#feat=es6-module .


Mas, na realidade, importamos não apenas nossos módulos, mas também bibliotecas. Há um excelente artigo sobre esse tópico: https://salomvary.com/es6-modules-in-browsers.html . E outro artigo igualmente bom que vale a pena mencionar é https://github.com/stken2050/esm-bundlerless .


Entre as outras coisas importantes desses artigos, esses pontos são mais importantes para criar um aplicativo React:


  • Suporte para importações de especificadores de pacotes (ou mapas de importação): quando escrevemos import React from 'react' , de fato, devemos importar algo como este https://cdn.com/react/react.production.js
  • Suporte para UMD: o React ainda está distribuído como UMD e, no momento, os autores ainda não concordaram em como distribuir a biblioteca como um módulo
  • Jsx
  • Importação de CSS

Vamos passar por todos os pontos por sua vez.


Estrutura do projeto


Primeiro, determinaremos a estrutura do projeto:


  • node_modules obviamente é aqui que as dependências serão colocadas
  • diretório src com index*.html e scripts de serviço
    • app diretamente o código do aplicativo no React

Suporte para importações de especificador de pacote


Para usar React através da import React from 'react'; devemos informar ao navegador onde procurar a fonte real, porque react não é um arquivo real, mas um ponteiro para uma biblioteca. Há um esboço para este https://github.com/guybedford/es-module-shims .


Vamos configurar o esboço e Reagir:


 $ npm i es-module-shims react react-dom --save 

public/index-dev.html o aplicativo a partir do arquivo public/index-dev.html :


 <!DOCTYPE html> <html> <body> <div id="root"></div> <script defer src="../node_modules/es-module-shims/dist/es-module-shims.js"></script> <script type="importmap-shim"> { "imports": { "react": "../node_modules/react/umd/react.development.js", "react-dom": "../node_modules/react-dom/umd/react-dom.development.js" } } </script> <script type="module-shim"> import './app/index.jsx'; </script> </body> </html> 

Onde src/app/index.jsx parece com isso:


 import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; (async () => { const {Button} = await import('./Button.jsx'); const root = document.getElementById('root'); ReactDOM.render(( <div> <Button>Direct</Button> </div> ), root); })(); 

E src/app/Button.jsx assim:


 import React from 'react'; export const Button = ({children}) => <button>{children}</button>; 

Isso vai funcionar? Claro que não. Mesmo apesar de tudo ser importado com sucesso de onde necessário.


Vamos para o próximo problema.


Suporte UMD


Maneira dinâmica


Com base no fato de o React ser distribuído como UMD, ele não pode ser importado diretamente, mesmo através de um stub (se o ticket foi fechado como reparado, você pode pular a etapa). Precisamos de alguma forma corrigir a fonte para que ela se torne compatível.


Os artigos acima me levaram a usar técnicos de serviço para isso, que podem interceptar e modificar solicitações e respostas de rede. Vamos criar o ponto de entrada principal src/index.js , onde configuraremos o SW e o aplicativo e o usaremos em vez de invocar diretamente o aplicativo ( src/app/index.jsx ):


 (async () => { try { const registration = await navigator.serviceWorker.register('sw.js'); await navigator.serviceWorker.ready; const launch = async () => import("./app/index.jsx"); //   SW       // https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#clientsclaim if (navigator.serviceWorker.controller) { await launch(); } else { navigator.serviceWorker.addEventListener('controllerchange', launch); } } catch (error) { console.error('Service worker registration failed', error); } })(); 

Crie um trabalhador de serviço ( src/sw.js ):


 //         //@see https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#clientsclaim self.addEventListener('activate', event => event.waitUntil(clients.claim())); const globalMap = { 'react': 'React', 'react-dom': 'ReactDOM' }; const getGlobalByUrl = (url) => Object.keys(globalMap).reduce((res, key) => { if (res) return res; if (matchUrl(url, key)) return globalMap[key]; return res; }, null); const matchUrl = (url, key) => url.includes(`/${key}/`); self.addEventListener('fetch', (event) => { const {request: {url}} = event; console.log('Req', url); const fileName = url.split('/').pop(); const ext = fileName.includes('.') ? url.split('.').pop() : ''; if (!ext && !url.endsWith('/')) { url = url + '.jsx'; } if (globalMap && Object.keys(globalMap).some(key => matchUrl(url, key))) { event.respondWith( fetch(url) .then(response => response.text()) .then(body => new Response(` const head = document.getElementsByTagName('head')[0]; const script = document.createElement('script'); script.setAttribute('type', 'text/javascript'); script.appendChild(document.createTextNode( ${JSON.stringify(body)} )); head.appendChild(script); export default window.${getGlobalByUrl(url)}; `, { headers: new Headers({ 'Content-Type': 'application/javascript' }) }) ) ) } else if (url.endsWith('.js')) { // rewrite for import('./Panel') with no extension event.respondWith( fetch(url) .then(response => response.text()) .then(body => new Response( body, { headers: new Headers({ 'Content-Type': 'application/javascript' }) }) ) ) } }); 

Para resumir o que foi feito:


  1. Criamos um mapa de exportação que associa um nome de pacote a uma variável global
  2. Crie uma tag de script na head com o conteúdo do script envolto em UMD
  3. Variável global exportada como exportação padrão

Para uma demonstração, um patch tão brutal será suficiente, mas isso pode não funcionar com todos os wrappers UMD. Algo mais confiável pode ser usado em troca.


Agora mude src/index-dev.html para usar o script de configuração:


 <!DOCTYPE html> <html> <body> <div id="root"></div> <script defer src="../node_modules/es-module-shims/dist/es-module-shims.js"></script> <script type="importmap-shim">...    </script> <!--  app/index.jsx  index.js --> <script type="module-shim" src="index.js"></script> </body> </html> 

Agora podemos importar React e React DOM.


Caminho estático


Note-se que existe outro caminho. Existe uma versão ES não oficial do React:


 npm install esm-react --save 

O mapa de importação ficará assim:


 { "imports": { "react": "../node_modules/esm-react/src/react.js", "react-dom": "../node_modules/esm-react/src/react-dom.js" } } 

Mas, infelizmente, o projeto está muito atrasado, a versão mais recente é 16.8.3 enquanto o React já é 16.10.2 .


Jsx


Existem duas maneiras de compilar o JSX. Podemos pré-montar o Babel tradicional a partir do console, ou isso pode ser feito no tempo de execução do navegador. Para a produção em si, a pré-compilação é preferível, mas no modo de desenvolvimento é possível em tempo de execução. Como já temos um trabalhador de serviço, vamos usá-lo.


Instale um pacote especial com Babel:


 $ npm install @babel/standalone --save-dev 

Agora adicione o seguinte ao Service Worker ( src/sw.js ):


 # src/sw.js //     importScripts('../node_modules/@babel/standalone/babel.js'); //     self.addEventListener('fetch', (event) => { //       } else if (url.endsWith('.jsx')) { event.respondWith( fetch(url) .then(response => response.text()) .then(body => new Response( //TODO  Babel.transform(body, { presets: [ 'react', ], plugins: [ 'syntax-dynamic-import' ], sourceMaps: true }).code, { headers: new Headers({ 'Content-Type': 'application/javascript' }) }) ) ) } }); 

Aqui usamos a mesma abordagem para interceptar solicitações de rede e reescrevê-las, usamos Babel para transformar o código fonte original. Observe que o plug-in para importações dinâmicas é chamado de syntax-dynamic-import , não como o habitual @babel/plugin-syntax-dynamic-import porque é uma versão autônoma.


CSS


No artigo mencionado, o autor usou a transformação de texto, iremos um pouco mais além e incorporaremos CSS na página. Para fazer isso, usaremos novamente o Service Worker ( src/sw.js ):


 //     self.addEventListener('fetch', (event) => { //      +  } else if (url.endsWith('.css')) { event.respondWith( fetch(url) .then(response => response.text()) .then(body => new Response( ` const head = document.getElementsByTagName('head')[0]; const style = document.createElement('style'); style.setAttribute('type', 'text/css'); style.appendChild(document.createTextNode( ${JSON.stringify(body)} )); head.appendChild(style); export default null; `, { headers: new Headers({ 'Content-Type': 'application/javascript' }) }) ) ); } }); 

Voila! Se agora abrirmos o src/index-dev.html em um navegador, veremos os botões. Verifique se o Service Worker necessário está instalado e não está em conflito com nada. Se você não tiver certeza, é possível abrir as Ferramentas de Desenvolvimento, ir para Application , em Service Workers , clicar em Unregister para todos os trabalhadores registrados e recarregar a página.


Produção


O código acima funciona como deveria no modo de desenvolvimento, mas, por si só, não queremos forçar os usuários do site a compilar código em seus navegadores, isso é completamente impraticável. Vamos criar algum tipo de modo de produção minimalista.


Crie um ponto de entrada src/index.html separado:


 <!DOCTYPE html> <html> <body> <div id="root"></div> <script type="module" src="index.js"></script> </body> </html> 

Como você pode ver, não há stubs aqui, usaremos outro método para reescrever os nomes dos pacotes. Como precisamos que o Babel compile o JSX novamente, vamos usá-lo para importMap.json caminhos em vez de importMap.json para o stub. Instale os pacotes necessários:


 $ npm install @babel/cli @babel/core @babel/preset-react @babel/plugin-syntax-dynamic-import babel-plugin-module-resolver --save-dev 

Adicione uma seção com scripts ao package.json :


 { "scripts": { "start": "npm run build -- --watch", "build": "babel src/app --out-dir build/app --source-maps --copy-files" } } 

Adicione o arquivo .babelrc.js :


 module.exports = { presets: [ '@babel/preset-react' ], plugins: [ '@babel/plugin-syntax-dynamic-import', [ 'babel-plugin-module-resolver', { alias: { 'react': './node_modules/react/umd/react.development.js', 'react-dom': './node_modules/react-dom/umd/react-dom.development.js' }, //       build resolvePath: (sourcePath, currentFile, opts) => resolvePath(sourcePath, currentFile, opts).replace('../../', '../') } ] ] } 

Deve-se ter em mente que esse arquivo será usado apenas para produção; no modo de desenvolvimento, configuramos o Babel no Service Worker.


Adicione o modo de combate ao Service Worker:


 // src/index.js if ('serviceWorker' in navigator) { (async () => { try { //   const production = !window.location.toString().includes('index-dev.html'); const config = { globalMap: { 'react': 'React', 'react-dom': 'ReactDOM' }, production }; const registration = await navigator.serviceWorker.register('sw.js?' + JSON.stringify(config)); await navigator.serviceWorker.ready; const launch = async () => { if (production) { await import("./app/index.js"); } else { await import("./app/index.jsx"); } }; // https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#clientsclaim if (navigator.serviceWorker.controller) { await launch(); } else { navigator.serviceWorker.addEventListener('controllerchange', launch); } } catch (error) { console.error('Service worker registration failed', error); } })(); } else { alert('Service Worker is not supported'); } 

Adicione as condições ao src/sw.js :


 // src/sw.js const {globalMap, production} = JSON.parse((decodeURIComponent(self.location.search) || '?{}').substr(1)); if (!production) importScripts('../node_modules/@babel/standalone/babel.js'); 

Substitua


 // src/sw.js if (!ext && !url.endsWith('/')) { url = url + '.jsx' with } 

Ativado


 // src/sw.js if (!ext && !url.endsWith('/')) { url = url + '.' + (production ? 'js' : 'jsx'); } 

Vamos criar um pequeno script de console build.sh (as pessoas com Windows podem criar o mesmo para Windows na imagem e semelhança) que coletará tudo o que você precisa no diretório de build :


 #  rm -rf build #   mkdir -p build/scripts mkdir -p build/node_modules #   cp -r ./node_modules/react ./build/node_modules/react cp -r ./node_modules/react-dom ./build/node_modules/react-dom #  ,      cp ./src/*.js ./build cp ./src/index.html ./build/index.html #  npm run build 

Seguimos esse caminho para que o diretório node_modules não incha na produção a partir de dependências necessárias apenas na fase de construção e no modo de desenvolvimento.


Repositório final: http://github.com/kirill-konshin/pure-react-with-dynamic-imports


Se agora abrirmos o build/index.html veremos a mesma saída que em src/index-dev.html mas desta vez o navegador não coletará nada, ele usará os arquivos coletados anteriormente por Babel.


Como você pode ver, há duplicação na solução: importMap.json , seção de alias do arquivo .babelrc.js e uma lista de arquivos a serem copiados para build.sh . Serve para uma demonstração, mas, em geral, deve ser de alguma forma automatizada.


A montagem está disponível em: https://kirill-konshin.imtqy.com/pure-react-with-dynamic-imports/index.html


Conclusão


Em geral, um produto completamente viável foi obtido, embora muito cru.


O HTTP2 deve cuidar de vários arquivos pequenos enviados pela rede.


Repositório onde você pode ver o código

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


All Articles