使用 React 构建微前端
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
我们都知道微服务架构有助于独立开发、部署和维护应用程序。例如 Uber,其预订和支付服务就是独立开发和部署的。同样,我们也可以独立开发和部署 React 应用程序。这意味着,您的应用程序被拆分成多个独立的应用程序,便于维护,这种概念被称为微前端。
今天,我们将看到一个使用微前端的 React 网站,其中我们创建了三个应用程序:博客应用程序、头部应用程序和容器应用程序(它们将博客和头部粘合在一起,代表一个单一的 Web 应用程序)。
创建 React 应用程序 -
让我们创建三个 React 应用程序,
- 博客(网站博客应用)
- 网站头部(网站头部)
- 容器(实际网站,我们将博客和页眉合并在了这里)
npx create-react-app container
npx create-react-app blogs
npx create-react-app header
博客应用程序 -
让我们创建一个常量,添加博客数组,
export const arrBlogs = [
{
"blogID": 1,
"blogName": "React Navigation",
"blogDetail": "Hello Developers! Let's see what's new in React Navigation 6.x.",
"blogURL": "https://www.kpiteng.com/blogs/react-nativagation-6.x"
},
{
"blogID": 2,
"blogName": "Securing React Native Application",
"blogDetail": "Discover a ways to develop secure react native application.",
"blogURL": "https://www.kpiteng.com/blogs/securing-react-native-application"
},
{
"blogID": 3,
"blogName": "Top 10 React Tricks Every Developer Should Use",
"blogDetail": "Discover a set of React best coding practices, tips and tricks that should be known by all developers.",
"blogURL": "https://www.kpiteng.com/blogs/top-10-react-tricks-every-developer-should-use"
}
]
让我们来编写博客列表的代码,创建一个名为Blog.js 的文件。
import React, { useState, useEffect } from "react";
import {arrBlogs} from './Constant';
import {
Link
} from "react-router-dom";
import "./App.css";
function App() {
return (
<div className="container mt-5">
<div className="row">
{
arrBlogs.map((blog, index) => {
return (
<div className="col-xs-12 col-sm-12 col-md-6 col-lg-4 col-xl-4 mb-5">
<div className="card">
<Link to={{pathname: `/blogdetail/${blog.blogID}`, id: blog.blogID, item: blog}} >
<div class="card-body">
<h5 class="card-title">{`#${blog.blogID}`}</h5>
<p class="card-text">{blog.blogName}</p>
<p class="card-text">{blog.blogDetail}</p>
</div>
</Link>
</div>
</div>
)
})
}
</div>
</div>
);
}
export default App;
博客位于 url.com/blogs,因此我们需要设置react-router-dom 和 history。
yarn add react-router-dom history
要查看博客详情,我们需要设置 BlogDetail 的代码,创建文件BlogDetail.js。
import React, { useState, useEffect } from "react";
import {arrBlogs} from './Constant';
import "./App.css";
function BlogDetail(props) {
const [blogDetail, setBlogDetail] = useState({});
useEffect(() => {
const blogID = parseInt(props.match.params.blogid);
const index = arrBlogs.findIndex((blog) => blog.blogID === blogID);
if (index !== -1){
setBlogDetail(arrBlogs[index])
}
}, []);
return (
<div className="container mt-5">
<div className="row">
<div className="card">
{
Object.keys(blogDetail).length > 0 && <>
<p>{`#${blogDetail.blogID}`}</p>
<p>{blogDetail.blogName}</p>
<p>{blogDetail.blogDetail}</p>
<p>{blogDetail.blogURL}</p>
</>
}
{
Object.keys(blogDetail).length === 0 &&
<p>We're sorry, Cound't find Blog</p>
}
</div>
</div>
</div>
);
}
export default BlogDetail;
最后,我们有了常量、博客和博客详情。现在让我们为博客和博客详情的路由编写代码。将 App.js 中的代码替换为以下代码:
import React, { useState, useEffect } from "react";
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import { createBrowserHistory } from "history";
import Blogs from './Blogs';
import BlogDetail from './BlogDetail';
import "./App.css";
const defaultHistory = createBrowserHistory();
function App({ history = defaultHistory }) {
return (
<Router>
<Switch>
<Route exact path="/" component={Blogs} />
<Route exact path="/blogdetail/:blogid" component={BlogDetail} />
</Switch>
</Router>
);
}
export default App;
现在,是时候运行应用程序了。我们可以看到博客列表,点击博客名称即可跳转到博客详情页面。
头部应用 -
在这里,我们简单地添加一个头部 div 元素来演示头部应用。接下来,让我们添加所有必需的依赖项。
yarn add react-router-dom history
让我们修改 App.js 的代码。
import React from "react";
import { createBrowserHistory } from "history";
import "./App.css";
const defaultHistory = createBrowserHistory();
function App({ history = defaultHistory }) {
return (
<div>
<p>KPITENG (Header Application)</p>
</div>
);
}
export default App;
现在,让我们运行该应用程序,它将显示一个简单的标题栏。
所以,我们已经准备好了两个应用程序,博客应用程序——我们在其中编写博客列表的代码,页眉应用程序——我们在其中编写在应用程序中显示页眉的代码。
容器应用程序 -
现在,是时候设置我们的容器应用程序了,它实际上会将 Header 应用程序和 Blogs 应用程序合并到我们的容器应用程序(我们的主网站)中。
让我们将 react-router-dom 和 history 添加到容器应用程序中。之后,让我们更新 App.js 的代码。
import React, { useState } from "react";
import { BrowserRouter, Switch, Route } from "react-router-dom";
import { createBrowserHistory } from "history";
import MicroFrontend from "./MicroFrontend";
import "./App.css";
const defaultHistory = createBrowserHistory();
const {
REACT_APP_HEADER_HOST: headerHost,
REACT_APP_BLOGS_HOST: blogHost,
} = process.env;
function Header({ history }) {
return <MicroFrontend history={history} host={headerHost} name="Header" />;
}
function Blogs({ history }) {
return <MicroFrontend history={history} host={blogHost} name="Blogs" />;
}
function BlogDetail({history}) {
return (
<div>
<MicroFrontend history={history} host={blogHost} name="Blogs" />
</div>
);
}
function Home({ history }) {
return (
<div className="container">
<Header />
<Blogs />
</div>
);
}
function App() {
return (
<BrowserRouter>
<React.Fragment>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/blogdetail/:blogid" component={BlogDetail} />
</Switch>
</React.Fragment>
</BrowserRouter>
);
}
export default App;
设置微前端 -
想想我的容器应用是如何知道头部应用和博客应用的。我们来逐一设置。
*设置 Web 应用程序端口 - *
容器应用程序 - 端口 3000
头部应用程序 - 端口 3001
博客应用程序 - 端口 3002
为此,请更新 package.json 文件。
容器应用程序
"scripts": {
"start": "PORT=3000 react-app-rewired start",
},
头部应用程序,
"scripts": {
"start": "PORT=3001 react-app-rewired start",
},
博客应用,
"scripts": {
"start": "PORT=3002 react-app-rewired start",
},
现在,在容器应用程序的根目录中创建.env文件。
REACT_APP_HEADER_HOST= http://localhost:3001
REACT_APP_BLOGS_HOST= http://localhost:3002
你知道,React App 会将整个应用程序打包到 main.js 文件中,其中包含渲染、挂载和卸载组件的函数。
Render Function Name: render{ApplicationName}
UnMount Function Name: unmount{ApplicationName}
所以,你的博客应用看起来是这样的:
renderBlogs
unmountBlogs
同样,头部应用程序看起来是这样的:
renderHeader
unmountHeader
让我们在容器应用程序中创建一个 MicroFrontend.js 文件,其中包含组件的挂载和卸载的业务逻辑。
import React, { useEffect } from "react";
function MicroFrontend({ name, host, history }) {
useEffect(() => {
const scriptId = `micro-frontend-script-${name}`;
const renderMicroFrontend = () => {
window[`render${name}`](`${name}-container`, history);
};
if (document.getElementById(scriptId)) {
renderMicroFrontend();
return;
}
fetch(`${host}/asset-manifest.json`)
.then((res) => res.json())
.then((manifest) => {
const script = document.createElement("script");
script.id = scriptId;
script.crossOrigin = "";
script.src = `${host}${manifest.files["main.js"]}`;
script.onload = () => {
renderMicroFrontend();
};
document.head.appendChild(script);
});
return () => {
window[`unmount${name}`] && window[`unmount${name}`](`${name}-container`);
};
});
return <main id={`${name}-container`} />;
}
MicroFrontend.defaultProps = {
document,
window,
};
export default MicroFrontend;
如您所见,MicroFrontend 组件将名称、主机和历史记录作为参数。请注意 fetch 函数,它会从主机获取 asset-manifest.json 文件,并使用 main.js 创建一个脚本对象,然后使用 render 函数来挂载组件。
为博客应用程序设置微前端 -
让我们安装 react-app-rewired 包,它会在不弹出应用程序的情况下覆盖构建配置。
yarn add react-app-rewired
在博客应用程序的根目录中创建 config.overrides.js 文件,并添加以下代码。
module.exports = {
webpack: (config, env) => {
config.optimization.runtimeChunk = false;
config.optimization.splitChunks = {
cacheGroups: {
default: false,
},
};
config.output.filename = "static/js/[name].js";
config.plugins[5].options.filename = "static/css/[name].css";
config.plugins[5].options.moduleFilename = () => "static/css/main.css";
return config;
},
};
现在,让我们更新 package.json 文件中的 scripts 部分。
"scripts": {
"start": "PORT=3002 react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject"
},
博客应用程序的最后一步是更新 index.js 文件。
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
window.renderBlogs = (containerId, history) => {
ReactDOM.render(
<App history={history} />,
document.getElementById(containerId),
);
};
window.unmountBlogs = containerId => {
ReactDOM.unmountComponentAtNode(document.getElementById(containerId));
};
if (!document.getElementById('Blogs-container')) {
ReactDOM.render(<App />, document.getElementById('root'));
}
为头部应用程序设置微前端 -
- 安装 react-app-rewired
- 更新 package.json
- 更新 index.js 文件
最后,我们运行容器应用程序(我们的主要Web应用程序)。
感谢阅读博客!
KPITENG | 数字转型
www.kpiteng.com/blogs | hello@kpiteng.com
联系我们 | 关注我们 - LinkedIn | Facebook | Instagram