路由拦截

  • 重写浏览器路由跳转事件
  • 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() } }

  1. - 我们通过监听浏览器的切换,实现自定义事件,定义微前端路由监听事件,达到路由拦截的目的。
  2. - 在执行过程中,将this指回全局原生事件,如果不做这部,目前没有发现会造成什么问题。
  3. - 我们在浏览器操作过程中,发现点击返回按钮无法触发微前端自定义事件的监听,所以在监听浏览器返回事件中,执行路由切换的方法。
  4. - main/micro/utils/index.js
  5. ```javascript
  6. /**
  7. * 给当前路由跳转打补丁
  8. * @param {*} globalEvent 全局原生事件
  9. * @param {*} eventName 事件名称
  10. * @returns
  11. *
  12. * window.history.pushState = patchRouter(window.history.pushState, 'micro_push')
  13. * window.history.pushState = 函数return的部分
  14. */
  15. export const patchRouter = (globalEvent, eventName) => {
  16. return function () {
  17. const e = new Event(eventName);
  18. // this 指向当前当前监听的函数
  19. globalEvent.apply(this, arguments); //? 不apply会造成什么问题?
  20. window.dispatchEvent(e)
  21. }
  22. }
  • main/micro/router/routerHanle.js

    1. export const turnApp = (e) => {
    2. console.log('路由切换了', e);
    3. }

    获取首个子应用

  • 整体逻辑

5-4~10 微前端框架 开发 - 图1

  • 注册完子应用,进行微前端的启动

    1. export const starMicroApp = () => {
    2. // 注册子应用
    3. registerMicroApps()
    4. // 启动
    5. start();
    6. };
  • 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
    }
    

    主应用生命周期

    5-4~10 微前端框架 开发 - 图2

  • 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())

image.png

  • 我们请求到的是一个页面的信息,我们可以直接挂载到子容器中

image.png

  • 此时我们的页面还不能加载出来,因为我们没有加载子应用的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
    }
    
  • 接下来对我们的子应用内容做解析

5-4~10 微前端框架 开发 - 图5

  • 我们获取到的资源会分为这三种情况

    • 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);
});

此时我们就可以在页面上看到子应用内容可以显示出来了
image.png