微前端架构是一种相似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。javascript
由此带来的变化是,这些前端应用能够独立运行、独立开发、独立部署。以及,它们应该能够在共享组件的同时进行并行开发——这些组件能够经过 NPM 或者 Git Tag、Git Submodule 来管理。css
注意:这里的前端应用指的是先后端分离的单应用页面,在这基础才谈论微前端才有意义。html
结合我最近半年在微前端方面的实践和研究来看,微前端架构通常能够由如下几种方式进行:前端
不一样的方式适用于不一样的使用场景,固然也能够组合一块儿使用。那么,就让咱们来一一了解一下,为之后的架构演进作一些技术铺垫。java
在一个单体前端、单体后端应用中,有一个典型的特征,即路由是由框架来分发的,框架将路由指定到对应的组件或者内部服务中。微服务在这个过程当中作的事情是,将调用由函数调用变成了远程调用,诸如远程 HTTP 调用。而微前端呢,也是相似的,它是将应用内的组件调用变成了更细粒度的应用间组件调用,即原先咱们只是将路由分发到应用的组件执行,如今则须要根据路由来找到对应的应用,再由应用分发到对应的组件上。react
在大多数的 CRUD 类型的 Web 应用中,也都存在一些极为类似的模式,即:首页 -> 列表 -> 详情:git
以下是一个 Spring 框架,用于返回首页的示例:github
@RequestMapping(value="/")
public ModelAndView homePage(){
return new ModelAndView("/WEB-INF/jsp/index.jsp");
}
复制代码
对于某个详情页面来讲,它多是这样的:web
@RequestMapping(value="/detail/{detailId}")
public ModelAndView detail(HttpServletRequest request, ModelMap model){
....
return new ModelAndView("/WEB-INF/jsp/detail.jsp", "detail", detail);
}
复制代码
那么,在微服务的状况下,它则会变成这样子:bootstrap
@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 应用的时候,每每须要刷新一下页面。
在几年前的一个项目里,咱们当时正在进行遗留系统重写。咱们制定了一个迁移计划:
整个系统并非一次性迁移过去,而是一步步往下进行。所以在完成不一样的步骤时,咱们就须要上线这个功能,因而就须要使用 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 做为一个很是古老的,人人都以为普通的技术,却一直很管用。
HTML 内联框架元素
<iframe>
表示嵌套的正在浏览的上下文,能有效地将另外一个 HTML 页面嵌入到当前页面中。
iframe 能够建立一个全新的独立的宿主环境,这意味着咱们的前端应用之间能够相互独立运行。采用 iframe 有几个重要的前提:
若是咱们作的是一个应用平台,会在咱们的系统中集成第三方系统,或者多个不一样部门团队下的系统,显然这是一个不错的方案。一些典型的场景,如传统的 Desktop 应用迁移到 Web 应用:
若是这一类应用过于复杂,那么它必然是要进行微服务化的拆分。所以,在采用 iframe 的时候,咱们须要作这么两件事:
加载机制。在什么状况下,咱们会去加载、卸载这些应用;在这个过程当中,采用怎样的动画过渡,让用户看起来更加天然。
通信机制。直接在每一个应用中建立 postMessage
事件并监听,并非一个友好的事情。其自己对于应用的侵入性太强,所以经过 iframeEl.contentWindow
去获取 iFrame 元素的 Window 对象是一个更简化的作法。随后,就须要定义一套通信规范:事件名采用什么格式、何时开始监听事件等等。
有兴趣的读者,能够看看笔者以前写的微前端框架:Mooa。
无论怎样,iframe 对于咱们今年的 KPI 怕是带不来一丝的好处,那么咱们就去造个轮子吧。
不管是基于 Web Components 的 Angular,或者是 VirtualDOM 的 React 等,现有的前端框架都离不开基本的 HTML 元素 DOM。
那么,咱们只须要:
第一个问题,建立 DOM 是一个容易解决的问题。而第二个问题,则一点儿不容易,特别是移除 DOM 和相应应用的监听。当咱们拥有一个不一样的技术栈时,咱们就须要有针对性设计出一套这样的逻辑。
尽管 Single-SPA 已经拥有了大部分框架(如 React、Angular、Vue 等框架)的启动和卸载处理,可是它仍然不是适合于生产用途。当我基于 Single-SPA 为 Angular 框架设计一个微前端架构的应用时,我最后选择重写一个本身的框架,即 Mooa。
虽然,这种方式的上手难度相对比较高,可是后期订制及可维护性比较方便。在不考虑每次加载应用带来的用户体验问题,其惟一存在的风险多是:第三方库不兼容。
可是,不论怎样,与 iFrame 相比,其在技术上更具备可吹牛逼性,更有看点。一样的,与 iframe 相似,咱们仍然面对着一系列的不大不小的问题:
而咱们即又要拆分应用,又想 blabla……,咱们还能怎么作?
组合式集成,即经过软件工程的方式在构建前、构建时、构建后等步骤中,对应用进行一步的拆分,并从新组合。
从这种定义上来看,它可能算不上并非一种微前端——它能够知足了微前端的三个要素,即:独立运行、独立开发、独立部署。可是,配合上前端框架的组件 Lazyload 功能——即在须要的时候,才加载对应的业务组件或应用,它看上去就是一个微前端应用。
与此同时,因为全部的依赖、Pollyfill 已经尽量地在首次加载了,CSS 样式也不须要重复加载。
常见的方式有:
应用间的关系以下图所示(其忽略图中的 “前端微服务化”):
这种方式看上去至关的理想,即能知足多个团队并行开发,又能构建出适合的交付物。
可是,首先它有一个严重的限制:必须使用同一个框架。对于多数团队来讲,这并非问题。采用微服务的团队里,也不会由于微服务这一个前端,来使用不一样的语言和技术来开发。固然了,若是要使用别的框架,也不是问题,咱们只须要结合上一步中的自制框架兼容应用就能够知足咱们的需求。
其次,采用这种方式还有一个限制,那就是:规范!****规范!****规范!。在采用这种方案时,咱们须要:
所以,这种方式看起来更像是一个软件工程问题。
如今,咱们已经有了四种方案,每一个方案都有本身的利弊。显然,结合起来会是一种更理想的作法。
考虑到现有及经常使用的技术的局限性问题,让咱们再次将目光放得长远一些。
在学习 Web Components 开发微前端架构的过程当中,我尝试去写了我本身的 Web Components 框架:oan。在添加了一些基本的 Web 前端框架的功能以后,我发现这项技术特别适合于做为微前端的基石。
Web Components 是一套不一样的技术,容许您建立可重用的定制元素(它们的功能封装在您的代码以外)而且在您的 Web 应用中使用它们。
它主要由四项技术组件:
<template>
和 <slot>
元素,用于编写不在页面中显示的标记模板。每一个组件由 link
标签引入:
<link rel="import" href="components/di-li.html">
<link rel="import" href="components/d-header.html">
复制代码
随后,在各自的 HTML 文件里,建立相应的组件元素,编写相应的组件逻辑。一个典型的 Web Components 应用架构以下图所示:
能够看到这边方式与咱们上面使用 iframe 的方式很类似,组件拥有本身独立的 Scripts
和 Styles
,以及对应的用于单独部署组件的域名。然而它并无想象中的那么美好,要直接使用纯 Web Components 来构建前端应用的难度有:
Web Components 中的 ShadowDOM 更像是新一代的前端 DOM 容器。而遗憾的是并非全部的浏览器,均可以彻底支持 Web Components。
Web Components 离如今的咱们太远,但是结合 Web Components 来构建前端应用,则更是一种面向将来演进的架构。或者说在将来的时候,咱们能够开始采用这种方式来构建咱们的应用。好在,已经有框架在打造这种可能性。
就当前而言,有两种方式能够结合 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 应用中。
另一种方式,则是相似于 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
复合型,对就是上面的几个类别中,随便挑几种组合到一块儿。
我就不废话了~~。
那么,咱们应该用哪一种微前端方案呢?答案见下一篇《微前端快速选型指南》
相关资料: