自己构建——微前端框架

75 阅读6分钟

实现微前端,我们的大致思路应该如下:

  1. 监视页面路由的变化
  2. 根据当前页面路由匹配子应用
  3. 主应用加载子应用
  4. 特定容器渲染子应用

准备工作

在qinkun里,主应用的main.js中引入两个函数registerMicroApps, start,仿照qinkun,我们创建自己的微前端框架文件夹,命名micro-fe , 我们在主应用的main.js仿照qiankun做出如下配置

//引入的不再是Vue构造函数了,引入的是一个名为createApp的工厂函数
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

import { registerMicroApps, start } from './micro-fe';

registerMicroApps([
    {
      name: 'vue1', // app name registered
      entry: 'http://localhost:9005/',
      container: '#micro-container',
      activeRule: '/vue1',
    },
    {
      name: 'vue2',
      entry: 'http://localhost:9005/',
      container: '#micro-container',
      activeRule: '/vue2',
    },
    {
        name: 'react',
        entry: 'http://localhost:9005/',
        container: '#micro-container',
        activeRule: '/react',
      },
 ]);
  
start();

createApp(App).use(router()).mount('#app')

监视路由变化

我们的satrt函数里,应该是我们微前端框架的核心逻辑,需要实现监视页面路由的变化、根据当前页面路由匹配子应用、主应用加载子应用、特定容器渲染子应用四个核心功能。 首先,我们实现路由的监听,路由分为hash路由和history路由。

hash路由

对于hash路由的变化,我们可以使用window.onhashchange来监听,比较容易。本实例中,我们暂时不考虑。

history路由

对于history路由, 其history.go、history.back、history.forword 等方法我们可以使用popstate事件进行监听。 (浏览器的前进后退会触发history.go、history.back方法)。

// micro-fe\文件夹下创建rewrite-router.js
// 在start函数里(micro-fe\index.js)进行引入使用

根据当前路由匹配子应用

  1. 获取当前的路由路径
  2. 去 apps 里面查找
// micro-fe\文件夹下创建handle-router.js文件

加载子应用,获取并渲染html内容

  • 使用ajax获取子应用对应域名下的html
  • 获取要渲染html的dom元素
  • 将获取的html挂载在获取的dom元素上

观察控制台,我们发现html已经被成功挂载在主应用的容器里,但是,页面并没有如我们预期的那样显示出来 原因在于,页面的渲染需要通过执行 js代码来生成内容,浏览器出于安全考虑,innerHTML中生成的script不会被加载。因此,script标签的解析及处理是微前端框架的第一步核心操作。

    //3.加载子应用
    // 使用 fetch 获取子应用对应域名下的html
    html = await fetch(app.entry).then(res => res.text())
    //获取要渲染html的dom元素
    const container = document.querySelector(app.container)
    //将获取的html挂载在获取的dom元素上
    container.innerHTML = html

资源请求函数封装

在乾坤框架里,对于html的资源请求,阿里做了单独的插件。这个插件包含三个核心对象

  • template:用于获取html模板
  • getExternalScripts()函数:获取所有的script标签代码
  • execScripts()函数:获取并执行所有的js脚本

类似的,我们也可以封装一个这样的函数。由于html和js内容都是通过fetch进行请求的,我们可以先将fetch函数进行封装。

//micro-fe\文件夹下创建fetch-resource.js文件
export const fetchResource = url => fetch(url).then(res => res.text())

// micro-fe\文件夹下创建import-html.js文件。

// 最后,我们将封装好的 importHT ML函数导出在micro-fe\handle-router.js中引入使用

获取所有的script标签及js代码

  1. 获取所有的script标签:
  2. 解析scripts报签内的js内容

观察浏览器控制台,可以看到,所有的js内容已经获取到了,虽然目前只是字符串形式

// 行内标签解析:直接使用innerHTML获取内容
// 外链标签解析:使用fecth函数请求内容

渲染子应用

现在,我们已经知道,执行完获取的子应用js代码后,页面上就应该能够渲染出我们的子应用界面。

// 执行获取的js内容
// 我们使用eval函数来执行这些脚本内容。

切换路由,我们会发现,我们的页面被成功渲染出来!!!但是,页面整体被替换成了vue2的内容,而不是渲染在我们指定的容器里。 这是为什么呢?其实很简单,当前我们的全局没有微前端的全局变量,vue2子应用在渲染时没有走微前端框架,直接渲染在了id = app这个容器里,而这个容器也是主应用的容器名称。

使用微前端框架执行render函数

要使子应用运行在微前端框架里,我们只需要在handle-router.js中添加变量window.MICRO_WEB = true即可

//配置全局变量
    window.__MICRO_WEB__ = true

此时,切换路由就不会出现app根节点被替换的问题,但由于vue2子应用的render函数没有执行,所以页面依旧不会有内容。我们需要做的就是拿到子应用的生命周期函数,手动执行render函数。

umd

子打包的时候,我们做了如下配置

    output: {
      // 把子应用打包成 umd 库格式
      libraryTarget: 'umd',                          //umd格式  支持comm.js引入  浏览器、node可以识别
      filename: 'vue2.js',                           //打包出的文件名称
      library: 'vue2',                               //全局可以通过window.vue2拿到的应用
      jsonpFunction: `webpackJsonp_${name}`,          
    },

//通过umd,我们在微前端框架里可以使用window对象来获取子应用的生命周期函数。
//子应用的所有信息
console.log(window['vue2']);


执行子应用的生命周期函数

对获取到的生命周期函数信息,我们可以将其绑定在我们的配置列表app里,然后使用app.mount 对这个生命周期函数进行调用即可。为了便于函数逻辑的添加,我们可以把生命周期函数的调用都封装成函数。

//详情:handle-router.js

卸载前一个子应用

当我们切换路由时,会发现页面内容不断变长,原因在于原先的子应用并没有被卸载。因此,我们需要实现应用切换时,卸载上一个子应用,实现这个功能,我们就需要得到浏览器的历史记录。 浏览器由于安全问题,没有提供路由的历史记录获取API,我们需要自己在路由监视的代码中自己维护一个路由记录。

//micro-fe\rewrite-router.js
// micro-fe\handle-router.js

样式资源文件的加载

// 在微前端框架里的子应用图片资源是加载不到的。
// 因为图片的路径是子应用ip + url (9004 + url)
// 在主应用请求后就变成了主应用ip+ url (8080 + url)
// 要解决此问题,我们可以在子应用里把路径写死,像这样
module.exports = {
    publicPath:'http://localhost:9004',
}


//webpack支持运行时的publicPath
import './public-path.js'
module.exports = {
    publicPath:webpack_public_path,
}

//./public-path.js
if (window.__MICRO_WEB__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}


//配置全局变量
window.__MICRO_WEB__ = true
window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = app.entry + "/"