在多页面应用程序中使用 React

我一直在玩的反应,到目前为止,我真的很喜欢它。我正在用 NodeJS 构建一个应用程序,并希望在应用程序的一些交互组件中使用 React。我不想让它单页应用程序。

我还没有在网上找到任何可以回答以下问题的东西:

如何在多页面应用程序中分解或捆绑 React 组件?

目前,我所有的组件都在一个文件中,尽管我可能永远不会在应用程序的某些部分加载它们。

到目前为止,我正在尝试使用条件语句通过搜索 React 将要呈现的容器的 ID 来呈现组件。我不能100% 确定使用 React 的最佳实践是什么。看起来像这样。

if(document.getElementById('a-compenent-in-page-1')) {
React.render(
<AnimalBox url="/api/birds" />,
document.getElementById('a-compenent-in-page-1')
);
}


if(document.getElementById('a-compenent-in-page-2')) {
React.render(
<AnimalBox url="/api/cats" />,
document.getElementById('a-compenent-in-page-2')
);
}


if(document.getElementById('a-compenent-in-page-3')) {
React.render(
<AnimalSearchBox url="/api/search/:term" />,
document.getElementById('a-compenent-in-page-3')
);
}

我仍然在阅读文档,还没有找到我需要一个多页面应用程序。

先谢谢你。

100370 次浏览

我正在从头开始构建一个应用程序,并且一直在学习,但是我认为您正在寻找的是 反应路由器。React-路由器将组件映射到特定的 URL。例如:

render((
<Router>
<Route path="/" component={App}>
<Route path="api/animals" component={Animals}>
<Route path="birds" component={Birds}/>
<Route path="cats" component={Cats}/>
</Route>
</Route>
<Route path="api/search:term" component={AnimalSearchBox}>
</Router>
), document.body)

在搜索案例中,“ term”可以作为 AnimalSearchBox 中的一个属性访问:

componentDidMount() {
// from the path `/api/search/:term`
const term = this.props.params.term
}

试试看。这个教程是一个把我在顶部的方面,我对这个和其他相关的主题的理解。


原答案如下:

我来这里也是为了寻找同样的答案。看看 这个的帖子是否能激发你的灵感。如果您的应用程序与我的应用程序类似,那么它的某些区域变化很小,并且只在主体中变化。您可以创建一个小部件,它的职责是根据应用程序的状态呈现一个不同的小部件。通过使用通量体系结构,您可以分派一个导航操作,该操作可以更改 body 小部件的开关状态,从而有效地仅更新页面的主体。

这就是我现在正在尝试的方法。

你正在使用 CMS 吗? 他们往往喜欢改变网址,这可能会破坏你的应用程序。

另一种方法是使用类似 反应栖息地的东西。

使用它,您可以注册组件,它们会自动暴露给 dom。

例子

注册组件:

container.register('AnimalBox', AnimalBox);
container.register('AnimalSearchBox', AnimalSearchBox);

然后,他们可以在你的领地像这样:

<div data-component="AnimalBox"></div>


<div data-component="AnimalSearchBox"></div>

以上将自动替换为您的反应组件。

然后,您也可以自动将属性(或道具)传递给组件:

<div data-component="AnimalBox" data-prop-size="small"></div>

这将向您的组件公开 size作为道具。有 额外的选择用于传递其他类型,如 json、 array、 int、 float 等。

目前,我正在做类似的事情。

这个应用程序并不是一个完整的 React 应用程序,我使用的是动态的 React,比如 CommentBox,它是 autark。并且可以包含在具有特殊参数的任何点上。.

但是,我所有的子应用程序都被加载并包含在一个单独的文件 all.js中,因此浏览器可以跨页面缓存它。

当我需要在 SSR 模板中包含一个应用程序时,我只需要包含一个 DIV,它的类是“ _ _ response-root”,还有一个特殊的 ID (要呈现的 React 应用程序的名称)

其逻辑非常简单:

import CommentBox from './apps/CommentBox';
import OtherApp from './apps/OtherApp';


const APPS = {
CommentBox,
OtherApp
};


function renderAppInElement(el) {
var App = APPS[el.id];
if (!App) return;


// get props from elements data attribute, like the post_id
const props = Object.assign({}, el.dataset);


ReactDOM.render(<App {...props} />, el);
}


document
.querySelectorAll('.__react-root')
.forEach(renderAppInElement)

<div>Some Article</div>
<div id="CommentBox" data-post_id="10" class="__react-root"></div>


<script src="/all.js"></script>

剪辑

由于 webpack 完全支持代码分割和 LazyLoading,我认为包含一个例子是有意义的,在这个例子中,你不需要将所有的应用程序装入一个包中,而是将它们分割并按需装入。

import React from 'react';
import ReactDOM from 'react-dom';


const apps = {
'One': () => import('./One'),
'Two': () => import('./Two'),
}


const renderAppInElement = (el) => {
if (apps[el.id])  {
apps[el.id]().then((App) => {
ReactDOM.render(<App {...el.dataset} />, el);
});
}
}

您可以在 webpack.config.js 文件中为应用程序提供几个入口点:

var config = {
entry: {
home: path.resolve(__dirname, './src/main'),
page1: path.resolve(__dirname, './src/page1'),
page2: path.resolve(__dirname, './src/page2'),
vendors: ['react']
},
output: {
path: path.join(__dirname, 'js'),
filename: '[name].bundle.js',
chunkFilename: '[id].chunk.js'
},
}

然后你可以在你的 src 文件夹中有三个不同的 html 文件和它们各自的 js 文件(例如第1页) :

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Page 1</title>
</head>
<body>
<div id="app"></div>
<script src="./vendors.js"></script>
<script src="./page1.bundle.js"></script>
</body>
</html>

JavaScript 文件:

import React from 'react'
import ReactDom from 'react-dom'
import App from './components/App'
import ComponentA from './components/ReactComponentA'
ReactDom.render(<div>
<App title='page1' />
<ReactComponentA/>
</div>, document.getElementById('app'))

然后可以为每个页面加载不同的 React 组件。

我知道已经有一段时间没有人问过这个问题了,但是希望这个问题对某些人有所帮助。

正如@Cocomico 提到的,您可以在 webpack.config.js 文件中为应用程序提供几个入口点。如果您正在寻找一个简单的 Webpack 设置(基于多个入口点的想法) ,允许您将 React 组件添加到静态页面,您可以考虑使用这个: https://github.com/przemek-nowicki/multi-page-app-with-react

我建议你看一下惯性: https://inertiajs.com/

有了惯性,你就可以构建应用程序,就像你一直选择的服务器端 web 框架一样。您可以使用框架的现有功能进行路由、控制器、中间件、身份验证、授权、数据获取等等。

唯一不同的是你的视图层。而不是使用服务器端呈现(例如,。Blade 或 ERB 模板) ,视图是 JavaScript 页面组件。这允许您使用 React、 Vue 或 Svelte 构建整个前端。

我重提这个老问题,因为我处在同样的情况下,没有找到一个答案,可以满足我的需要。因此,基于@webdeb 的回答,我编写了一个小框架,它使用 CRA (没有弹出)来在任何 HTML 页面中注入尽可能多的组件,同时保留了 CRA 的所有优点。

DR

你可以检查我的公共回购 给你,其中包含所有需要的文件和链接到一个 中篇文章,我在那里彻底解释所有这些东西。

大概的意思

诀窍是像平常一样安装 CRA,并按如下方式更新 index.js文件:

import React, { Suspense } from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import reportWebVitals from './reportWebVitals';


//list here all the components that could be inserted in a web page
const apps = {
'App': React.lazy(() => import('./App')),
'TestComponent1': React.lazy(() => import('./TestComponent1')),
'TestComponent2': React.lazy(() => import('./TestComponent2')),
}


//event manager to communicate between the components
const bridgeEvent = new EventTarget();
//common fallback for all the components
function Fallback() {
return <div>Loading...</div>;
}
const renderAppInElement = (el) => {
if(apps[el.dataset.reactComponent] && !el.dataset.rendered){
//get the component's name stored in the data-react-component attribute
const App = apps[el.dataset.reactComponent];
//render the component, inject all the HTML attributes and the Event bridge
ReactDOM.render(
<Suspense fallback={<Fallback />}>
<App  {...el.dataset} bridgeEvent={bridgeEvent}/>
</Suspense>
, el);
el.dataset.rendered = true;
}
else if(el.dataset.rendered){
console.log('el', el, 'is already rendered')
}
}


//ONLY FOR THE DEV PHASE
const rootEl = document.getElementById('root');
//generate components without attributes
if(process.env.REACT_APP_RENDER_CMP){
const components = process.env.REACT_APP_RENDER_CMP.split(',');
  

components.forEach(item => {
const componentEl = document.createElement('div');
componentEl.setAttribute("data-react-component", item);
componentEl.className = "__react-cmp";
rootEl.append(componentEl);
});
}
//generate components with attributes
if(process.env.REACT_APP_RENDER_CMP_WITH_ATTRS){
let componentsWithAttrs;
try{
componentsWithAttrs = JSON.parse(process.env.REACT_APP_RENDER_CMP_WITH_ATTRS);
}
catch(e){
console.log('fail to parse REACT_APP_RENDER_CMP_WITH_ATTRS', e);
}
if(componentsWithAttrs){
componentsWithAttrs.forEach(cmp => {
const componentEl = document.createElement('div');
componentEl.setAttribute("data-react-component", cmp.class);
componentEl.className = "__react-cmp";
Object.keys(cmp.data).forEach(attrKey => {
componentEl.setAttribute(attrKey, cmp.data[attrKey]);
});
rootEl.append(componentEl);
});
}
}


//the default name of the global object is ReactComponents, but it could be customized via the REACT_APP_NAMESPACE environment variable
const appNamespace = process.env.REACT_APP_NAMESPACE || "ReactComponents";
window[appNamespace] = {
ready: false,
parseComponents(container){
//parse the container or the whole document and inject all the components in the containers that have a "__react-cmp" class
(container || document)
.querySelectorAll('.__react-cmp')
.forEach(renderAppInElement);
}
}
window[appNamespace].parseComponents();
window[appNamespace].ready = true;


//if dynamic parsing must be done via the window.ReactComponents.parseComponents() method
//check the availability of window.ReactComponents object via window.ReactComponents.ready property
//or define a window.ReactComponentsAsyncInit() method to be notified of the availability
if(typeof window[`${appNamespace}AsyncInit`] === 'function'){
window[`${appNamespace}AsyncInit`]();
}


// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
reportWebVitals();

然后,您可以添加 REACT_APP_RENDER_CMP和/或 REACT_APP_RENDER_CMP_WITH_ATTRS环境变量来测试您的组件,同时使用 CRA 的开发服务器。你的 .env.development.local文件可能看起来像:

#this will render the TestComponent1 and TestComponent2 without any attributes
REACT_APP_RENDER_CMP="TestComponent1,TestComponent2"


#this will render TestComponent1 with the data-test-attribute attribute set to "test attribute value"
REACT_APP_RENDER_CMP_WITH_ATTRS="[{"class":"TestComponent1","data":{"data-test-attribute":"test attribute value"}}]"

构建文件之后,您应该拥有 index.html文件,其中包含所有需要包含在多页应用程序的每个页面中的 .js.css文件,这些文件应该加载您的 React 组件。不要忘记在 .env文件中添加 INLINE_RUNTIME_CHUNK=false,以避免任何内联的 javascript!

然后,将组件的容器添加到 HTML 页面中您希望它们显示的位置,例如:

<div class="__react-cmp" data-react-component="TestComponent1"></div>

应该执行在 CRA 的 index.js文件中声明的 parseComponents(),用 .__react-cmp类抓取 div,然后将其用作 TestComponent1 React 组件的容器。

在专用的 回购文章中,我解释了如何用 CRA 的 BUILD_PATH环境变量改变构建路径(这样你就可以把构建好的文件放在服务器或 CDN 中) ,我还提供了一个加载程序,它可以解析构建好的 index.html文件,并在页面中动态插入所有需要的 .js.css文件(这样你只需要包含加载程序,而不是所有的文件)。下面是加载程序的外观,假设它的文件名是 cmp-loader.js并且驻留在构建的 index.html文件旁边:

(async () => {
const head = document.getElementsByTagName('head')[0];
const scriptSrcRegexp = new RegExp('<script.*?src="(.*?)"', 'gmi');


//get the exact script's src as defined in the src attribute
const scriptSrc = scriptSrcRegexp.exec(document.currentScript.outerHTML);
//all the resources should be relative to the path of this script
const resourcesPath = (scriptSrc && scriptSrc.length > 1) ? scriptSrc[1].replace('cmp-loader.js', '') : '';


//get the index content
const indexHTML = await (await fetch(resourcesPath+'index.html', {cache:'reload'})).text();


//assume that all the .js and .css files to load are in the "static" folder
const reactCSSRegexp = new RegExp(`<link href="${resourcesPath}static\/css\/(.*?)\.css" rel="stylesheet">`, 'gm');
const reactJSRegexp = new RegExp(`<script (.*?) src="${resourcesPath}static\/js\/(.*?)\.js"><\/script>`, 'gm');


//grab all the css tags
const ReactCSS = [].concat(indexHTML.match(reactCSSRegexp)).join('');
//grab all the js tags
const ReactJS = [].concat(indexHTML.match(reactJSRegexp)).join('');


//parse and execute the scripts
const scriptsDoc = new DOMParser().parseFromString(ReactJS, 'text/html');
Array.from(scriptsDoc.getElementsByTagName('script')).forEach(item => {
const script = document.createElement('script');
[...item.attributes].forEach(attr => {
script.setAttribute(attr.name, attr.value)
})
head.appendChild(script);
});
//inject the CSS
head.insertAdjacentHTML('beforeend', ReactCSS);
})().catch(e => {
console.log('fail to load react-cmp', e)
});