前端微服务QianKun实现

1.发现问题

  1. 在生产大型前端项目时候,随着需求、模块的不断增加。项目的打包维护效率越来越低。

  2. 项目中部分稳定的模块,在项目迭代中不希望打包这部分模块。

  3. 不同团队间开发同一个应用技术栈不同。

  4. 开发过程中,因为一些需求要升级技术栈,但是因为项目太大,升级相对复杂,容易出问题。

  5. 现行iframe解决方案弊端:

    1. 刷新问题

      iframe 页面没有自己的历史记录,使用的是基座(父页面)的浏览历史。 所以,当iframe 页在内部进行跳转时,浏览器地址栏无变化,基座中加载的 src 资源也无变化,当浏览器刷新时,无法停留在iframe内部跳转后的页面上,需要用户重新走一遍操作,用户产生痛点。

    2. 遮罩问题

      UI 不同步,DOM 结构不共享,iframe 页产生的带遮罩的弹窗,只能遮罩 iframe 区域。

    3. 通信问题

      全局上下文完全隔离,内存变量不共享。与基座非同源下,iframe 无法直接获取基座 url 的参数。

    4. 加载问题

      影响主页面加载,阻塞onload事件,本身加载也很慢,页面缓存过多会导致电脑卡顿。

    类似问题还有很多,从业务价值上看,损失了用户体验;从开发角度,有必要通过技术来提升业务体验的同时,提高产品后期更新迭代维护成本。

2.关于微前端qiankun

微前端:将不同的功能按照不同的维度拆分成多个子应用。通过主应用来加载这些子应用。这些子应用可以独立打包,独立部署,可以分别使用不同技术栈如Vue、React。当路径切换时加载不同的子应用。

Single-SPA:前端微服务化的 JavaScript 前端解决方案,实现了路由劫持和应用加载(即根据不同的路由加载不同的应用)。css和js需要进行隔离。否则容易造成全局污染。

qiankun :基于Single-SPA,,解决了Single-SPA需要自行处理Css样式隔离,Js执行隔离问题。提供多个API,接入简单。基座不受限子应用的技术栈,子应用独立部署维护,接入时基座同步更新;又可独立运行

3. qiankun API 说明

registerMicroApps(apps, lifeCycles?)

  • 参数

    • apps - Array<RegistrableApp> - 必选,微应用的一些注册信息
    • lifeCycles - LifeCycles - 可选,全局的微应用生命周期钩子
  • 类型

    • RegistrableApp

      name - string - 必选,微应用的名称,微应用之间必须确保唯一。

      entry - string | html - 必选,微应用的入口。

      • 配置为字符串时,表示微应用的访问地址,例如 https://qiankun.umijs.org/guide/
      • 配置为对象时,html 的值是微应用的 html 内容字符串如 '<span>子应用<span>',而不是微应用的访问地址。微应用的 publicPath 将会被设置为 /

      container - string | HTMLElement - 必选,微应用的容器节点的选择器或者 Element 实例。如container: '#root'container: document.querySelector('#root')

      activeRule - string |function (location)-必选,浏览器 url 发生变化会调用 activeRule 里的规则,当配置为字符串时会直接跟 url 中的路径部分做前缀匹配,匹配成功表明当前应用会被激活
      为函数时,函数会传入当前location作为参数,包含当前的请求路径等信息,根据函数返回值判断是否激活,如location.pathname.startsWith('/app1')。

    • LifeCycles

      • beforeLoad - Lifecycle | Array<Lifecycle> - 可选
      • beforeMount - Lifecycle | Array<Lifecycle> - 可选
      • afterMount - Lifecycle | Array<Lifecycle> - 可选
      • beforeUnmount - Lifecycle | Array<Lifecycle> - 可选
      • afterUnmount - Lifecycle | Array<Lifecycle> - 可选

start

参数

  • opts - Options 可选

