导语 | 微前端是将Web应用由单一的单体应用转变为多个小型前端应用聚合为一的一种手段。本文从微前端的基础理论出发,对其核心技术进行阐述,最后结合项目进行简单的应用实践。
一、微前端背景
微前端提供了一种技术:可以将多个独立的Web应用聚合到一起,提供统一的访问入口。一个微前端应用给用户的感观就是一个完整的应用,但是在技术角度上是由一个个独立的应用组合通过某种方式组合而成的。
为了防止概念有点抽象,可以看一个具体的例子:上图是一个微前端的demo,主应用中有导航栏,footer组件以及左边的侧边栏组件,而右面是子应用部分,这里的子应用并没有集成在主应用中,只是通过微前端的框架内嵌到主应用中,可是给用户的感受就是一个完整的项目。
目前的微前端框架一般都具有以下三个特点:
技术栈无关:主框架不限制接入应用的技术栈,子应用具备完全自主权。
独立性强:独立开发、独立部署,子应用仓库独立。
状态隔离:运行时每个子应用之间状态隔离。
2014年:Martin Fowler和James Lewis共同提出了微服务的概念。微服务是一种开发软件的架构和组织方法,其中软件由通过明确定义的API进行通信的小型独立服务组成。
微服务的主要思路是:
将应用分解为小的、互相连接的微服务,一个微服务完成某个特定功能。
每个微服务都有自己的业务逻辑和适配器,不同的微服务,可以使用不同的技术去实现。
使用统一的网关进行调用。
可以看到微服务的主要思路是化繁为简,通过更加细致的划分,使得服务内部更加内聚,服务之间耦合性降低,有利于项目的团队开发和后期维护。把微服务的概念应用到前端,前端微服务/微前端服务就诞生了,简称其为微前端。
2018年: 第一个微前端工具single-spa在github上开源。
2019年: 基于single-spa的qiankun问世。
2020年:Module Federation(webpack5)把项目中模块分为本地模块和远程模块,远程模块不属于当前构建,在运行时从所谓的容器加载。加载远程模块是异步操作。当使用远程模块时,这些异步操作将被放置在远程模块和入口之间的下一个chunk的加载操作中,从而实现微前端的构建。
二、微前端的实现方式
微前端的第一种实现思路是服务端集成,即通过Nginx配置反向代理来实现不同路径映射到不同应用(如下图所示),这样可以实现项目的独立开发和部署。
但同时这种做法也会丢失SPA的体验,每一次命中路由都会重新请求资源,不能局部更新当前页面。
另一种方法就是运行时集成,这种方法一种实现就是使用iframe,通过配置不同的src加载不同的子应用页面。
iframe优点:
iframe 自带的样式、环境隔离机制使得它具备天然的沙盒机制。
嵌入子应用比较简单。
iframe缺点:
iframe功能之间的跳转是无效的,刷新页面无法保存状态。
URL的记录完全无效,刷新会返回首页。
主应用劫持快捷键操作,事件冒泡不穿透到主文档树上。
模态弹窗的背景是无法覆盖到整个应用。
iframe应用加载失败,内容发生错误主应用无法感知,通信麻烦。
综上,iframe也可以实现微前端,但是需要解决其自身的诸多弊端。公司的无界微前端就是基于iframe实现的。
三、现有开源方案
single-spa是一个用于前端微服务化的JavaScript前端解决方案。single-spa的核心就是定义了一套协议。协议包含主应用的配置信息和子应用的生命周期,通过这套协议,主应用可以方便的知道在什么情况下激活哪个子应用。
在single-spa中的配置信息也称为:Root Config,如下就是具体的配置项。需要配置子应用的名称,加载方式以及加载时机。
{
name: "subApp1", //子应用的名称
app: () =>//告诉主应用如何加载子应用的代码,
System.import("/a/b/subAPP/code"),
activeWhen: "/subApp1", //告诉主应用何时激活子应用
}
single-spa提供registerApplication将子应用的信息注册到主应用中。
singleSpa.registerApplication(
{
name: 'appName',
app: () => System.import('appName'),
activeWhen: '/appName',
})
在上面的代码中System.import让人比较在意,这是什么呢?
这个问题要从主应用如何加载子应用说起,在single-spa中子应用要实现生命周期函数,然后导出给主应用使用。关键就是这个“导出”的实现,这涉及到JavaScript的模块化问题,即需要把子应用打包成一个包含生命周期的模块,让主应用引入。
JavaScript的模块化,如何在页面中引入模块?
JavaScript的模块化就是将JavaScript程序拆分为可按需导入的单独模块的机制。Node.js已经提供这个能力很长时间了,还有很多的Javascript库和框架已经开始了模块的使用(例如CommonJS和基于AMD的其他模块系统 如RequireJS,以及最新的Webpack和Babel)。目前最新的浏览器也开始原生支持模块功能。
在
实现import axios from ‘axios’还需要借助于importmap
第一点虽然可以实现导入,但是每次Import都要写入固定的地址,或者在不同的script中多次引入时就要重复书写,这样造成代码的冗余,所以这里可以使用importmap,使变量名和其相应的地址一一映射,允许控制js的import语句或import()表达式获取的库的url,并允许在非导入上下文中重用这个映射,这样就不用重复书写地址了。
SystemJS
import maps的兼容性如下图所示,所以想在生产环境下使用还是需要一些兼容实现方案,SystemJS就是解决这个问题的。
systemjs是一个模块加载器,和requirejs类似,systemjs参考import maps规范实现了自己的alias(类似requirejs-paths或者webpack alias)。具体用法在下面的demo中。
// 通过systemjs来引入别的文件
System.import('./test.js');
// systemjs也支持通过下面的方式定义资源 ,用来给资源定义一个key
// 直接通过名称引用
System.import('vue');
这里总结一下single-spa是如何通过以上方法加载子应用的:
在主应用中注册子应用的配置信息,主应用运行时根据配置信息去请求子应用的manifest.json配置文件,这个文件中是子应用打包出的入口js和js文件的依赖关系,主应用通过动态的构造script标签去加载这些js文件,这里就完成了其注册过程。
这样在主应用检测路由命中子应用的规则之后就会触发其渲染函数,把子应用挂载到相应的dom下。
生命周期
single-spa的另一个关键点就是生命周期,子应用生命周期包含bootstrap,mount,unmount三个回调函数。主应用在管理子应用的时候,通过子应用暴露的生命周期函数来实现子应用的启动和卸载。
load:当应用匹配路由时就会加载脚本(非函数,只是一种状态)。
bootstrap:应用内容首次挂载到页面前调用。
Mount:当主应用判定需要激活这个子应用时会调用, 实现子应用的挂载、页面渲染等逻辑。
unmount:当主应用判定需要卸载这个子应用时会调用, 实现组件卸载、清理事件监听等逻辑。
unload:非必要函数,一般不使用。unload之后会重新启动bootstrap流程。
通过以上两点的分析,大致了解了一下sing-spa的主要思想,但是single-spa毕竟是第一个微前端框架,他也有一定的缺点。
single-spa的文档略显凌乱,概念也比较多,在初次学习时上手难度较高。
single-spa是通过js文件去加载子应用,当文件名是乱码名时,每次子应用更新,父应用要更新引入配置文件,更新多项目时比较麻烦。
single-spa本身缺少js隔离和css隔离,虽然现在已经可以引入其他的包去解决,但是并没有做到开箱即用的程度。
所以在基本了解其思路之后,我们可以不妨看一下其他的方案都是如何实现和优化的。
qiankun是基于single-spa提出的微前端框架, 提供了更加开箱即用的API(single-spa+sandbox+import-html-entry)。
其主要有六个比较明显的特点,在下文依次展开。
首先是子应用的加载方式与single-spa有明显的不同,single-spa注册子应用本质上是JS Entry,即通过从某一地址引入js文件来加载整个子应用。
singleSpa.registerApplication({
'appName',
() => System.import('appName'),
location => location.pathname.startsWith('appName'),
});
但是qiankun注册子应用的方式是通过一个url,即使用HTML Entry的方式来引入子应用。
registerMicroApps([
{
name: 'react app',
entry: '//localhost:7100',
container: '#yourContainer',
activeRule: '/yourActiveRule'
},
]);
start();
通过固定的url引入的好处是:子项目大多是已经上线的项目,url是固定的,所以不用频繁更新主应用中的注册信息,便于主应用的整合和开发。
qiankun-子应用的加载
Html Entry方法的主要步骤如下:首先通过url获取到整个Html文件,从html中解析出html,js和css文本,在主应用中创建容器,把html更新到容器中,然后动态创建style和script标签,把子应用的css和js赋值在其中,最后把容器放置在主应用中。
// 解析 HTML,获取 html,js,css 文本
const {htmlText, jsText, cssText} = importHTMLEntry('https://xxxx.com')
// 创建容器
const $= document.querySelector(container)
$container.innerHTML = htmlText
// 创建 style 和 js 标签
const $style = createElement('style', cssText)
const $script = createElement('script', jsText)
$container.appendChild([$style, $script])
如何解析html?
通过url请求到子应用的index.html。
用正则匹配到其中的js/css相关标签,进行记录,然后删去。
删去html/head/body等标签。
返回html文本。
如何解析js?
使用正则匹配