MontoApp(四)- Web Components 方案

本节继续介绍微前端的另一种方案:Web Components。它是根据浏览器原生支持的组件化方案来封装的微应用。其实现方式类似于动态 script 方案,

image.png

仓库地址:tutorial/web-components-cookie 分支

1. 主应用

还是老样子,启动一个node服务:

app.listen(port.main, host);

然后配置请求微服务列表的接口:

app.post("/microapps", function (req, res) {

  console.log("main cookies: ", req.cookies);

  // 设置一个响应的 Cookie 数据
  res.cookie("main-app", true);

  // 这里可以是管理后台新增菜单后存储到数据库的数据
  // 从而可以通过管理后台动态配置微应用的菜单
  res.json([
    {
      name: "micro1",
      id: "micro1",
      // 自定义元素名称
      customElement: "micro-app-1",
      // 这里暂时以一个入口文件为示例
      script: `http://${host}:${port.micro}/micro1.js`,
      style: `http://${host}:${port.micro}/micro1.css`,
      prefetch: true,
    },
    {
      name: "micro2",
      id: "micro2",
      customElement: "micro-app-2",
      script: `http://${host}:${port.micro}/micro2.js`,
      style: `http://${host}:${port.micro}/micro2.css`,
      prefetch: true,
    },
  ]);
});

主应用的 html 页面与 动态 script 基本页面一致,这里只展示不同之处:

hashChangeListener() {
  // Web Components 方案
  // 微应用的插槽
  const $slot = document.getElementById("micro-app-slot");

  window.addEventListener("hashchange", () => {
    this.microApps?.forEach(async (microApp) => {
      // Web Components 方案
      const $webcomponent = document.querySelector(
        `[micro-id=${microApp.id}]`
      );

      if (microApp.id === window.location.hash.replace("#", "")) {
        console.time(`fetch microapp ${microApp.name} static`);
        // 加载 CSS 样式
        microApp?.style &&
          !this.hasLoadStyle(microApp) &&
          (await this.loadStyle(microApp));
        // 加载 Script 标签
        microApp?.script &&
          !this.hasLoadScript(microApp) &&
          (await this.loadScript(microApp));
        console.timeEnd(`fetch microapp ${microApp.name} static`);

        // 动态 Script 方案
        // window?.[microApp.mount]?.("#micro-app-slot");

        // Web Components 方案
        // 如果没有在 DOM 中添加自定义元素,则先添加处理
        if (!$webcomponent) {
          // Web Components 方案
          // 自定义元素的标签是微应用先定义出来的,然后在服务端的接口里通过 customElement 属性进行约定
          const $webcomponent = document.createElement(
            microApp.customElement
          );
          $webcomponent.setAttribute("micro-id", microApp.id);
          $slot.appendChild($webcomponent);
        // 如果已经存在自定义元素,则进行显示处理
        } else {
          $webcomponent.style.display = "block";
        }
      } else {
        this.removeStyle(microApp);
        // 动态 Script 方案
        // window?.[microApp.unmount]?.();

        // Web Components 方案
        // 如果已经添加了自定义元素,则隐藏自定义元素
        if ($webcomponent) {
          $webcomponent.style.display = "none";
        }
      }
    });
  });
}

2. 子应用改造

子应用也启动一下:

// 这里捎带手配置子应用 ajax 请求的跨域
app.use((req, res, next) => {
  // 跨域请求中涉及到 Cookie 信息传递,值不能为 *,必须是具体的地址信息
  res.header("Access-Control-Allow-Origin", `http://你的域名.com:${port.main}`);
  res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
  res.header("Allow", "GET, POST, OPTIONS");
  // 允许跨域请求时携带 Cookie
  res.header("Access-Control-Allow-Credentials", true);
  next();
});

app.use(
  express.static(path.join("public", "micro"), {
    etag: true,
    lastModified: true,
  })
);

app.post("/cors", function (req, res) {
  console.log("micro cookies: ", req.cookies);

  // 增加 SameSite 和 Secure 属性,从而使浏览器支持 iframe 子应用的跨站携带 Cookie
  // 注意 Secure 需要 HTTPS 协议的支持
  const cookieOptions = { sameSite: "none", secure: true };

  // 设置一个响应的 Cookie 数据
  res.cookie("micro-app", true, cookieOptions);
  res.json({
    hello: "true",
  });
});

app.listen(port.micro, host);

子应用的入口js要改造一下,暴露出来生命周期钩子:

class MicroApp2Element extends HTMLElement {
  constructor() {
    super();
  }

  // [生命周期回调函数] 当 custom element 自定义标签首次被插入文档 DOM 时,被调用
  connectedCallback() {
    console.log(`[micro-app-2]: 执行 connectedCallback 生命周期回调函数`);
    // 挂载应用
    // 相对比动态 Script,组件内部可以自动进行 mount 操作,不需要对外提供手动调用的 mount 函数,从而防止不必要的全局属性冲突
    this.mount();
  }

  // [生命周期回调函数] 当 custom element 从文档 DOM 中删除时,被调用
  disconnectedCallback() {
    console.log(
      `[micro-app-2]: 执行 disconnectedCallback 生命周期回调函数`
    );
    // 卸载处理
    this.unmount();
  }

  mount() {
    const $micro = document.createElement("h1");
    $micro.textContent = "微应用2";
    // 将微应用的内容挂载到当前自定义元素下
    this.appendChild($micro);

    // 新增 Ajax 请求,用于请求 micro1.js 所在的服务
    // 需要注意 micro1.js 动态加载在主应用 localhost:4000 下,因此请求是跨域的
    window
      .fetch("http://30.120.112.54:3000/cors", {
        method: "post",
        credentials: "include"
      })
      .then((res) => res.json())
      .catch((err) => {
        console.error(err);
      });
  }
}

window.customElements.define("micro-app-2", MicroApp2Element);

对比于动态Script方案,Web Components方案有以下特点:

  • 不需要对外抛出全局API(挂载与卸载),Web Components 天然支持,可复用性更强。
  • 更标准化,Web Components的能力后续会随着w3c标准逐步提升。
  • 也是动态 script 的优点:可以非常便捷的进行移植和组件替换

当然,Web Components还是存在一些不足:

  • 兼容性问题。对旧版本浏览器兼容性不好。
  • 有一定上手难度,需要额外学习Web Components相关知识。
  • 子应用是侵入式的

Was this page helpful?