qiankun是什么
qiankun 是一个基于single-spa
实现的库。微前端的一种方案。具体可以去官网了解。
基本使用
基座
// main.js 添加以下配置
registerMicroApps([
{
name: 'reactApp',
entry: '//localhost:8081',
container: '#subContainer',
activeRule: location => location.pathname.startsWith('/reactApp'),
},
{
name: 'vue2App',
entry: '//localhost:8082',
container: '#subContainer',
activeRule: '/vue2App',
},
{
name: 'vue3App',
entry: '//localhost:8083',
container: '#subContainer',
activeRule: '/vue3App',
},
]);
// 启动 qiankun
start();
// 给子应用预留位置
<template>
<div id="subContainer"></div>
</template>
子应用
vue2版本
// 在src目录下添加public-path.js文件并加入以下内容
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
import './public-path';// 放最前面!
import Vue from 'vue';
import App from './App.vue';
import router from './router/index';
Vue.config.productionTip = false;
Vue.use(ElementUI);
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();
}
export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}
export async function mount(props) {
console.log('[vue] props from main framework', props);
render(props);
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
}
// vue.config.js
const { defineConfig } = require('@vue/cli-service');
const {name} = require('./package').name;
module.exports = defineConfig({
devServer: {
port: 8082,
headers: {
'Access-Control-Allow-Origin': '*',
}
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd', // 把微应用打包成 umd 库格式
chunkLoadingGlobal: `webpackJsonp_${name}`,
},
},
})
vue3版本
配置基本与vue2
一致,除了unmount
方法。
export async function unmount() {
instance.unmount();
instance._container.innerHTML = '';
instance = null;
}
react版本
public-path
// 在src目录下添加public-path.js文件并加入以下内容
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
入口文件 index.js
import './public-path';
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
let root = null;
function render(props = {}) {
const { container, router: mainRouter } = props;
root = ReactDOM.createRoot(container ? container.querySelector('#root') : document.querySelector('#root'));
React.mainRouter = mainRouter;
root.render(
<React.StrictMode>
<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/reactApp' : '/'}>
<App />
</BrowserRouter>
</React.StrictMode>
);
}
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {
console.log('[react16] react app bootstraped');
}
export async function mount(props) {
console.log('[react16] props from main framework', props);
render(props);
}
export async function unmount(props) {
root.unmount();
}
webpack配置
如果使用create-react-app
创建react
项目,react
会webpack
配置隐藏到node-module
目录里,导致我们难以修改webpack
配置。
有两种方法可以解决:执行yarn eject
命令将隐藏的webpack
配置弹出;使用第三方库(推荐)。
下面我使用第三方库craco
,具体配置可以参考官方文档。
安装
npm i -D @craco/craco
在根目录下创建一个craco.config.js
文件。
const { name } = require('./package');
module.exports = {
webpack: {
configure(config) {
config.output.library = `${name}-[name]`;
config.output.libraryTarget = 'umd';
config.output.chunkLoadingGlobal = `webpackJsonp_${name}`;
config.output.globalObject = 'window';
return config;
}
},
devServer: {
port: 8081,
open: false,
headers: {
'Access-Control-Allow-Origin': '*',
}
},
};
改变package.json
文件scripts
属性值。
// package.json
"scripts": {
- "start": "react-scripts start"
+ "start": "craco start"
- "build": "react-scripts build"
+ "build": "craco build"
- "test": "react-scripts test"
+ "test": "craco test"
}
应用间跳转
参考链接: qiankun微应用之间、主微应用之间相互跳转方式总结与实践
-
通过
window.history.pushState(state, title, url)
方式跳转。state:一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填null。
title:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null。
url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。
const state = { page_id: 1, user_id: 5 };
const url = "hello-world.html";
history.pushState(state, null, url);
- 通过
props
属性传递主应用路由实例给子应用,子应用通过调用路由实例方法完成跳转。
应用间通信
- 官方提供的
initGlobalState
。提供了三个方法initGlobalState
、onGlobalStateChange
、setGlobalState
。调用setGlobalState
会触发onGlobalStateChange
方法。
import { initGlobalState } from 'qiankun';
// 初始化state
const actions = initGlobalState({});
// 监听变化
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log('主应用检测到state变更:', state, prev);
});
// 改变state
actions.setGlobalState({num: 3});
// 将actions通过props传递给子应用,子应用拿到方法后可以为actions添加onGlobalStateChange方法,也可以监听到state的改变
export default actions;
但是这个方法在使用的时候,控制台会出现一个警告,globalState
将会被移除在qiankun3.0
。
2. 使用vuex
或者redux
。很简单,就是将创建好的store
通过props
传递给子组件。
总结
其实这几种无非就是通过props
去传递一个对象或者方法,在子应用去使用调用这些方法属性。
样式冲突解决方法
qiankun提供的sandbox
配置 { sandbox : { strictStyleIsolation: true } }
,实现形式为将整个子应用放到原生的Shadow DOM
内进行嵌入,完全隔离了主子应用。
// 启动 qiankun
start({
prefetch: true,// 预加载子资源
sandbox: {
strictStyleIsolation: true
}
});
// 创建一个sandbox
const app = document.getElementById('app')
const shadow = app.attachShadow('mode: open');
缺点:完全隔离,子应用无法使用父应用的全局样式,父应用也无法拿到子应用的dom
。如果需要抽离公共组件库,子应用构建后,无法用到父应用的组件库。
qiankun提供的实验性沙箱
添加experimentalStyleIsolation: true
属性,qiankun会自动为子应用所有的样式增加后缀标签,如:div[qiankun-child]
。相当于vue
中的scoped
。如下图。
// 启动 qiankun
start({
prefetch: true,// 预加载子资源
sandbox: {
experimentalStyleIsolation: true// 实验性沙箱
}
});
但是其实我们的应用一般来说都是vue
或者react
,vue
和react
都有自己样式组件化的方法了,所以使用vue
和react
样式组件化能解决绝大部分样式污染的问题。
原理
监听路由变化
若是hash
路由通过hashchange
原生事件监听并拿到变化后的hash
。若是history
路由则通过popstate
原生事件监听浏览器的后退前进,还需改写window.history.pushState
和window.history.replaceState
,以便拿到变化后url
。
丐版实现
let prePath = window.location.pathname;// 保存上一个路由路径,用于卸载上一个子应用
let curPath = '';
window.addEventListener('popstate', () => {
prePath = curPath;
curPath = window.location.pathname;
handleRoute();// 拿到路由后匹配路由
}
const originPushState = window.history.pushState;
window.history.pushState = (...arg) => { pathChange(originPushState, ...arg); };
const originReplaceState = window.history.replaceState;
window.history.replaceState = (...arg) => { pathChange(originReplaceState, ...arg); };
function pathChange(fn, ...arg) {
fn.apply(window.history, arg);
prePath = curPath;
curPath = window.location.pathname;
handleRoute();// 拿到路由后匹配路由
}
匹配子应用
通过遍历配置项得到其中的activeRule
与当前路由进行匹配,匹配成功加载路由。
加载子应用
- 拿到匹配路由的
entry
,并通过fetch
获取其中内容,拿到html
文本; import-html-entry
解析html
,拿去script
标签的- 通过
eval
执行script
标签的内容; - 通过
umd
模块获取子应用暴露出来的生命周期,调用子应用mount
方法,完成子应用加载。
渲染子进程
将html
插入预留的container
中。