最近公司准备做一下项目整体架构的优化,采用微前端的架构,将项目拆分为一个基座主服务和N个子服务。
根据市场的反馈准备入手京东的微前端框架micro-app
,所以让我调研了一下,目前体验还不错。
首先介绍一下什么是微前端。
微前端的概念是由ThoughtWorks
在2016年提出的,它借鉴了微服务的架构理念,核心在于将一个庞大的前端应用拆分成多个独立灵活的小型应用,每个应用都可以独立开发、独立运行、独立部署,再将这些小型应用融合为一个完整的应用,或者将原本运行已久、没有关联的几个应用融合为一个应用。微前端既可以将多个项目融合为一,又可以减少项目之间的耦合,提升项目扩展性,相比一整块的前端仓库,微前端架构下的前端仓库倾向于更小更灵活。
它主要解决了两个问题:
在micro-app
之前,业内已经有一些开源的微前端框架,比较流行的有2个:single-spa
和qiankun
。
single-spa
是通过监听url change
事件,在路由变化时匹配到渲染的子应用并进行渲染,这个思路也是目前实现微前端的主流方式。同时single-spa
要求子应用修改渲染逻辑并暴露出三个方法:bootstrap
、mount
、unmount
,分别对应初始化、渲染和卸载,这也导致子应用需要对入口文件进行修改。因为qiankun
是基于single-spa
进行封装的,所以这些特点也被qiankui
继承下来,并且需要对webpack
配置进行一些修改。
micro-app
并没有沿袭single-spa
的思路,而是借鉴了WebComponent
的思想,通过CustomElement
结合自定义的ShadowDom
,将微前端封装成一个类WebComponent
组件,从而实现微前端的组件化渲染。并且由于自定义ShadowDom
的隔离特性,micro-app
不需要像single-spa
和qiankun
一样要求子应用修改渲染逻辑并暴露出方法,也不需要修改webpack
配置,是目前市面上接入微前端成本最低的方案。
WebComponent
组件内,从而实现在基座应用中嵌入一行代码即可渲染一个微前端应用。micro-app
还提供了js沙箱、样式隔离、元素隔离、预加载、数据通信、静态资源补全等一系列完善的功能。micro-app
没有任何依赖,这赋予它小巧的体积和更高的拓展行。micro-app
做了诸多兼容,在任何技术框架中都可以正常运行。了解完以上内容之后,就可以着手实战一下了,实测非常简单。
因为micro-app
对主服务和子服务的技术栈没有任何要求,所以,我们新建三个项目,my-app(React)
、my-app1(React)
、my-app2(Vue)
。
my-app
是整体项目的主服务,也就是基座,my-app1
和my-app2
都是平级的子服务。
因为是写demo
,所以React
和Vue
项目都基于其框架提供的脚手架create-react-app
和vue-cli
来构建,不基于脚手架的话用webpack
自己构建也没有任何问题。
首先我们先建立一个干净的空文件夹,名字为micro-app-demo
,通过VSCode
进入这个目录。
通过脚手架create-react-app
创建一个React
项目,并启动
npx create-react-app my-app
cd my-app
npm start
启动成功,可以看到主服务地址http://localhost:3000
。
然后安装一下micro-app
。
npm i @micro-zoe/micro-app --save
下载完成之后,进入项目的入口文件,create-react-app
创建的项目默认的入口文件是index.js
。
import microApp from '@micro-zoe/micro-app';
microApp.start();
index.js
完整代码。
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import microApp from '@micro-zoe/micro-app';
microApp.start();
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
然后主服务的配置就可以先暂停了,等子服务创建了再引入。
第一个子服务my-app1
也通过create-react-app
构建并启动。
npx create-react-app my-app1
cd my-app1
npm start
因为默认的3000
端口已经被主服务my-app
用了,所以会启动子服务my-app1
会提示是否要换另一端口启动。
输入y。
然后就可以看到它启动成功了。
可以看到子服务1的地址是http://localhost:3001
。
然后重点来了,理论上,通过micro-app
构建微前端项目,在服务间不通信的前提下,子服务只需要配置跨域就可以,其他都不需要弄,可以说是完全零侵入、低成本的方案。
通过create-react-app
构建的项目默认就进行了跨域的相关配置。
如果不放心,或者想更改webpack
的配置,可以打开控制台,输入
npm run eject
在package.json
中也可以看到这个指令。
输入完之后就一直yes
就行了。
可能你会遇到这种报错。
因为我们在构建完项目之后进行了一些代码的更改,所以提示你要先保存这些更改,才可以eject
。
git
指令大家应该都熟悉
git add .
git commit -m 'your commit'
然后再npm run eject
就没问题了。
my-app1
的项目目录内就多了一个config
文件夹,打开其中的webpackDevServer.config.js
,这就是脚手架帮我们封装的基础版的webpack
的一些配置项,39-43行可以看到关于跨域的处理。
所以你不需要做额外处理。
然后为了在视觉上方便区分这几个服务,我们在主页面内都加一个h1
标签。
my-app1/src/App.js
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1>子服务1 my-app1</h1>
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
子服务my-app2
用vue-cli
构建一个Vue
项目。
前提你要先全局安装过vue-cli
,如果没有先运行以下指令安装vue-cli
。
npm install -g @vue/cli
然后确定本地有vue-cli
的前提下运行以下指令。
vue create my-app2
cd my-app2
npm run serve
create
的时候选一个默认配置的就可以,我选的vue2
的默认配置。
可以看到子服务my-app2
启动成功了,地址是http://localhost:8080
。
同样在主页面加入一个h1
标签。
my-app2/src/App.vue
<template>
<div id="app">
<h1>子服务2 my-app2</h1>
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
通过vue-cli
构建的Vue
项目默认是没有进行跨域配置的,所以我们需要手动打开my-app2
根目录下的vue.config.js
文件,抄写一下my-app1
中做的跨域处理,即以下代码:
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Headers': '*',
},
},
})
更改vue.config.js
的配置后需要重新启动项目才能生效,所以记得讲my-app2
这个子服务,停止再启动。
回到主服务my-app
中。
你可以将my-app1
和my-app2
这两个子服务想象成你封装的两个组件,可以任意的在任何地方进行调用。
只需要通过micro-app
提供的组件来配置就可以,比如下方这种方式。
<micro-app
name='app1'
url='http://localhost:3000/'
baseroute='/my-page'
>
</micro-app
常规使用,只需要写三个属性,name
子服务的应用名称(必传),url
子服务的应用地址(必传),baseroute
主服务分配给子服务的基础路由,(可选)。
我们在src
目录下新建四个文件,三个作为页面,一个配置路由规则,给子服务们分配一下路由。
my-app/src/Page.jsx
import React from "react";
const Page = () => {
return (
<div>
<h1>主服务 Page</h1>
</div>
)
};
export default Page;
my-app/src/Page1.jsx
import React from "react";
const Page1 = () => {
return (
<div>
<micro-app
name="my-app1"
url="http://localhost:3001"
baseroute="my-app1"
></micro-app>
</div>
)
};
export default Page1;
my-app/src/Page2.jsx
import React from "react";
const Page2 = () => {
return (
<div>
<micro-app
name="my-app2"
url="http://localhost:8080"
baseroute="my-app2"
></micro-app>
</div>
)
};
export default Page2;
my-app/src/route.js
因为create-react-app
脚手架默认没有安装react-router
,所以在主服务my-app
里安一下。
npm install react-router-dom --save
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Page from './Page';
import Page1 from './Page1';
import Page2 from './Page2';
export default function AppRoute() {
return (
<BrowserRouter>
<Routes>
<Route path='/' element={<Page />}>
</Route>
<Route path='/my-app1' element={<Page1 />}>
</Route>
<Route path='/my-app2' element={<Page2 />}>
</Route>
</Routes>
</BrowserRouter>
)
}
my-app/src/App.js
import './App.css';
import AppRoute from './route';
function App() {
return (
<div className="App">
<AppRoute />
</div>
);
}
export default App;
走到这里,理论上你就可以看效果了。
http://localhost:3000
会访问主服务的默认页面,承载在Page.jsx
内。
http://localhost:3000/my-app1
会访问子服务1 my-app1
的默认页面,承载在Page1.jsx
内。
http://localhost:3000/my-app2
会访问子服务2 my-app2
的默认页面,承载在Page2.jsx
内。
通过域名我们也可以看到,这三个页面都在主服务http://localhost:3000
下可以访问的,是不是很神奇。
我有一个主服务,然后可以调用N个其他域名启动的子服务,集成在一起。
子服务理论上也可以独立运行,通过域名访问也可以看到这一点。
我分别访问http://localhost:3001
和http://localhost:8080
这些子服务没有受到任何影响。
这就是micro-app
这个微前端框架的强大之处。
通过实践,我们可以发现micro-app
这个微前端框架的,代码侵入性极低,我们只在主服务里写了几行关于微前端配置的代码,子服务在不进行通信的前提下,只需要配置跨域即可。
如此低成本就能将N个独立的子服务集成到一起,对于原服务也不会有任何影响。
甚至不限制任何技术栈,我们的主服务是React
,子服务1是React
,子服务2是Vue
,也可以很轻松的进行集成。