类型

  • Options

    • prefetch - boolean | 'all' | string[] | function - 可选,是否开启预加载,默认为 true

      配置为 true 则会在第一个微应用 mount 完成后开始预加载其他微应用的静态资源

      配置为 'all' 则主应用 start 后即开始预加载所有微应用静态资源

      配置为 string[] 则会在第一个微应用 mounted 后开始加载数组内的微应用资源

      配置为 function 则可完全自定义应用的资源加载时机 (首屏应用及次屏应用)

    • sandbox - boolean | { strictStyleIsolation?: boolean, experimentalStyleIsolation?: boolean } - 可选,是否开启沙箱,默认为 `true。

      默认情况下沙箱可以确保单实例场景子应用之间的样式隔离,但是无法确保主应用跟子应用、或者多实例场景的子应用样式隔离。当配置为 { strictStyleIsolation: true } 时表示开启严格的样式隔离模式。这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响。

      当 配置为 { strictStyleIsolation: true } 时,qiankun 会改写子应用所添加的样式为所有样式规则增加一个特殊的选择器规则来限定其影响范围

    • singular - boolean | ((app: RegistrableApp<any>) => Promise<boolean>); - 可选,是否为单实例场景,单实例指的是同一时间只会渲染一个微应用。默认为 true

setDefaultMountApp(appLink)

  • 参数

    • appLink - string - 必选
  • 用法

    设置主应用启动后默认进入的微应用。

  • 示例

    import { setDefaultMountApp } from 'qiankun';
    setDefaultMountApp('/vueApp');
    

4.qiankun案例实现

首先我们创建两个应用;一个子应用,一个父应用;(我们需要父应用加载子应用)

1.主应用安装qiankun

npm i qiankun -S

2.主应用页面中通过router路由把子页面挂载到 id="vue" 的标签上

  <div>
    <el-menu router mode="horizontal" >
      <!--基座中可以放自己的路由-->
      <el-menu-item index="/">基座</el-menu-item>
      <!--引用其他子应用-->
      <el-menu-item index="/vue">vue应用01</el-menu-item>
      <el-menu-item index="/react">vue应用02</el-menu-item>
    </el-menu>
    <div><router-view></router-view></div>
    <div id="vue"></div>
    <div id="react"></div>
  </div>

3.主应用入口js文件中注册子应用、配置路由指向

import { registerMicroApps, start } from 'qiankun'
import actions from './actions'

const apps=[
   {
    name: 'vueApp', // 应用的名字
    entry: 'http://localhost:10002/', // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
    container: '#vue', // 容器名
    activeRule: '/vue', // 触发的路径
    props: { //传递参数数据,
      getGlobalState:actions.getGlobalState, //下发获取初始全局数据方法
      username:"吴亦凡"
    }, 
  }
  {
    name: 'reactApp',
    entry: '//localhost:10003', 
    container: '#react',
    activeRule: '/react',
  }
]

registerMicroApps(apps); //注册子应用
start({
  prefetch: false // 可选,关闭预加载
  singular: true, // 可选,是否为单实例场景,单实例指的是同一时间只会渲染一个微应用。默认为 true。
});

4.子应用配置(不需要额外安装任何其他依赖即可接入 qiankun 主应用)

在入口js中导出 bootstrapmountunmount 三个生命周期钩子,以供主应用在适当的时机调用。

import actions from './actions'

let instance = null
function render(props) {
  instance = new Vue({
    router,
    render: h => h(App)
  }).$mount('#app') // 这里是挂载到自己的html中  基座会拿到这个挂载后的html 将其插入进去
}

if (window.__POWERED_BY_QIANKUN__) { // (判断是自启动还是父应用调用)动态添加publicPath,主要解决的是微应用动态载入的 脚本、样式、图片 等地址不正确的问题
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
} else  render()  // 默认独立运行

/**
 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
 */
export async function bootstrap(props) {
}

/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props) {
  actions.setActions(props) //子项目的入口文件中设置子应用的全局state
  render(props)
}

/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount(props) {
  instance.$destroy() //页面卸载
}

在webpack打包增加如下配置使主应用能正确识别微应用暴露出来的一些信息

module.exports = {
    devServer: {
      port: 10002,
      headers: {
        'Access-Control-Allow-Origin': '*'   //子应用开启跨域
      }
    },
    configureWebpack: {
      output: {
        library: 'vueApp',  //库名,与主应用注册的微应用name保持一致
        libraryTarget: 'umd'  //UMD,一种javascript打包模式,让你打包好的代码模块能被主应用访问
      }
    }
  }

运行展示

案例.png

上图中 在子应用在进入时调用mount方法,同时通过props将数据传给子应用。

由上图可以看出子应用1被挂载主应用中id=“vue”的标签中

5.常见问题

1.样式隔离问题

qiankun 将会自动隔离子应用之间的样式(开启沙箱的情况下),沙箱模式下 qiankun 会为每个子应用的容器包裹上一个 shadow dom 节点,从而确保子应用的样式不会对全局造成影响。

主应用子应用之间样式隔离可以给主应用所有样式添加一个前缀。

start({ 
   sandbox :{strictStyleIsolation: true} //沙箱开启 默认为true
})

2.Js隔离问题

qiankun框架通过沙箱(可以理解为一个黑盒,用于隔离当前执行的环境作用域和外部的其他作用域)实现js隔离(无需手动配置)

  • legacySandBox //基于 Proxy API 来实现

  • proxySandBox //基于 Proxy API 来实现

  • snapshotSandBox。 //不支持 Proxy API 的低版本浏览器中,会降级为 snapshotSandBox

legacySandBox

本质上还是操作 window 对象,存在三个应用池,分别用于子应用卸载时还原主应用的状态和子应用加载时还原子应用的状态。

通过激活沙箱时还原子应用的状态,卸载时还原主应用的状态来实现沙箱隔离。

proxySandBox

用于多实例场景,不会直接操作 window 对象。并且为了避免子应用操作或者修改主应用上诸如 window、document、location 这些重要的属性,会遍历这些属性到子应用 window 副本(fakeWindow)上。所以在激活和卸载的时候也不需要操作状态池更新 / 还原主子应用的状态了。相比较看来,proxySandBox 是现阶段 qiankun 中最完备的沙箱模式,完全隔离了主子应用的状态,不会像 legacySandBox 模式下在运行时期间仍然会污染 window。

snapshotSandBox

在不支持 Proxy 的场景下会降级为 snapshotSandBox。

原理即子应用激活 / 卸载时分别去通过快照的形式记录/还原状态来实现沙箱的。

3.路由跳转问题

子项目跳转到另一个子项目不能直接使用<router-link>或者router.push/router.replace,因为子项目的路由跳转只会机遇子项目的base。<a> 链接可以跳转但是会刷新。

解决方案:主项目的路由实例对象挂载到全局状态传过去,使用父项目的这个 router 跳转

 this.initialState.router.push("/"); //父应用
 //  this.initialState.router.push('/vue/about')  //跳转子应用

4.通信问题

  1. 主应用与微应用的通信
  2. 微应用之间的通信

通信方式:

1.qiankun 官方提供的通信方式 - Actions 通信(详情见全局状态通信)。

优点:
  • 使用简单;

  • 官方支持性高;

  • 适合通信较少的业务场景;

    缺点:

  • 子应用独立运行时,需要额外配置无 Actions 时的逻辑;

  • 子应用需要先了解状态池的细节,再进行通信;

  • 由于状态池无法跟踪,通信场景较多时,容易出现状态混乱、维护困难等问题;

2.使用Vuex进行状态管理

优点:

  • 子应用无法随意污染主应用的状态池,只能通过主应用暴露的 shared 实例的特定方法操作状态池,从而避免状态池污染产生的问题。
  • 子应用将具备独立运行的能力

具体实现

1.微应用原本就没有使用Vuex进行状态管理

主应用向微应用传递store实例

   props:{store}, //共享主应用的store实例

2.微应用本身就有自己store实例

此时要考虑父子应用store分离

需要在入口文件中添加如下一行代码:

Vue.prototype.microStore = microStore

此时在子应用的各个页面都能够通过this.microStore访问自身的store。(this.microStore.state)

子应用之间传值:子应用的Vue实例属于同级实例,不能做到响应式,需要通过Vue现有的API方法Vue.observable(store)将共享的store实例进行响应式设置

5.主项目路由模式如何选择

    由于 `qiankun` 是通过 `location.pathname` 值来判断当前应该加载哪个子项目的,所以需要给每个子项目注入不同的路由 `path`,而 `hash` 模式子项目路由跳转不改变 `path`,所以无影响,`history` 模式子项目路由设置 `base` 属性即可。

如果主项目使用 hash 模式,那么得用 location.hash 值来判断当前应该加载哪个子项目,并且子项目都得是 hash 模式,还需要给子项目所有的路由都添加一个前缀,子项目的路由跳转如果之前使用的是 path 也需要修改,用 name 跳转则不用。

如果主项目是 hash 模式,子项目为 history 模式,那么跳转到子项目之后,无法跳转到另一个 history 模式的子项目,也无法回到主项目的页面。

6.微应用打包之后 css 中的字体文件和图片加载 404

原因是 qiankun 将外链样式改成了内联样式,但是字体文件和背景图片的加载路径是相对路径。

css 文件一旦打包完成,就无法通过动态修改 publicPath 来修正其中的字体文件和背景图片的路径。

解决方案:

1.所有图片等静态资源上传至 `cdn`,`css` 中直接引用 `cdn` 地址

2.借助 `webpack` 的 `url-loader` 将字体文件和图片打包成 `base64`(适用于字体文件和图片体积小的项目)
module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif|webp|woff2?|eot|ttf|otf)$/i,
        use: [
          {
            loader: 'url-loader',
            options: {},
          },
        ],
      },
    ],
  },
};
3.借助 webpack 的 file-loader ,在打包时给其注入完整路径(适用于字体文件和图片体积比较大的项目)
const publicPath = process.env.NODE_ENV === 'production' ? 'https://qiankun.umijs.org/' : `http://localhost:${port}`;
module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif|webp)$/i,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: 'img/[name].[hash:8].[ext]',
              publicPath,
            },
          },
        ],
      },
    ],
   },
 }
      

