微前端究竟是什么?微前端核心技术揭秘!
2022-04-02 17:40:10来源:知乎
微前端提供了一种技术:可以将多个独立的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-spasingle-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)。目前最新的浏览器也开始原生支持模块功能。
在<script>标签上添加type=“module”来实现导入导出<script type="module"> import {test} from "./test.js"; document.body.innerHTML = test("1111"); </script>实现import axios from ‘axios’还需要借助于importmap
第一点虽然可以实现导入,但是每次Import都要写入固定的地址,或者在不同的script中多次引入时就要重复书写,这样造成代码的冗余,所以这里可以使用importmap,使变量名和其相应的地址一一映射,允许控制js的import语句或import()表达式获取的库的url,并允许在非导入上下文中重用这个映射,这样就不用重复书写地址了。
<script type="importmap"> { "imports": { "lodash": "/node_modules/lodash/lodash.js" } }</script> <script type="module"> import {hello} from "lodash"; document.body.innerHTML = hello("John");</script>SystemJS
import maps的兼容性如下图所示,所以想在生产环境下使用还是需要一些兼容实现方案,SystemJS就是解决这个问题的。
systemjs是一个模块加载器,和requirejs类似,systemjs参考import maps规范实现了自己的alias(类似requirejs-paths或者webpack alias)。具体用法在下面的demo中。
<script src="https://cdn.bootcss.com/systemjs/6.2.6/system.js"></script> // 通过systemjs来引入别的文件 System.import("./test.js"); // systemjs也支持通过下面的方式定义资源 ,用来给资源定义一个key <script type="systemjs-importmap"> { "imports": { "vue": "https://cdn.bootcss.com/vue/2.6.11/vue.js" } } </script> // 直接通过名称引用 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隔离,虽然现在已经可以引入其他的包去解决,但是并没有做到开箱即用的程度。所以在基本了解其思路之后,我们可以不妨看一下其他的方案都是如何实现和优化的。
(二)qiankunqiankun是基于single-spa提出的微前端框架, 提供了更加开箱即用的API(single-spa+sandbox+import-html-entry)。
其主要有六个比较明显的特点,在下文依次展开。
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?
使用正则匹配<script>标签。对于内联js的内容会直接记录到一个对象中。对于外链js会使用fetch请求到内容,然后记录到这个对象中。最后在加载子应用时直接把内容赋值在动态构建的script中。如何解析css?
正则匹配子元素
一行文字
这样的结果就是子样式污染了父样式(效果如下图)。
使用严格样式隔离解决一下这个问题:获取到子应用的根节点,然后打开影子模式,把原来的dom结构赋值到代理的影子根节点中,然后清空原来的dom结构。
function openShadow(domNode) { var shadow = domNode.attachShadow({ mode: "open" }); shadow.innerHTML = domNode.innerHTML; domNode.innerHTML = ""; } var bodyNode = document.getElementById("app1"); openShadow(bodyNode);
现在可以在dom树中看到,原来的子应用以及开启了影子模式,其中的子dom都在影子中,效果如右图所示,实现了父子之间的样式隔离。
experimentalStyleIsolation第二种父子样式隔离是实验性样式隔离 ,即通过运行时修改CSS选择器来实现子应用间的样式隔离。
下面也是一个模拟污染的demo,可以看到主应用和子应用有重名的选择器,子应用在后面,所以父样式被覆盖,造成了污染。
父应用
子应用
这里首先获取到子应用,然后通过正则匹配其中的所有
Copyright 2015-2020 三好网 版权所有