微前端(Micro-Frontends)是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用分解成一些更小、更简单的小模块,而在用户侧看起来还是一个内聚的产品。各个前端应用还可以独立运行、独立开发、独立部署。微前端不是单纯的前端框架或者工具,而是一套架构体系。
qiankun是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。
安装
$ yarn add qiankun # 或者 npm i qiankun -S
创建基座项目,并在基座项目中引入qiankun
这里我们基座项目使用的是vue项目,版本为vue@3.0.0,具体创建项目教程请参考创建vue,项目目录结构如下:
|-- qiankun
|-- .DS_Store
|-- .browserslistrc
|-- .eslintrc.js
|-- .gitignore
|-- README.md
|-- babel.config.js
|-- package-lock.json
|-- package.json
|-- public
| |-- favicon.ico
| |-- index.html
|-- src
|-- .DS_Store
|-- App.vue
|-- main.js
|-- assets
| |-- logo.png
|-- components
| |-- HelloWorld.vue
|-- router
| |-- index.js
|-- views
|-- About.vue
|-- Home.vue
创建子应用
创建两个子应用,分别为Vue项目和React项目:
vue目录结构:
|-- .DS_Store
|-- .browserslistrc
|-- .eslintrc.js
|-- .gitignore
|-- README.md
|-- babel.config.js
|-- package-lock.json
|-- package.json
|-- vue.config.js
|-- public
| |-- favicon.ico
| |-- index.html
|-- src
|-- App.vue
|-- main.js
|-- assets
| |-- logo.png
|-- components
| |-- HelloWorld.vue
|-- router
| |-- index.js
|-- views
|-- About.vue
|-- Home.vue
react目录结构:
react项目创建请参考创建 react
|-- .DS_Store
|-- .eslintrc.js
|-- .gitignore
|-- README.md
|-- config-overrides.js
|-- package-lock.json
|-- package.json
|-- yarn.lock
|-- public
| |-- favicon.ico
| |-- index.html
| |-- logo192.png
| |-- logo512.png
| |-- manifest.json
| |-- robots.txt
|-- src
|-- App.css
|-- App.js
|-- App.test.js
|-- index.css
|-- index.js
|-- logo.svg
|-- reportWebVitals.js
|-- setupTests.js
基座中引入
在基座项目中引入qiankun,修改main.js文件
import { registerMicroApps, start } from "qiankun";
const apps = [
{
name: 'vueApp',
entry: '//localhost:8081', // 默认会加载这个html,解析里面的js,动态执行(子应用必须支持跨域)
container: '#vue',
activeRule: '/vue',
props: { a: 1 }
},
{
name: 'reactApp',
entry: "//localhost:3000",
container: "#react",
activeRule: '/react'
}
]
registerMicroApps(apps);
start({
// sandbox: { experimentalStyleIsolation: true } //沙箱默认开启
})
createApp(App).use(ElementPlus).use(router).mount('#appBase')
registerMicroApps的相关入参请参考入参,其中:
container是针对微应用挂载的节点,
entry微应用的入口,微应用的访问地址;
activeRule微应用的激活规则,当配置为字符串时会直接跟 url 中的路径部分做前缀匹配,匹配成功表明当前应用会被激活。所以我们需要在基座项目中修改App.vue,在该项目中增加微应用的挂载节点,修改如下:
<template>
<div id="nav">
<el-menu class="el-menu-demo" :router="true" mode="horizontal">
<el-menu-item index="/">Home</el-menu-item>
<el-menu-item index="/vue">vue应用</el-menu-item>
<el-menu-item index="/react">react应用</el-menu-item>
</el-menu>
<router-view />
<div id="vue"></div>
<div id="react"></div>
</div>
</template>
子应用文件修改
我们需要根据协议,在子应用的入口文件中导出三个生命周期函数,以供主应用在适当的时机调用:
/**
* 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap
*/
export async function bootstrap() { }
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
* @param {*} props
*/
export async function mount(props) {
console.log(props);
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
* @param {*} props
*/
export async function unmount(props) {
console.log(app);
}
为了能让子应用也可独立运行,我们可以根据qiankun在全局注册一个变量window.__POWERED_BY_QIANKUN__,来区分子应用是否运行在qiankun中:
vue
// vue
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
let app = null
function render(props) {
app = createApp(App).use(router) //挂载到自己的html.基座会拿到挂载后的html,直接插入
app.mount('#app')
}
if (window.__POWERED_BY_QIANKUN__) { // 动态添加publicPath
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
if (!window.__POWERED_BY_QIANKUN__) { //
render()
}
// 子组件的协议
/**
* 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap
*/
export async function bootstrap() { }
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
* @param {*} props
*/
export async function mount(props) {
console.log(props);
render(props)
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
* @param {*} props
*/
export async function unmount(props) {
console.log(app);
app.unmount() // 卸载
}
react
// react
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
function render() {
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
}
if (window.__POWERED_BY_QIANKUN__) { // 动态添加publicPath
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
if (!window.__POWERED_BY_QIANKUN__) {
render()
}
export async function bootstrap() { };
export async function mount() {
render()
};
export async function unmount() {
ReactDOM.unmountComponentAtNode(document.getElementById('root'))
};
子应用配置修改
vue
//vue vue.config.js
module.exports = {
devServer: {
headers: {
'Access-Control-Allow-Origin': '*' //支持跨域
}
},
configureWebpack: {
output: {
library: 'vueApp',
libraryTarget: 'umd',
}
},
publicPath: 'http://localhost:8081/'
}
react
module.exports = {
webpack: (config) => {
config.output.library = 'reactApp';
config.output.libraryTarget = 'umd';
config.output.publicPath = 'http://localhost:3000/' // 静态资源路径
return config
},
devServer: (configFunction) => {
return function (proxy, allowedHost) {
const config = configFunction(proxy, allowedHost);
config.headers = {
"Access-Control-Allow-Origin": "*"
}
return config
}
}
}
运行结果
子应用独立运行结果展示
基座运行结果