转载:微前端的技术选型和对比

前言:css

该文章是转自某一位大神的,连接地址在标题头部。html

这篇文章很清晰的叙述了微前端的技术选型。对于大多如今作saas化服务的场景,多少能带到一些思考。前端

其中包括国内资料及其有限的single-spa的相关解释和分析java

之前是摘抄内容:react

为何微前端开始在流行——Web 应用的聚合

采用新技术,更多不是由于先进,而是由于它能解决痛点。webpack

过去,我一直有一个疑惑,人们是否真的须要微服务,是否真的须要微前端。毕竟,没有银弹。当人们考虑是否采用一种新的架构,除了考虑它带来好处以外,仍然也考量着存在的大量的风险和技术挑战。git

前端遗留系统迁移

自微前端框架 Mooa 及对应的《微前端的那些事儿》发布的两个多月以来,我陆陆续续地接收到一些微前端架构的一些咨询。过程当中,我发现了一件颇有趣的事:解决遗留系统,才是人们采用微前端方案最重要的缘由。github

这些咨询里,开发人员所遇到的状况,与我以前遇到的情形并类似,个人场景是:设计一个新的前端架构。他们开始考虑前端微服务化,是由于遗留系统的存在。web

过去那些使用 Backbone.js、Angular.js、Vue.js 1 等等框架所编写的单页面应用,已经在线上稳定地运行着,也没有新的功能。对于这样的应用来讲,咱们也没有理由浪费时间和精力重写旧的应用。这里的那些使用旧的、再也不使用的技术栈编写的应用,能够称为遗留系统。而,这些应用又须要结合到新应用中使用。我遇到的较多的状况是:旧的应用使用的是 Angular.js 编写,而新的应用开始采用 Angular 2+。这对于业务稳定的团队来讲,是极为常见的技术栈。npm

在即不重写原有系统的基础之下,又能够抽出人力来开发新的业务。其不只仅对于业务人员来讲, 是一个至关吸引力的特性;对于技术人员来讲,不重写旧的业务,同时还能作一些技术上的挑战,也是一件至关有挑战的事情。

后端解耦,前端聚合

而前端微服务的一个卖点也在这里,去兼容不一样类型的前端框架。这让我又联想到微服务的好处,及许多项目落地微服务的缘由:

在初期,后台微服务的一个很大的卖点在于,可使用不一样的技术栈来开发后台应用。可是,事实上,采用微服务架构的组织和机构,通常都是中大型规模的。相较于中小型,对于框架和语言的选型要求比较严格,如在内部限定了框架,限制了语言。所以,在充分使用不一样的技术栈来发挥微服务的优点这一点上,几乎是不多出现的。在这些大型组织机构里,采用微服务的缘由主要仍是在于,使用微服务架构来解耦服务间依赖。

而在前端微服务化上,则是偏偏与之相反的,人们更想要的结果是聚合,尤为是那些 To B(to Bussiness)的应用。

在这两三年里,移动应用出现了一种趋势,用户不想装那么多应用了。而每每一家大的商业公司,会提供一系列的应用。这些应用也从某种程度上,反应了这家公司的组织架构。然而,在用户的眼里他们就是一家公司,他们就只应该有一个产品。类似的,这种趋势也在桌面 Web 出现。聚合成为了一个技术趋势,体如今前端的聚合就是微服务化架构。

兼容遗留系统

那么,在这个时候,咱们就须要使用新的技术、新的架构,来容纳、兼容这些旧的应用。而前端微服务化,正好是契合人们想要的这个卖点罢了。

实施微前端的六种方式

微前端架构是一种相似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。

由此带来的变化是,这些前端应用能够独立运行、独立开发、独立部署。以及,它们应该能够在共享组件的同时进行并行开发——这些组件能够经过 NPM 或者 Git Tag、Git Submodule 来管理。

注意:这里的前端应用指的是先后端分离的单应用页面,在这基础才谈论微前端才有意义。

结合我最近半年在微前端方面的实践和研究来看,微前端架构通常能够由如下几种方式进行:

  1. 使用 HTTP 服务器的路由来重定向多个应用
  2. 在不一样的框架之上设计通信、加载机制,诸如 Mooa 和 Single-SPA
  3. 经过组合多个独立应用、组件来构建一个单体应用
  4. iFrame。使用 iFrame 及自定义消息传递机制
  5. 使用纯 Web Components 构建应用
  6. 结合 Web Components 构建

基础铺垫:应用分发路由 -> 路由分发应用

在一个单体前端、单体后端应用中,有一个典型的特征,即路由是由框架来分发的,框架将路由指定到对应的组件或者内部服务中。微服务在这个过程当中作的事情是,将调用由函数调用变成了远程调用,诸如远程 HTTP 调用。而微前端呢,也是相似的,它是将应用内的组件调用变成了更细粒度的应用间组件调用,即原先咱们只是将路由分发到应用的组件执行,如今则须要根据路由来找到对应的应用,再由应用分发到对应的组件上。

后端:函数调用 -> 远程调用

在大多数的 CRUD 类型的 Web 应用中,也都存在一些极为类似的模式,即:首页 -> 列表 -> 详情:

  • 首页,用于面向用户展现特定的数据或页面。这些数据一般是有限个数的,而且是多种模型的。
  • 列表,即数据模型的聚合,其典型特色是某一类数据的集合,能够看到尽量多的数据概要(如 Google 只返回 100 页),典型见 Google、淘宝、京东的搜索结果页。
  • 详情,展现一个数据的尽量多的内容。

以下是一个 Spring 框架,用于返回首页的示例:

@RequestMapping(value="/") public ModelAndView homePage(){ return new ModelAndView("/WEB-INF/jsp/index.jsp"); }

对于某个详情页面来讲,它多是这样的:

@RequestMapping(value="/detail/{detailId}") public ModelAndView detail(HttpServletRequest request, ModelMap model){ .... return new ModelAndView("/WEB-INF/jsp/detail.jsp", "detail", detail); }

那么,在微服务的状况下,它则会变成这样子:

@RequestMapping("/name")
public String name(){
    String name = restTemplate.getForObject("http://account/name", String.class);
    return Name" + name;
}

然后端在这个过程当中,多了一个服务发现的服务,来管理不一样微服务的关系。

前端:组件调用 -> 应用调用

在形式上来讲,单体前端框架的路由和单体后端应用,并无太大的区别:依据不一样的路由,来返回不一样页面的模板。

const appRoutes: Routes = [
  { path: 'index', component: IndexComponent },
  { path: 'detail/:id', component: DetailComponent },
];

而当咱们将之微服务化后,则可能变成应用 A 的路由:

const appRoutes: Routes = [
  { path: 'index', component: IndexComponent },
];

外加之应用 B 的路由:

const appRoutes: Routes = [
  { path: 'detail/:id', component: DetailComponent },
];

而问题的关键就在于:怎么将路由分发到这些不一样的应用中去。与此同时,还要负责管理不一样的前端应用。

路由分发式微前端

路由分发式微前端,即经过路由将不一样的业务分发到不一样的、独立前端应用上。其一般能够经过 HTTP 服务器的反向代理来实现,又或者是应用框架自带的路由来解决。

就当前而言,经过路由分发式的微前端架构应该是采用最多、最易采用的 “微前端” 方案。可是这种方式看上去更像是多个前端应用的聚合,即咱们只是将这些不一样的前端应用拼凑到一块儿,使他们看起来像是一个完整的总体。可是它们并非,每次用户从 A 应用到 B 应用的时候,每每须要刷新一下页面。

在几年前的一个项目里,咱们当时正在进行遗留系统重写。咱们制定了一个迁移计划:

  1. 首先,使用静态网站生成动态生成首页
  2. 其次,使用 React 计划栈重构详情页
  3. 最后,替换搜索结果页

整个系统并非一次性迁移过去,而是一步步往下进行。所以在完成不一样的步骤时,咱们就须要上线这个功能,因而就须要使用 Nginx 来进行路由分发。

以下是一个基于路由分发的 Nginx 配置示例:

http {
  server {
    listen       80;
    server_name  www.phodal.com;
    location /api/ {
      proxy_pass http://http://172.31.25.15:8000/api;
    }
    location /web/admin {
      proxy_pass http://172.31.25.29/web/admin;
    }
    location /web/notifications {
      proxy_pass http://172.31.25.27/web/notifications;
    }
    location / {
      proxy_pass /;
    }
  }
}

在这个示例里,不一样的页面的请求被分发到不一样的服务器上。

随后,咱们在别的项目上也使用了相似的方式,其主要缘由是:跨团队的协做。当团队达到必定规模的时候,咱们不得不面对这个问题。除此,还有 Angluar 跳崖式升级的问题。因而,在这种状况下,用户前台使用 Angular 重写,后台继续使用 Angular.js 等保持再有的技术栈。在不一样的场景下,都有一些类似的技术决策。

所以在这种状况下,它适用于如下场景:

  • 不一样技术栈之间差别比较大,难以兼容、迁移、改造
  • 项目不想花费大量的时间在这个系统的改造上
  • 现有的系统在将来将会被取代
  • 系统功能已经很完善,基本不会有新需求

而在知足上面场景的状况下,若是为了更好的用户体验,还能够采用 iframe 的方式来解决。

使用 iFrame 建立容器

iFrame 做为一个很是古老的,人人都以为普通的技术,却一直很管用。

HTML 内联框架元素 <iframe> 表示嵌套的正在浏览的上下文,能有效地将另外一个 HTML 页面嵌入到当前页面中。

iframe 能够建立一个全新的独立的宿主环境,这意味着咱们的前端应用之间能够相互独立运行。采用 iframe 有几个重要的前提:

  • 网站不须要 SEO 支持
  • 拥有相应的应用管理机制。

若是咱们作的是一个应用平台,会在咱们的系统中集成第三方系统,或者多个不一样部门团队下的系统,显然这是一个不错的方案。一些典型的场景,如传统的 Desktop 应用迁移到 Web 应用:

Angular Tabs 示例

若是这一类应用过于复杂,那么它必然是要进行微服务化的拆分。所以,在采用 iframe 的时候,咱们须要作这么两件事:

  • 设计管理应用机制
  • 设计应用通信机制

加载机制。在什么状况下,咱们会去加载、卸载这些应用;在这个过程当中,采用怎样的动画过渡,让用户看起来更加天然。

通信机制。直接在每一个应用中建立 postMessage 事件并监听,并非一个友好的事情。其自己对于应用的侵入性太强,所以经过 iframeEl.contentWindow 去获取 iFrame 元素的 Window 对象是一个更简化的作法。随后,就须要定义一套通信规范:事件名采用什么格式、何时开始监听事件等等。

有兴趣的读者,能够看看笔者以前写的微前端框架:Mooa

无论怎样,iframe 对于咱们今年的 KPI 怕是带不来一丝的好处,那么咱们就去造个轮子吧。

自制框架兼容应用

不管是基于 Web Components 的 Angular,或者是 VirtualDOM 的 React 等,现有的前端框架都离不开基本的 HTML 元素 DOM。

那么,咱们只须要:

  1. 在页面合适的地方引入或者建立 DOM
  2. 用户操做时,加载对应的应用(触发应用的启动),并能卸载应用。

第一个问题,建立 DOM 是一个容易解决的问题。而第二个问题,则一点儿不容易,特别是移除 DOM 和相应应用的监听。当咱们拥有一个不一样的技术栈时,咱们就须要有针对性设计出一套这样的逻辑。

尽管 Single-SPA 已经拥有了大部分框架(如 React、Angular、Vue 等框架)的启动和卸载处理,可是它仍然不是适合于生产用途。当我基于 Single-SPA 为 Angular 框架设计一个微前端架构的应用时,我最后选择重写一个本身的框架,即 Mooa