7.qiankun 是否兼容 ie

兼容.但是 IE 环境下(不支持 Proxy 的浏览器)只能使用单实例模式,qiankun 会自动将 singular 配置为 true

8.全局状态管理

qiankun通过initGlobalState, onGlobalStateChange, setGlobalState实现主应用的全局状态管理,然后默认会通过props将通信方法传递给子应用。

案例实现:

主应用:
import { initGlobalState } from 'qiankun'
export const initialState = {
  globalLocation: {
    id: 1234,
    station: '北京'
  }
}
const actions = initGlobalState(initialState) //初始化全局数据

// 自定义一个获取state的方法下发到子应用(通过props)
actions.getGlobalState = (key) => {
  // 有key,表示取globalState下的某个子级对象
  // 无key,表示取全部
  return key ? initialState[key] : initialState
}

actions.onGlobalStateChange((newState, prev) => {  //监听全局状态
  for (let key in newState) {
    initialState[key] = newState[key]
  }
});

export default actions
子应用:
class Actions {
  // 默认值为空 Action
  actions = {
    onGlobalStateChange: function () { },
    setGlobalState: function () { },
    getGlobalState: function () { },
  };
  /**
   * 设置 actions
   */
  setActions(actions) {
    this.actions = actions
  }

  //拿到入口文件中设置子应用的全局方法
  onGlobalStateChange(...args) {
    return this.actions.onGlobalStateChange(...args)
  }
  setGlobalState(...args) {
    return this.actions.setGlobalState(...args)
  }
  getGlobalState(...args) {
    return this.actions.getGlobalState(...args)
  }
}
const actions = new Actions()
export default actions
  

子应用在mount函数调用时设置子应用全局状态

import actions from './actions'

export async function mount(props) {
  actions.setActions(props) //子项目的入口文件中设置子应用的全局state
  render(props)
}

此时在 主应用/子应用 即可拿到调用setGlobalState更新数据通信


案例.gif
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容