本文试图整合当前可用的工具,并找出是否有可能在没有像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
目录
包说明符导入支持
通过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' }) }) ) ) } });
总结已完成的操作:
- 我们创建了一个导出映射,将包名称与全局变量关联
- 在
head
创建一个脚本标签,并将脚本内容包装在UMD中 - 将全局变量导出为默认导出
对于演示而言,这样残酷的补丁就足够了,但这可能不适用于所有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> <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应该能够处理通过网络发送的一堆小文件。
您可以在其中查看代码的存储库