先看使用方式 qiankun.umijs.org/zh/guide/ge…
主应用
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'reactApp',
entry: '//localhost:3000', // 子应用html入口
container: '#subContainer',// // 渲染到哪了;在主应用给个div#subContainer用于承载微应用内容
activeRule: '/app-react', // 路由匹配规则
},
{
name: 'vueApp',
entry: '//localhost:8080',
container: '#subContainer',
activeRule: '/app-vue',
},
{
name: 'angularApp',
entry: '//localhost:4200',
container: '#subContainer',
activeRule: '/app-angular',
},
]);
// 启动 qiankun
start();
综合上述,就是根据路由匹配规则,当pathname匹配到/app-react时,请求加载entry: '//localhost:3000'内容,解析后渲染到主应用的div#subContainer下;
<el-main>
{/*主应用路由渲染出口 */}
<router-view></router-view>
{/* 微前端子应用渲染出口 */}
<div id="subContainer"></div>
</el-main>
整个qiankun要实现的就4个 1. 监视路由变化; 2. 匹配子应用; 3. 加载子应用; 4. 渲染子应用
- 所以我们先简历一个index文件,导出两个方法 registerMicroApps、start;
index.js
import { rewriteRouter } from './rewrite-router';
import { handleRouter } from './handle-router'
let _apps = [];
export const getApps = () => _apps
export const registerMicroApps = (apps) => {
_apps = apps
}
export const start = () => {
// 微前端运行原理
// 1. 监视路由变化; 2. 匹配子应用; 3. 加载子应用; 4. 渲染子应用
rewriteRouter();
// 初始执行匹配 解决rewriteRouter只会在路由变化时触发,初始化和刷新浏览器不会触发;需要手动调用一次
handleRouter();
}
2. 监视路由变化
rewriteRouter.js
import { handleRouter } from './handle-router'
let prevRoute = ''
let nextRoute = ''
// 手动管理路由历史,用于加载和卸载
export const getPrevRoute = () => prevRoute
export const getNextRoute = () => nextRoute
export const rewriteTouter = () => {
// 监听hash路由变化
window.onhashchange = () => {
console.log('监听hash路由变化了')
handleRouter()
}
// 监听history变化
// history.go、 history.back、 history.forward
window.addEventListener('popstate',() => {
// popstatec触发的时候,路由已经完成导航了
prevRoute = nextRoute
nextRoute = window.location.pathname
console.log('popstate')
// 路由变化后,2. 匹配子应用; 3. 加载子应用; 4. 渲染子应用
handleRouter()
})
// 对于pushState,repalceState需要使用函数重写的方式进行劫持
// 监听 history中的pushState
const rawpushState = window.history.pushState
window.history.pushState = (...args) => {
prevRoute = window.location.pathname;
rawpushState.apply(window.history, args)
nextRoute = window.location.pathname;
console.log('监控到 pushState 变化了')
handleRouter()
};
// 监听 history中的pushState
const rawReplaceState = window.history.replaceState
window.history.replaceState = (...args) => {
prevRoute = window.location.pathname;
rawReplaceState.apply(window.history, args)
nextRoute = window.location.pathname;
console.log('监控到 replaceState 变化了')
handleRouter()
}
}
3. 处理路由变化
handleRouter.js
import { getApps } from '.'
import { importHTML } from './import-html'
import { getPrevRoute } from './rewrite-router'
// 2. 匹配子应用; 3. 加载子应用; 4. 渲染子应用
export const handleRouter = async () => {
// 卸载上一个应用 浏览器出于安全考虑,是没有提供路由历史记录的;
// 上一个应用
const prevApp = apps.find(item => getPrevRoute().startsWith(item.activeRule))
if (prevApp) {
await unmount(prevApp)
}
// 2. 匹配子应用
console.log(window.location.pathname)
// 2.2 去apps里面去查找
const apps = getApps();
// // 获取下一个路由应用
const app = apps.find(item => getNextRoute().startsWith(item.activeRule))
// const app = apps.find(item => window.location.pathname.startsWith(item.activeRule));
if (!app) {
window.__POWERED_BY_QIANKUN__ = false
return;
}
// 3. 加载子应用
const { template, execScripts } = await importHTML(app.entry);
const container = document.querySelector(app.container)
container.appendChild(template)
// 配置全局环境变量
window.__POWERED_BY_QIANKUN__ = true
// webpack配置publicPath默认是'/' 那么在主应用的域名下,子应用的图片路径'/img/logo.png'会找不到,因为它应该在子应用的域名下
// webpack提供了另外一种运行时赋予publicPath的方式 在子应用时引入即可
// if (window.__POWERED_BY_QIANKUN__) {
// __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
// }
window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = app.entry + '/'
const appExports = await execScripts();
app.bootstrap = appExports.bootstrap
app.mount = appExports.mount
app.unmount = appExports.unmount
await bootstrap(app)
await mount(app)
// 3.1 请求获取子应用的资源: HTML、CSS、JS
// 3.1.1 请求方法有axios fetch 还有一些包装好的库
// const html = await fetch(app.entry) // 拿到的是html的普通文本 text
// const container = document.querySelector(app.container)
// container.innerHTML = html 此时能在<div id="subapp-container"></div>看到html内容都在里面了,但是页面是看不到的
// 3.1.2. 客户端渲染需要执行 JavaScript来生成内容
// 3.1.3. 浏览器处于安全考虑,innerHTML中的 script 是不会加载执行的
// 3.2 手动加载子应用的script
// 3.2.1 执行 script中的代码 eval 或者 new Function (const test = new Function('name', 'console.log(name)') test('a'))
// 3.2
// 4. 渲染子应用
// 5. js沙箱,css沙箱
// css沙箱 一种方式 shadow-root (open) 把元素都放在shadow-root下
// const subApp = document.getElementById('subapp')
// const shadow = subApp.attachShadow({ mode: 'open' })
// shadow.innerHTML = `
// <p>这是通过shadow dom 添加的内容</p>
// <style> p { color: red }</style>
// `
// 浏览器原生的video标签就是通过shadow dom进行样式隔离的
// 参考MDN上 shadow DOM讲解 https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_shadow_DOM
// 第二种方式,通过添加选择器范围的方式div[data-qiankun='app-vue1']#app
// js沙箱 有快照沙箱
}
async function bootstrap(app) {
app.bootstrap && await app.bootstrap()
}
async function mount(app) {
app.mount && await app.mount({
container: document.querySelector(app.container)
})
}
async function unmount(app) {
app.unmount && await app.unmount()
}
4. 加载子应用并解析
import-html.js
import { fetchResource } from "./fetch-resource"
// <!-- subApp/index.html-->
// <!DOCTYPE html>
// <html>
// <head>
// <meta charset="UTF-8"></meta>
// <title>test</title>
// </head>
// <body>
// <script src="https://unpkg.com/unm.js" entry></script>
// <script>
// console.log('aaaa')
// </script>
// </body>
// </html>
/**
*
* @param {*} url 传入url
* template 处理之后的html模版
* getExternalScripts scripts URL from template
* getExternamStyleSheets styleSheets URL from template
* execScripts
*/
// https://github.com/kuitos/import-html-entry
export const importHTML = async (url) => {
const html = await fetchResource(url)
const template = document.createElement('div');
template.innerHTML = html;
const scripts = template.querySelectorAll('script');
// 获取所有的script标签的代码:[代码,代码]
function getExternalScripts () {
return Promise.all(scripts.map(script => {
const src = script.getAttribute('src')
if (!src) {
return Promise.resolve(script.innerHTML)
} else {
// src 可能是https://unpkg.com/unm.js 也可能是/js/chunk-vendors.js
return fetchResource(
src.startsWith('http') ? src : `${url}${src}`
)
}
}))
}
// 获取并执行所有的 script 脚本代码
async function execScripts() {
const scriptsTexts = await getExternalScripts()
// scriptsTexts.forEach(code => {
// // eval 执行的代码可以访问外部变量
// eval(code)
// }); // 这时应用渲染出来了,但是把主应用干掉了;
// 原因是因为它在子应用运行时,是不是在bootstrap、mount、unmount中渲染的; 而是在判断全局没有__POVERED_BY_QIANKUN 直接整个渲染的 里面此时并没有父应用传入的container 所以就直接挂在到了最高层级的#app上了
// 现在应该是拿到匹配的子应用的入口中配置的 bootstrap、mount、unmount 去手动调用
// 怎么拿到子应用中的这三个周期函数?
// 因为我们在vue.config.js中配置了 configureWebpack: {
// output: {
// library: `${name}-[name]`,
// libraryTarget: 'umd', // 把微应用打包成 umd 库格式
// jsonpFunction: `webpackJsonp_${name}`,
// },
// },
// js打包成了umd 库格式; umd是什么格式?看示例 use中的dist
// console.log(window['app-vue-app1'])
// 这样需要知道每个子应用暴露到全局时的名字 本身名字变化,代码就拿不到了
// return window['app-vue-app1']
// 假如我想通过commonJs方式拿到 那就i可以手动构造一个
// 手动构造一个 CommonJS对象环境
const module = { exports: {} }
const exports = module.exports
// 也就是umd.js中的
// CommonJS 模块规范
// if (typeof exports === "object" && typeof module === 'object') {
// module.exports = factory();
// }
scriptsTexts.forEach(code => {
// eval 执行的代码可以访问外部变量
eval(code)
})
console.log(module.exports)
return module.exports
}
return {
template,
getExternalScripts,
execScripts
}
}
fetchResource.js
export const fetchResource = url => fetch(url).then(res => res.text())
查看umd.js
(function webpackUiversalModuleDefinition(root, factory) {
// root => window
// factory => function() { // 子应用代码 return { ... } // 导出结果 }
// CommonJS 模块规范
if (typeof exports === "object" && typeof module === 'object') {
module.exports = factory();
}
// 兼容AMD模块规范 很早的
else if (typeof difine === 'function' && difine.amd) {
define([], factory);
}
// CommonJS
else if (typeof exports === "object") {
exports['app-vue-app1'] = factory();
}
// window['xxx'] = factory() 结果就挂在到了整个app上,可以通过this
else {
root['app-vue-app1'] = factory();
}
})( window, function() {
// 这是内部的代码
// 最后会返回到出的结果
return {
a: 1,
b: 2
}
})