👽 MicroApp微前端框架渲染挂载实现(WebComponent新特性)

769 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情

关于微前端的不同解决方案总结,可以结合下我之前的文章~

# 👾 浅谈微前端解决方案

WebComponent介绍:

我们可以通过使用新特性CustomElement自定义一个html标签,来表示一个原生组件,以此来代替如今框架中React、vue中的组件。

如何使用?

class microApp extends HTMLElement {
  constructor() {
    super();
  }
}
window.customElements.define("micro-app", microApp);

这样我们在模板中就可以使用了!

<micro-app></micro-app>

有意思的是,这个原生组件与我们框架中的组件一样,有着自己的生命周期钩子。

connectedCallback:当组件被插入到文档中触发,有利于我们进行代码初始化。

disconnectedCallback:当组件从文档中被移除时触发,有利于我们进行代码清除。

attributeChangedCallback(attrName, oldVal, newVal):当组件中设置的响应式数据发生变更时触发,结合observedAttributes属性一起使用。

adoptedCallback:当组件被插入到一个新的文档中触发。

参考MDN中Document.adoptNode的API介绍。

Document.adoptNode()

从其他的 document 文档中获取一个节点。该节点以及它的子树上的所有节点都会从原文档删除 (如果有这个节点的话), 并且它的ownerDocument属性会变成当前的 document 文档。之后你可以把这个节点插入到当前文档中。

步入本文主题:实现一个迷你版的microApp。

microApp核心要点:巧妙利用CustomElement,使用htmlEntry+各种隔离机制实现子应用挂载、运行与卸载。

首先我们思考,为什么微前端和webcomponent扯上了关系?

图出自microApp官网 image.png

可以看到,我们利用了webcomponent原生组件特性,来充当一个基座应用与子应用的媒介,因为其具有天然的原生支持,可以直接被注册在基座之中,通信也变得十分轻松,天然的生命周期钩子使得它可以进行子应用不同状态的流转操作。

本文主要实现的是对于子应用的挂载渲染操作,自然使用到的便是webcomponent的connectedCallback钩子。

下面来看具体如何实现,这边我们使用html编写基座,vue3编写子应用。

基座代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>微前端解决方案 -WebComponent</title>
</head>
<script src="./index.mjs" type="module"></script>
<body>
    <div>微前端解决方案 -WebComponent</div>
    <div>
      // active-url 表示子应用激活url
      <micro-app active-url="http://127.0.0.1:8002/"></micro-app>
    </div>
</body>
</html>

子应用代码如下:

main.js

import { createApp } from "vue";
import App from "./App.vue";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";

createApp(App).use(ElementPlus).mount("#app");

webpack.config.js

module.exports = {
    devServer: {
        port:8002,
        headers:{
            'Access-Control-Allow-Origin':'*'
        }
    },
}

microApp组件代码:

import { fetchHtml, executeScript } from "./fetch.mjs";

class microApp extends HTMLElement {
  // 子应用激活url
  get activeUrl() {
    return this.getAttribute("active-url");
  }
  // 需要被监听的属性放在这里定义
  static get observedAttributes() {
    // return ['name']
  }
  get container() {
    return this.shadowRoot;
  }
  constructor() {
    super();
  }
  // 生命周期钩子: component被插入dom中触发
  async connectedCallback() {
    // 注意:getAttribute只能在这时候拿到
    // this.activeUrl = this.getAttribute("active-url");
    const temp = await this.load();
    this.bootstrap(temp);
    Promise.resolve().then(() => {
      this.mount();
    });
  }
  // 加载资源
  async load() {
    const temp = await fetchHtml(this.activeUrl);
    return temp;
  }
  // 初始化子应用
  bootstrap(temp) {
    // 创建文档片段插入组件中
    const template = document.createElement("template");
    template.innerHTML = temp;
    const fra = document.createDocumentFragment();
    fra.appendChild(template.content.cloneNode(true));
    this.appendChild(fra);
  }
  // 挂载子应用
  mount() {
    // 执行相关script
    executeScript(document);
  }
}
window.customElements.define("micro-app", microApp);

工具函数:

// 保存script脚本
const sourceSet = new Set();

// 封装请求资源
async function fetchResource(url) {
  const res = await fetch(url);
  // res.text 也是一个promise
  return res.text();
}

// 请求html文件
export async function fetchHtml(url) {
  let template = await fetchResource(url);
  return resolveScript(template, url);
}

// 对html源码进行处理
export function resolveScript(template, activeUrl) {
  return template
    .replace(/<head[^>]*>[\s\S]*?<\/head>/i, (match) => {
      return match
        .replace(/<head/i, "<micro-app-head")
        .replace(/<\/head>/i, "</micro-app-head>");
    })
    .replace(/<body[^>]*>[\s\S]*?<\/body>/i, (match) => {
      return match
        .replace(/<body/i, "<micro-app-body")
        .replace(/<\/body>/i, "</micro-app-body>");
    })
    .replace(/src="(.*?)"/g, (_, text) => {
      sourceSet.add(`${activeUrl}${text}`);
      return `src="${activeUrl}${text}"`;
    })
    .replace(/href="(.*?)"/g, (_, text) => {
      return `href="${activeUrl}${text}"`;
    });
}

// 包装所有script脚本
function getExternalScript() {
  return Promise.all(Array.from(sourceSet).map((url) => fetchResource(url)));
}

// 执行脚本
export async function executeScript(document) {
  const scripts = await getExternalScript();
  scripts.forEach((code) => {
    (function (document) {
      eval(code);
    })(document);
  });
}

最终效果如下:

image.png

欢迎朋友们在评论区讨论~