IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    如何选择微服务框架

    admin发表于 2023-01-18 05:50:04
    love 0

    选对微服务框架很重要

    作者:许仙

    前言

    ​ 随着业务的发展,很多程序应用变成一个 Monolithic-Applications(巨石应用)。同时由于维护的团队人员都比较分散,工程大,导致开发调试效率低,上线困难(代码合并相互依赖),成为阻塞业务发展的一个重要因素。

    ​ 于是行业创建了微服务框架,它主要解决两个问题:

    随着项目迭代应用越来越庞大,难以维护;
  • 跨团队或跨部门协作开发项目导致效率低下的问题;
  • 什么是微前端

    ​ 微前端(Micro Frontends)是一种类似于微服务的架构,是一种由独立交付的多个前端应用组成整体的架构风格,将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的应用,而在用户看来仍然是内聚的单个产品。

    ​ Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently –Micro Frontends

    ​ 多个团队通过独立发布功能的方式共同构建现代Web应用程序的技术、策略和方法

    ​ 看完概念可能还是对微前端的理解有些模糊,简单来说,如果有一个内容和应用功能丰富的前端项目,而这个项目随着时间和需求的推移,项目会变得不再简单而是越来越庞大,不太容易能在项目中添加新的功能,往往会牵一发而动全身,开发和维护成本也越来越高。微前端呢,就可以将项目拆分和细化成多个独立的子应用,子应用之间可以独立进行开发和维护。又或者说自己的项目中存在一些历史项目,而这些项目使用的是老框架,但是这些项目需要结合到新框架中来使用还不能放弃,但是也没有足够的精力和时间重写旧的的逻辑,微前端就可以将这些系统进行整合,在几乎不修改逻辑的同时能够兼容新旧两套系统并行运行。这也是我所在部门现在遇到的相同情况

    Micro-Frontend微前端- lightsong - 博客园
    Micro-Frontend微前端- lightsong – 博客园

    为什么要使用微前端

    优点

    • 独立技术栈: 主框架不限制接入应用的技术栈,每个应用的技术栈选型可以配合团队以及业务需求进行选择,目前前端主流开发的技术栈是React,Vue2、Vue3、Angular等
    • 独立开发、独立部署。子应用的仓库独立,前后端可以独立开发,部署完成后主框架同步更新子应用。
    • 独立运行:每个子应用之间的状态隔离,相当于并联电路一样,单独子应用运行失败不会影响到其他项目
    • 数据共享:子应用之间可以共享主应用的数据,也可共享兄弟应用的数据,完成应用通信。

    缺点

    • 复杂度从代码转向基础设施,因为项目更像一个综合性的集合,从单纯的代码变为了一个丰富的项目集成
    • 需要建立全面的微前端周边设施,比如调试工具、监控系统以及部署平台,才能发挥微前端的架构优势

    什么时候使用微前端

    • 项目技术栈过于老旧,相关技能的开发人员少,功能扩展吃力,重构成本高,维护成本高
    • 项目过于庞大,开发,部署效率底下,且出现问题,造成全局崩盘,不好维护
    • 项目组存在不同技术栈,但是需要接入同一套主系统中

    常见的微前端解决方案

    image-20230112103535450
    image-20230112103535450

    当前微前端主要采用的是组合式应用路由方案,它的核心是主从思想,也就是包括一个基座(MainApp)应用和若干个微应用(MicroApp)应用。基座应用基本上是一个前端Spa项目,主要功能是负责应用注册、路由映射以及消息分发等,而微应用是一个个独立的前端项目,这些项目不限于采用React、Vue或者Angular等技术栈开发,每个微应用都被注册到基座应用中,由基座进行管理,基本流程如下图所示:

    image-20230112104359663
    image-20230112104359663

    Qiankun

    ​ 想必看到这里对微前端有了一定的认识,接下来让我们来看看微前端架构Qiankun

    ​ Qiankun是一个蚂蚁金服基于single-spa的微前端实现库。它的核心设计理念一个是简单一个是解耦或者是技术栈无关。简单,是对于用户而言只是一个类似Jquery的裤,用户只需要调用几个Qiankun提供的Api即可完成应用的微前端改造。同时由于Qiankun的一些特性,使得微应用的接入比较简单。而解耦/技术栈无关,是为了确保微应用真正具备独立开发、独立运行的能力,设计了HTML entry、沙箱、应用通信等功能。

    特性

    • 基于single-spa封装,提供了开箱即用的api
    • 技术栈无关,项目可以使用独立的技术栈进行开发,不论是React、Vue、Angular或者是其他框架
    • HTML entry接入方式
    • 样式隔离,确保微应用之间样式互相不干扰
    • JS 沙箱,确保微应用之间 全局变量/事件 不冲突
    • 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度
    • umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统

    与single-spa的区别

    ​ single-spa 是一个将多个单页面应用聚合为一个整体应用的 JavaScript 微前端框架,虽然Qiankun是基于single-spa开发,但是在此基础上新设计了很多解决single-spa问题的一些功能

    • single-spa没有沙箱隔离,而Qiankun则有就是沙箱机制,一种是快照沙箱,在应用沙箱挂载或卸载时记录快照,在切换时依据快照恢复环境。一种是使用Proxy代理沙箱,不影响全局环境。
    • single-spa在开发过程中我们需要手动去写调用子应用js的方法,而Qiankun不需要,只需要传入响应的apps的配置即可
    • 最明显的区别是single-spa使用的是js entry的方式加载资源,而Qiankun使用的是HTML entry的方式加载资源,Qiankun是通过自己实现了一个解析html字符串获取静态资源地址的解析依赖库import-html-entry,方便资源的预加载
      js entry
      原理
      将css打包进js,生成一个json的配置文件(标识了子应用资源文件的相对路径地址)
    • 主应用通过插入script标签src属性的方式加载子应用资源文件
    缺点
    • 打包时,需要额外对工程化代码进行修改,需要生成一份资源配置文件给主应用加载
    • 打包时,需要额外对样式打包做修改,需要把 css 打包进 js 中,也增加了编译后的包体积
    • 打包时,不能在 html 中插入行内 script 代码
  • HTML entry
    直接将子应用打出来 HTML 作为入口,主框架可以通过 fetch html 的方式获取子应用的静态资源,同时将 HTML document 作为子节点塞到主框架的容器中。这样不仅可以极大的减少主应用的接入成本,子应用的开发方式及打包方式基本上也不需要调整,而且可以天然的解决子应用之间样式隔离的问题。(该方式的原理会在之后的进阶处给出,感兴趣的可以简单了解一下
  • 遇到的问题

    • 微应用加载时会404:原因是webpack加载资源的时候没有使用正确的publicPath,一是可以通过使用webpack运行时publicPath配置,Qiankun会在微应用bootStrap之前注入一个运行时的 publicPath 变量,之后需要做的是在微应用的 entry js 的顶部添加__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__。二是可以通过使用webpack静态publicPath配置,需要在webpack的publicPath配置设置成一个绝对路径的url
    • 如果我们想独立运行某个微应用, 可以
      if (!window.__POWERED_BY_QIANKUN__) {
        render();
      }
      
      export const mount = async () => render();
      
    • 微应用之间的跳转
      主应用和微应用都是hash模式,主应用可以根据hash进行微应用的判断
    • history模式的微应用跳转或者微应用跳主应用页面,可以使用history.pushState(),也可以将主应用的路由实例通过props传给微应用,通过这个实例进行跳转

    进阶

    HTML entry

    ​ HTML entry 是通过import-html-empty直接获取子应用html文件,解析html文件的资源,加载入主应用

    // 使用
    import importHTML from 'import-html-entry'
    
    /* 核心功能
      调用fetch请求html资源
      调用processTpl处理资源
      调用getEmbedHtml对processTpl处理后的资源中链接的远程js、css资源去到本地并嵌入html中
    */
    importHTML(url, opts = {}) {
      // 核心伪代码
      const html = fetch(url)
      const {
        template,
        scripts,
        styles,
        entry
      } = processTpl(html, ...rest) //  对象1
      
      return getEmbedHtml().then(() => {
        //  对象2
        return {
          template,
          assetPublicPath,
          getExternalScripts,
          getExternalStyleSheets,
          execScripts
        }
      })
    }
    
    // 获取到的对象1
    {
        template: 经过处理的html,
        scripts: [脚本],
        styles: [样式],
        entry: 入口脚本的地址
    }
    
    // 获取到的对象2
    {
        template // template 是 link 替换为 style 后的 template,
        assetPublicPath // 静态资源地址,
        getExternalScripts: // 获取外部脚本,最终得到所有脚本的代码内容,
        getExternalStyleSheets // 获取外部样式文件的内容,
        execScripts: // 脚本执行器,让 JS 代码(scripts)在指定 上下文 中运行
    }
    
    • getExternalStyleSheets
      将html中link标签转为style标签
    • 循环加载style标签,内联style通过substring的方式获取style样式字符串,外联style,通过fetch get方式获取href地址对应的样式字符串
  • getExternalScripts
    按顺序获取html的script,并用数组进行保存
  • 循环加载script数组,内联script用substring的方式获取代码字符串,外联用fetch get方式获取src地址对应的代码字符串
  • 最后返回一个元素是可执行代码字符串的数组,然后交由execScripts进行运行
  • Garfish

    ​ Garfish是字节跳动推出的一款微前端框架,包含构建微前端系统时所需要的基本能力,任意前端框架均可使用。接入简单,可轻松将多个前端应用组合成内聚的单个产品。

    特性

    • 支持任意多种框架、技术体系接入
    • 支持独立开发、独立测试、独立部署
    • 强大的预加载能力,自动记录用户应用加载习惯增加加载权重,应用切换时间极大缩短
    • 支持依赖共享,极大程度的降低整体的包体积,减少依赖的重复加载
    • 内置数据收集,有效的感知到应用在运行期间的状态

    设计理念

    ​ 设计理念也是采取基座+子应用分治的形势,部署平台负责进行服务发现和服务注册,将注册的应用列表信息发至基座,通过基座来动态控制子应用的渲染和销毁,并提供应用之间的通信和公共依赖管理

    image-20230112115055138
    image-20230112115055138

    核心能力

    加载器(loader)

    ​ Garfish底层使用Garfish.loader来进行资源加载,并且可以用来添加资源加载过程中的回调以及资源的缓存策略。

    • 负责注册平台侧提供的应用列表
    • 负责下载和解析子应用入口资源
      HTML入口类型:拆解html dom、script、style
    • JS入口类型:提供基础dom容器
  • 预加载能力
  • 解析子应用导出内容
  • 缓存机制

    • Garfish通过loadApp加载子应用后保留App的实例
    • 缓存子应用的执行上下文,第二遍执行时不执行所有代码来提升整体的渲染速度
    • 在第二次应用加载时可以启动缓存模式
      在应用第一次渲染时的路径,是先下载html,然后拆分html、渲染dom、渲染style样式、执行js、执行提供的provider中的函数
    • 在第二次渲染时可以将整个渲染流程简化为,还原子应用的 html 内容, 然后 执行 provider 中的渲染函数。因为子应用的真实执行环境并未被销毁,而是通过 render 和 destroy 控制对应应用的渲染和销毁

    沙箱隔离

    ​ 基于 ShadowDom 实现样式隔离, 将容器节点变为 shadowdom,子应用节点操作转发到容器内,动态增加的样式和节点都会放置容器内,查询节点操作转发到容器内,事件向上传播,避免 React 依赖事件委托的库失效。能够提供代码执行能力、手机执行代码时的副作用,以及销毁收集副作用的能力,能够完全支持浏览器主子应用的样式隔离

    路由托管

    ​ 主流的前端框架基本上都是可以通过路由驱动的,开发者只需要配置路由的规则,即可在进入制定路由后载入子应用,这降低了单页应用的复杂度,我们还可以借用单页应用的路由驱动模式,将每个子应用作为组件,并且只托管子应用的根路由,二级及以下路由交由子应用自己负责。Garfish主要提供了一下三条策略:

    • 提供 Router Map,减少典型中台应用下的开发者理解成本
    • 为不同子应用提供不同的 basename 用于隔离应用间的路由抢占问题
    • 路由发生变化时能准确激活并触发应用视图更新

    子应用通信

    建立通信桥梁(channel)

    ​ Garfish.channel为Garfish的实例属性,用于应用间的通信问题。我在实际的使用Garfish过程中,这一功能主要用于路由的监听以及登录信息的监听

    • 路由的监听,就是子应用配置自己的路由,然后当主应用点击进入子应用时,子应用通过channel将自己的路由配置发送给主应用进行页面的跳转。
        // 子应用
        useEffect(() => {
          if (window.Garfish) {
            // moduleRouteConfig为子应用的路由配置
            window?.Garfish.channel.emit("router", moduleRouteConfig); 
          } 
          return () => {
            handleCleanMainInfo();
          };
        }, []);
    
      // 主应用
      useEffect(() => {
        // 获取到子应用传递的路由配置,并且进行一些路由计算逻辑
        window.Garfish.channel.on("router", handleRouterChange)
        return () => {
          window.Garfish.channel.removeListener("router", handleRouterChange)
        }
      })
    
    • 登陆信息的监听,如果子应用是从主应用进入的,那么如果子应用的登陆信息过期,肯定是需要主应用获取该信息,并且提供主应用的登陆功能进行重新登陆,而不是子应用自己的登陆功能。因此就需要channel的通信使得主应用能够拥有收到子应用的登录信息过期的消息的能力,子应用也能拥有向主应用发送登录信息过期的消息的能力。
      // 子应用
          useEffect(() => {
            // mainInfo 为子应用下用户的登录信息
            if (JSON.stringify(mainInfo) !== "{}") {
              window?.Garfish.channel.emit("loginExpired", mainInfo);
            }
          }, [mainInfo]);
      
      
        // 主应用
        useEffect(() => {
          // 接收子应用传递的登录信息,并且进行登录信息过期的逻辑处理
          window.Garfish.channel.on("loginExpired", handleLoginExpired)
          return () => {
            window.Garfish.channel.removeListener("loginExpired", handleLoginExpired)
          }
        }, [])
      
    提供共享机制

    ​ Garfish提供实例Garfish.setExternal用于实现主子应用间的依赖共享。因为前端的一些依赖包(比如lodash)比较大而且内容更新的不频繁,这样如果主子应用都使用了这个相同的依赖包,下载多份便显得不太合适,因此提供了这个依赖共享,可以只用下载一份依赖包,供主子应用的共同使用。而且使用这个功能还有一些注意事项:

    • 主、子应用若开启进行依赖共享能力,需要构建成 ‘umd’ 规范且保证 jsonpFunction 唯一
    • 若子应用通过依赖共享主应用核心包,则子应用将不能独立运行
    • 主应用的若升级版本会可能会对子应用产生影响
    使用
    • 主应用
      主应用webpack配置
      // 主应用 webpack.config.js
      module.exports = {
        output: {
          // 需要配置成 umd 规范
          libraryTarget: 'umd',
          // 请求确保该值与子应用的值不相同避免与子应用发生影响
          jsonpFunction: 'main-app-jsonpFunction'
        },
      };
      
    • 设置external
    // 主应用 index.jsx
    import React from 'react';
    import * as lodash from 'lodash';
    import Garfish from 'garfish';
    
    Garfish.setExternal({
      react: React,
      'lodash': lodash,
    });
    
  • 子应用
    // 子应用 webpack.config.js
    module.exports = {
      output: {
        // 需要配置成 umd 规范
        libraryTarget: 'umd',
        // 修改不规范的代码格式,避免逃逸沙箱
        globalObject: 'window',
        // 请求确保每个子应用该值都不相同,否则可能出现 webpack chunk 互相影响的可能
        jsonpFunction: 'vue-app-jsonpFunction',
        // 保证子应用的资源路径变为绝对路径,避免子应用的相对资源在变为主应用上的相对资源,因为子应用和主应用在同一个文档流,相对路径是相对于主应用而言的
        publicPath: 'http://localhost:8000'
      },
      externals: {
        react: 'react',
        'lodash': 'lodash',
      }
    };
    
  • 热更新

    ​ 模块热更新也叫 HMR,全称是 Hot Module Replacement,指当你在更改并保存代码时,构建工具将会重新进行编译打包,并将新的包模块发送至浏览器端,浏览器用新的包模块替换旧的,从而可以在不刷新浏览器的前提下达到修改的功能

    热更新常用方案
    • 配置webpack的hot.module回调
      // webpack.config.js
      module.exports = {
        devServer: {
          hot: true,
        },
      }
      
      // index.tsx
      import ReactDOM from 'react-dom';
      import App from './components/App';
      
      // 设置 hot.module 回调开启热更新:
      if ((module as any).hot) {
        (module as any).hot.accept(['./components/App'], () => {
          ReactDOM.render(<App />, document.getElementById('root'));
        });
      }
      
    • React-fast-refreshReact官方提供的插件,容错能力更高
      module.exports = {
        entry: './index.js',
        module: {
          rules: [
            {
              test: /\.js$/,
              exclude: /node_modules/,
              loader: 'babel-loader',
              options: {
                presets: ['@babel/env', '@babel/preset-react'],
                plugins: [require.resolve('react-refresh/babel')], // 为 react-refresh 添加
              },
            },
          ],
        },
        plugins: [
          isDevelopment && new ReactRefreshPlugin(), // 为 react-refresh 添加
          new HtmlWebpackPlugin({
            template: './index.html',
          }),
        ],
      };
      
    使用
    // webpack.config.js
    module.exports = {
      output: {
        // 开发环境设置 true 将会导致热更新失效
        clean: process.env.NODE_ENV === 'production' ? true : false,
        filename: '[name].[contenthash].js',
        chunkFilename: '[name].[contenthash].js',
        // 需要配置成 umd 规范
        libraryTarget: 'umd',
        // 修改不规范的代码格式,避免逃逸沙箱
        globalObject: 'window',
        jsonpFunction: 'garfishDemo',
        // 保证子应用的资源路径变为绝对路径
        publicPath: 'http://localhost:8080',
      },
    };
    
    遇到的问题
    子应用热更新未生效
    • 首先检查子应用作为独立应用启动时,是否具备热更新能力
      如果子应用运行没有 HMR 能力,需要排查问题
      若使用了 react-fast-refresh。webpack 的 externals 配置项会导致 react-refresh 失效,可在 dev 环境下可以先关闭配置
    • 若使用了 react-fast-refresh,如果热更新没有生效,请确认版本是否满足要求
  • 如果子应用作为独立应用启动时,具备热更新能力,但微前端模式下未生效,可能原因:若主应用使用了 react-fast-refresh webpack 插件作为热更新配置,请关闭该配置,根组件可使用 webpack hot.module 回调,达到热更新效果
  • 使用Garfish与Qiankun的对比

    ​ 在实际使用Garfish与Qiankun后,遇到了一个比较棘手的问题。Qiankun的子应用互相切换时,由于自己的生命周期以及渲染方法,会导致子应用切换后会出现样式丢失的问题,而Garfish在子应用切换时并没有这个问题,而且页面的渲染和展示也会比Qiankun更加快和流畅。

    使用Qiankun架构的子应用切换

    qiankun
    qiankun

    使用Garfish架构的子应用切换

    garfish
    garfish

    总结

    ​ Qiankun和Garfish这两款微前端架构都各有自己的一些优点和特性,但是在使用的过程中发现,Qiankun的子应用切换导致的样式丢失问题降低了用户体验度,而Garfish的使用以及页面跳转显得更加的流畅。而且Garfish的应用通信相比于Qiankun来说较为简单,而且对于全局变量来说方便管理,防止数据管理的混乱。从这些角度来看,Garfish的使用是更加符合我们项目目前的需求以及应用。

    参考

    https://www.garfishjs.org/ Garfish官网

    https://qiankun.umijs.org/zh Qiankun官网



沪ICP备19023445号-2号
友情链接