【qiankun原理】框架

182 阅读3分钟
// 定义一个全局变量,用于存储已注册的子应用配置
let apps = [];

/**
 * 注册子应用配置
 * @param {Array} appConfigs 子应用配置数组
 */
export function registerMicroApps(appConfigs) {
    apps = appConfigs;
}

/**
 * 启动微前端应用
 */
export function start() {
    // 监听路由变化,当路由发生变化时重新匹配子应用
    window.addEventListener('popstate', handleRouterChange);
    // 初始执行一次子应用匹配
    handleRouterChange();
}

/**
 * 获取已注册的子应用配置
 * @returns {Array} 子应用配置数组
 */
export function getApps() {
    return apps;
}

/**
 * 处理路由变化
 */
export async function handleRouterChange() {
    // 获取子应用配置
    const apps = getApps();

    // 根据当前路由匹配子应用
    const app = apps.find(item => window.location.pathname.startsWith(item.activeRule));

    // 如果未匹配到子应用,不进行处理
    if (!app) return;

    // 创建代理沙箱
    const proxySandbox = new ProxySandbox();
    // 激活代理沙箱,允许子应用修改全局变量
    proxySandbox.active();

    // 加载子应用的HTML、JS和CSS资源
    const { htmlTemplate, getExternalScripts, getExternalStylesheet, callScripts } = await importHtml(app.entry);

    // 执行子应用的生命周期钩子并获取返回的对象
    const appExports = await callScripts();

    // 将子应用的生命周期函数放入子应用配置中
    app.bootstrap = appExports.bootstrap;
    app.mount = appExports.mount;
    app.unmount = appExports.unmount;

    // 获取子应用的样式表
    const styleSheets = await getExternalStylesheet();
    app.styleSheets = styleSheets;

    // 渲染子应用到Shadow DOM中
    bootstrapApp(app);
    mountApp(app, proxySandbox);
}

/**
 * 启动子应用的bootstrap生命周期
 * @param {Object} app 子应用配置
 */
async function bootstrapApp(app) {
    if (app.bootstrap) {
        await app.bootstrap();
    }
}

/**
 * 将子应用渲染到指定容器中
 * @param {Object} app 子应用配置
 * @param {Object} proxySandbox 代理沙箱实例
 */
async function mountApp(app, proxySandbox) {
    // 创建 Shadow DOM 容器
    const shadowRootContainer = document.querySelector(app.container) || document.getElementById(app.container);
    const shadowRoot = shadowRootContainer.attachShadow({ mode: 'open' });

    // 渲染子应用内容到 Shadow DOM 中
    const microDocument = await createShadowDocument(app);
    shadowRoot.appendChild(microDocument);

    // 激活代理沙箱,允许子应用操作全局变量
    proxySandbox.active();

    // 启动子应用的mount生命周期
    if (app.mount) {
        await app.mount({ container: shadowRoot });
    }
}

/**
 * 创建 Shadow DOM 容器,并渲染子应用内容
 * @param {Object} app 子应用配置
 * @returns {DocumentFragment} 创建的 Shadow DOM 内容
 */
async function createShadowDocument(app) {
    const microDocument = document.createElement('html');
    const container = document.createElement('div');

    // 启动子应用的mount生命周期,渲染内容到container中
    app.mount && await app.mount({ container });

    // 将子应用的样式表插入到 Shadow DOM 中
    app.styleSheets.forEach(cssCode => {
        const styleAttr = document.createElement('style');
        styleAttr.textContent = cssCode;
        microDocument.appendChild(styleAttr);
    });

    microDocument.appendChild(container);
    return microDocument;
}

/**
 * 代理沙箱类,用于隔离全局变量访问
 */
class ProxySandbox {
    proxyWindow;
    sandboxRunning;

    constructor() {
        const rawWindow = window;
        const fakeWindow = {};
        const proxyWindow = new Proxy(fakeWindow, {
            set: (target, prop, value) => {
                if (this.sandboxRunning) {
                    target[prop] = value;
                    return true;
                } else {
                    return false;
                }
            },
            get: (target, prop) => {
                const value = prop in target ? target[prop] : rawWindow[prop];
                return value;
            }
        });

        this.sandboxRunning = false;
        this.proxyWindow = proxyWindow;
    }

    active() {
        this.sandboxRunning = true;
    }

    inactive() {
        this.sandboxRunning = false;
    }
}

/**
 * 模拟从 URL 获取 HTML、JS 和 CSS 资源,并返回相关内容
 * @param {string} url 资源 URL
 * @returns {Object} 包含资源内容的对象
 */
async function importHtml(url) {
    // 模拟从 URL 获取 HTML 资源,实际应用中可以使用 fetch 或其他方法
    const html = await fetch(url).then(res => res.text());
    const htmlTemplate = document.createElement('div');
    htmlTemplate.innerHTML = html;

    // 模拟从 HTML 中提取外部脚本的 URL,实际应用中需要根据具体情况解析 HTML
    function getExternalScripts() {
        // 在实际应用中,从 htmlTemplate 中提取 script 标签,获取外部脚本的 URL
        // 然后可以使用 fetch 或其他方法获取脚本内容
        const scripts = [];

        return Promise.all(scripts);
    }

    // 模拟从 HTML 中提取外部样式表的 URL,实际应用中需要根据具体情况解析 HTML
    function getExternalStylesheet() {
        // 在实际应用中,从 htmlTemplate 中提取 link 标签,获取外部样式表的 URL
        // 然后可以使用 fetch 或其他方法获取样式表内容
        const styles = [];

        return Promise.all(styles);
    }

    // 模拟从 HTML 中提取并执行的代码块,实际应用中需要根据具体情况解析 HTML
    function callScripts() {
        // 在实际应用中,从 htmlTemplate 中提取 script 标签的内容
        const scriptsCode = [];
        
        // 执行脚本并返回结果
        const module = { exports: {} };
        const exports = module.exports;
        scriptsCode.forEach(code => {
            eval(code);
        });
        return exports;
    }

    return {
        htmlTemplate,
        getExternalScripts,
        getExternalStylesheet,
        callScripts,
    };
}

// 导出函数和类
export default {
    registerMicroApps,
    start,
    getApps
};

// 使用
import { registerMicroApps, start } from 'mini-qiankun';

// 注册微应用
registerMicroApps([
    {
        name: 'react app1',
        entry: '//localhost:8001',
        container: '#app1', // 挂载点的id (挂载到哪个div中)
        activeRule: '/qiankun/app1', // 微前端的路由
    },
    {
        name: 'vue app2',
        entry: '//localhost:8002',
        container: '#app2',
        activeRule: '/qiankun/app2',
    },
]);
    
// 启动qiankun
start();

// 对应端口启动微应用后即可渲染到对应的div中
function App(){
    return 
    <>
      <div id='app1' class="scale"></div>
      <div id='app2' class="scale"></div>
    </>
}