React在Ember应用中的微侵占
#javascript #react #Web组件 #ember

在这篇文章中,我们将研究如何将React应用程序与其自己的路由机制集成为Ember应用程序中的微悬挂。如果您在这里,那么我希望您已经知道这些框架,所以让我们看看为什么要首先将React应用程序集成到Ember应用程序中。

为什么?

世界正在朝着微悬挂(MFE)架构迈进。 Ember最适合大规模应用程序,它具有创建强大企业应用程序所需的一切,而React是一个带有大量开源插件的轻量级库,这使其非常可定制,并且非常适合我们的MFE。

是的,我听到了。还有其他框架,例如SolidJsSvelteJs,它们正在获得普及,并且可能是一场艰巨的竞争。这些框架不会在捆绑包中运送任何与框架相关的代码,并且在编译时间而不是浏览器中进行大部分繁重升降机,这使其重量极轻,甚至比自身反应。但是我们都知道,React是JavaScript框架空间中无可争议的领导者,并且已经知道并已经从事React工作的大量开发人员令人难以置信。因此,我们选择了“反应”作为MFE的首选框架。

设置存储库

我们将使用PNPM workspaces为此POC设置monorepo。如果您想将此MFE添加到现有应用程序中,我强烈建议您将您的应用程序放在单独的存储库中。我们肯定不希望任何过时的工具和设置成为阻止我们新MFE的最新技术的阻滞剂。

这是我使用命令行设置monorepo结构的方式。

npm install -g ember-cli
ember -v
mkdir ember-react-mfe
cd ember-react-mfe
pnpm init
git init
echo -e "node_modules" > .gitignore
echo "Monorepo with react MFE in ember app" > README.md
echo "packages:\n  - apps/*" > pnpm-workspace.yaml
mkdir apps
cd apps
ember new web-app --skip-npm --skip-bower --no-welcome --skip-git
pnpm create vite admin-app --template react-ts
cd ../
pnpm install
npm pkg set scripts.admin="pnpm --filter admin-app"
npm pkg set scripts.app="pnpm --filter web-app"

我们的基本monorePo设置现在已经完成,您应该能够通过启动Dev Server启动Dev Server

来验证两个应用程序

启动主要的Ember Web应用程序

pnpm app start

启动React Admin App

pnpm admin dev

React应用程序

让我们首先通过安装react-router-dom NPM软件包将路由功能添加到我们的React应用程序中。

pnpm admin add react-router-dom

创建示例页面

  • 使用所有必要的路由创建路由器文件。 为了简单目的,我在同一文件中添加了所有组件,而不是创建单独的文件。

在理想的世界中,应该懒惰以支持从单独的文件夹和文件中分配的代码。无论您在正常的反应应用程序中所做的一切都可以在没有任何问题的情况下完成。

touch apps/admin-app/src/router.tsx

从此要点复制Router.tsx文件的内容。
https://gist.github.com/vinomanick/919ad40ab98716695837c207526ee156

  • 更新main.tsx文件,以支持路由而不是渲染默认的应用程序组件。
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import { RouterProvider } from "react-router-dom";
import { generateRouter } from "./router.tsx";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <RouterProvider router={generateRouter({ basename: "/" })} />
  </React.StrictMode>
);

  • 运行服务器并确保所有路线都按预期工作。

  • 让我们删除与应用程序相关的文件,然后以下更新index.css。

rm -rf apps/admin-app/src/App.tsx apps/admin-app/src/App.css

将React应用程序转换为Web组件

  • React应用程序可以直接在Ember应用中渲染,但是最好将整个应用程序包装在Web组件中,以在主机和MFE之间对样式进行强封装。

  • main.tsx文件负责将React App安装到LOAD上的root上。我们需要更改该逻辑并将React应用程序安装在Web组件Shadow dom中存在的元素中,并在Web组件中加载样式,如下所示。

