一、背景
随着时间的推移,大型企业或团队的组织结构、软件架构都在不断的更新变化。随之而来的便是多渠道、多样化体验以及对不同技术栈的引入。同时,业务也在不断扩展,导致各个项目越来越臃肿,应用越来越难以维护。
而现有的 Web SPA 针对上述问题,并不能很好的拓展和部署。
二、微前端定义
Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. — Micro Frontends
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。
微前端架构具备的几个核心价值:技术栈无关、独立开发、独立运行、独立部署、增量升级。
微前端的核心设计理念:解耦、聚合、技术栈无关。其思路来源于微服务的思想。将微服务的理念应用在浏览器端。即将 Web 应用由单一的单体应用转变为多个微应用聚合为一的应用。
目前主流的框架有 Single-SPA、qiankun、icestark、Mooa,其中qiankun、Mooa都是基于 Single-SPA 的封装。而Mooa是一款为Angular服务的为前端框架。
2.1 微应用
微应用是指在开发时应用都是以单一、微小应用的形式存在的,而在运行时,则是通过构建系统合并这些应用,并组合成一个新的应用。
2.2 为什么不是iframe
iframe 作为一个非常古老的技术,却一直很管用。iframe 可以创建一个全新的独立的宿主环境,这意味着我们的前端应用之间可以相互独立运行。从这个角度来看,iframe又何尝不是微前端的一种实现呢。
既然如此,为什么不用 iframe,为什么大部分微前端方案又不约而同放弃了 iframe 方案呢?
主要可以从以下几个方面分析:
- url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
- UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
- 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
- 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
其中有的问题比较好解决(问题1),有的问题我们可以睁一只眼闭一只眼(问题4),但有的问题我们则很难解决(问题3)甚至无法解决(问题2),而这些无法解决的问题恰恰又会给产品带来非常严重的体验问题, 最终导致我们舍弃了 iframe 方案。
2.3 Web Components
Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的web应用中使用它们。
三、微前端落地时架构技术选择
- 从架构上,常规web应用的架构类型分为两种,一种是MPA,另一种是SPA。它们各有各的优缺点。
- MPA的优点在于部署简单,具备独立开发和独立部署的特性。但是,它的缺点是页面跳转时需要重新加载整个页面。
- SPA能极大保证多个任务之间串联的流畅性,内容的改变不需要重新加载整个页面,但问题是通常一个SPA是一个技术栈的应用,很难共存多个技术栈方案的选型,且不利于SEO。
- 从运行特性上,微前端包含两个类别,一类是单实例,另一类是多实例。
微前端架构的核心诉求是实现能支持自由组合的微前端架构,将其他的SPA应用以及其他组件级别的应用自由的组合到平台中。
四、技术细节上的决策
为了实现上述的方案,在技术细节上的决策需要注意以下问题:
1、是如何做到子应用之间的技术无关;
2、是如何设计路由和应用导入;
3、是如何做到应用隔离;
4、是基础应用之间资源的处理以及跨应用间通信的选择。
下面以 qiankun为例:
导出相应生命周期的钩子
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log('react app bootstraped');
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount(props) {
ReactDOM.unmountComponentAtNode(
props.container ? props.container.querySelector('#root') : document.getElementById('root'),
);
}
/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props) {
console.log('update props', props);
}
对于应用的导入方式的选择:Config Entry与HTML Entry
Config Entry: 配置每个子应用的 js 和 css,包含内联的部分。(不推荐)
通过在主应用中注册子应用依赖哪些JS。这种方案一目了然,但是最大的问题是ConfigEntry的方式很难描述出一个子应用真实的应用数据信息。真实的子应用会有一些title信息,依赖容器ID节点信息,渲染时会依赖节点做渲染,如果只配JS和CSS,那么很多信息是会丢失的,有可能会导致间接上的依赖。
loadMicroApp({
name: 'configEntry',
entry: {
scripts: ['//t.com/t.js'],
styles: ['//t.com/t.css'],
},
});
HTML Entry: Config Entry 的进阶版,简化开发者使用,但是把解析消耗留给了用户
HTML Entry直接接入访问地址。使得应用的信息可以得到完整的保留,且接入应用地址只需配一次,子应用的原始开发模式得到完整保留,因为子应用接入只需要告知主应用url在哪,包括在不接入主应用时独立的打开。它的缺点是将解析的消耗留给了运行时。
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'reactApp',
entry: '//localhost:3000',
container: '#container',
activeRule: '/app-react',
},
{
name: 'vueApp',
entry: '//localhost:8080',
container: '#container',
activeRule: '/app-vue',
},
{
name: 'angularApp',
entry: '//localhost:4200',
container: '#container',
activeRule: '/app-angular',
},
]);
// 启动 qiankun
start();
对于css隔离问题: 子应用之间样式互不影响,切换时加载与卸载
- 动态样式表:根据生命周期装载、卸载样式表
- 工程化手段(BEM、CSS Modules、CSS in JS、Web Components)
- Shadow DOM:Shadow DOM 允许将隐藏的 DOM 树附加到常规的 DOM 树中——它以 shadow root 节点为起始根节点,在这个根节点的下方,可以是任意元素,和普通的 DOM 元素一样,隐藏的 DOM 样式和其余 DOM 是完全隔离的,类似于 iframe 的样式隔离效果。
对于js隔离:通过 Proxy 来实现的沙箱模式,即在应用的 bootstrap 及 mount 两个生命周期开始之前分别给全局状态打下快照,然后当应用切出/卸载时,将状态回滚至 bootstrap 开始之前的阶段,确保应用对全局状态的污染全部清零。
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。简单来说就是,可以在对目标对象设置一层拦截。无论对目标对象进行什么操作,都要经过这层拦截。
优点: 可以同时运行多个沙箱;不会污染 window 环境
结语
以上便是我在对微前端的一些了解,如果有不对的地方欢迎指正。之后我也会结合业务实践,输出对 qiankun 的一些思考。