微前端–qiankun(3)
微前端的运行原理
- 监听路由变化
- 匹配子应用
- 加载子应用
- 渲染子应用
监听路由变化
window.onhashchange
和window.addEventListener('popstate',() => {})
等等,下篇文章我写个
匹配子应用
const currentApp = apps.find(app => window.location.pathname.startWith(app.activeRule))
加载子应用
function handleRouter = async () => {
// 匹配子应用
// 加载资源
const html = await fetch(currentApp.entry.then(res => res.text())
// 将 html 渲染到指定的容器内
const container = document.querySelector(currentApp.container)
}
渲染子应用
export const importHTML = url => {
const html = await fetch(currentApp.entry).then(res => res.text()
const template = document.createElement('div')
template.innerHTML = html
const scripts = template.querySelectAll('script')
const getExternalScripts = () => {
console.log('解析所有脚本: ', scripts)
}
const execScripts = () => {}
return {
template, // html 文本
getExternalScripts, // 获取 Script 脚本
execScripts, // 执行 Sript 脚本
}
}
const getExternalScripts = async () => {
return Promise.all(Array.from(scripts).map(script => {
// 获取 scr 属性
const src = script.getAttribute('src')
if (!src) {
return Promise.resolve(script.innerHTML)
} else {
return fetch(src.startWith('http') ? src : `${url}${src}`).then(res => res.text())
}
}))
}
const execScripts = async () => {
const scripts = await getExternalScripts()
scripts.forEach(code => {
eval(code)
})
}
qiankun实现
qiankun的核心理念
- 子应用的注册和加载
- 应用间通信
- 样式隔离和共享
- 状态管理: 支持主应用和子应用之间的共享状态管理,保持数据的一致性
安装依赖并引入主项目
在主应用目录中新建一个micro-app.js
const microApps = [
{
name: 'sub-react',
entry: '//localhost:9001/',
activeRule: '/sub-react',
container: '#subapp-viewport', // 子应用挂载的div
props: {
routerBase: '/sub-react'
}
},
{
name: 'sub-vue',
entry: '//localhost:9002/',
activeRule: '/sub-vue',
container: '#subapp-viewport', // 子应用挂载的div
props: {
routerBase: '/sub-vue'
}
},
{
name: 'sub-vue3',
entry: '//localhost:9003/',
activeRule: '/sub-vue3',
container: '#subapp-viewport', // 子应用挂载的div
props: {
routerBase: '/sub-vue3'
}
}
]
export default microApps;
这里其实和状态管理工具结合起来,代码实例如下
# 主应用
export const microApps = [
{
name: 'micro-clouds',
entry: process.env.VUE_APP_CLOUDS,
activeRule: '/micro-clouds',
container: '#subapp2', // 子应用挂载的div
props: {
routerBase: '/micro-clouds',
// 父应用的store传递给子应用
mainStore: store,
// 父应用传递给子应用的登录信息
user: utils.getStorage('user')
}
}
]
# 子应用接受父应用传过来的登录信息并写入storage中
export async function mount(props) {
// 父级传过来的登录信息写进子系统缓存中
utils.setStorage("user", props.user);
// 标记当前启动形式为微服务启动
store.commit("app/microChange", true);
await render(props);
}
对于子应用:子应用的vue.config.js应该设置跨域。将子应用打包输出格式为UMD【后面写这个】
module.exports = {
outputDir: 'dist',
assetsDir: 'static',
filenameHashing: true,
devServer: {
hot: true,
disableHostCheck: true,
port,
overlay: {
warnings: false,
errors: true,
},
headers: {
'Access-Control-Allow-Origin': '*',
},
},
// 自定义webpack配置
configureWebpack: {
resolve: {
alias: {
'@': resolve('src'),
},
},
output: {
// 把子应用打包成 umd 库格式
library: `${name}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
在app.vue文件中配置
export default {
mounted () {
registerMicroApps(microApps);
start();
}
}
当微应用信息注册完之后,一旦浏览器url发生变化,便会自动触发qiankun的匹配逻辑,所有的activeRule规则规则上的微应用就会加载到指定的container上,同时依次调用微应用暴露出来的钩子。
微应用
微应用需要在自己的入口js中导出bootstrap, mount, unmount三个生命周期钩子。
- bootstrap: 只会再微应用初始胡的时候调用一次,下次微应用重新进入的时候会直接调用mount钩子,不会重复触发bootstrap,这李可以做一些全局变量的初始化
- mount: 应用每次进入都会调用mount方法,这里触发应用的渲染方法
- unmount: 每次应用切出/卸载都会调用的方法
- update: 可选生命周期钩子,仅仅使用loadMicroApp方法的时候加载微应用的时候生效。
子应用的main.js
import './public-path';
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue';
import routes from './router';
let router = null;
let instance = null;
let history = null;
function render(props = {}) {
const { container } = props;
history = createWebHistory(window.__POWERED_BY_QIANKUN__ ? '/sub-vue3' : '/');
router = createRouter({
history,
routes,
});
instance = createApp(App);
instance.use(router);
instance.mount(container ? container.querySelector('#sub-vue3') : '#sub-vue3');
}
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {
console.log('%c ', 'color: green;', 'vue3.0 app bootstraped');
}
function storeTest(props) {
props.onGlobalStateChange &&
props.onGlobalStateChange(
(value, prev) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev),
true,
);
props.setGlobalState &&
props.setGlobalState({
ignore: props.name,
user: {
name: props.name,
},
});
}
export async function mount(props) {
storeTest(props);
render(props);
instance.config.globalProperties.$onGlobalStateChange = props.onGlobalStateChange;
instance.config.globalProperties.$setGlobalState = props.setGlobalState;
}
export async function unmount() {
instance.unmount();
instance._container.innerHTML = '';
instance = null;
router = null;
history.destroy();
}
子应用的public-path.js
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
面包屑融合父子应用的路由
这里用的是vuex
const actions = {
// 处理父应用的to.match
getBreadcrumb({ commit }, matched) {
const levelList = matched.filter((item) => item.meta && item.meta.title && item.meta.breadcrumb !== false);
commit('setBreadcrumb', JSON.parse(JSON.stringify(levelList)));
},
// 处理微应用的to.match
getMicroBreadcrumb({ commit }, matchedList) {
const microLevel = matchedList.filter((ele) => ele.meta && ele.meta.title && ele.meta.breadcrumb !== false);
/*
* 为了解决这个告警采用 JSON.parse(JSON.stringify(microLevel)) vue3.0需要注意的地方
* runtime-core.esm-bundler.js:198 [Vue warn]: Avoid app logic that relies on enumerating keys on a component instance.
* The keys will be empty in production mode to avoid performance overhead.
*/
commit('setMicroBreadcrumb', JSON.parse(JSON.stringify(microLevel)));
}
}
# 主应用面包屑组件中
const getBreadcrumb = async () => {
// 判断是微应用就去微应用上报的to.match
if (route.path.startsWith('/micro')) {
// 这里需要做一个vuex异步处理,否则取不到子应用上报的to.match
store.subscribe((mutation, state) => {
if (mutation.type === 'app/setMicroBreadcrumb') {
levelList.value = store.state.app.microBreadcrumbs
}
})
} else {
await store.dispatch('app/getBreadcrumb', route.matched)
levelList.value = store.state.app.breadcrumbs
}
}
# 主应用面包屑组件中
const getBreadcrumb = async () => {
// 判断是微应用就去微应用上报的to.match
if (route.path.startsWith('/micro')) {
// 这里需要做一个vuex异步处理,否则取不到子应用上报的to.match
store.subscribe((mutation, state) => {
if (mutation.type === 'app/setMicroBreadcrumb') {
levelList.value = store.state.app.microBreadcrumbs
}
})
} else {
await store.dispatch('app/getBreadcrumb', route.matched)
levelList.value = store.state.app.breadcrumbs
}
}
qiankun样式隔离
- shadow dom
- scoped css =>
start({ sanbox: true})
- strictStylelsolation=> 乾坤为每个微应用的容器包裹一个shadow dom节点,所有的子节点都会被#shadow-root包裹,从而保证微应用的让是不会对全局产生影响。
- exprimentalStylelsolation=> experimentalstyleIsolation被设置为true,qiankun会改写子应用所添加的样式队则增加一个特殊的选择器规则来限定其影响的范围。
Vue Scoped
在vue的单文件组件中使用< style scoped>
,vue会自动将改样式应用于当前组件的元素,并在编译的过程中为css规则添加要给唯一的属性选择器。
h3 {
background-color: pink;
color: blue;
}
// ======= 使用 style scoped 后 ====>
h3[data-v-469af010] {
background-color: pink;
color: blue;
}
CSS Modules
这个使用webpack配置一下就可以了,这种方法也能实现组件级别样式隔离,能够设置子组件和全局样式。
CSS 沙箱
单实例场景下的子应用和子应用的样式隔离
单实例场景下的子应用和主应用的样式隔离
多实例场景下的子应用与子应用的样式隔离
多实例场景下的子应用和主应用的样式隔离
命名约定
通过给特定范围内的元素添加特定的类型或者命名前缀,然后再css中通过类选择器或者属性选择器来应用相应的样式。
CSS Modules
// webpack.config.js
{
test: /\.(le|c)ss$/,
use: ['style-loader', {
loader: 'css-loader',
options: {
modules: true // 开启 css modules
}
}, 'less-loader'],
}
// webpack.config.js
{
test: /\.(le|c)ss$/,
use: ['style-loader', {
loader: 'css-loader',
options: {
modules: true // 开启 css modules
}
}, 'less-loader'],
}
import React from 'react';
import styles from './Button.module.css';
interface ButtonProps {
disabled?: boolean;
onClick: () => void;
}
const Button: React.FC<ButtonProps> = ({ disabled, onClick, children }) => {
const buttonClasses = `${styles.button} ${disabled ? styles['button--disabled'] : ''}`;
return (
<button className={buttonClasses} onClick={onClick} disabled={disabled}>
{children}
</button>
);
};
export default Button;
CSS-in-js
将css样式卸载JavaScript代码中的方式,通过将样式与组件绑定在一起,实现了样式的局部化和杀向话。
import React from 'react';
import styled from 'styled-components';
const Button = styled.button`
background-color: blue;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: darkblue;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`;
const ExampleComponent = () => {
return (
<div>
<Button onClick={() => console.log('Button clicked')}>Click me</Button>
<Button disabled>Disabled Button</Button>
</div>
);
};
export default ExampleComponent;
Shadow DOM
创建一个隔离的dom子树,其中的样式和脚本不会影响外部的dom,通过再元素上应用shadow dom,可以将样式限定再shadow dom内部,实现样式的沙箱化
- 兼容性问题
- 可能会用到第三方UI库,将这些组件挂载到全局document.body时, 由于子应用的样式只能作用于shadow dom中,所以这些组件的样式是无法作用于shadow dom,这样就会导致样式丢失的问题。
experimentalStyleIsolation样式隔离
当experimentalStyleIsolation 被设置为 true 时,qiankun 会改写子应用所添加的样式为所有样式规则增加一个特殊的选择器规则来限定其影响范围,官方文档中的例子如下:
// 假设应用名是 react16
.app-main {
font-size: 14px;
}
div[data-qiankun-react16] .app-main {
font-size: 14px;
}
微前端框架
- qiankun: function + proxy + with
- micro-app: web components
- wujin: web components 和 iframe