import React from "react";
import ReactDOM from "react-dom/client";
import indexStyle from "./index.css?inline";
import { RouterProvider } from "react-router-dom";
import { generateRouter } from "./router.tsx";

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

  connectedCallback() {
    console.debug("Admin MFE element added to app");
    const mountPoint = this.createMountPoint();
    const props: Record<string, any> = this.getProps(this.attributes);
    this.addStyles();
    this.mountReactApp(mountPoint, props.options);
  }

  /**
   *  Creates a mount point and attach it to the shadow dom
   */
  createMountPoint() {
    const mountPoint = document.createElement("div");
    mountPoint.setAttribute("id", "admin-mfe");
    mountPoint.style.height = "100%";
    mountPoint.style.overflow = "auto";
    this.attachShadow({ mode: "open" }).appendChild(mountPoint);
    return mountPoint;
  }

  /**
   *  Mounts the react app in to the given element
   */
  mountReactApp(element: HTMLElement, { basename = "" }: { basename: string }) {
    ReactDOM.createRoot(element).render(
      <React.StrictMode>
        <RouterProvider router={generateRouter({ basename })} />
      </React.StrictMode>
    );
  }

  /**
   * Adds the style to the web component as an inline tag
   */
  addStyles() {
    const id = "admin-style";
    const style = document.createElement("style");
    style.id = id;
    style.appendChild(document.createTextNode(indexStyle));
    this.shadowRoot?.appendChild(style);
  }

  getProps(attributes: NamedNodeMap) {
    return [...attributes]
      .filter((attr) => attr.name !== "style")
      .map((attr) => this.convert(attr.name, attr.value))
      .reduce((props, prop) => ({ ...props, [prop.name]: prop.value }), {});
  }

  convert(attrName: string, attrValue: any) {
    let value: any = attrValue;
    if (attrValue === "true" || attrValue === "false") {
      value = attrValue === "true";
    } else if (!isNaN(attrValue) && attrValue !== "") {
      value = +attrValue;
    } else if (/^{.*}/.exec(attrValue)) {
      value = JSON.parse(attrValue);
    }
    return { name: attrName, value: value };
  }
}

customElements.define("admin-mfe", AdminMFE);

本地开发服务器更新

  • 通过删除root div并添加Web组件来更新index.html。身体应该是这样的。
  <body>
    <script type="module" src="/src/main.tsx"></script>
    <admin-mfe />
  </body>
  • 运行DEV服务器并检查DOM,您应该能够在内部和Web组件中看到整个应用程序。

将Web组件作为可加载脚本公开

对于Vite Bundler,输入文件是index.html,因此离开所有其他编译的资产都会自动识别指纹。因此,我们将无法简单地调用主机应用程序捆绑的index-<hash>.js。为了解决我们需要进行以下操作,

  • 更新Vite配置,以生成带有所有指纹资产及其位置的清单文件。
export default defineConfig({
  plugins: [react()],
  build: {
    manifest: true,
  },
});
  • 在公共文件夹中为我们的应用创建一个新的入口点。公共文件夹中添加的任何内容都将通过VITE复制到Dist文件夹。
touch apps/admin-app/public/admin-mfe.js
  • 用以下内容更新它,
import manifest from "./manifest.json" assert { type: "json" };

export const mountApp = ({ element, options }) => {
  const { baseURL, ...rest } = options;
  if (!baseURL) {
    throw Error(
      "Please provide the baseURL in the options for the admin MFE to load"
    );
  }
  import(`${baseURL}${manifest["index.html"].file}`).then(() => {
    const adminMfe = document.createElement("admin-mfe");
    adminMfe.setAttribute("options", JSON.stringify(rest));
    element.appendChild(adminMfe);
  });
};

这将揭示一种称为Mountapp的方法,该方法可以由主机应用程序将Web组件连接到主机应用程序元素。

构建React应用程序并将其用于Ember应用程序

  • 在手表模式下构建React应用程序
pnpm admin build --watch
  • 打开一个新终端,运行预览命令
pnpm admin preview

预览命令将在http://localhost:4173上运行静态服务器,该服务器指向React App Dist文件夹。

因此,主机应用必须向http://localhost:4173/fw-chat-admin.js提出请求,该应用将充当我们的MFE的切入点。

恭喜您直到这里,我们将使用React应用程序的设置完成。让我们看看如何在Ember应用程序中链接和加载此应用程序。

Ember应用

路线设置

更新Ember Router.js以创建2种新路由,与React Ember不同,Ember默认情况下带有路由器。在Ember中存在逻辑的一条普通路线和我们要安装管理员MFE的另一条管理路线。

Router.map(function () {
  this.route('dashboard');
  this.route('admin', function () {
    this.route('item', { path: '*' });
  });
});

我们不必模拟React MFE中存在的确切路由结构。取而代之的是,我们可以将应用程序加载到管理路线中,React将负责过渡到子路由并更新浏览器URL。

但是,问题是,当用户输入URL'/admin/groups'主机应用程序时,由于它不是根据的有效途径,因此当用户输入URL'/admin/groups'时。为了避免这种情况,我们创建了一条虚拟的儿童路线,称为item admin内部带有球模式。

