微前端 - qiankun学习笔记
前言
qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。
本文主要记录下如何接入 qiankun 微前端。主应用使用 vue2,子应用使用 vue3、react。
一、主应用
主应用不限技术栈,只需要提供一个容器 DOM,然后注册微应用并 start 即可。
1、创建项目
// @vue/cli 5.0.4
npm install @vue/cli -g
vue create main-vue
主应用选择 vue2.x 版本。 具体创建步骤,便不在此一一叙述。
项目创建之后,配置路由,页面布局等。整体效果如下图。
二、安装 qiankun
npm i qiankun -S
3、 注册微应用并启动
新建微应用子列表文件 micros/app.js
// src/micros/app.js
// 子应用列表
const apps = [
{
name: 'vue2-app', // 子应用app name 推荐与子应用的package的name一致
entry: '//localhost:8081/', // 子应用的入口地址,就是你子应用运行起来的地址
container: '#micro-container', // 挂载子应用内容的dom节点 `# + dom id`【见上面app.vue】
activeRule: '/vue2App' // 子应用的路由前缀
},
{
name: 'vue3-app',
entry: '//localhost:8082/',
container: '#micro-container',
activeRule: '/vue3App'
},
{
name: 'react-app',
entry: '//localhost:8083/',
container: '#micro-container',
activeRule: '/react'
}
]
export default apps
注册微应用
// src/micros/index.js
import { addGlobalUncaughtErrorHandler, registerMicroApps, start} from 'qiankun'
// 微应用的信息
import apps from './app'
/**
* 注册微应用
* 第一个参数 - 微应用的注册信息
* 第二个参数 - 全局生命周期钩子
*/
registerMicroApps(apps, {
// qiankun 生命周期钩子 - 微应用加载前
beforeLoad: (app) => {
// 加载微应用前,加载进度条
console.log('before load=====', app.name)
return Promise.resolve()
},
// qiankun 生命周期钩子 - 微应用挂载后
afterMount: (app) => {
// 加载微应用前,进度条加载完成
console.log('after mount=====', app.name)
return Promise.resolve()
}
}
)
/**
* 添加全局的未捕获异常处理器
*/
addGlobalUncaughtErrorHandler((event) => {
console.error(event)
const { message: msg } = event
if (msg && msg.includes('died in status LOADING_SOURCE_CODE')) {
console.error('微应用加载失败,请检查应用是否可运行')
}
})
// 导出 qiankun 的启动函数
export default start
配置主应用路由
// src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'home',
component: () => import('@/components/Home.vue'),
children: [{
path: '/',
name: 'hello',
component: () => import('@/views/HomeView.vue')
},{
path: '/vue2App',
name: 'vue2App'
}, {
path: '/vue3App',
name: 'vue3App'
}, {
path: '/vue3App/list',
name: 'vueAppList'
}, {
path: '/react',
name: 'react'
}]
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
页面设置子应用的挂载节点
<template>
<div class="wrapper">
<MyHeader></MyHeader>
<el-container class="content">
<el-aside width="200px">
<MySider></MySider>
</el-aside>
<el-main>
<router-view></router-view>
<!-- 挂载子应用节点 -->
<div id="micro-container"></div>
</el-main>
</el-container>
</div>
</template>
<script>
import MyHeader from './Header.vue'
import MySider from './Sider.vue'
export default {
name: 'MyHome',
components: {
MyHeader,
MySider
},
data() {
return {
}
}
}
</script>
<style lang="less">
.content {
height: calc(100% - 50px);
}
</style>
在 main.js 中引入并启动 qiankun
// src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import '../src/assets/style/reset.less'
import './plugins/element.js'
import start from '@/micros'
// 启动
start()
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
二、微应用
微应用分为有 webpack 构建和无 webpack 构建项目,有 webpack 的微应用(主要是指 Vue、React、Angular)需要做的事情有:
新增 public-path.js 文件,用于修改运行时的 publicPath。
微应用建议使用 history 模式的路由,需要设置路由 base,值和它的 activeRule 是一样的。
在入口文件最顶部引入 public-path.js,修改并导出三个生命周期函数。
修改 webpack 打包,允许开发环境跨域和 umd 打包。
无 webpack 构建的微应用直接将 lifecycles 挂载到 window 上即可。
微应用无需安装 qiankun。
三、vue2-app 微应用
1、创建项目
// vue-cli 2.9.6
npm install vue-cli -g
npm install webpack-cli -g
npm init webpack vue-app
2、在 src 目录新增 public-path.js
// src/public-path.js
if(window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
3、修改路由文件,建议使用history 模式的路由,并设置路由 base,值和它的 activeRule 是一样的。
// src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
Vue.use(Router)
export default new Router({
mode: 'history',
base: window.__POWERED_BY_QIANKUN__ ? "/vue2App" : "/",
routes: [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld
}
]
})
4、入口文件 main.js 修改,为了避免根 id #app 与其他的 DOM 冲突,需要限制查找范围。并导出三个生命周期函数。
// src/main.js
import Vue from 'vue'
import App from './App'
import router from './router'
import "./public-path";
Vue.config.productionTip = false
// 定义一个Vue实例
let instance = null
// 渲染方法
function render(props = {}) {
const { container } = props
instance = new Vue({
router,
render: (h) => h(App)
}).$mount(container ? container.querySelector('#app'): '#app')
}
// 独立运行时
if(!window.__POWERED_BY_QIANKUN__) {
render()
}
//暴露主应用生命周期钩子
/**
* bootstrap : 在微应用初始化的时候调用一次,之后的生命周期里不再调用
*/
export async function bootstrap() {
console.log('vue2-app bootstraped');
}
/**
* mount : 在应用每次进入时调用
*/
export async function mount(props) {
console.log('vue2-app mount', props);
render(props);
}
/**
* unmount :应用每次 切出/卸载 均会调用
*/
export async function unmount() {
console.log("vue2-app unmount")
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
}
5、修改 webpack 打包,允许开发环境跨域和 umd 打包。
// build/webpack.base.conf.js
'use strict'
const config = require('../config')
const APP_NAME = require('../package.json').name
module.exports = {
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath,
// 微应用的包名,这里与主应用中注册的微应用名称一致
library: APP_NAME,
// 将你的 library 暴露为所有的模块定义下都可运行的方式
libraryTarget: "umd",
// 按需加载相关,设置为 webpackJsonp_VueMicroApp 即可
jsonpFunction: `webpackJsonp_${APP_NAME}`,
},
...
}
// build/webpack.base.conf.js
'use strict'
const config = require('../config')
const APP_NAME = require('../package.json').name
module.exports = {
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath,
// 微应用的包名,这里与主应用中注册的微应用名称一致
library: APP_NAME,
// 将你的 library 暴露为所有的模块定义下都可运行的方式
libraryTarget: "umd",
// 按需加载相关,设置为 webpackJsonp_VueMicroApp 即可
jsonpFunction: `webpackJsonp_${APP_NAME}`,
},
...
}
运行效果如下:
四、vue3-app 微应用
1、创建项目
// @vue/cli 5.0.4
npm install @vue/cli -g
vue create vue3-app
2、在 src 目录新增 public-path.ts
// src/public-path.ts
if ((window as any).__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
3、修改路由文件。
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "home",
component: () => import("@/components/Home.vue"),
children: [
{
path: "/",
name: "index",
component: () => import("../views/HomeView.vue"),
},
{
path: "/list",
name: "list",
component: () => import("../views/AboutView.vue"),
},
],
},
];
const router = createRouter({
history: createWebHistory(
window.__POWERED_BY_QIANKUN__ ? "/vue3App" : process.env.BASE_URL
),
routes,
});
export default router;
4、入口文件 main.ts 修改
// src/main.ts
import Vue, { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import "./public-path.ts";
// 定义一个Vue实例
let instance: Vue.App<Element>;
// 需要定义该接口,否则`/src/router/index.ts`无法使用`Window.__POWERED_BY_QIANKUN__`
declare global {
interface Window {
__POWERED_BY_QIANKUN__?: string;
}
}
interface IRenderProps {
container: Element | string;
}
// 渲染方法
function render(props: IRenderProps) {
const { container } = props;
instance = createApp(App);
instance
.use(store)
.use(router)
.mount(
container instanceof Element
? (container.querySelector("#app") as Element)
: (container as string)
);
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render({ container: "#app" });
}
//暴露主应用生命周期钩子
/**
* bootstrap : 在微应用初始化的时候调用一次,之后的生命周期里不再调用
*/
export async function bootstrap() {
console.log("vue3-app bootstraped");
}
/**
* mount : 在应用每次进入时调用
*/
export async function mount(props: any) {
console.log("mount vue3-app", props);
render(props);
}
/**
* unmount :应用每次 切出/卸载 均会调用
*/
export async function unmount() {
console.log("unmount vue3-app app");
instance.unmount();
}
修改 webpack 打包,允许开发环境跨域和 umd 打包。
注意:webpack5 中 jsonpFunction 修改为 chunkLoadingGlobal
// vue.config.js
const path = require("path");
const APP_NAME = require("./package.json").name;
function resolve(dir) {
return path.join(__dirname, dir);
}
module.exports = {
outputDir: "dist",
assetsDir: "static",
filenameHashing: true,
devServer: {
host: "localhost",
hot: true,
port: 8082,
client: {
overlay: {
errors: true,
warnings: false,
},
},
// 配置跨域请求头,解决开发环境的跨域问题
headers: {
"Access-Control-Allow-Origin": "*",
},
},
// 自定义webpack配置
configureWebpack: {
resolve: {
alias: {
"@": resolve("src"),
},
},
output: {
// 把子应用打包成 umd 库格式
// // 微应用的包名,这里与主应用中注册的微应用名称一致
library: APP_NAME,
// 将你的 library 暴露为所有的模块定义下都可运行的方式
libraryTarget: "umd",
// 按需加载相关,设置为 webpackJsonp_微应用名称 即可
chunkLoadingGlobal: `webpackJsonp_${APP_NAME}`,
},
},
};
运行效果如下:
五、react-app 微应用
1、创建项目,以 create-react-app 生成的 react 17 项目为例,搭配 react-router-dom 6.x。
npx create-react-app react-app --template typescript
npm i react-router-dom
在根目录下添加 .env 文件,设置项目监听的端口
// react-app/.env
PORT=8083
BROWSER=none
2、新建 public-path.ts
// src/public-path.ts
if ((window as any).__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
3、如上面代码报错,可以通过补充定义进行修复。以下代码最好放到全局引入的 TypeScript 定义文件中。
interface Window {
__POWERED_BY_QIANKUN__?: string
__INJECTED_PUBLIC_PATH_BY_QIANKUN__?: string
}
declare let __webpack_public_path__: string | undefined
4、设置 history 模式路由的 base
// src/App.tsx
import React from 'react';
import { Routes, Route, BrowserRouter} from 'react-router-dom'
import './App.css';
import Home from './components/home';
function App() {
return (
<div>
{/* 设置路由命名空间 */}
<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/react' : '/'}>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</BrowserRouter>
</div>
);
}
export default App;
5、修改入口文件 index.tsx
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import './types.d.ts'
import "./public-path";
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log('react-app bootstraped');
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props: any) {
console.log('react-app mount');
ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount(props: any) {
console.log('react-app unmount');
ReactDOM.unmountComponentAtNode(props.container ? props.container.querySelector('#root') : document.getElementById('root'));
}
/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props: any) {
console.log('react-app update props', props);
}
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// 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();
6、webpack 配置,安装插件 react-app-rewired
npm install react-app-rewired -D
7、修改 package.json
// react-app/package.json
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject"
}
8、在 react-app-rewired 配置完成后,新建 config-overrides.js 文件来配置 webpack。
// react-app/config-overrides.js
const path = require("path");
const APP_NAME = require("./package.json").name;
module.exports = {
webpack: (config) => {
// 微应用的包名,这里与主应用中注册的微应用名称一致
config.output.library = APP_NAME;
// 将你的 library 暴露为所有的模块定义下都可运行的方式
config.output.libraryTarget = "umd";
// 按需加载相关,设置为 webpackJsonp_VueMicroApp 即可
config.output.chunkLoadingGlobal = `webpackJsonp_${APP_NAME}`;
config.output.globalObject = 'window';
config.output.publicPath = `//localhost:${process.env.PORT}/`;
config.resolve.alias = {
...config.resolve.alias,
"@": path.resolve(__dirname, "src"),
};
return config;
},
devServer: function (configFunction) {
return function (proxy, allowedHost) {
const config = configFunction(proxy, allowedHost);
// 关闭主机检查,使微应用可以被 fetch
// config.disableHostCheck = true;
config.allowedHosts = "all";
// 配置跨域请求头,解决开发环境的跨域问题
config.headers = {
"Access-Control-Allow-Origin": "*",
};
// 配置 history 模式
config.historyApiFallback = true;
return config;
};
},
};
9、运行效果如下