前言
研究了qiankun框架的源代码以及实现原理,框架简单而言就是创建的一个简单的html+js项目,然后利用爬虫读取对应地址的文件,再挂载到自己html指定的dom结点上展示,因为获取不同的文件挂载到dom元素,故qiankun本身写的js和css可以影响到挂载的文件,也就是像bootstrap、mount、unmount等等生命周期在微应用没有下载qiankun框架也能使用的原理,但需要被爬虫读取的文件是允许该地址跨域访问,而且在原先的项目不受影响,也无法实现真正意义上的单点登录,但可以模拟出来。
前端爬取html的方法:
挂载的dom元素:
实现的效果:
可以看到在我本地端口10086启动项目,依然能正常启动vue项目调取的接口:
相关代码:
-
手写qiankun的registerMicroApps()和start()方法
registerMicroApps是用来根据传入的子应用的信息数组来注册子应用;start使用启动这些子应用。
微前端的运行原理:1. 监听路由变化 2.匹配子应用 3.加载子应用 4.渲染子应用
import { handleRouter } from "./js/handle-router.js" import { rewriteRouter } from "./js/rewrite-router.js" // 暂存的apps,主要考虑到外部要拿这个要注册的子应用 let _apps = []; // 外部拿到子应用列表所要执行的方法 const getApps = function () { return _apps } // 注册子应用的方法 const registerMicroApps = function (apps) { _apps = apps console.log(apps,'apps'); } // 启动子应用的方法 const start = function () { console.log('start!'); // 微前端的运行原理,1. 监听路由变化 2.匹配子应用 3.加载子应用 4.渲染子应用 // 1. 监听路由变化 rewriteRouter(); // 初始执行匹配 handleRouter(); // 2.匹配子应用 // 3.加载子应用 // 4.渲染子应用 } registerMicroApps([ { name: 'vueApp', entry: 'http://172.20.10.2:7101', container: '#container', activeRule: '/vue', } ]); // 启动 qiankun start(); // 导出 export { registerMicroApps, start, getApps }
2. 创建handle-router.js来匹配、加载、渲染子应用
// 此函数用来处理路由跳转后,让其处理子应用即匹配子应用、加载子应用、渲染子应用 import { getApps } from "../index.js"; import { importHTML } from "./import-html.js"; import { getNextRoute, getPrevRoute } from "./rewrite-router.js"; export const handleRouter = async function () { const apps = getApps(); //获取当前子应用列表数组 // 需要先判断是否还有上一个子应用 let preRoute = getPrevRoute() //获取上一个路由路径window.location.pathname let nextRoute = getNextRoute() //获取跳转后的路由路径 // 获取上一个路由的子应用 const preApp = apps.find(item => preRoute.startsWith(item.activeRule)) // 2.2 然后子apps子应用中查找 // [name, entry, container, activeRule, mount, unmout, bootStrap] // 获取跳转后的子应用 const app = apps.find(item => nextRoute.startsWith(item.activeRule))//str.startWith(str1),字符串str如果以str1开头,那么就返回true // 如果有上一个应用,那么就先销毁,然后再加载当前的子应用 if (preApp) { await unmount(preApp)//此时preApp已经有自己的声明周期钩子的,这是在上一个子应用中已经设置的 } // console.log(app,'app'); if (!app) { //如果当前路由路径pathname没有子应用,直接return返回 return } // 2.匹配子应用 // 2.1 首先获取当前的路由路径 window.location.pathname // console.log(window.location.pathname) // 3.加载子应用 // 加载子应用就是请求获取app的entry资源,资源有很多种,有HTML、css、js,所以我们要一个个来处理 // 先来请求html资源,可以使用很多异步请求方式:ajax、aiox、fetch // const html = await fetch(app.entry).then(res => res.text()) //res为请求的所有资源,res.text()为请求到的数据的普通文本即页面的html // console.log(html) // 配置全局环境变量 window.__POWERED_BY_QIANKUN__ = true; window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = app.entry + "/" //获取自己封装的加载子应用的api const { template, //teplate为处理之后的html模板字符串 getExternalScripts, //调用它会得到所有的script脚本 execScripts //用来执行文档中所有的script脚本 } = await importHTML(app.entry) //这里必须是异步调用 // // 4.渲染子应用,渲染到container中 // // 获取渲染的容器 const container = document.querySelector(app.container) // console.log(container,'container'); container.append(template) // // 拿到html中的script脚本 // getExternalScripts().then(res => { // }) // 执行获取到的脚本,用于获取声明周期 const appExports = await execScripts() // console.log(appExports); app.bootstrap = appExports.bootstrap app.mount = appExports.mount app.unmount = appExports.unmount // 调用子应用的声明周期钩子函数来进行渲染 await bootstrap(app) await mount(app) // container.innerHTML = template //虽然此时在container中已经含有此html页面了,但是依然不能够在页面中渲染出页面 //以上不能渲染成功的原因如下: // a.客户端渲染需要通过js来生成内容 // b. 浏览器处于安全考虑,innerHTML中的script不会加载执行 ,所以需要我们手动加载执行 // 手动加载子应用的script,执行script中的代码 // 执行script中的字符串代码可以使用:1.使用eval(str)函数来执行字符串str的js内容;2:使用new Function } // handleRouter(); async function mount(app) { app.mount && (await app.mount({ container: document.querySelector(app.container) })) } async function unmount(app) { app.unmount && (await app.unmount(app.container)) } async function bootstrap(app) { app.bootstrap && (await app.bootstrap(app.container)) }
3. rewrite-router.js 重写路由监听的函数
// 重写路由监听的函数,主要是实现pushState和replaceState在实现路由跳转时,popState事件监听函数不能够监听到其跳转,所以需要重写这两个路由跳转方法 // 引入handleRouter函数,用来处理路由跳转后,让其匹配子应用、加载子应用、渲染子应用 import { handleRouter } from "./handle-router.js"; // 用于记录上一个路由 let preRoute = "" let nextRoute = window.location.pathname const getPrevRoute = function () { return preRoute } const getNextRoute = function () { return nextRoute } export { getPrevRoute, getNextRoute } export const rewriteRouter = function () { // 路由的两种模式:hash、history // 监听hash路由使用window.onhashchange // 这里我们使用history路由来实现,采用history.go、history.back、history.forword方法来进行路由跳转 // 在history路由中我们使用onpopstate事件函数来监听history路由的变化,但是popstate事件函数只能监听到history.go、forward、back的切换路由方式, window.addEventListener("popstate", () => { // popState触发的时候,路由已经完成导航了 // 且之前的路由preRoute的就等于之前的nextRoute, preRoute = nextRoute // 而跳转的nextRoute路由就是的当前最新的window.loacation.pathname nextRoute = window.location.pathname handleRouter(); }) // 但是它不能够监听到pushState添加历史记录(就是在页面中点击某个a标签进行跳转的方式,点击页面顺序:a->b->c,记录的历史记录中a、b、c都存在,而replaceState则不同)、replaceState(点击页面顺序:a->b->c,记录的历史记录中只有a->c,即用c代替了b记录,b记录被删除了)切换路由的方式 // 对于pushState、replaceState需要通过函数重写的方式进行劫持,也就是说我们重写pushState和replaceState // 但是我们一般都是pushState来跳转链接,是通过this.$router.replace()来触发;而pushState()是通过this.$router.push()来触发 // 重写pushState方法 const rawPushState = window.history.pushState window.history.pushState = function (...args) { // 导航前 preRoute = window.location.pathname //记录跳转前的路由路径 rawPushState.apply(window.history, args) //使用pushState进行跳转 // 导航后 nextRoute = window.location.pathname //记录跳转后的路由路径 handleRouter() console.log("终于监视到pushState了"); } // 重写replaceState方法 const rawReplaceState = window.history.replaceState window.history.replaceState = function (...args) { // 导航前 preRoute = window.location.pathname //记录跳转前的路由路径 rawReplaceState.apply(window.history, args) // 导航后 nextRoute = window.location.pathname //记录跳转后的路由路径 // handleRouter() // console.log("终于监视到replaceState了"); } }
4. import-html.js 用于解析出html代码中的script脚本的方法
// 用于解析出html代码中的script脚本的方法 // 其实qiankun框架里面有用一个库即import-html-entry,其封装了一些从html文件中提取script标签,并动态执行script脚本的方法,而且这个库中也封装了沙箱机制 import { fetchResource } from "./fetch-resource.js"; // 这里我们仿造import-html-entry库,然后自己手写几个类似的方法 export const importHTML = async function (url) { console.log(url,'拿到的url'); // 加载子应用就是请求获取app的entry资源,资源有很多种,有HTML、css、js,所以我们要一个个来处理 const html = await fetchResource(url); console.log(html); // 先来请求html资源,可以使用很多异步请求方式:ajax、aiox、fetch const template = document.createElement("div") template.innerHTML = html // 获取template的dom下的所有script脚本 const scripts = template.querySelectorAll("script") // 获取所有script标签脚本代码,最后返回一个数组的形式 const getExternalScripts = function () { // promise.all的返回值是一个promise数组 return Promise.all(Array.from(scripts).map(script => { const src = script.getAttribute("src") if (!src) {//如果script脚本没有src,那么就是普通的script标签里面的script代码 // 那么就只返回script里面的代码,并封装成promise对象 return Promise.resolve(script.innerHTML) } else {//表示此script脚本是外链的资源,资源在src中 return fetchResource(//需要判断src是以http开头比如http://www.nativejs.com,则资源是http外网资源; // 如果是一种相对路径资源比如:/src/res则需要手动加上子应用的域名 src.startsWith("http") ? src : url + src )//直接发送异步请求 } })) } // 获取并执行所有的script脚本代码 const execScripts = async function () { // 拿到html中的scripts脚,它是一个script代码字符串构成的数组 const scripts = await getExternalScripts() // 手动的构造一个commonJs环境,commonJs规则,里面有一个module对象,还有一个exports对象并且指向module.exports对象 const module = { exports: {} } const exports = module.exports // console.log(scripts); // 执行scripts数组中的script字符串代码,这里依然是使用eval函数来执行字符串代码 scripts.forEach(script => { // eval执行的代码可以访问外部代码 eval(script) }) // 由于子模块到出的库格式为umd库,并且将返回的数据挂载到了window对象上, // 所以我们可以在window对象上拿到子应用的生命周期钩子函数,需要注意的是生命周期钩子必须写在子应用的入口文件main.js,然后webpack打包的时候首先进入入口文件,然后再递归查找依赖的文件进行打包 // 因为我们自己构造了commonJs环境,那么我就能够通过module.exports拿到回调函数factory()返回的结果 // console.log(module.exports) return module.exports } return { template, //teplate为处理之后的html模板字符串 getExternalScripts, //调用它会得到所有的script脚本 execScripts //用来执行文档中所有的script脚本 } }
5. fetch-resource.js 请求的所有资源
// 异步请求函数 export const fetchResource = function (url) { const html = fetch(url).then(res => res.text()) //res为请求的所有资源,res.text()为请求到的数据的普通文本即页面的html return html }
6. 主应用与子应用的数据交互 qiankun官方提供了actions通信
qiankun内部使用initGlobalState(state)定义全局状态,该方法执行后返回一个MicroAppStateActions实例,实例中包含三个方法,分别是onGlobalStateChange、setGlobalState、offGlobalStateChange
import { initGlobalState, MicroAppStateActions } from 'qiankun'; // 初始化 state const actions: MicroAppStateActions = initGlobalState(state); actions.onGlobalStateChange((state, prev) => { // state: 变更后的状态; prev 变更前的状态 console.log(state, prev); }); actions.setGlobalState(state); actions.offGlobalStateChange();
相关demo可以下载:https://github.com/zengxudong-bit/qiankun-functional-demo