微前端
最后更新于
这有帮助吗?
最后更新于
这有帮助吗?
最近看了 yy 的<<微架构落地实践>>,想在公司内部实践一下微前端,公司有个游戏管理后台的项目,最开始的时候是采用 js+jquery 的方式,后来又演进为 iframe + vue 的方式,而现在又要增加新的模块,有两种方式, 一种继续以 iframe 的方式嵌入进去.还有一种就是引申出我们的微前端方案.
增量升级
简单、解耦的代码库
独立部署
微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将单页面前端应用由单一应用转变为把多个小型前端应用聚合为一的应用。各个前端应用还可以独立开发、独立部署。同时,它们也可以进行并行开发—这些组件可以通过 NPM,GIT 或者 SVN 来管理。
应用自治,即每个子应用之间不存在依赖关系,替换其中一个子应用而整体不受影响 单一职责,即微前端架构应该满足单一职责的原则 技术栈无关,即每个子应用可以用不同的前端框架去编写
1.路由分发式 通过 HTTP 服务器的反向代理功能,将请求路由映射到对应的应用上
2.前端微服务化 在不同的框架上设计通信和加载机制,以在一个页面内加载对应的应用
3.微应用 通过软件工程的方式,在部署构建环境中,把多个独立的应用组合成一个应用
4.微件化 将业务功能拆分成独立的 chunk 代码,加载即可运行
5.前端容器化 iframe
6.应用组件化 Web Components 技术
而微前端方案又分为三种:
一种是以蚂蚁金服 qiankun 为代表的工程之间技术栈无关型。
另一种是以美团外卖为代表的工程之间技术栈统一型。
我们看一下
import-html-entry 仓库中,同样是通过 fetch 等请求库,但拿到的是作为 umd 包的子工程模块内容,然后 eval 执行。eval 执行时更改了绑定的 window 对象,这样做主要是为了不把子工程导出值都绑定到 window 上,而是绑定到自定义的 window.proxy 上,做自定义的隔离处理。这里还用到了类似 SystemJ s global loading 的自动寻找新增全局变量的方式来找到子模块暴露的全局入口,这也是这个方案的一大特点,即不需要对子应用的代码做特殊改造或者约定,只要 webpack 配置成 umd 输出即可,正如 systemjs global 本来就是给加载 jquery 等传统库准备的。这比较适用于子应用还需要独立部署的情形。
贴出的代码里可以看到 loadAsyncSubapp 和 subappRoutes,也提到了触发 jsonp 钩子 window.madSubapp ,种种迹象显示他们的方案是通过 jsonp 实现的。可以通过 webpack 设置 libraryTarget 为 jsonp,这样配置的的打包产物在加载时会执行全局的 jsonp 方法,传入主模块的 export 值作为参数。
Webpack module federation
JS 模块动态加载的原理主要有两种: jsonp 方式是用动态 script 标签直接加载并解析的,而 fetch 请求方式是拿到模块内容,之后需要用 eval 或者 new Function 这类方法来进行解析。虽然原理并不复杂,但是大家可以看到,为了达到环境隔离或者直接使用输出值等不同效果,
html entry 和 js entry jS Entry 的方式通常是子应用将资源打成一个 entry script,比如 single-spa 的 example 中的方式。但这个方案的限制也颇多,如要求子应用的所有资源打包到一个 js bundle 里,包括 css、图片等资源。除了打出来的包可能体积庞大之外的问题之外,资源的并行加载等特性也无法利用上。
HTML Entry 则更加灵活,直接将子应用打出来 HTML 作为入口,主框架可以通过 fetch html 的方式获取子应用的静态资源,同时将 HTML document 作为子节点塞到主框架的容器中。这样不仅可以极大的减少主应用的接入成本,子应用的开发方式及打包方式基本上也不需要调整,而且可以天然的解决子应用之间样式隔离的问题(后面提到)。
HTML Entry 方案下,主框架注册子应用的方式则变成:
本质上这里 HTML 充当的是应用静态资源表的角色,在某些场景下,我们也可以将 HTML Entry 的方案优化成 Config Entry,从而减少一次请求,如:
总结
一种较新的方法是通过 CSS 模块 或各种 CSS-in-JS 库,以编程的方式写 CSS。某些开发者还会使用 shadow DOM 来隔离样式。
Shadow DOM 是被设计为用于构建基于组件的应用程序的工具,它为你能遇到的 Web 开发中的常见问题提供了解决方案:
隔离 DOM:组件的 DOM 是自包含的(例如,document.querySelector(),不会返回组件的 shadow DOM 中的节点)。这也简化了 Web 应用程序中的 CSS 选择器,因为 DOM 组件是隔离的,它使你能够使用更通用的 id 或者 class 而无需担心命名冲突。 Scoped CSS:在 shadow DOM 中定义的 CSS 是具有作用域的,样式规则不会泄漏,页面样式不会干扰它。 Composition:为你的组件设计一个基于标记的声明式 API。
使用自定义事件通信,是降低耦合的一种好方法。不过这样做会使微应用之间的接口变得模糊。 可以考虑 React 应用中常见的机制:自上而下传递回调和数据。 第三种选择是使用地址栏作为通信桥梁
我们推荐的模式是 Backends For Frontends 模式,其中每个前端应用程序都有一个相应的后端,后端的目的仅仅是为了满足该前端的需求。BFF 模式起初的粒度可能是每个前端平台(PC 页面、手机页面等)对应一个后端应用,但最终会变为每个微应用对应一个后端应用。 这里要说明一下,一个后端应用可能有独立业务逻辑和数据库的,也可能只是下游服务的聚合器。 如果微前端应用只有一个与之通信的 API,并且该 API 相当稳定,那么为它单独构建一个后台可能根本没有太大价值。指导原则是:构建微前端应用的团队不必等待其他团队为其构建什么事物
依赖库。重复的依赖项是微前端的一个常见缺点。即使在应用程序之间共享这些依赖也非常困难,我们来讨论如何实现依赖库的共享。
然后,用 script 向每个 index.html 文件添加几个标签,以从共享内容服务器中获取这两个库:
使用 single-spa, 需要子应用抛出生命周期,主应用根据 Location 的变化来对 app 进行动态的 mount 和 unmount 实现子应用的切换。 single-spa 会劫持 location 的变化事件,保证执行完子应用的 mount 操作之,再由子应用的路由系统接管 url change 事件。
webpack 拆包和动态加载 webpack 的拆包和动态加载时的模块加载也是通过 jsonp 实现的,每一个拆出来的包,也叫 chunk,都是被包在一个全局 jsonp 方法中的,模块被加载时 jsonp 方法就会被执行,这个 jsonp 方法会去注册这个 chunk 和它所依赖的 chunk,在它所依赖的所有 chunk 都加载好之后时候会去触发该 chunk 的入口模块(entry module)的执行。熟悉这个过程对我们排查生产环境中拆包产物的部署问题有很大帮助,图解如下: