中台架构之微前端


最近公司搭建中台项目,记录的一点心得。在中台建设完成后,通过微服务实现了后端应用的解耦,提高了中台应用的弹性伸缩能力。但由于微服务拆分,也会导致项目团队和服务的碎片化,给前端项目集成带来一定的复杂度。如何降低前端集成的复杂度?做到后端解耦,前端聚合?这是一个很有意思的话题。

一般的思路而言会开发一整套前端,通过路由分发请求到不同的后端接口,这样前端开发需要对接很多个不同系统的后端接口,那能不能前端也用微服务的思路,把要对接系统的前端页面嵌入到我的页面中来,这样即使对接很多系统,也只需把这些系统的前端页面动态的嵌入到我的页面中,就像拼图一样,对接后端接口的过程也可以省略掉。确实有这样的解决方案,它就是微前端

微前端概述

微前端概念是 ThoughtWorks 在 2016 年提出来的,它将微服务理念扩展到前端开发,解决中台微服务化后,前端由于仍为单体而存在的逻辑繁杂和臃肿的问题。微前端是按照前端设计方法在前端同时构建多个可以独立部署、完全自治、松耦合的页面组合,其中每个组合只负责特定的 UI 元素和功能。一次开发,多端复用”,这也与中台服务共享理念是一脉相承。

从单体前端到微前端

传统的软件项目往往采用单体式架构,前端往往由一个团队创建并维护一个 Web 应用程序,通过 API 网关从微服务调用服务。随着时间的推移,前端往往会越来越臃肿,越来越难维护。

为了提高用户体验,实现统一运营,很多企业开始缩减 APP 的数量,通过一个 APP 集成所有应用功能。试想如果将企业内所有前端页面、流程设计以及前端与后端集成的工作都交给前端项目,将原来独立和分散的应用,展示在一个巴掌大的手机屏幕上。前端项目将会面对无数的中台项目和成千上万不太熟悉的 API 接口,这绝对会是一场灾难。

从单体前端到微前端如何实现前端复用,降低前端集成的复杂度?在前端设计时,我们可以参考微服务设计方法,遵循单一职责和共享原则,按照领域模型和微服务边界,将前端页面进行拆分和组合形成微前端,与中台微服务组合成业务单元。业务单元定义:在前后端分离架构模式下,微前端页面与中台微服务共同组成一个业务单元。在同一个业务单元内,从前端页面、中台微服务到后端数据可以独立开发、测试、部署和运维,在业务单元内自包含完成中台领域内的部分或全部业务功能。项目职责和系统边界从前端项目主要负责前端页面的集成、页面风格设计和流程控制,以及与微前端集成相关的微前端加载、微前端注册、页面路由以及数据共享。通用和专属中台项目既面向第三方生态圈提供 API 服务,也面向前端集成主页面提供微前端页面复用。微前端和中台微服务组合成业务单元为多渠道业务提供从前端到后台的页面和业务逻辑复用。在面向多渠道业务页面复用时,微前端需要做好页面风格适配,以满足不同渠道界面风格的要求。通过职责分工和应用边界的清晰划分,前端项目专注于微前端集成,后端项目专职做好本业务领域内中台微服务开发和微前端集成,确保领域内前端页面和后端业务逻辑作为一个业务单元整体高可用。由于微前端和微服务之间的 API 集成已由中台项目完成,前端项目可基于微前端实现拼图式开发,在实现前后端复用的同时,大大降低前端集成复杂度。

什么是微前端?

  1. 复用别的的项目页面:如果我们的项目需要开发某个新的功能,而这个功能另一个项目已经开发好,我们想直接复用时。注意:我们需要的只是别人项目的这个功能页面的「内容部分」,不需要别人项目的顶部导航和菜单。一个比较笨的办法就是直接把别人项目这个页面的代码拷贝过来,但是万一别人不是 vue 开发的,或者说 vue 版本、UI 库等不同,以及别人的页面加载之前操作(路由拦截,鉴权等)我们都需要拷贝过来,更重要的问题是,别人代码有更新,我们如何做到同步更新。长远来看,代码拷贝不太可行,问题的根本就是,我们需要做到让他们的代码运行在他们自己的环境之上,而我们对他们的页面仅仅是“引用”。这个环境包括各种插件( vuevuexvue-router 等),也包括加载前的逻辑(读 cookie,鉴权,路由拦截等)。私有 npm 可以共享组件,但是依然存在技术栈不同/UI库不同等问题。
  2. 巨无霸项目的自由拆分组合
    • 代码越来越多,打包越来越慢,部署升级麻烦,一些插件的升级和公共组件的修改需要考虑的更多,很容易牵一发而动全身
    • 项目太大,参与人员越多,代码规范比较难管理,代码冲突也频繁。
    • 产品功能齐全,但是客户往往只需要其中的部分功能。剥离不需要的代码后,需要独立制定版本,独立维护,增加人力成本。

微前端的价值和意义

现在越来越多的公司都在进行微前端的落地和应用,微前端主要技术方向有 Mooa、Single-SPA、Web Components、Vue 等开源前端框架,在此不做赘述。微前端是前端建设的一个非常重要的方向和关注点,通过微前端的集成模式可以减轻系统开发的复杂度,降低前端集成的难度。它的主要价值和意义如下:前端集成简单:前端项目只需关注前端集成主页面与微前端的集成,不涉及到 API 集成和中台技术实现细节,可真正实现模块化集成,实现拼图式的开发,降低前端集成的复杂度和成本。2、项目职责专一:中台项目从数据库、中台到微前端界面端到端地完成领域逻辑功能开发,确保领域业务单元内从前端到后端可用。由于团队职责专一,项目成员都熟悉团队内的业务和技术,从而降低开发过程因为沟通和集成出问题的风险。

微前端的特点

微前端的诞生也是为了解决以上两个问题,在微前端框架下,一个页面中的不同模块可以使用不同的技术栈,并且每个模块可以动态加载

  1. 复用(嵌入)别人的项目页面,但是别人的项目运行在他自己的环境之上。
  2. 巨无霸应用拆分成一个个的小项目,这些小项目独立开发部署,又可以自由组合进行售卖。
  3. 技术栈无关,各个子项目可以自由选择框架,可以自己制定开发规范。
  4. 快速打包,独立部署,互不影响,升级简单。
  5. 可以很方便的复用已有的功能模块,避免重复开发。

生命周期

前端微架构与后端微架构的最大不同之处,也在于此——生命周期。微前端应用作为一个客户端应用,每个应用都拥有自己的生命周期:

Load:决定加载哪个应用,并绑定生命周期
bootstrap:获取静态资源
Mount:安装应用,如创建 DOM 节点
Unload:删除应用的生命周期
Unmount:卸载应用,如删除 DOM 节点、取消事件绑定

这部分的内容事实上,也就是微前端的一个难点所在,如何以合适的方式来加载应用——毕竟每个前端框架都各自不同,其所需要的加载方式也是不同的。当我们决定支持多个框架的时候,便需要在这一部分进入更细致的研究。

技术方式

路由分发式:通过 HTTP 服务器的反向代理功能,来将请求路由到对应的应用上。
前端微服务化:在不同的框架之上设计通讯、加载机制,以在一个页面内加载对应的应用。
微应用:通过软件工程的方式,在部署构建环境中,组合多个独立应用成一个单体应用。
微件化:开发一个新的构建系统,将部分业务功能构建成一个独立的 chunk 代码,使用时只需要远程加载即可。
前端容器化:通过将 iFrame 作为容器,来容纳其它前端应用。
应用组件化:借助于 Web Components 技术,来构建跨框架的前端应用。

集成方式

主框架与子应用集成的方式,成为一个需要重点关注的技术决策。微前端架构模式下,子应用打包的方式,基本分为两种:构建时组合 VS 运行时组合

  • 构建时:子应用通过Package Regiter,也可以是npm package,也可以是git tags等其他发布方式,与主应用一起打包发布
  • 运行时:各子应用自己打包,主应用运行时动态加载子应用资源

两者的优缺点也很明显:

方式 优点 缺点
构建时 主应用,与子应用可打包优化,如共享依赖等 主应用与子应用工具链耦合,工具链也是技术栈的一部分。子应用每次发版本,主应用也需要重新打包
运行时 主应用,子应用之间完全解耦,子应用完全技术栈无关 会多出一些运行时复杂度和overhead

在确定了运行时载入的方案后,另一个需要决策的点是,我们需要子应用提供什么形式的资源作为渲染入口?

  • JS Entry 的方式通常是子应用将资源打成一个entry script,比如 single-spa 的 example 中的方式。但这个方案的限制也颇多,如要求子应用的所有资源打包到一个 js bundle 里,包括 css、图片等资源。除了打出来的包可能体积庞大之外的问题之外,资源的并行加载等特性也无法利用上。
  • HTML Entry 则更加灵活,直接将子应用打出来 HTML作为入口,主框架可以通过 fetch html 的方式获取子应用的静态资源,同时将 HTML document 作为子节点塞到主框架的容器中。这样不仅可以极大的减少主应用的接入成本,子应用的开发方式及打包方式基本上也不需要调整,而且可以天然的解决子应用之间样式隔离的问题

*1.JS Entry *:即在开发时,应用都是以单一、微小应用的形式存在,而在运行时,则通过构建系统合并这些应用,组合成一个新的应用。微应用化更多的是以软件工程的方式,来完成前端应用的开发,因此又可以称之为组合式集成。对于一个大型的前端应用来说,采用的架构方式,往往会是通过业务作为主目录,而后在业务目录中放置相关的组件,同时拥有一些通用的共享模板。

2.HTML Entry :指的是一段可以直接嵌入在应用上运行的代码,它由开发人员预先编译好,在加载时不需要再做任何修改或者编译。而微前端下的微件化则指的是,每个业务团队编写自己的业务代码,并将编译好的代码部署(上传或者放置)到指定的服务器上,在运行时,我们只需要加载相应的业务模块即可。对应的,在更新代码的时候,我们只需要更新对应的模块即可。

解决方案

目前微前端主要有两种解决方案:iframe 方案和 single-spa 方案。比较成熟的框架是蚂蚁金服开源的qiankun。它是基于single-spa

iframe方案

iframe 大家都很熟悉,使用简单方便,提供天然的 js/css 隔离,也带来了数据传输的不便,一些数据无法共享(主要是本地存储、全局变量和公共插件),两个项目不同源(跨域)情况下数据传输需要依赖 postMessageiframe 有很多坑,但是大多都有解决的办法:

  • 页面加载问题:iframe 和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载,阻塞 onload 事件。每次点击都需要重新加载,虽然可以采用 display:none 来做缓存,但是页面缓存过多会导致电脑卡顿。「(无法解决)」

  • 布局问题:iframe 必须给一个指定的高度,否则会塌陷。解决办法:子项目实时计算高度并通过 postMessage 发送给主页面,主页面动态设置 iframe 高度。有些情况会出现多个滚动条,用户体验不佳。

  • 弹窗及遮罩层问题:弹窗只能在 iframe 范围内垂直水平居中,没法在整个页面垂直水平居中。

    解决办法1:通过与框架页面消息同步解决,将弹窗消息发送给主页面,主页面来弹窗,有侵入性

    解决办法2:修改弹窗的样式:隐藏遮罩层,修改弹窗的位置。

  • iframe 内的 div 无法全屏:弹窗的全屏,指的是在浏览器可视区全屏。这个全屏指的是占满用户屏幕。

  • 浏览器前进/后退问题:iframe 和主页面共用一个浏览历史,iframe 会影响页面的前进后退。大部分时候正常,iframe 多次重定向则会导致浏览器的前进后退功能无法正常使用。并且 iframe 页面刷新会重置(比如说从列表页跳转到详情页,然后刷新,会返回到列表页),因为浏览器的地址栏没有变化,iframesrc 也没有变化。

  • iframe 加载失败的情况不好处理:非同源的 iframe 在火狐及 chorme 都不支持 onerror 事件。

    解决办法1:onload 事件里面判断页面的标题,是否 404 或者 500

    解决办法2:使用 try catch 解决此问题,尝试获取 contentDocument 时将抛出异常。

single-spa 微前端方案

spa 单页应用时代,我们的页面只有 index.html 这一个 html 文件,并且这个文件里面只有一个内容标签 <div id="app"></div>,用来充当其他内容的容器,而其他的内容都是通过 js 生成的。也就是说,我们只要拿到了子项目的容器 <div id="app"></div> 和生成内容的 js,插入到主项目,就可以呈现出子项目的内容。

<link href=/css/app.c8c4d97c.css rel=stylesheet>
<div id=app></div>
<script src=/js/chunk-vendors.164d8230.js> </script>
<script src=/js/app.6a6f1dda.js> </script> 

我们只需要拿到子项目的上面四个标签,插入到主项目的 HTML 中,就可以在父项目中展现出子项目。这里有个问题,由于子项目的内容标签是动态生成的,其中的 img/video/audio 等资源文件和按需加载的路由页面 js/css 都是相对路径,在子项目的 index.html 里面,可以正确请求,而在主项目的 index.html 里面,则不能。

举个例子,假设我们主项目的网址是 www.baidu.com ,子项目的网址是 www.taobao.com ,在子项目的 index.html 里面有一张图片 <img src="./logo.jpg">,那么这张图片的完整地址是 www.taobao.com/logo.jpg,现在将这个图片的 img 标签生成到了父项目的 index.html,那么图片请求的地址是 www.baidu.com/logo.jpg,很显然,父项目服务器上并没有这张图。解决思路:

  1. 这里面的 js/css/img/video 等都是相对路径,能否通过 webpack 打包,将这些路径全部打包成绝对路径?这样就可以解决文件请求失败的问题。
  2. 能否手动(或借助 node )将子项目的文件全部拷贝到主项目服务器上,node 监听子项目文件有更新,就自动拷贝过来,并且按 js/css/img 文件夹合并
  3. 能否像 CDN 一样,一个服务器挂了,会去其他服务器上请求对应文件。或者说服务器之间的文件共享,主项目上的文件请求失败会自动去子服务器上找到并返回。通常做法是动态修改 webpack 打包的 publicPath,然后就可以自动注入前缀给这些资源。single-spa 是一个微前端框架,基本原理如上,在上述呈现子项目的基础上,还新增了 bootstrapmountunmount 等生命周期。

single-spa 社区公认的主流方案,可以基于它做二次开发,single-spa 上手并不简单,也不能开箱即用,开发部署更是需要修改大量的 webpack 配置,对子项目的改造也非常多。更多是基于 single-spa 做业务定制封装,是比较好的策略。

qiankun 方案

qiankun 是蚂蚁金服开源的一款框架,它是基于 single-spa 的。他在 single-spa 的基础上,实现了开箱即用,除一些必要的修改外,子项目只需要做很少的改动,就能很容易的接入。如果说 single-spa 是自行车的话,qiankun 就是个汽车。微前端中子项目的入口文件常见的有两种方式:JS entryHTML entryqiankun官方文档快速上手 。下面是一个简单的配置例子

主应用

主应用 js 文件引入 qiankun 注册子应用,并编写导航页显示跳转逻辑。

<!DOCTYPE html>
<html lang="zh">

<head>
  <meta charset="UTF-8">
  <title>QianKun Example</title>
</head>

<body>
  <div class="mainapp">
    <!-- 标题栏 -->
    <header class="mainapp-header">
      <h1>导航</h1>
    </header>
    <div class="mainapp-main">
      <!-- 侧边栏 -->
      <ul class="mainapp-sidemenu">
        <li class="app1">应用一</li>
        <li class="app2">应用二</li>
      </ul>
      <!-- 子应用  -->
      <main id="subapp-container"></main>
    </div>
  </div>

  <script src="./index.js"></script>
</body>

</html>

主应用 js 入口文件:

import { registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start, initGlobalState } from 'qiankun';
import './index.less';

/**
 * 主应用 **可以使用任意技术栈**
 * 以下分别是 React 和 Vue 的示例,可切换尝试
 */
import render from './render/ReactRender';
// import render from './render/VueRender';

/**
 * Step1 初始化应用(可选)
 */
render({ loading: true });

const loader = loading => render({ loading });

/**
 * Step2 注册子应用
 */
registerMicroApps(
  [
    {
      name: 'app1',
      entry: process.env.NODE_ENV === 'production' ? '//192.168.2.192:7100' : '//localhost:7100',
      container: '#subapp-viewport',
      loader,
      activeRule: '/app1',
    },
    {
      name: 'app2',
      entry: process.env.NODE_ENV === 'production' ? '//192.168.2.192:7101' : '//localhost:7101',
      container: '#subapp-viewport',
      loader,
      activeRule: '/app2',
    }
  ],
  {
    beforeLoad: [
      app => {
        console.log('[LifeCycle] before load %c%s', 'color: green;', app.name);
      },
    ],
    beforeMount: [
      app => {
        console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
      },
    ],
    afterUnmount: [
      app => {
        console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
      },
    ],
  },
);

const { onGlobalStateChange, setGlobalState } = initGlobalState({
  user: 'qiankun',
});

onGlobalStateChange((value, prev) => console.log('[onGlobalStateChange - master]:', value, prev));

setGlobalState({
  ignore: 'master',
  user: {
    name: 'master',
  },
});

/**
 * Step3 设置默认进入的子应用
 */
// setDefaultMountApp('/app1');

/**
 * Step4 启动应用
 */
start();

runAfterFirstMounted(() => {
  console.log('----------------------------------')
  console.log(process.env.NODE_ENV)
  console.log('----------------------------------')
  console.log('[MainApp] first app mounted');
});

//浏览器地址入栈
function push(subapp) { history.pushState(null, subapp, subapp) }

//配合导航页显示逻辑
function initPortal(){
  //主应用跳转
  document.querySelector('.app1').onclick = () => {
    document.querySelector('.mainapp-sidemenu').style.visibility = 'hidden'
    push('/app1')
  }
  document.querySelector('.app2').onclick = () => {
    document.querySelector('.mainapp-sidemenu').style.visibility = 'hidden'
    push('/app2')
  }

  //回到导航页
  document.querySelector('.mainapp-header h1').onclick = () => {
    push('/')
  }

  if(location.pathname !== '/'){
    document.querySelector('.mainapp-sidemenu').style.visibility = 'hidden'
  }else{
    document.querySelector('.mainapp-sidemenu').style.visibility = 'visible'
  }
  if(location.pathname.indexOf('login') > -1){
    document.querySelector('.mainapp-header').style.display = 'block'
  }else{
    document.querySelector('.mainapp-header').style.display = 'none'
  }

  //监听浏览器前进回退
  window.addEventListener('popstate', () => { 
    if(location.pathname === '/'){
      document.querySelector('.mainapp-sidemenu').style.visibility = 'visible'
    }
    if(location.pathname.indexOf('login') > -1){
      document.querySelector('.mainapp-header').style.display = 'block'
    }else{
      document.querySelector('.mainapp-header').style.display = 'none'
    }
  }, false)
}

initPortal()

docker nginx 配置

此处 nginx 主要作用是用于端口目录转发,并配置主应用访问子应用的跨域问题。

# nginx.conf

http {
    server {
      listen    8889;
      server_name 192.168.2.192;

      location /app1 {
        proxy_pass  127.0.0.1:7100

        try_files $uri $uri/ /index.html;
      }
    }

    server {
      listen    8889;
      server_name 192.168.2.192;

      location /app2 {
        proxy_pass  127.0.0.1:7101

        try_files $uri $uri/ /index.html;
      }
    }
}

子应用适配框架

下面子应用以常规 vue 项目为例。

入口文件 main.js

在入口文件增加 qiankun 环境判断,判断当前是 qiankuan 环境的则将子应用引入到主应用框架内,然后在主框架内执行正常的 vue 元素挂载。

// 在所有代码的文件之前引入判断
if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

import Vue from "vue";
import App from "./App";
import router from "./router";

let instance = null;

function render(props = {}) {
  // 此处 container 是主应用生成的用于装载子应用的 div 元素
  // 如 <div id="__qiankun_microapp_wrapper_for_app_1_1596504716562__" />
  const { container } = props;

  instance = new Vue({
    router,
    render: h => h(App),
  }).$mount(container ? container.querySelector('#app') : '#app');
}

if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

function storeTest(props) {
  props.onGlobalStateChange &&
    props.onGlobalStateChange(
      (value, prev) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev),
      true,
    );
  props.setGlobalState &&
    props.setGlobalState({
      ignore: props.name,
      user: {
        name: props.name,
      },
    });
}

export async function bootstrap() {
  console.log('[vue] vue app bootstraped');
}

export async function mount(props) {
  console.log('[vue] props from main framework', props);
  storeTest(props);
  render(props);
}

export async function unmount() {
  instance.$destroy();
  instance.$el.innerHTML = '';
  instance = null;
}

router 配置

路由需要根据 qiankun 环境配置 base 路径,以及设置路由的 history 模式。

// router/index.js
const router = new Router({
  // 此处 /app1 是子应用在主应用注册的 activeRule
  base: window.__POWERED_BY_QIANKUN__ ? '/app1' : '/',
  mode: 'history',
  routes: [
    {
        ……
        ……
    }
  ]
})

// portal/index.js
registerMicroApps(
  [
    {
      name: 'app1',
      entry: process.env.NODE_ENV === 'production' ? '//192.168.2.192:7100' : '//localhost:7100',
      container: '#subapp-viewport',
      loader,
      activeRule: '/app1',
    }
  ]
)

字体图标与 css 背景图片路径问题

默认情况下,在 css 引用的资源使用 url-loader 加载打包出来是相对路径的,所以会出现子应用的资源拼接到主应用的 domain 的情况,造成加载资源失败。因为 element-ui 的字体图标是在 css 里面引入的,还有相关背景图片的引入也是在 css 里,所以需要配置 webpackurl-loader,生产模式情况下直接指定资源前缀。使之成为绝对路径。

module: {
  rules: [
    {
      test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
      loader: "url-loader",
      options: {
        limit: 10000,
        name: utils.assetsPath("img/[name].[hash:7].[ext]"),
        //这里 192.168.2.192:7100 是子应用部署地址
        publicPath: process.env.NODE_ENV === 'production' ? '//192.168.2.192:7100' : ''
      }
    },
    {
      test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
      loader: "url-loader",
      options: {
        limit: 10000,
        name: utils.assetsPath("fonts/[name].[hash:7].[ext]"),
        publicPath: process.env.NODE_ENV === 'production' ? '//192.168.2.192:7100' : ''
      }
    }
  ]
}

这样配置后,打包出来的 css 样式文件会变成:

/* static/css/app.e99e9aae.css */

background-header {
    background: url(//192.168.2.192:7100/static/img/bg_header.790a94f4.png);
}

资源是绝对路径,就不会出现上面子应用资源加载失败的情况。但是,这种前端配置文件写死路径的做法灵活性并不好,比如不能做到编译一次,任意部署,因为路径写死,所以如果需要部署到其他服务器的话,就需要重新编译了。接下来,讲的是实现灵活部署的方案。

结合 nginx 配置资源引用路径代理转发

我们在只编译打包一次前端应用的前提下,为了实现灵活部署,需要借助 nginx 来实现。

module.exports = {
  chainWebpack: (config) => 

    config.module
      .rule("fonts")
      .test(/\.(woff2?|eot|ttf|otf)(\?.*)?$/)
      .use("url-loader")
      .loader("url-loader")
      .options({
        limit: 4096,
        name: 'static/fonts/[name].[hash:8].[ext]',
        publicPath: process.env.NODE_ENV === 'production' ? '/live' : '',
      });

    config.module
      .rule("images")
      .test(/\.(png|jpe?g|gif|webp)(\?.*)?$/)
      .use("url-loader")
      .loader("url-loader")
      .options({
        limit: 4096,
        // name 代表 url-loader 会将资源写到 static/fonts/ 下
        name: 'static/img/[name].[hash:8].[ext]',
        // publicPath 代表资源引入 url 会生成 /live 的前缀,比如 /live/static/img/bg_header.790a94f4.png
        publicPath: process.env.NODE_ENV === 'production' ? '/live' : '',
      });

  }
}

假设主应用部署地址是 192.168.2.192,子应用的部署地址是 192.168.2.193:7102。打包编译部署后,子应用的 css 文件里面的图片资源引用 url 如下:

/* static/css/app.e99e9aae.css */

background-header {
    background: url(/live/static/img/bg_header.790a94f4.png);
}

主应用加载子应用的时候,子应用的资源拼接主应用 domian 后,子应用的 css 文件里面的图片资源加载路径 url 就会变成:

192.168.2.192/live/static/img/bg_header.790a94f4.png

此时的关键就是要访问到子应用的资源,而不是去主应用资源目录去找。所以我们采用 nginx 路径代理转发端口的方案,当应用访问 192.168.2.192/live 这个路径时,经过 nginx 进行路径代理转发配置,转发到子应用端口。

配置 nginx 代理规则:

# nginx.conf

http {
  server {
    listen    80;
    server_name 192.168.2.192;

    location /live {
      proxy_pass  192.168.2.192:7102
      try_files $uri $uri/ /index.html;
    }
  }
}

此时真正访问的资源路径(子应用资源路径)是:

192.168.2.193:7102/static/img/bg_header.790a94f4.png

icestark 方案

类似于 single-spa 实现,React 技术栈友好,阿里的另一个轮子。icestark官方文档地址,原理上应该和qiankun大同小异,在使用上API不同会有些区别,文档比较详细就不多说了


Author: 顺坚
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source 顺坚 !
评论
 Previous
中台架构与微服务战略 中台架构与微服务战略
传统企业平台都是烟囱式的系统架构,企业内部为了迎合业务发展不停的打造各种系统,导致各系统间的重复功能建设和维护带来的重复投资。重复投资不仅消耗的是人力,财力还有时间。但打通烟囱式系统间交互的集成和协作成本高昂,各大企业不得不借助ESB产品,
2022-03-06
Next 
失血模型与充血模型 失血模型与充血模型
领域模型是对领域内的概念类或现实世界中对象的可视化表示。又称概念模型、领域对象模型、分析对象模型。它专注于分析问题领域本身,发掘重要的业务领域概念,并建立业务领域概念之间的关系。 业务对象模型(也叫领域模型domain model)是描述业
2022-02-20
  TOC