虽然,这种方式的上手难度相对比较高,可是后期订制及可维护性比较方便。在不考虑每次加载应用带来的用户体验问题,其惟一存在的风险多是:第三方库不兼容。

可是,不论怎样,与 iFrame 相比,其在技术上更具备可吹牛逼性,更有看点。一样的,与 iframe 相似,咱们仍然面对着一系列的不大不小的问题:

  • 须要设计一套管理应用的机制。
  • 对于流量大的 toC 应用来讲,会在首次加载的时候,会多出大量的请求

而咱们即又要拆分应用,又想 blabla……,咱们还能怎么作?

组合式集成:将应用微件化

组合式集成,即经过软件工程的方式在构建前、构建时、构建后等步骤中,对应用进行一步的拆分,并从新组合。

从这种定义上来看,它可能算不上并非一种微前端——它能够知足了微前端的三个要素,即:独立运行、独立开发、独立部署。可是,配合上前端框架的组件 Lazyload 功能——即在须要的时候,才加载对应的业务组件或应用,它看上去就是一个微前端应用。

与此同时,因为全部的依赖、Pollyfill 已经尽量地在首次加载了,CSS 样式也不须要重复加载。

常见的方式有:

  • 独立构建组件和应用,生成 chunk 文件,构建后再归类生成的 chunk 文件。(这种方式更相似于微服务,可是成本更高)
  • 开发时独立开发组件或应用,集成时合并组件和应用,最后生成单体的应用。
  • 在运行时,加载应用的 Runtime,随后加载对应的应用代码和模板。

应用间的关系以下图所示(其忽略图中的 “前端微服务化”):

组合式集成对比

这种方式看上去至关的理想,即能知足多个团队并行开发,又能构建出适合的交付物。

可是,首先它有一个严重的限制:必须使用同一个框架。对于多数团队来讲,这并非问题。采用微服务的团队里,也不会由于微服务这一个前端,来使用不一样的语言和技术来开发。固然了,若是要使用别的框架,也不是问题,咱们只须要结合上一步中的自制框架兼容应用就能够知足咱们的需求。

其次,采用这种方式还有一个限制,那就是:规范!****规范!****规范!。在采用这种方案时,咱们须要:

  • 统一依赖。统一这些依赖的版本,引入新的依赖时都须要一一加入。
  • 规范应用的组件及路由。避免不一样的应用之间,由于这些组件名称发生冲突。
  • 构建复杂。在有些方案里,咱们须要修改构建系统,有些方案里则须要复杂的架构脚本。
  • 共享通用代码。这显然是一个要常常面对的问题。
  • 制定代码规范。

所以,这种方式看起来更像是一个软件工程问题。

如今,咱们已经有了四种方案,每一个方案都有本身的利弊。显然,结合起来会是一种更理想的作法。

考虑到现有及经常使用的技术的局限性问题,让咱们再次将目光放得长远一些。

纯 Web Components 技术构建

在学习 Web Components 开发微前端架构的过程当中,我尝试去写了我本身的 Web Components 框架:oan。在添加了一些基本的 Web 前端框架的功能以后,我发现这项技术特别适合于做为微前端的基石。

Web Components 是一套不一样的技术,容许您建立可重用的定制元素(它们的功能封装在您的代码以外)而且在您的 Web 应用中使用它们。

它主要由四项技术组件:

  • Custom elements,容许开发者建立自定义的元素,诸如 。
  • Shadow DOM,即影子 DOM,一般是将 Shadow DOM 附加到主文档 DOM 中,并能够控制其关联的功能。而这个 Shadow DOM 则是不能直接用其它主文档 DOM 来控制的。
  • HTML templates,即 <template> 和 <slot> 元素,用于编写不在页面中显示的标记模板。
  • HTML Imports,用于引入自定义组件。

每一个组件由 link 标签引入:

<link rel="import" href="components/di-li.html">
<link rel="import" href="components/d-header.html">

随后,在各自的 HTML 文件里,建立相应的组件元素,编写相应的组件逻辑。一个典型的 Web Components 应用架构以下图所示:

Web Components 架构

能够看到这边方式与咱们上面使用 iframe 的方式很类似,组件拥有本身独立的 Scripts 和 Styles,以及对应的用于单独部署组件的域名。然而它并无想象中的那么美好,要直接使用纯 Web Components 来构建前端应用的难度有:

  • 重写现有的前端应用。是的,如今咱们须要完成使用 Web Components 来完成整个系统的功能。
  • 上下游生态系统不完善。缺少相应的一些第三方控件支持,这也是为何 jQuery 至关流行的缘由。
  • 系统架构复杂。当应用被拆分为一个又一个的组件时,组件间的通信就成了一个特别大的麻烦。

Web Components 中的 ShadowDOM 更像是新一代的前端 DOM 容器。而遗憾的是并非全部的浏览器,均可以彻底支持 Web Components。

结合 Web Components 构建

Web Components 离如今的咱们太远,但是结合 Web Components 来构建前端应用,则更是一种面向将来演进的架构。或者说在将来的时候,咱们能够开始采用这种方式来构建咱们的应用。好在,已经有框架在打造这种可能性。

就当前而言,有两种方式能够结合 Web Components 来构建微前端应用:

  • 使用 Web Components 构建独立于框架的组件,随后在对应的框架中引入这些组件
  • 在 Web Components 中引入现有的框架,相似于 iframe 的形式

前者是一种组件式的方式,或者则像是在迁移将来的 “遗留系统” 到将来的架构上。

在 Web Components 中集成现有框架

现有的 Web 框架已经有一些能够支持 Web Components 的形式,诸如 Angular 支持的 createCustomElement,就能够实现一个 Web Components 形式的组件:

platformBrowser()
  .bootstrapModuleFactory(MyPopupModuleNgFactory)
    .then(({injector}) => {
      const MyPopupElement = createCustomElement(MyPopup, {injector});
      customElements.define(‘my-popup’, MyPopupElement);
});

在将来,将有更多的框架可使用相似这样的形式,集成到 Web Components 应用中。

集成在现有框架中的 Web Components

另一种方式,则是相似于 Stencil 的形式,将组件直接构建成 Web Components 形式的组件,随后在对应的诸如,如 React 或者 Angular 中直接引用。

以下是一个在 React 中引用 Stencil 生成的 Web Components 的例子:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

import 'test-components/testcomponents';

ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();

在这种状况之下,咱们就能够构建出独立于框架的组件。

一样的 Stencil 仍然也只是支持最近的一些浏览器,好比:Chrome、Safari、Firefox、Edge 和 IE11

复合型

复合型,对就是上面的几个类别中,随便挑几种组合到一块儿。

我就不废话了~~。

微前端架构选型指南

在上一节《实施前端微服务化的六七种方式》中,介绍了在实施微前端的过程当中,咱们采用的一些不一样方案的架构方案。在这篇文章中,我将总结如何依据不一样的状况来选择合适的方案。

快速选型指南图

我仍是直接先给结论:

微前端选型指南

关键点的相关解释以下:

框架限制。在后台微服务系统里,人们使用其它语言的库来开发新的服务,如用于人工智能的 Python。可是在前端,几乎不存在这种可能性。因此当咱们的前端框架只有一个时,咱们在采用微前端的技术时,可选范围就更大了。而遗憾的是,多数组织须要兼容遗留系统。

IE 问题。不管是在几年前,仍是在今年,咱们实施微前端最早考虑的就是对于 IE 的支持。在我遇到的项目上,基本上都须要支持 IE,所以在技术选型上就受限必定的限制。而在咱们那些不须要支持 IE 的项目上,他们就可使用 WebComponents 技术来构建微前端应用。

依赖独立。即各个微前端应用的依赖是要统一管理,仍是要在各个应该中本身管理。统一管理能够解决重复加载依赖的问题,独立管理会带来额外的流量开销和等待时间。

微前端方案的对比:简要对比

若是你对上述的几个方面,仍然不是很熟悉的话,请阅读《实施前端微服务化的六七种方式》。

方式 开发成本 维护成本 可行性 同一框架要求 实现难度 潜在风险
路由分发 这个方案太普通了
iFrame 这个方案太普通了
应用微服务化 ★★★★ 针对每一个框架作定制及 Hook
微件化 ★★★★★ 针对构建系统,如 webpack 进行 hack
微应用化 ★★★ 统一不一样应用的构建规范
纯 Web Components ★★ 新技术,浏览器的兼容问题
结合 Web Components ★★ 新技术,浏览器的兼容问题

一样的,一些复杂概念的解释以下:

应用微服务化,即每一个前端应用一个独立的服务化前端应用,并配套一套统一的应用管理和启动机制,诸如微前端框架 Single-SPA 或者 mooa 。

微件化,即经过对构建系统的 hack,使不一样的前端应用可使用同一套依赖。它在应用微服务化的基本上,改进了重复加载依赖文件的问题。

微应用化,又能够称之为组合式集成,即经过软件工程的方式,在开发环境对单体应用进行拆分,在构建环境将应用组合在一块儿构建成一个应用。详细的细节,能够期待后面的文章《一个单体前端应用的拆解与微服务化》

微前端方案的对比:复杂方式

以前看到一篇微服务相关的 文章,介绍了不一样微服务的区别,其采用了一种比较有意思的对比方式特别详细,这里就使用一样的方式来展现:

架构目标 描述
a. 独立开发 独立开发,而不受影响
b. 独立部署 能做为一个服务来单独部署
c. 支持不一样框架 能够同时使用不一样的框架,如 Angular、Vue、React
d. 摇树优化 能消除未使用的代码
e. 环境隔离 应用间的上下文不受干扰
f. 多个应用同时运行 不一样应用能够同时运行
g. 共用依赖 不一样应用是否共用底层依赖库
h. 依赖冲突 依赖的不一样版本是否致使冲突
i. 集成编译 应用最后被编译成一个总体,而不是分开构建

那么,对于下表而言,表中的 a~j 分别表示上面的几种不一样的架构考虑因素。

(PS:考虑到 Web Components 几个单词的长度,暂时将它简称为 WC~~)

方式 a b c d e f g h i
路由分发 O O O O O O      
iFrame O O O O O O      
应用微服务化 O O O     O      
微件化 O O     - - O -  
微应用化 O O   O - - O - O
纯 WC O O   O O O - - O
结合 WC O O O O O O     O

图中的 O 表示支持,空白表示不支持,- 表示不受影响。

再结合以前的选型指南:

微前端选型指南

(PS:本图采用 Keynote 绘制)

你是否找到你想到的架构了?

如何解构单体前端应用——前端应用的微服务式拆分

刷新页面?路由拆分?No,动态加载组件。

本文分为如下四部分:

  • 前端微服务化思想介绍
  • 微前端的设计理念
  • 实战微前端架构设计
  • 基于 Mooa 进行前端微服务化

前端微服化

对于前端微服化来讲,有这么一些方案:

  • Web Component 显然能够一个很优秀的基础架构。然而,咱们并不可能去大量地复写已有的应用。
  • iFrame。你是说真的吗?
  • 另一个微前端框架 Single-SPA,显然是一个更好的方式。然而,它并不是 Production Ready。
  • 经过路由来切分应用,而这个跳转会影响用户体验。
  • 等等。

所以,当咱们考虑前端微服务化的时候,咱们但愿:

  • 独立部署
  • 独立开发
  • 技术无关
  • 不影响用户体验

独立开发

在过去的几星期里,我花费了大量的时间在学习 Single-SPA 的代码。可是,我发现它在开发和部署上真的太麻烦了,彻底达不到独立部署地标准。按 Single-SPA 的设计,我须要在入口文件中声名个人应用,而后才能去构建:

declareChildApplication('inferno', () => import('src/inferno/inferno.app.js'), pathPrefix('/inferno'));

同时,在个人应用里,我还须要去指定个人生命周期。这就意味着,当我开发了一个新的应用时,必须更新两份代码:主工程和应用。这时咱们还很可能在同一个源码里工做。

当出现多个团队的时候,在同一份源码里工做,显然变得至关的不可靠——好比说,对方团队使用的是 Tab,而咱们使用的是 2 个空格,隔壁的老王用的是 4 个空格。

独立部署

一个单体的前端应用最大的问题是,构建出来的 js、css 文件至关的巨大。而微前端则意味着,这个文件被独立地拆分红多个文件,它们即可以独立去部署应用。

咱们真的须要技术无关吗?

等等,咱们是否真的须要技术无关?若是咱们不须要技术无关的话,微前端问题就很容易解决了。

事实上,对于大部分的公司和团队来讲,技术无关只是一个无关痛痒的话术。当一家公司的几个创始人使用了 Java,那么极有可能在将来的选型上继续使用 Java。除非,一些额外的服务来使用 Python 来实现人工智能。所以,在大部分的状况下,仍然是技术栈惟一。

对于前端项目来讲,更是如此:一个部门里基本上只会选用一个框架。

因而,咱们选择了 Angular。

不影响用户体验

使用路由跳转来进行前端微服务化,是一种很简单、高效的切分方式。然而,路由跳转地过程当中,会有一个白屏的过程。在这个过程当中,跳转前的应用和将要跳转的应用,都失去了对页面的控制权。若是这个应用出了问题,那么用户就会一脸懵逼。

理想的状况下,它应该能够被控制。

微前端的设计理念

设计理念一:中心化路由

互联网本质是去中心化的吗?不,DNS 决定了它不是。TAB,决定了它不是。

微服务从本质上来讲,它应该是去中心化的。可是,它又不能是彻底的去中心化。对于一个微服务来讲,它须要一个服务注册中心:

服务提供方要注册通告服务地址,服务的调用方要能发现目标服务。

对于一个前端应用来讲,这个东西就是路由。

从页面上来讲,只有咱们在网页上添加一个菜单连接,用户才能知道某个页面是可使用的。

而从代码上来讲,那就是咱们须要有一个地方来管理咱们的应用:**发现存在哪些应用,哪一个应用使用哪一个路由。

管理好咱们的路由,实际上就是管理好咱们的应用。

设计理念二:标识化应用

在设计一个微前端框架的时候,为每一个项目取一个名字的问题纠结了我好久——怎么去规范化这个东西。直到,我再一次想到了康威定律:

系统设计(产品结构等同组织形式,每一个设计系统的组织,其产生的设计等同于组织之间的沟通结构。

换句人话说,就是同一个组织下,不可能有两个项目的名称是同样的。

因此,这个问题很简单就解决了。

设计理念三:生命周期

Single-SPA 设计了一个基本的生命周期(虽然它没有统一管理),它包含了五种状态:

  • load,决定加载哪一个应用,并绑定生命周期
  • bootstrap,获取静态资源
  • mount,安装应用,如建立 DOM 节点
  • unload,删除应用的生命周期
  • unmount,卸载应用,如删除 DOM 节点

因而,我在设计上基本上沿用了这个生命周期。显然,诸如 load 之类对于个人设计是多余的。

设计理念四:独立部署与配置自动化

从某种意义上来讲,整个每系统是围绕着应用配置进行的。若是应用的配置能自动化,那么整个系统就自动化。

当咱们只开发一个新的组件,那么咱们只须要更新咱们的组件,并更新配置便可。而这个配置自己也应该是能自动生成的。

实战微前端架构设计

基于以上的前提,系统的工做流程以下所示:

系统工做流

总体的工程流程以下所示:

  1. 主工程在运行的时候,会去服务器获取最新的应用配置。
  2. 主工程在获取到配置后,将一一建立应用,并为应用绑定生命周期。
  3. 当主工程监测到路由变化的时候,将寻找是否有对应的路由匹配到应用。
  4. 当匹配对对应应用时,则加载相应的应用。

故而,其对应的结构下图所示:

Architecture

总体的流程以下图所示:

Workflow

独立部署与配置自动化

咱们作的部署策略以下:咱们的应用使用的配置文件叫 apps.json,由主工程去获取这个配置。每次部署的时候,咱们只须要将 apps.json 指向最新的配置文件便可。配置的文件类以下所示:

  1. 96a7907e5488b6bb.json
  2. 6ff3bfaaa2cd39ea.json
  3. dcd074685c97ab9b.json

一个应用的配置以下所示:

{
  "name": "help", "selector": "help-root", "baseScriptUrl": "/assets/help", "styles": [ "styles.bundle.css" ], "prefix": "help", "scripts": [ "inline.bundle.js", "polyfills.bundle.js", "main.bundle.js" ] }

这里的 selector 对应于应用所须要的 DOM 节点,prefix 则是用于 URL 路由上。这些都是自动从 index.html 文件和 package.json 中获取生成的。

应用间路由——事件

因为如今的应用变成了两部分:主工程和应用部分。就会出现一个问题:只有一个工程能捕获路由变化。当由主工程去改变应用的二级路由时,就没法有效地传达到子应用。在这时,只能经过事件的方式去通知子应用,子应用也须要监测是不是当前应用的路由。

if (event.detail.app.name === appName) { let urlPrefix = 'app' if (urlPrefix) { urlPrefix = `/${window.mooa.option.urlPrefix}/` } router.navigate([event.detail.url.replace(urlPrefix + appName, '')]) }

类似的,当咱们须要从应用 A 跳转到应用 B 时,咱们也须要这样的一个机制:

window.addEventListener('mooa.routing.navigate', function(event: CustomEvent) {
  const opts = event.detail
  if (opts) {
    navigateAppByName(opts)
  }
})

剩下的诸如 Loading 动画也是相似的。

大型 Angular 应用微前端的四种拆分策略

上一个月,咱们花了大量的时间不熂设计方案来拆分一个大型的 Angular 应用。从使用 Angular 的 Lazyload 到前端微服务化,进行了一系列的讨论。最后,咱们终于有告终果,采用的是 Lazyload 变体:构建时集成代码 的方式。

过去的几周里,做为一个 “专业” 的咨询师,一直忙于在为客户设计一个 Angular 拆分的服务化方案。主要是为了达成如下的设计目标:

  • 构建插件化的 Web 开发平台,知足业务快速变化及分布式多团队并行开发的需求
  • 构建服务化的中间件,搭建高可用及高复用的前端微服务平台
  • 支持前端的独立交付及部署

简单地来讲,就是要支持应用插件化开发,以及多团队并行开发。

应用插件化开发,其所要解决的主要问题是:臃肿的大型应用的拆分问题。大型前端应用,在开发的时候要面临大量的遗留代码、不一样业务的代码耦合在一块儿,在线上的时候还要面临加载速度慢,运行效率低的问题。

最后就落在了两个方案上:路由懒加载及其变体与前端微服务化

前端微服务化:路由懒加载及其变体

路由懒加载,即经过不一样的路由来将应用切成不一样的代码快,当路由被访问的时候,才加载对应组件。在诸如 Angular、Vue 框架里均可以经过路由 + Webpack 打包的方式来实现。而,不可避免地就会须要一些问题:

难以多团队并行开发,路由拆分就意味着咱们仍然是在一个源码库里工做的。也能够尝试拆分红不一样的项目,再编译到一块儿。

每次发布须要从新编译,是的,当咱们只是更新一个子模块的代码,咱们要从新编译整个应用,再从新发布这个应用。而不能独立地去构建它,再发布它。

统一的 Vendor 版本,统一第三方依赖是一件好事。可问题的关键在于:每当咱们添加一个新的依赖,咱们可能就须要开会讨论一下。

然而,标准 Route Lazyload 最大的问题就是难以多团队并行开发,这里之因此说的是 “难以” 是由于,仍是有办法解决这个问题。在平常的开发中,一个小的团队会一直在一个代码库里开发,而一个大的团队则应该是在不一样的代码库里开发。

因而,咱们在标准的路由懒加载之上作了一些尝试。

对于一个二三十人规模的团队来讲,他们可能在业务上归属于不一样的部门,技术上也有一些不一致的规范,如 4 个空格、2 个空格仍是使用 Tab 的问题。特别是当它是不一样的公司和团队时,他们可能要放弃测试、代码静态检测、代码风格统一等等的一系列问题。

微服务化方案:子应用模式

除了路由懒加载,咱们还能够采用子应用模式,即每一个应用都是相互独立地。即咱们有一个基座工程,当用户点击相应的路由时,咱们去加载这个独立 的 Angular 应用;若是是同一个应用下的路由,就不须要重复加载了。并且,这些均可以依赖于浏览器缓存来作。

除了路由懒加载,还能够采用的是相似于 Mooa 的应用嵌入方案。以下是基于 Mooa 框架 + Angular 开发而生成的 HTML 示例:

<app-root _nghost-c0="" ng-version="4.2.0">
  ...
  <app-home _nghost-c2="">
    <app-app1 _nghost-c0="" ng-version="5.2.8" style="display: none;"><nav _ngcontent-c0="" class="navbar"></app-app1>
    <iframe frameborder="" width="100%" height="100%" src="http://localhost:4200/app/help/homeassets/iframe.html" id="help_206547"></iframe>
  </app-home>
</app-root>

Mooa 提供了两种模式,一种是基于 Single-SPA 的实验作的,在同一页面加载、渲染两个 Angular 应用;一种是基于 iFrame 来提供独立的应用容器。

解决了如下的问题:

  • 首页加载速度更快,由于只须要加载首页所须要的功能,而不是全部的依赖。
  • 多个团队并行开发,每一个团队里能够独立地在本身的项目里开发。
  • 独立地进行模块化更新,如今咱们只须要去单独更新咱们的应用,而不须要更新整个完整的应用。

可是,它仍然包含有如下的问题:

  • 重复加载依赖项,即咱们在 A 应用中使用到的模块,在 B 应用中也会从新使用到。有一部分能够经过浏览器的缓存来自动解决。
  • 第一次打开对应的应用须要时间,固然预加载能够解决一部分问题。
  • 在非 iframe 模式下运行,会遇到难以预料的第三方依赖冲突。

因而在总结了一系列的讨论以后,咱们造成了一系列的对比方案:

方案对比

在这个过程当中,咱们作了大量的方案设计与对比,便想写一篇文章对比一下以前的结果。先看一下图:

Angular 代码拆分对比

表格对比:

x 标准 Lazyload 构建时集成 构建后集成 应用独立
开发流程 多个团队在同一个代码库里开发 多个团队在不一样的代码库里开发 多个团队在不一样的代码库里开发 多个团队在不一样的代码库里开发
构建与发布 构建时只须要拿这一份代码去构建、部署 将不一样代码库的代码整合到一块儿,再构建应用 将直接编译成各个项目模块,运行时经过懒加载合并 将直接编译成不一样的几个应用,运行时经过主工程加载
适用场景 单一团队,依赖库少、业务单一 多团队,依赖库少、业务单一 多团队,依赖库少、业务单一 多团队,依赖库多、业务复杂
表现方式 开发、构建、运行一体 开发分离,构建时集成,运行一体 开发分离,构建分离,运行一体 开发、构建、运行分离

详细的介绍以下:

标准 LazyLoad

开发流程:多个团队在同一个代码库里开发,构建时只须要拿这一份代码去部署。

行为:开发、构建、运行一体

适用场景:单一团队,依赖库少、业务单一

LazyLoad 变体 1:构建时集成

开发流程:多个团队在不一样的代码库里开发,在构建时将不一样代码库的代码整合到一块儿,再去构建这个应用。

适用场景:多团队,依赖库少、业务单一

变体-构建时集成:开发分离,构建时集成,运行一体

LazyLoad 变体 2:构建后集成

开发流程:多个团队在不一样的代码库里开发,在构建时将编译成不一样的几份代码,运行时会经过懒加载合并到一块儿。

适用场景:多团队,依赖库少、业务单一

变体-构建后集成:开发分离,构建分离,运行一体

前端微服务化

开发流程:多个团队在不一样的代码库里开发,在构建时将编译成不一样的几个应用,运行时经过主工程加载。

适用场景:多团队,依赖库多、业务复杂

前端微服务化:开发、构建、运行分离

总对比

整体的对好比下表所示:

x 标准 Lazyload 构建时集成 构建后集成 应用独立
依赖管理 统一管理 统一管理 统一管理 各应用独立管理
部署方式 统一部署 统一部署 可单独部署。更新依赖时,须要全量部署 可彻底独立部署
首屏加载 依赖在同一个文件,加载速度慢 依赖在同一个文件,加载速度慢 依赖在同一个文件,加载速度慢 依赖各自管理,首页加载快
首次加载应用、模块 只加载模块,速度快 只加载模块,速度快 只加载模块,速度快 单独加载,加载略慢
前期构建成本 设计构建流程 设计构建流程 设计通信机制与加载方式
维护成本 一个代码库很差管理 多个代码库很差统一 后期须要维护组件依赖 后期维护成本低
打包优化 可进行摇树优化、AoT 编译、删除无用代码 可进行摇树优化、AoT 编译、删除无用代码 应用依赖的组件没法肯定,不能删除无用代码 可进行摇树优化、AoT 编译、删除无用代码

前端微服务化:使用微前端框架 Mooa 开发微前端应用

Mooa 是一个为 Angular 服务的微前端框架,它是一个基于 single-spa,针对 IE 10 及 IFRAME 优化的微前端解决方案。

Mooa 概念

Mooa 框架与 Single-SPA 不同的是,Mooa 采用的是 Master-Slave 架构,即主-从式设计。

对于 Web 页面来讲,它能够同时存在两个到多个的 Angular 应用:其中的一个 Angular 应用做为主工程存在,剩下的则是子应用模块。

  • 主工程,负责加载其它应用,及用户权限管理等核心控制功能。
  • 子应用,负责不一样模块的具体业务代码。

在这种模式下,则由主工程来控制整个系统的行为,子应用则作出一些对应的响应。

微前端主工程建立

要建立微前端框架 Mooa 的主工程,并不须要多少修改,只须要使用 angular-cli 来生成相应的应用:

ng new hello-world

而后添加 mooa 依赖

yarn add mooa

接着建立一个简单的配置文件 apps.json,放在 assets 目录下:

[{
    "name": "help",
    "selector": "app-help",
    "baseScriptUrl": "/assets/help",
    "styles": [
      "styles.bundle.css"
    ],
    "prefix": "help",
    "scripts": [
      "inline.bundle.js",
      "polyfills.bundle.js",
      "main.bundle.js"
    ]
  }
]]

接着,在咱们的 app.component.ts 中编写相应的建立应用逻辑:

mooa = new Mooa({
  mode: 'iframe',
  debug: false,
  parentElement: 'app-home',
  urlPrefix: 'app',
  switchMode: 'coexist',
  preload: true,
  includeZone: true
});

constructor(private renderer: Renderer2, http: HttpClient, private router: Router) {
  http.get<IAppOption[]>('/assets/apps.json')
    .subscribe(
      data => {
        this.createApps(data);
      },
      err => console.log(err)
    );
}

private createApps(data: IAppOption[]) {
  data.map((config) => {
    this.mooa.registerApplication(config.name, config, mooaRouter.hashPrefix(config.prefix));
  });

  const that = this;
  this.router.events.subscribe((event) => {
    if (event instanceof NavigationEnd) {
      that.mooa.reRouter(event);
    }
  });

  return mooa.start();
}

再为应用建立一个对应的路由便可:

{
  path: 'app/:appName/:route',
  component: HomeComponent
}

接着,咱们就能够建立 Mooa 子应用。

Mooa 子应用建立

Mooa 官方提供了一个子应用的模块,直接使用该模块便可:

git clone https://github.com/phodal/mooa-boilerplate

而后执行:

npm install

在安装完依赖后,会进行项目的初始化设置,如更改包名等操做。在这里,将咱们的应用取名为 help。

而后,咱们就能够完成子应用的构建。

接着,执行:yarn build 就能够构建出咱们的应用。

将 dist 目录一下的文件拷贝到主工程的 src/assets/help 目录下,再启动主工程便可。

导航到特定的子应用

在 Mooa 中,有一个路由接口 mooaPlatform.navigateTo,具体使用状况以下:

mooaPlatform.navigateTo({
  appName: 'help',
  router: 'home'
});

它将触发一个 MOOA_EVENT.ROUTING_NAVIGATE 事件。而在咱们调用 mooa.start() 方法时,则会开发监听对应的事件:

window.addEventListener(MOOA_EVENT.ROUTING_NAVIGATE, function(event: CustomEvent) {
  if (event.detail) {
    navigateAppByName(event.detail)
  }
})

它将负责将应用导向新的应用。

嗯,就是这么简单。DEMO 视频以下:

Demo 地址见:http://mooa.phodal.com/

GitHub 示例:https://github.com/phodal/mooa

前端微服务化:使用特制的 iframe 微服务化 Angular 应用

Angular 基于 Component 的思想,可让其在一个页面上同时运行多个 Angular 应用;能够在一个 DOM 节点下,存在多个 Angular 应用,即相似于下面的形式:

<app-home _nghost-c3="" ng-version="5.2.8"> <app-help _nghost-c0="" ng-version="5.2.2" style="display:block;"><div _ngcontent-c0=""></div></app-help> <app-app1 _nghost-c0="" ng-version="5.2.3" style="display:none;"><nav _ngcontent-c0="" class="navbar"></div></app-app1> <app-app2 _nghost-c0="" ng-version="5.2.2" style="display:none;"><nav _ngcontent-c0="" class="navbar"></div></app-app2> </app-home>

可这同样一来,不免须要作如下的一些额外的工做:

  • 建立子应用项目模板,以统一 Angular 版本
  • 构建时,删除子应用的依赖
  • 修改第三方模块

而在这其中最麻烦的就是第三方模块冲突问题。思来想去,在三月中旬,我在 Mooa 中添加了一个 iframe 模式。

iframe 微服务架构设计

在这里,总的设计思想和以前的《如何解构单体前端应用——前端应用的微服务式拆分》中介绍是一致的:

Mooa 架构

主要过程以下:

  • 主工程在运行的时候,会去服务器获取最新的应用配置。
  • 主工程在获取到配置后,将一一建立应用,并为应用绑定生命周期。
  • 当主工程监测到路由变化的时候,将寻找是否有对应的路由匹配到应用。
  • 当匹配对对应应用时,则建立或显示相应应用的 iframe,并隐藏其它子应用的 iframe。

其加载形式与以前的 Component 模式并无太大的区别:

Mooa Component 加载

而为了控制不一样的 iframe 须要作到这么几件事:

  1. 为不一样的子应用分配 ID
  2. 在子应用中进行 hook,以通知主应用:子应用已加载
  3. 在子应用中建立对应的事件监听,来响应主应用的 URL 变化事件
  4. 在主应用中监听子程序的路由跳转等需求

由于大部分的代码能够与以前的 Mooa 复用,因而我便在 Mooa 中实现了相应的功能。

微前端框架 Mooa 的特制 iframe 模式

iframe 能够建立一个全新的独立的宿主环境,这意味着咱们的 Angular 应用之间能够相互独立运行,咱们惟一要作的是:创建一个通信机制。

它能够不修改子应用代码的状况下,能够直接使用。与此同时,它在通常的 iframe 模式进行了优化。使用普通的 iframe 模式,意味着:咱们须要加载大量的重复组件,即便通过 Tree-Shaking 优化,它也将带来大量的重复内容。若是子应用过多,那么它在初始化应用的时候,体验可能就没有那么友好。可是与此相比,在初始化应用的时候,加载全部的依赖在主程序上,也不是一种很友好的体验。

因而,我就在想能不能建立一个更友好地 IFrame 模式,在里面对应用及依赖进行处理。以下,就是最后生成的页面的 iframe 代码:

<app-home _nghost-c2="" ng-version="5.2.8"> <iframe frameborder="" width="100%" height="100%" src="http://localhost:4200/assets/iframe.html" id="help_206547" style="display:block;"></iframe> <iframe frameborder="" width="100%" height="100%" src="http://localhost:4200/assets/iframe.html" id="app_235458 style="display:none;"></iframe> </app-home>

对,两个 iframe 的 src 是同样的,可是它表现出来的确实是两个不一样的 iframe 应用。那个 iframe.html 里面实际上是没有内容的:

<!doctype html>
<html lang="en"> <head> <meta charset="utf-8"> <title>App1</title> <base href="/"> <meta name="viewport" content="width=device-width,initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> </head> <body> </body> </html>

(PS:详细的代码能够见 https://github.com/phodal/mooa

只是为了建立 iframe 的须要而存在的,对于一个 Angular 应用来讲,是否是一个 iframe 的区别并不大。可是,对于咱们而言,区别就大了。咱们可使用本身的方式来控制这个 IFrame,以及咱们所要加载的内容。如:

  • 共同 Style Guide 中的 CSS 样式。如,在使用 iframe 集成时,移除不须要的
  • 去除不须要重复加载的 JavaScript。如,打包时不须要的 zone.min.js、polyfill.js 等等

注意:对于一些共用 UI 组件而言,仍然须要重复加载。这也就是 iframe 模式下的问题。

微前端框架 Mooa iframe 通信机制

为了在主工程与子工程通信,咱们须要作到这么一些事件策略:

发布主应用事件

因为,咱们使用 Mooa 来控制 iframe 加载。这就意味着咱们能够经过 document.getElementById 来获取到 iframe,随后经过 iframeEl.contentWindow 来发布事件,以下:

let iframeEl: any = document.getElementById(iframeId) if (iframeEl && iframeEl.contentWindow) { iframeEl.contentWindow.mooa.option = window.mooa.option iframeEl.contentWindow.dispatchEvent( new CustomEvent(MOOA_EVENT.ROUTING_CHANGE, { detail: eventArgs }) ) }

这样,子应用就不须要修改代码,就能够直接接收对应的事件响应。

监听子应用事件

因为,咱们也但愿能直接在主工程中处理子程序的事件,而且不修改原有的代码。所以,咱们也使用一样的方式来在子应用中监听主应用的事件:

iframeEl.contentWindow.addEventListener(MOOA_EVENT.ROUTING_NAVIGATE, function(event: CustomEvent) {
  if (event.detail) {
    navigateAppByName(event.detail)
  }
})

示例

一样的咱们仍以 Mooa 框架做为示例,咱们只须要在建立 mooa 实例时,配置使用 iframe 模式便可:

this.mooa = new Mooa({ mode: 'iframe', debug: false, parentElement: 'app-home', urlPrefix: 'app', switchMode: 'coexist', preload: true, includeZone: true }); ... that.mooa.registerApplicationByLink('help', '/assets/help', mooaRouter.matchRoute('help')); that.mooa.registerApplicationByLink('app1', '/assets/app1', mooaRouter.matchRoute('app1')); this.mooa.start(); ... this.router.events.subscribe((event: any) => { if (event instanceof NavigationEnd) { that.mooa.reRouter(event); } });
相关文章
相关标签/搜索