手写微前端框架qiankun

46 阅读1分钟
let _apps = [];

let prevRoute = '';
let nextRoute = window.location.pathname;

export const registerMicroApps = (apps) => {
    _apps = apps;
};

export const importHTML = async (url) => {
    const html = await fetch(url).then(res => res.text());
    const template = document.createElement('div');
    template.innerHTML = html;
    const scripts = template.querySelectorAll('script');

    const getExternalScripts = () => {
        return Promise.all(Array.from(scripts).map(script => {
            const src = script.getAttribute('src');
            if (!src) {
                return Promise.resolve(script.innerHTML);
            } else {
                return fetch(src.startsWith('http') ? src : `${url}${src}`).then(res => res.text());
            }
        }))
    }

    const execScripts = async () => {
        const scripts = await getExternalScripts();
        // 手动的构造一个commonjs模块化环境
        const module = { exports: {} }
        // eslint-disable-next-line no-unused-vars
        const exports = module.exports;
        scripts.forEach(code => eval(code));
        return module.exports;
    }

    return {
        template,
        getExternalScripts,
        execScripts,
    }
}

const handleRouter = async () => {
    // 卸载上一个应用
    const prevApp = _apps.find(item => prevRoute.startsWith(item.activeRule));
    // 2. 匹配子应用
    const app = _apps.find(item => window.location.pathname.startsWith(item.activeRule));
    if (prevApp) {
        await unmount(prevApp)
    }
    if (!app) return;
    // 3. 加载子应用 请求获取子应用的资源
    /**
     * 1. 客户端渲染需要通过执行js来生成内容
     * 2. 浏览器出于安全考虑 innerHTML中的script不会加载执行 需手动加载子应用的script并执行
     * const html = await fetch(app.entry).then(res => res.text());
     * const container = document.querySelector(app.container);
     * container.innerHTML = html;
     */
     const { template, execScripts } = await importHTML(app.entry);
    
    // 4. 渲染子应用
    const container = document.querySelector(app.container);
    container.appendChild(template);

    window.__POWERED_BY_QIANKUN__ = true;
    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);
}

export const start = () => {
    // 1. 监视路由变化
    /**
     * hash: window.onhashchange
     * history路由
     * history.go history.back history.forward 使用popstate事件:window.onpopstate
     * pushState replaceState 需要通过函数重写的方式进行劫持
     */
    window.addEventListener("popstate", () => {
        prevRoute = nextRoute;
        nextRoute = window.location.pathname;
        handleRouter();
    });
    const rawPushState = window.history.pushState;
    window.history.pushState = (...args) => {
        // 导航前
        prevRoute = window.location.pathname;
        rawPushState.apply(window.history, args);
        // 导航后
        nextRoute = window.location.pathname;
        handleRouter();
    }
    const rawReplaceState = window.history.replaceState;
    window.history.replaceState = (...args) => {
        prevRoute = window.location.pathname;
        rawReplaceState.apply(window.history, args);
        nextRoute = window.location.pathname;
        handleRouter();
    }
    handleRouter();
};

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({
        container: document.querySelector(app.container)
    }))
}

`