什么是微前端
用于构建具有多个可以独立发布功能的* *团队的现代 Web 应用程序的**技术、策略和方法。 微前端一词于2016 年底首次出现在ThoughtWorks Technology Radar中。微前端是把微服务的概念扩展到前端领域。当前一个功能强大并且丰富的前端应用往往连接这很多微服务,随着时间的推移,业务的发展,单页面的前端应用会变得格外复杂且难以维护。微前端是思想是将前端应用视为独立团队开发的功能组合,每个团队专攻不同的业务。
优点
- 增量升级, 新技术的发展太快了,如果项目只在老的架构或者技术体系下迭代,将会失去很多好的用户体验而且降低开发效率,微前端可以实现增量升级,就是在保留原来项目部分的基础上,新功能或者模块采用新技术来实现。
- 简单、解耦的代码库,微前端的代码库通常可以没有耦合的分开为好几个较小的库,这样既方便管理也有利于防止组件之间的耦合而产生的问题和冲突。
- 独立部署,这是很重要的优势了,和微服务一样,拒绝“一颗老鼠屎坏了一锅粥”的好办法就是微前端了,子项目之间的部署分开。
- 团队自治,技术无关的每个微应用都可以有不同团队来负责,大家可以使用各自擅长的技术栈。
简而言之,微前端就是将大而可怕的东西切成更小、更易于管理的部分,然后明确它们之间的依赖关系。我们的技术选择、我们的代码库、我们的团队和我们的发布流程都应该能够相互独立地运行和发展,而无需过度协调。
方案
1、路由分发模式(服务端集成)
即通过路由将不同的业务分发到不同的、独立前端应用上。其通常可以通过 HTTP 服务器的反向代理来实现,又或者是应用框架自带的路由来解决。这应该也是微前端最简单的方式之一了,但其实这更像是多个应用的聚合,只是把多个应用简单的拼凑在一起,看起来一个整体,实际上应用之间并没有什么关系,从一个应用到另一个应用的跳转会出现白屏。实现方式大概是:一个服务器位于前端,通过不同的路由请求不同的微前端服务。
server {
listen 8080;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
ssi on;
# Redirect / to /browse
rewrite ^/$ http://localhost:8080/browse redirect;
# Decide which HTML fragment to insert based on the URL
location /browse {
set $PAGE 'browse';
}
location /order {
set $PAGE 'order';
}
location /profile {
set $PAGE 'profile'
}
# All locations should render through index.html
error_page 404 /index.html;
}
优点:
- 实现简单
缺点:
- 应用之间跳转白屏,因为要重新加载资源;
- 应用之间的状态共享需要通过cookie,session等实现;
参考:www.phodal.com/blog/implem…
2、通过iframe运行时集成(容器化)
在浏览器中组合应用程序的最简单方法之一是简单的 iframe。就其本质而言,iframe 可以轻松地从独立的子页面构建页面。它们还在样式和全局变量方面提供了很好的隔离度,不会相互干扰。
<html>
<head>
<title>Feed me!</title>
</head>
<body>
<h1>Welcome to Feed me!</h1>
<iframe id="micro-frontend-container"></iframe>
<script type="text/javascript">
const microFrontendsByRoute = {
'/': 'https://browse.example.com/index.html',
'/order-food': 'https://order.example.com/index.html',
'/user-profile': 'https://profile.example.com/index.html',
};
const iframe = document.getElementById('micro-frontend-container');
iframe.src = microFrontendsByRoute[window.location.pathname];
</script>
</body>
</html>
优点:
- iframe 自带的样式、环境隔离机制使得它具备天然的沙盒机制;
- 接入简单粗暴,成本低;
- 可跨站点整合;
缺点:
- 浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
- UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
- 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
- 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。无法感知,通信麻烦;
3、JS运行集成(DOM集成)
JS集成和 iframe 很相似,都是在运行时将子应用渲染到一个 DOM 元素中,不同的是 iframe 只需要改变 src 属性就可以完成加载,而 DOM 集成应用需要通过 JS 来拉取应用资源再渲染到一个普通的 DOM 元素中。
<html>
<head>
<title>Feed me!</title>
</head>
<body>
<h1>Welcome to Feed me!</h1>
<!-- These scripts don't render anything immediately -->
<!-- Instead they attach entry-point functions to `window` -->
<script src="https://browse.example.com/bundle.js"></script>
<script src="https://order.example.com/bundle.js"></script>
<script src="https://profile.example.com/bundle.js"></script>
<div id="micro-frontend-root"></div>
<script type="text/javascript">
// These global functions are attached to window by the above scripts
const microFrontendsByRoute = {
'/': window.renderBrowseRestaurants,
'/order-food': window.renderOrderFood,
'/user-profile': window.renderUserProfile,
};
const renderFunction = microFrontendsByRoute[window.location.pathname];
// Having determined the entry-point function, we now call it,
// giving it the ID of the element where it should render itself
renderFunction('micro-frontend-root');
</script>
</body>
</html>
优点:
- 拥有spa体验
- 状态共享
缺点:
- 样式可能相互影响
- JS变量污染
从single-spa到qiankun
1、single-spa
single-spa 支持构建时集成也支持运行时集成,但是官方推荐运行时集成,因为这样才能独立部署。传统的 SPA 应用通常都是类似 webpack 这样的 bundler 构建生成的,生成的代码也不只有一个 js,通常是一个 index.html 的入口,里面引入了其他的 JS、CSS等资源。而想运行时集成光靠 single-spa 还不够,single-spa 并不提供将应用资源引入的功能。single-spa 官方推荐的是使用 systemjs 配合 import maps 来进行应用的引入。import maps 是一个浏览器规范,截止到 2020年2月也只在 chorme 中被实现,并且只有切换到开发者特性后才能实现,它的主要作用是可以 import url,例如:import('http://localhost:8080/app-a.js')。而 systemjs 相当于 import maps 的一个 pollyfill,说是 pollyfill 但其实更像一种替补手段,想要用 systemjs 来进行 import(url),必须把应用打包成 systemjs 能够识别的 bundle,也就是把 webpack 的 output.libraryTarget 属性设置为 system,只有这样才能在运行时进行应用的加载。
Single-spa 在一定程度上来说已经可以帮我们实现微前端了,但是实现的部分也很基础,还有很多问题需要解决。比如改造老项目,大部分的老项目并没有打包成一个 js,并且接入微前端也不是一次性全部拆分,可能是先拆出去一部分。将已有模块拆分成子项目,需要将子项目打包成system js 能够导入的 JS,这需要对项目配置做一定的改变。引入项目以后,还需要考虑到子项目对其他模块的影响。总的来说 single-spa 是一个非常基础的为前端框架,应用引入麻烦,生产级别的应用隔离也没有实现。
zh-hans.single-spa.js.org/docs/config…
2、qiankun
针对single-spa的缺点,qiankun做了以下优化:
2.1 优化应用注册
Qiankun 使用 import-html-entry 来进行子应用的引入代替Single-spa 中的 systemjs 或 import maps 。import-html-entry 是 qiankun 的开源团队开发的一个运行时 import url 的插件,支持直接 import html,可以将 html 异步 import 并塞入 DOM 中,子应用直接通过 html 渲染和拉取。这比使用 systemjs import 要方便太多了,现有的 webpack 构建的项目不需要改变太多配置就可以直接引入,这极大的降低了接入成本。
2.2 优化应用隔离
JS隔离:
qiankun 选择自己做一个 sandbox 来进行应用的隔离,对于全局 JS 变量,qiankun 通过在应用的装载卸载生命周期对 window 对象打快照来初始化不同应用的全局变量,这样只有当前应用的全局变量会被保留,当切换到别的应用时,又会重新初始化其他应用的变量快照,不会同时存在多个应用的全局变量,也就避免了污染。提供了三种不同场景使用的沙箱,分别是 snapshotSandbox、legacySandbox、proxySandbox。
- 快照沙箱(snapshotSandbox):激活沙箱将window快照信息存在windowSnapshot,退出沙箱再对比window数据,改变了就与快照做diff改变的就值就还原;
- 代理单例沙箱(legacySandbox):单例和多例都是用ES6的proxy实现的,创建三个变量来记录沙箱新增、修改的全局变量和持续保存全局变量,代替快照模式中的快照,避免所有变量的快照,提升性能;
- 代理多例沙箱(proxySandbox):激活沙箱,每次对window变量操作都先从自己内部的fakeWindow中找,找不到再到外部的rawWindow中找;
CSS隔离:
虽然 import-html-entry在切换应用的时候会清空 DOM 中的上一个应用的 index.html,所有的 stylesheet link 也会被清除,但是qiankun还是做了样式隔离。主要有两种,一种是从工程化的角度,利用Css Module,给每个选择器加上不同的类名,防止类名冲突;还有一种就是利用浏览器的Shadow dom来进行严格隔离。
画图理解
大致的流程是:
1、通过添加监听事件,当浏览器Location发生改变,拦截onhashchange和onpopstate事件,并mock浏览器history的pushState()和replaceState()方法。
2、根据路由匹配,加载命中的子项目资源,其实这里会把变化放在队列中,就像时间循环的队列一样,等待js引擎的处理;
3、如果前面已经有子应用被挂载就卸载它(unmounted)并加载(bootstrap)和挂载匹配的子应用;如果没有就直接加载(bootstrap)和挂载匹配的子应用;
其实过程并没有这么简单,比如在加载新应用的时候需要启动沙箱,可能是存快照也可能是proxy代理,用fakeWindow存储变量等。
微前端项目不仅仅是qiankun
一个微前端的接入框架,可以让项目具备微前端的能力。把一个单体架构应用拆分一个个的子应用,子应用需要具备独立开发、部署的能力,这意味着子应用需要具备和主项目同样的构建、基础能力。项目的拆分意味着代码不能共享,所以组件库可能也是需要的。
1、脚手架
对于同样技术栈的子项目来说,项目的配置尽量保持一致,以减少后期的维护成本,也让升级更加方便,比如可以借助vue-cli二次开发等等,这里就不多赘述。
2、公共组件库
常见的两种方式是npm包管理和git submodule:
- npm包管理:版本粒度小,松耦合,但是发布和更新麻烦,问题定位也麻烦;
- git submodule:版本粒度相对大,代码耦合度大,但是问题定位方便;
两个方法各有千秋,看个人取舍了。
总结
了解微前端并不是因为它可能会成为一种趋势,而是因为它里面包含了很多基础知识,概念,可以拓展知识面。微前端到底好不好,也只能是视项目而定了。
参考: