原文地址:www.lishuaishuai.com/architectur…javascript
大型组织的组织结构、软件架构在不断地发生变化。移动优先(Mobile First)、App中台(One App)、中台战略等,各类口号在不断的提出、修改和演进。同时,业务也在不断地发展,致使应用不断膨胀,进一步映射到软件架构上。css
现有Web应用(SPA)不能很好的拓展和部署,随着时间的推移,各个项目变得愈来愈臃肿,web应用变得愈来愈难以维护。html
微前端是一种相似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还能够独立运行、独立开发、独立部署。前端
Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. — Micro Frontendsjava
关键词:解耦、聚合、技术栈无关、独立运行、独立开发、独立部署、易拓展git
《前端架构从入门到微前端》一书中,将微前端的实施分为六种:github
路由分发式微前端,即经过路由将不一样的业务分发到不一样的、独立前端应用上。其一般能够经过 HTTP 服务器的反向代理来实现,又或者是应用框架自带的路由来解决。如图:web
前端微服务化,是微服务架构在前端的实施,每一个前端应用都是彻底独立(技术栈、开发、部署、构建独立)、自主运行的,最后经过模块化的方式组合出完成的应用。json
采用这种方式意味着,一个页面上能够同时存在两个以上的前端应用在运行。如图:bootstrap
目前主流的框架有 Single-SPA、qiankun、Mooa,后二者都是基于 Single-SPA
的封装。
微应用化是指在开发时应用都是以单1、微小应用的形式存在的,而在运行时,则是经过构建系统合并这些应用,并组合成一个新的应用。
微应用化大都是以软件工程的方式来完成前端应用的聚合,所以又能够称之为组合式集成。
微应用化只能使用惟一的一种前端框架。
如图:
微件化(Widget)是一段能够直接嵌入应用上运行的代码,它由开发人员预先编译好,在加载时不须要再作任何修改或编译。微前端下的微件化是指,每一个业务团第编写本身的业务代码,并将编译好的代码部署到指定的服务器上,运行时只须要加载指定的代码便可。
如图:
iFrame 做为一个很是古老的,人人都以为普通的技术,却一直很管用。
HTML 内联框架元素
<iframe>
表示嵌套的正在浏览的上下文,能有效地将另外一个 HTML 页面嵌入到当前页面中。
iframe 能够建立一个全新的独立的宿主环境,这意味着咱们的前端应用之间能够相互独立运行。采用 iframe 有几个重要的前提:
在不少业务场景下,不免会遇到一些难以解决的问题,那么能够引入 iframe 来解决。
Web Components 是一套不一样的技术,容许开发者建立可重用的定制元素(它们的功能封装在代码以外)而且在您 Web 应用中使用它们。
在真正的项目上使用 Web Components技术,离如今还有一些距离,结合 Web Components 来构建前端应用,是一种面向将来演进的架构。或者说在将来能够采用这种方式来构建应用。
如图:
在真实的业务场景中,每每是上面提到六种方式中的几种的结合使用,或者是某种方式的变种。下面看我遇到的真实场景。
现有三个内部系统,下面称之为 old-a、old-b和C,其中,old-a和old-b是老旧的先后端未分离项目,C为先后端分离的SPA应用(React + HIUI),三个系统的架构图大体以下:
能够看到,old-a 运行在一台服务器1上,old-b运行在服务器2上,C系统的前端资源在服务器2上,而且C没有本身的域名。
三个系统均在后端同窗维护和开发,他们的需求以下:
考虑开发同窗的需求和开发成本、维护成本、将来的可拓展性,系统改造关键点以下:
整体的改造方案使用微前端的思想进行。对上面提到的六种方式进行对比(点击查看大图):
对于上面几种方式,在具体的实施使用了路由分发、iFrame、应用微服务化、微应用化的融合方式。或者说是某种方案的变种,由于改造以后同时具有了这几种方案的特色。
对于C系统和正在开发的x个系统使用 single-spa 作改造,对于老旧的系统 old-a 和 old-b 使用 iframe 接入。
改造后以下图:
此时,两个老系统分别部署在各自的服务器,C 和将来的多个应用部署在同一台服务器。而后,在 Nginx 层 为老系统分配了两个路由(暂且称之为 old-a 和old-b),分别将请求打到各自的服务器,根路由打到 C 和 xx 应用的服务器。
使用React 框架的 C 和 xx 应用基于 single-spa 改造后,那么老系统 iframe 如何接入?
在配置菜单时,老系统路由会被带上标识,统一交给其中一个应用以 iframe 的方式处理。
如图:
改造后微前端架构图:
官方示例:
// single-spa-config.js
import { registerApplication, start } from 'single-spa'
registerApplication("applicationName", loadingFunction, activityFunction)
start()
function loadingFunction() {
return import("src/app1/main.js")
}
function activityFunction(location) {
return location.pathname.indexOf("/app1/") === 0
}
复制代码
当增长一个应用的时候,就须要对 single-spa-config.js
文件进行修改。
经过可配置的方式实现子应用注册:
// single-spa-config.js
import * as singleSpa from 'single-spa'
import config from './manifest.json'
registerApp(config)
singleSpa.start()
function registerApp(conf) {
conf.forEach(application => {
singleSpa.registerApplication(
application.name,
() => import(`./${application.name}.app/index.js`),
pathPrefix(application.activeRule, application.strict),
)
})
}
function pathPrefix(prefix, strict) {
return function(location) {
if (strict) {
return location.pathname === prefix
}
return location.pathname.startsWith(`${prefix}`)
}
}
复制代码
// manifest.json
[
{
"name": "layout",
"activeRule": "/"
},
{
"name": "welcome",
"activeRule": "/",
"strict": true
},
{
"name": "iframe",
"activeRule": "/link"
},
{
"name": "app1",
"activeRule": "/app1"
},
{
"name": "app2",
"activeRule": "/app2"
}
]
复制代码
将域名统一的一大好处是 iframe 域名和主应用域名同源。没有了跨域 能够在 layout
统一 SSO 登陆,经过 cookie 共享让其余模块拿到登陆信息。
因为这次改造,应用之间不涉及数据共享,因此没有顶级 store 的概念。模块之间的简单通讯 能够经过 postMessage
或基于浏览器原生事件作通讯。
// 应用 A
window.dispatchEvent(
new CustomEvent('iframe:change', { detail: { path: '/a/b/c'} })
)
// 应用 B
window.addEventListener('iframe:change', (event) => {
console.log(event.detail.path)
})
复制代码
样式的隔离有不少种处理方式,如:BEM、CSS Module、css前缀、动态加载/卸载样式表、Web Components自带隔离机制等。
这次采用添加 css 前缀来隔离样式,好比 postcss 插件:postcss-plugin-namespace
。可是这个插件并不知足需求,咱们的应用分布在 src/
下,并以 name.app
的方式命名,须要给不一样的应用添加不一样的前缀。所以使用本身定制的插件:
postcss.plugin('postcss-plugin-namespace', function() {
return function(css) {
css.walkRules(rule => {
if (rule.parent && rule.parent.type === 'atrule' && rule.parent.name !== 'media') return
const filePath = rule.source && rule.source.input.file
const appName = /src\/(\S*?)\//.exec(filePath)[1] || ''
const namespace = appName.split('.')[0] || ''
rule.selectors = rule.selectors.map(s => `#${namespace} ${s === 'body' ? '' : s}`)
})
}
})
复制代码
有一个可严重可不严重的问题,如何确保子应用之间的全局变量不会互相干扰,实现js的隔离。广泛的作法是给全局变量添加前缀,这种方式相似 css 的 BEM,经过约定的方式来避免冲突。这种方式简单,但不是很靠谱。
qiankun 内部的实现方式是经过 Proxy
来实现的沙箱模式,即在应用的 bootstrap
及 mount
两个生命周期开始以前分别给全局状态打下快照,而后当应用切出/卸载时,将状态回滚至 bootstrap
开始以前的阶段,确保应用对全局状态的污染所有清零。有兴趣的同窗能够看源码。
上面的改造已经基本知足了业务需求,针对此业务还有更进一步的作法,达到更好的体验:
上面提到,这次的实践方式是微前端实现方式中几种的结合,或者是某种方式的变种。也许在理论上并非最优的,可是在具体的问题中要是最优解。架构设计必需要与当前要解决的问题相匹配,“没有最优的架构,只有最合适的架构”。
微前端不是一个框架或者工具,而是一套架构体系。
这套体系除了微前端的基础设施外还须要具有微前端配置中心(版本管理、发布策略、动态构建、中心化管理)、微前端观察工具(应用状态可见、可控)等。
整个体系的搭建将是一个庞大的工程,目前大部分团队是在使用微前端的模式和思想来解决现有系统中的痛点。
公众号: