[译] 微前端:将来前端开发的新趋势 — 第三部分

微前端:将来前端开发的新趋势 — 第三部分

作好前端开发不是件容易的事情,而比这更难的是扩展前端开发规模以便于多个团队能够同时开发一个大型且复杂的产品。本系列文章将描述一种趋势,能够将大型的前端项目分解成许多个小而易于管理的部分,也将讨论这种体系结构如何提升前端代码团队工做的有效性和效率。除了讨论各类好处和代价以外,咱们还将介绍一些可用的实现方案和深刻探讨一个应用该技术的完整示例应用程序。html

建议按照顺序阅读本系列文章:前端

跨应用通讯

关于微前端最多见的问题就是如何让它们互相交流。通常来讲,咱们建议通讯越少越好,由于这一般会从新引入不恰当的耦合,而这种耦合是咱们首先想要避免的。node

也就是说,某种程度上的通讯一般是须要的。自定义事件容许微前端直接通讯,这是最小化直接耦合的好方法,虽然它确实使肯定和执行微前端之间存在的约定变得更加困难。另外,向下传递回调和数据(在这里是从容器应用向下到微前端)的 React 模型也是一种好方法,它使得约定更加明确。第三种方法是使用地址栏做为通讯机制,后面咱们会谈到更多细节react

若是你在使用 redux,经常使用的方法是创建一个对于整个应用单1、全局、共享的 store。然而,若是微前端都应该是它本身的独立应用,那么它们每一个都有本身的 redux store 是有意义的。Redux 文档甚至提到“将 Redux 应用程序隔离为更大应用程序中的组件”做为拥有多个 store 的正当理由。android

不管选择哪一种方式,咱们但愿微前端经过发送消息或者事件来彼此通讯,避免任何状态共享。就像跨微服务共享数据库,只要咱们共享数据结构和领域模型,就会产生大量的耦合,这会变得很是难以维护。webpack

与样式同样,有几种不一样的方法能够在这方面起到很好的做用。最重要的事情是对你正在引入的耦合考虑深远,以及你将如何保持约定。就像微服务之间的集成同样,若是没有跨不一样应用程序和团队的协调升级过程,你就没法对集成作出重大变动。ios

你也应该考虑如何自动验证集成没有挂掉。功能测试是一种方式,但咱们更但愿限制编写功能测试的数量,以便控制实现和维护它们的成本。或者你能够实施某种形式的消费者驱动的约定,这样每一个微前端能够指定它对于其余微前端的依赖,无需在浏览器中实际集成和运行它们。git


后端通讯

若是咱们有独立的团队在前端应用程序上独立工做,那么后端开发呢?咱们很是相信全栈团队的价值,他们从可视化代码到 API 开发、数据库和基础架构代码负责整个应用的开发。BFF 模式在这里发挥了做用,每个前端应用都有一个对应的后端来单独知足前端的需求。虽然 BFF 模式最初可能意味着每一个前端通道(web、mobile 等)的专用后端,它能够很容易地扩展为每个微前端的后端。github

这里有不少因素须要考虑。BFF 多是自包含的,具备本身的业务逻辑和数据库,或者也可能只是一个下游服务的聚合器。若是有下游服务,那么拥有微前端及其 BFF 的团队可能有或可能没有意义,来拥有一些这样的服务。若是微前端只有一个与之通信的 API ,而且它至关稳定,那么构建 BFF 就根本没有多大价值了。有一个指导原则是,团队构建一个特定微前端时不该该必须得等其余团队来为他们构建东西。所以若是每当给微前端添加新功能时也要求后端更改,那么由同一个团队拥有的 BFF 就是一个很好的案例。web

该图表显示三对前端/后端。第一个后端只与本身的数据库对话。其余两个后端与共享下游服务进行通讯。两种方法都是有效的

图 7:有不少不一样的方式构建你的前/后端关系

其余常见问题有,应该如何经过服务器对微前端应用的用户进行身份验证和受权?明显咱们的用户应该只须要认证一次,所以鉴权一般成为属于容器应用拥有的普遍关注的问题。容器可能有某种登陆形式,咱们经过它得到某种令牌。令牌由容器保存,能够在初始化时注入到每一个微前端中。最终,微前端能够在任何发送给服务器的请求中携带令牌,而后服务器就能够执行任何须要的验证。


测试

在测试方面,咱们认为笨重前端和微前端之间没有太大区别。一般来说,你用来测试笨重前端的任何策略均可以应用于每一个微前端。也就是说,每一个微前端都应该有本身全面的自动化测试套件来保证代码的质量和正确性。

明显的障碍是各类微前端与容器应用的集成测试。这个可使用你喜欢的功能/端对端测试工具(好比 Selenium 或 Cypress)来完成,可是不要过分使用。功能测试应该只涵盖没法在测试金字塔较低级别测试的方面。咱们的意思是,使用单元测试涵盖低级别业务逻辑和渲染逻辑,功能测试只用来验证页面是否正确渲染。例如,你能够在特定 URL 上加载彻底集成的应用程序,并断言相应微前端的硬编码标题出如今页面上。

若是用户的使用跨越微前端,那么你能够用功能测试来测试这些,但要保证功能测试专一于验证前端的整合,而不是每一个微前端的内部业务逻辑,这应该已经被单元测试所涵盖。正如刚才提到的,用户驱动的约定有助于直接指定微前端之间发生的交互,而不会出现集成环境和功能测试的瑕疵。


案例详解

本文后面的大部份内容将详细解释咱们的示例应用程序实现的一种方式。咱们将重点关注容器应用和微前端如何使用 JavaScript 整合在一块儿,这多是最有趣也最复杂的部分。你能够在 demo.microfrontends.com 看到实时部署的最终结果,全部源代码均可以在 Github 上看到。

整个微前端示例应用的首页“概览”截图

图 8:整个微前端示例应用的首页“概览”

该示例彻底使用 React 开发,有必要说明的是,React 没有垄断这个架构。可使用许多不一样的工具或框架来实现微前端。这里咱们使用 React 是由于它的受欢迎程度以及咱们对它的熟悉程度。

容器

咱们从容器开始,由于它是咱们用户的入口。让咱们从它的 package.json 中看看能够发现什么:

{
  "name": "@micro-frontends-demo/container",
  "description": "Entry point and container for a micro frontends demo",
  "scripts": {
    "start": "PORT=3000 react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test"
  },
  "dependencies": {
    "react": "^16.4.0",
    "react-dom": "^16.4.0",
    "react-router-dom": "^4.2.2",
    "react-scripts": "^2.1.8"
  },
  "devDependencies": {
    "enzyme": "^3.3.0",
    "enzyme-adapter-react-16": "^1.1.1",
    "jest-enzyme": "^6.0.2",
    "react-app-rewire-micro-frontends": "^0.0.1",
    "react-app-rewired": "^2.1.1"
  },
  "config-overrides-path": "node_modules/react-app-rewire-micro-frontends"
}
复制代码

reactreact-scripts 依赖能够看出它是经过 create-react-app 建立的 React 应用。更有趣的是那没有的:任何说起咱们将要组成以造成咱们的最终应用程序的微前端。若是咱们在这里将它们指定为库依赖项,那么咱们将走向构建时集成的道路,那就会像以前提到的会致使在咱们的发布周期中有问题的耦合。

react-scripts 1.x 版本能够在单个页面中拥有多个应用而不产生冲突,但在 2.x 版本使用一些 webpack 特性,当两个以上应用在单个页面渲染时会致使错误。基于这个缘由咱们使用 react-app-rewired 覆盖一些 webpack 内部的 react-scripts 配置。它会修复这些错误,让咱们继续依靠 react-scripts 来管理咱们的构建工具。

为了了解咱们如何选择和展现微前端,咱们来看一下 App.js。咱们使用 React Router 将当前 URL 与预约义的路由列表进行匹配,而且渲染相应组件:

<Switch>
  <Route exact path="/" component={Browse} />
  <Route exact path="/restaurant/:id" component={Restaurant} />
  <Route exact path="/random" render={Random} />
</Switch>
复制代码

Random 组件不那么有趣 —— 它只是重定向到随机选择的餐厅 URL 对应的页面。BrowseRestaurant 是这样:

const Browse = ({ history }) => (
  <MicroFrontend history={history} name="Browse" host={browseHost} />
);
const Restaurant = ({ history }) => (
  <MicroFrontend history={history} name="Restaurant" host={restaurantHost} />
);
复制代码

这两种状况,咱们渲染 MicroFrontend 组件。除了 history 对象(后面会变得重要),咱们指定应用的惟一名称,以及 bundle 下载的主机地址。在本地运行时,这个配置驱动的 URL 相似于 http://localhost:3001,生产环境则相似 https://browse.demo.microfrontends.com

App.js 中选择了一个微前端,如今咱们将在 MicroFrontend.js 渲染它,这只是另外一个 React 组件:

class MicroFrontend extends React.Component {
  render() {
    return <main id={`${this.props.name}-container`} />;
  }
}
复制代码

这不是完整的类,咱们很快会看到它更多的方法。

渲染时,咱们要作的就是在页面上放置带有微前端惟一 ID 的容器元素。这是咱们告诉微前端渲染本身的地方。咱们使用 React 的 componentDidMount 做为下载和渲染微前端的触发器:

componentDidMount 是 React 组件的生命周期函数,它只会在组件实例首次在 DOM 中“渲染”时被框架调用。

MicroFrontend 类……

componentDidMount() {
    const { name, host } = this.props;
    const scriptId = `micro-frontend-script-${name}`;

    if (document.getElementById(scriptId)) {
      this.renderMicroFrontend();
      return;
    }

    fetch(`${host}/asset-manifest.json`)
      .then(res => res.json())
      .then(manifest => {
        const script = document.createElement('script');
        script.id = scriptId;
        script.src = `${host}${manifest['main.js']}`;
        script.onload = this.renderMicroFrontend;
        document.head.appendChild(script);
      });
  }
复制代码

咱们必须从静态清单文件中获取脚本的 URL,由于 react-scripts 输出的编译后 JavaScript 文件名中包含便于缓存的哈希值。

首先咱们检查有惟一 ID 的相关脚本是否已经下载,若是下载了,咱们能够当即渲染它。若是没有,咱们获取从适当的主机获取 asset-manifest.json 文件,以便查找主脚本资产的完整 URL。一旦咱们设置了脚本的 URL,剩下的就是将它附加到文档中,使用 onload 处理程序渲染微前端:

MicroFrontend 类

renderMicroFrontend = () => {
    const { name, history } = this.props;

    window[`render${name}`](`${name}-container`, history);
    // E.g.: window.renderBrowse('browse-container, history');
  };
复制代码

在上面的代码中咱们调用了 window.renderBrowse 全局函数,它被咱们刚刚下载的脚本放在那里。咱们给微前端应该渲染的 <main> 元素分配一个 ID 和 history 对象,咱们很快会解释。这个全局函数的签名是容器应用和微前端之间的关键约定。这是任何通信或集成应该发生的地方,所以保持它至关轻量级使其易于维护,并在将来添加新的微前端。每当咱们想要作一些须要更改此代码的事情时,咱们应该仔细地思考它对于咱们的代码库的耦合以及约定的维护意味着什么。

最后一件是处理清理工做。当咱们的 MicroFrontend 组件卸载时(从 DOM 中移除),咱们也想卸载相应的微前端。为此,每一个微前端都定义了一个相应的全局函数,咱们在适当的 React 生命周期方法中调用它:

MicroFrontend 类……

componentWillUnmount() {
    const { name } = this.props;

    window[`unmount${name}`](`${name}-container`);
  }
复制代码

就它自己的内容而言,容器直接渲染的全部内容是网站的顶层头部和导航栏,由于这些在全部页面中都是不变的。这些元素的 CSS 已通过仔细编写,以确保它只对标题中的元素进行样式化,因此它不该该与微前端内的任何样式代码冲突。

这就是容器应用的结尾!它至关初级,但这给了咱们一个 shell,能够在运行时动态下载咱们的微前端,并将它们粘合在一块儿造成一个单一页面上的内容。这些微前端能够单独部署在生产上,无需改变任何其余微前端或容器自己。

建议按照顺序阅读本系列文章:

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索