Navbar设置

  • 让我们创建一个导航组件,以便我们可以从NAV栏中从主机和MFE路线进行导航并验证水疗中心。
touch apps/web-app/app/components/app-nav.hbs
touch apps/web-app/app/components/app-nav.js

app-nav.hbs

<nav>
  <LinkTo @route='dashboard'>Dashboard</LinkTo>
  <button {{on 'click' this.handleAdminClick}}>Admin</button>
</nav>

app-nav.js

import Component from '@glimmer/component';
import { action } from '@ember/object';
import { service } from '@ember/service';

export default class AppNavComponent extends Component {
  @service router;

  @action handleAdminClick() {
    if (this.router.currentURL.startsWith('/admin')) {
      window.postMessage(
        { type: 'navigateToAdminMfeHome' },
        window.location.origin
      );
    }
    this.router.transitionTo('/admin');
  }
}

注意:
Ember仅知道管理路线,如果用户已在React中导航到admin/groups页面,即使浏览器URL已由React更新,Ember也不会意识到它。

如果用户单击“管理员”链接,则它不会做任何事情根路由如果用户在任何管理员路线中时单击Admin nav链接。

如果它们在其他路由器上,那么正常的过渡本身将起作用。

  • 更新application.hbs文件以添加AppNav组件。
{{page-title 'WebApp'}}
<AppNav />
{{outlet}}

来自React消息的听众

  • 就像主机应用程序如何发送事件以应对几个案例一样,React也将发送事件以捕获我们需要在应用程序级别上有一个侦听器,所以让我们创建一个。
touch apps/web-app/app/routes/application.js
import Route from '@ember/routing/route';
import { service } from '@ember/service';
export default class ApplicationRoute extends Route {
  @service router;

  setupController() {
    super.setupController(...arguments);
    const { router } = this;

    window.addEventListener('message', function (event) {
      console.log('inside host app listener', event.data);
      const { data } = event;
      if (!data.type) {
        return;
      }
      if (data.type === 'navigateToHost') {
        router.transitionTo(data.payload.route);
      }
    });
  }
}

如果您想从React导航到Ember路线,那么我们需要发送带有路由信息的navigateToHost的帖子消息,以便Ember可以执行所需的过渡。

路由模板

  • 按计划创建2个路由的路由模板,并如下更新内容
touch apps/web-app/app/templates/dashboard.hbs
touch apps/web-app/app/templates/admin.hbs

仪表板

Dashboard page present in ember
<LinkTo @route='admin.item' @model='groups'>Groups</LinkTo>

我们可以创建从Ember创建导航链接,以使用此管理员路由并传递模型Prop。

Admin.hbs

<div {{admin-mfe-mount}}></div>
{{outlet}}

此文件中存在的此div将充当根div,我们将在其中安装所有管理员及其子路由的React Web组件应用程序。

管理MFE安装修饰符

我们将利用Ember修饰符包将React应用程序附加到Ember中的Div。
因此,让我们安装Ember修饰符包。

pnpm app add -D ember-modifier

创建Admin-Mfe-Mount修饰符,

mkdir apps/web-app/app/modifiers
touch apps/web-app/app/modifiers/admin-mfe-mount.js
import Modifier from 'ember-modifier';

export default class AdminMFEMountModifier extends Modifier {
  modify(element) {
    import('http://localhost:4173/admin-mfe.js').then((module) => {
      module.mountApp({
        element,
        options: {
          basename: '/',
          baseURL: 'http://localhost:4173/',
        },
      });
    });
  }
}

现在运行Ember服务器并验证管理员MFE已成功渲染在给定元素中。

演示:

Image description

优点缺点

在Ember应用程序中使用了React MFE后,让我们讨论使用上述方法将React React React React纳入Ember.js应用程序的利弊。

优点

  • 逐步迁移ember代码库
  • 易于将应用程序分配给多个MFE,并为每个微货币提供一个专用团队。
  • 提供了一个机会,可以摆脱不稳定的旧代码,并慢慢将旧代码转换为最新技术。
  • 由于将其作为Web组件渲染,因此Cookie和会话将从主机应用程序中延长,因此无需在MFE中具有专用的身份验证机制,同时仍可以实现IFRAME方法提供的封装。
  • 可以将MFE的整个版本和部署周期与主机应用程序分解。

缺点

  • 框架在您的生产捆绑包中额外的40 kb(反应运行时)

样品回购

该帖子的代码托管在github here中。

请看一下GitHub存储库,让我知道您的反馈,在评论部分中查询。如果您觉得这里有更多的利弊,请告诉我们。

参考