路由拦截
- 重写浏览器路由跳转事件
- main/micro/router/rewriteRouter.js ```javascript import { patchRouter } from ‘../utils’ import { turnApp } from ‘./routerHanle’
// 重写window路由跳转 export const rewriteRouter = () => { window.history.pushState = patchRouter(window.history.pushState, ‘micro_push’) window.history.replaceState = patchRouter(window.history.replaceState, ‘micro_replace’) window.addEventListener(‘micro_push’, turnApp) window.addEventListener(‘micro_replace’, turnApp) // 监听 浏览器返回按钮事件 window.onpopstate = function () { turnApp() } }
- 我们通过监听浏览器的切换,实现自定义事件,定义微前端路由监听事件,达到路由拦截的目的。
- 在执行过程中,将this指回全局原生事件,如果不做这部,目前没有发现会造成什么问题。
- 我们在浏览器操作过程中,发现点击返回按钮无法触发微前端自定义事件的监听,所以在监听浏览器返回事件中,执行路由切换的方法。
- main/micro/utils/index.js
```javascript
/**
* 给当前路由跳转打补丁
* @param {*} globalEvent 全局原生事件
* @param {*} eventName 事件名称
* @returns
*
* window.history.pushState = patchRouter(window.history.pushState, 'micro_push')
* window.history.pushState = 函数return的部分
*/
export const patchRouter = (globalEvent, eventName) => {
return function () {
const e = new Event(eventName);
// this 指向当前当前监听的函数
globalEvent.apply(this, arguments); //? 不apply会造成什么问题?
window.dispatchEvent(e)
}
}
main/micro/router/routerHanle.js
export const turnApp = (e) => {
console.log('路由切换了', e);
}
获取首个子应用
整体逻辑
注册完子应用,进行微前端的启动
export const starMicroApp = () => {
// 注册子应用
registerMicroApps()
// 启动
start();
};
main\micro\start.js
// 启动微前端框架 export const start = () => { // 首先验证当前子应用列表是否为空 const apps = getList() if (!apps.length) { // 子应用列表为空 throw Error('子应用列表为空,请正确注册') } // 有子应用列容,查找到符合当前路由的子应用 const app = currentApp() console.log('app', app) if (app) { const { pathname, hash } = window.location const url = pathname + hash window.history.pushState('', '', url) // 设置当前子应用标识 window.__CURRENT_SUB_APP__ = app.activeRule } }
验证当前子应用列表是否为空
- 不为空的话,获取当前路由对应的子应用
- 如果有就跳转到对应的路由
并且全局标识当前子应用
main\micro\utils\index.js ```javascript // 获取当前子应用 export const currentApp = () => { const currentUrl = window.location.pathname return filterApp(‘activeRule’, currentUrl) }
// 过滤出当前子应用 const filterApp = (key, value) => { const currentApp = getList().filter(item => item[key] === value) return currentApp && currentApp.length ? currentApp[0] : [] }
- 此时,我们监听路由跳转时,会执行两次,我们可以通过判断当前子应用是否做了切换,来处理我们路由跳转的监听
- main\micro\router\routerHanle.js
```javascript
import { isTurnChild } from "../utils";
export const turnApp = (e) => {
if (isTurnChild()) {
console.log('子应用切换了');
}
}
main\micro\utils\index.js
// 子应用是否做了切换 export const isTurnChild = () => { if (window.__CURRENT_SUB_APP__ === window.location.pathname) { return false } return true }
主应用生命周期
main\micro\router\routerHanle.js ```javascript import { lifeCycle } from “../lifeCycle.js”; import { isTurnChild } from “../utils”;
export const turnApp = async (e) => { if (isTurnChild()) { // 微前端的声明周期执行 await lifeCycle() } }
- main\micro\utils\index.js
- 在工具函数中对判断子应用是否切换的逻辑做优化
- 设置上一个子应用和当前子应用
- 通过`.match(/(\/\w+)/)`来匹配`/vue3/``/vue2`两种格式的`pathname`
- 在工具函数添加通过路由获取子应用`findAppByRoute`
```javascript
// 通过路由获取子应用
export const findAppByRoute = (router) => {
return filterApp('activeRule', router)
}
// 过滤出当前子应用
const filterApp = (key, value) => {
const currentApp = getList().filter(item => item[key] === value)
return currentApp.length ? currentApp[0] : {}
}
// 子应用是否做了切换
export const isTurnChild = () => {
// 上一个子应用
window.__ORIGIN_APP__ = window.__CURRENT_SUB_APP__
// 如果子应用和当前路由相等,则没有做切换
if (window.__CURRENT_SUB_APP__ === window.location.pathname) {
return false
}
// 匹配 /vue3/ /vue2 输出 /vue3 格式
const currentApp = window.location.pathname.match(/(\/\w+)/)
if (!currentApp) {
return false
}
window.__CURRENT_SUB_APP__ = currentApp[0]
// console.log(window.__ORIGIN_APP__, window.__CURRENT_SUB_APP__);
return true
}
- main\micro\lifeCycle.js\index.js
- 在生命周期模块中,获取上一个子应用和下一个要跳转的子应用
- 如果有上一个子应用,执行销毁逻辑
- 先执行子应用销毁
- 再执行主应用销毁
- 执行下一个子应用加载之前的逻辑
- 先执行主应用加载之前
- 再执行子应用加载之前
- 执行下一个子应用的加载完成的逻辑
- 先执行子应用加载完成
- 再执行主应用加载完成
- 由于主应用每个声明周期都是一个数组,所以要通过遍历的方式等待所有
Promise.all
都执行完成才能进行下一步操作 ```javascript import { getMainLifeCycle } from “../const/mainLifeCycle”; import { findAppByRoute } from “../utils”;
export const lifeCycle = async () => { // 获取到上一个子应用 const prevApp = findAppByRoute(window.ORIGIN_APP); // 获取到要跳转的子应用 const nextApp = findAppByRoute(window.CURRENT_SUB_APP); console.log(prevApp, nextApp); if (!nextApp) { return } // 有上一个子应用,销毁子应用 if (prevApp && prevApp.destoryed) { await destoryed(prevApp) } // 下一个子应用 const app = await beforeLoad(nextApp) await mounted(app) } // 微前端的生命周期 - beforeLoad export const beforeLoad = async (app) => { await runMainLifecycle(‘beforeLoad’) app && app.beforeLoad && app.beforeLoad() const appContext = null return appContext } // 微前端的生命周期 - mounted export const mounted = async (app) => { app && app.mounted() await runMainLifecycle(‘mounted’) } // 微前端的生命周期 - destoryed export const destoryed = async (app) => { app && app.destoryed && app.destoryed() // 对应的执行以下主应用的声明周期 await runMainLifecycle(‘destoryed’) } // 执行主应用生命周期 export const runMainLifecycle = async (type) => { const mainLife = getMainLifeCycle() await Promise.all(mainLife[type].map(async item => await item())) }
<a name="jkh2t"></a>
# 获取需要展示的页面-加载和解析 html
![](https://cdn.nlark.com/yuque/0/2022/jpeg/243804/1657797719958-1f6cd0c8-7dfa-4936-9ef2-e41f45dee030.jpeg)
- 首先我们在`beforeLoad`留了`appContext`,这里用来接收我们子应用的上下文
- main\micro\lifeCycle.js\index.js
```javascript
export const beforeLoad = async (app) => {
await runMainLifecycle('beforeLoad')
app && app.beforeLoad && app.beforeLoad()
const appContext = await loadHtml(app) // 获取 子应用 显示内容
return appContext
}
- 接下里是对子应用信息进行解析
main\micro\loader\index.js ```javascript // 加载html的方法 export const loadHtml = async (app) => { // 1. 子应用需要显示在哪里 let container = app.container // #id // 2. 子应用入口 let entry = app.entry const html = await parseHtml(entry) const ct = document.querySelector(container) if (!ct) { throw new Error(‘容器不存在,请查看’) } ct.innerHTML = html // 将子应用内容挂载到子应用容器中
return app }
export const parseHtml = async (entry) => { // 通过 get 请求获取页面信息 const html = await fetchResource(entry) return html }
- 正常情况下我们的html内容显示,是通过get请求取到的
![image.png](https://cdn.nlark.com/yuque/0/2022/png/243804/1657797942833-2516f5e2-5c41-40b9-ab9e-fd77393b6ad4.png#clientId=u47465e31-3b74-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=156&id=u564be83b&margin=%5Bobject%20Object%5D&name=image.png&originHeight=172&originWidth=353&originalType=binary&ratio=1&rotation=0&showTitle=false&size=11193&status=done&style=none&taskId=u80eeed3e-b134-4154-89a9-d2e75998a81&title=&width=320.9090839535738)
- 所以我们也可以通过get请求到页面信息
- main\micro\utils\fetchResource.js
```javascript
export const fetchResource = (url) => fetch(url).then(res => res.text())
- 我们请求到的是一个页面的信息,我们可以直接挂载到子容器中
此时我们的页面还不能加载出来,因为我们没有加载子应用的js、css等资源文件,我们需要解析出子应用的资源文件,并进行加载,才能将子应用,显示在主应用中。
获取需要展示的页面-加载和解析 js
我们要对
parseHtml
进行改造,将html挂载到一个div元素上,作为我们要解析的根元素export const parseHtml = async (entry) => { // 通过 get 请求获取页面信息 const html = await fetchResource(entry) const div = document.createElement('div') div.innerHTML = html // 针对此元素做处理 return html }
接下来对我们的子应用内容做解析
我们获取到的资源会分为这三种情况
- dom,挂载到子容器中
- scriptUrl数组,后面用作请求资源使用
script数组,script标签中有可以直接执行的内容
/** * 对子应用页面进行解析,获取资源 * @param {*} root 根元素 * @param {*} entry 子应用入口 * @returns [dom, scriptUrl, script] */ export const getResources = (root, entry) => { const scriptUrl = [] // 完整url的数组 const script = [] // script内部有执行内容的数组 const dom = root.outerHTML // outerHtml 包含当前节点根标签内容 // 深度解析 function deepParse(element) { const children = element.children const parent = element.parent // 1. 处理位于 script 中的内容 if (element.nodeName.toLowerCase() === 'script') { const src = element.getAttribute('src') // 如果没有src,说明script标签中有内容 if (!src) { script.push(element.outerHTML) } else { if (src.startsWith('http')) { scriptUrl.push(src) // 完成url地址 } else { scriptUrl.push(`http:${entry}/${src}`) // 本地url地址 } } if (parent) { parent.replaceChild(document.createComment('此 js 文件已经被微前端替换'), element) } } // link 也会有js的内容 if (element.nodeName.toLowerCase() === 'link') { const href = element.getAttribute('href') if (href.endsWith('.js')) { if (href.startsWith('http')) { scriptUrl.push(href) // 完成url地址 } else { scriptUrl.push(`http:${entry}/${href}`) // 本地url地址 } } } // children 中包含一些 script 相关的内容 for (let i = 0; i < children.length; i++) { deepParse(children[i]) } } deepParse(root) return [dom, scriptUrl, script] }
修改
parseHtml
解析过程export const parseHtml = async (entry) => { // 通过 get 请求获取页面信息 const html = await fetchResource(entry) const div = document.createElement('div') div.innerHTML = html // 针对此元素做处理 // 标签、link、script(src,js) let allScript = [] // 这里的js资源都变成可执行的资源 const [dom, scriptUrl, script] = await getResources(div, entry) // console.log([dom, scriptUrl, script]); // 获取所有js资源 const fetchedScripts = await Promise.all(scriptUrl.map(async item => await fetchResource(item))) allScript = script.concat(fetchedScripts) return [dom, allScript] }
我们对
loadHtml
也做些修改// 加载html的方法 export const loadHtml = async (app) => { // 1. 子应用需要显示在哪里 let container = app.container // #id // 2. 子应用入口 let entry = app.entry const [dom, scripts] = await parseHtml(entry) console.log(scripts); const ct = document.querySelector(container) if (!ct) { throw new Error('容器不存在,请查看') } ct.innerHTML = dom return app }
到现在为止,我们拿到了子应用的页面,和所有的js资源,后面我们需要让js资源进行执行。
执行js脚本
我们可以通过
new Function
或者eval
来执行我们JavaScript代码main\micro\sandbox\performScript.js ```javascript // 执行应用的 js 内容 new Function 篇 export const performScript = (script, appName, global) => { new Function(script).call(window, window); } // 执行应用中的 js 内容 eval篇 export const performScriptForEval = (script, appName, global) => { eval(script) }
- 之前我们已经获取到所有需要执行的js脚本,通过这两个函数都可以达到执行效果
- main\micro\loader\index.js
```javascript
const [dom, allScript] = await parseHtml(entry)
allScript.map((item) => {
performScript(item)
// performScriptForEval(item);
});
此时我们就可以在页面上看到子应用内容可以显示出来了