qiankun实现原理学习

332 阅读5分钟

先看使用方式 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. 渲染子应用

  1. 所以我们先简历一个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
    }
})