在没有Webpack的浏览器中进行React,JSX,ES模块的导入(包括动态)

本文试图整合当前可用的工具,并找出是否有可能在没有像Webpack这样的收集器进行初步编译的情况下,在React上创建可用于生产的应用程序,或者至少将这种编译减至最少。


所描述的一切都是实验性的,我故意在某些地方偷工减料。 我绝不建议在实际生产中执行类似的操作。


很长时间以来,直接在浏览器中使用ECMAScript模块( <script type="module"/>import Foo from './foo';import('./Foo') )的功能已经不是什么新鲜事物了,它是受支持的功能: https: //caniuse.com/#feat=es6-module


但实际上,我们不仅导入模块,还导入库。 关于此主题的文章非常出色: https : //salomvary.com/es6-modules-in-browsers.html 。 另外值得一提的好文章是https://github.com/stken2050/esm-bundlerless


在这些文章中的其他重要内容中,这些要点对于创建React应用程序最重要:


  • 支持包说明符导入(或导入映射):实际上,当我们import React from 'react'编写import React from 'react'我们应该导入类似https://cdn.com/react/react.production.js
  • 支持UMD:React仍以UMD的形式分发,目前作者尚未就如何将库作为模块分发达成共识
  • x
  • CSS导入

让我们依次讨论所有要点。


项目结构


首先,我们将确定项目的结构:


  • node_modules很明显,这是放置依赖项的地方
  • 带有index*.html和服务脚本的src目录
    • app直接在React上应用代码

包说明符导入支持


通过import React from 'react';来使用反应import React from 'react'; 我们必须告诉浏览器在哪里寻找真正的来源,因为 react不是真正的文件,而是指向库的指针。 这个https://github.com/guybedford/es-module-shims有一个存根。


让我们设置存根和React:


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

我们将从文件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> 

src/app/index.jsx看起来像这样:


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

src/app/Button.jsx像这样:


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

这样行吗? 当然不是 即使所有内容均已从必要的位置成功导入。


让我们继续下一个问题。


UMD支持


动态方式


基于React作为UMD分发的事实, 即使通过存根也无法将其直接导入(如果票是在维修时关闭的,则可以跳过此步骤)。 我们需要以某种方式修补源,以使其兼容。


上面的文章提示我为此使用Service Workers,它可以拦截和修改网络请求和响应。 创建主入口点src/index.js ,在这里我们将配置SW和App并使用它,而不是直接调用应用程序( 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); } })(); 

创建一个Service Worker( 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' }) }) ) ) } }); 

总结已完成的操作:


  1. 我们创建了一个导出映射,将包名称与全局变量关联
  2. head创建一个脚本标签,并将脚本内容包装在UMD中
  3. 将全局变量导出为默认导出

对于演示而言,这样残酷的补丁就足够了,但这可能不适用于所有UMD包装器。 可以使用更可靠的东西作为回报。


现在更改src/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">...    </script> <!--  app/index.jsx  index.js --> <script type="module-shim" src="index.js"></script> </body> </html> 

现在我们可以导入React和React DOM。


静态路径


应当指出,还有另一种方式。 React有一个非正式的ES版本:


 npm install esm-react --save 

导入图将如下所示:


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

但是不幸的是该项目远远落后,最新版本是16.8.3而React已经是16.10.2


x


有两种方法可以编译JSX。 我们可以从控制台中预先组装传统的Babel,也可以在浏览器运行时完成。 对于生产本身,预编译是可取的,但是在开发模式下,可以在运行时进行。 由于我们已经有一个Service Worker,我们将使用它。


使用Babel安装特殊软件包:


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

现在将以下内容添加到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' }) }) ) ) } }); 

在这里,我们使用相同的方法来拦截网络请求并重写它们,我们使用Babel来转换原始源代码。 请注意,动态导入的插件称为syntax-dynamic-import ,与通常的@babel/plugin-syntax-dynamic-import因为它是独立版本。


的CSS


在提到的文章中,作者使用了文本转换,我们将进一步介绍,并将CSS嵌入页面中。 为此,我们将再次使用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' }) }) ) ); } }); 

瞧! 如果现在在浏览器中打开src/index-dev.html ,我们将看到按钮。 确保已安装了必需的Service Worker,并且没有任何冲突。 如果不确定,则以防万一,您可以打开Dev Tools,转到Service Workers Application ,然后为所有已注册的Worker单击Unregister ,然后重新加载页面。


生产量


上面的代码在开发模式下可以正常工作,但是就其本身而言,我们不想强迫站点用户在其浏览器中编译代码,这是完全不切实际的。 让我们建立一种简约的生产模式。


创建一个单独的src/index.html入口点:


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

如您所见,这里没有存根,我们将使用另一种方法重写包名称。 由于我们需要Babel再次编译JSX,因此将使用它来importMap.json存根importMap.json路径,而不是importMap.json 。 安装必要的软件包:


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

package.json添加带有脚本的部分:


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

添加.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('../../', '../') } ] ] } 

应当记住,此文件仅用于生产,在开发模式下,我们在Service Worker中配置了Babel。


将战斗模式添加到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'); } 

将条件添加到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'); 

更换


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


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

让我们创建一个小的控制台脚本build.sh (使用Windows的人可以在Windows中以图像和相似方式创建相同的脚本),该脚本会将您需要的所有内容收集到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 

我们采用这种方式,以便node_modules目录node_modules因仅在构建阶段和开发模式下所需的依赖项而在生产中膨胀。


最终存储库: http : //github.com/kirill-konshin/pure-react-with-dynamic-imports


如果现在打开build/index.html则将看到与src/index-dev.html相同的输出,但是这次浏览器将不收集任何内容,它将使用Babel先前收集的文件。


如您所见,该解决方案具有重复项: .babelrc.js文件的alias部分以及要复制到build.sh的文件列表。 它将用于演示,但通常应以某种方式实现自动化。


该程序集可从以下网址获得: https//kirill-konshin.imtqy.com/pure-react-with-dynamic-imports/index.html


结论


通常,尽管非常原始,但是获得了完全可行的产品。


HTTP2应该能够处理通过网络发送的一堆小文件。


您可以在其中查看代码的存储库

Source: https://habr.com/ru/post/zh-CN474672/


All Articles