• 浅析微前端框架 single-spa


    浅析微前端框架 single-spa

    一、应用场景

    将一个单体大型应用,转化为多个可以独立运行、独立开发、独立部署、独立维护的应用的聚合。
    解决的问题:
    1、随着项目迭代应用越来越庞大,难以维护。
    2、跨团队或跨部门协作开发项目导致效率低下的问题。

    二、基于 single-spa 二次开发的微前端解决方案(蚂蚁金服的 qiankun)

    (qinkun 文档地址):https://qiankun.umijs.org/zh/guide

    三、使用方法

    以 single-spa 为例,我们一起配置一下

    1、配置子应用 app1

    • 用 vue-cli 脚手架搭建一个轻量级的 vue 工程
    • 改造 vue.config.js
    const package = require('./package.json')
    module.exports = {
      // 告诉子应用在这个地址加载静态资源,否则会去基座应用的域名下加载
      publicPath: '//localhost:8091',
      // 开发服务器
      devServer: {
        port: 8081,
      },
      configureWebpack: {
        // 导出umd格式的包,在全局对象上挂载属性package.name,基座应用需要通过这个全局对象获取一些信息,比如子应用导出的生命周期函数
        output: {
          // library的值在所有子应用中需要唯一
          library: package.name,
          libraryTarget: 'umd',
        },
      },
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 安装 single-spa-vue

    single-spa-vue 负责为 vue 应用生成通用的生命周期钩子,在子应用注册到 single-spa 的基座应用时需要用到

    npm i single-spa-vue -S
    
    • 1
    • 改造入口文件 main.js
    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    import singleSpaVue from 'single-spa-vue'
    Vue.config.productionTip = false
    const appOptions = {
      el: '#microApp', //基座上面用来挂载子应用的dom节点
      router,
      render: (h) => h(App),
    }
    // 支持应用独立运行、部署,不依赖于基座应用
    if (!window.singleSpaNavigate) {
      delete appOptions.el
      new Vue(appOptions).$mount('#app')
    }
    // 基于基座应用,导出生命周期函数
    const vueLifecycle = singleSpaVue({
      Vue,
      appOptions,
    })
    export function bootstrap(props) {
      console.log('app1 bootstrap')
      return vueLifecycle.bootstrap(() => {})
    }
    export function mount(props) {
      console.log('app1 mount')
      return vueLifecycle.mount(() => {})
    }
    export function unmount(props) {
      console.log('app1 unmount')
      return vueLifecycle.unmount(() => {})
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 更改试图文件
    
    <template>
      <div class="home">
        <h1>app1 home pageh1>
      div>
    template>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    
    <template>
      <div class="about">
        <h1>app1 about pageh1>
      div>
    template>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 环境配置文件
    # .env文件:应用独立运行时的开发环境配置
    NODE_ENV=development
    VUE_APP_BASE_URL=/
    
    • 1
    • 2
    • 3
    # .env.micro文件:作为子应用运行时的开发环境配置
    NODE_ENV=development
    VUE_APP_BASE_URL=/
    
    • 1
    • 2
    • 3
    • 修改路由文件
    // /src/router/index.js
    // ...
    const router = new VueRouter({
      // 此处子应用1先用history模式
      mode: 'history',
      // 通过环境变量来配置路由的 base url
      base: process.env.VUE_APP_BASE_URL,
      routes,
    })
    // ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 在 package.json 的 script 中添加命令
    {
      "name": "app1",
      // ...
      "scripts": {
        // 独立运行
        "serve": "vue-cli-service serve",
        // 作为子应用运行
        "micro": "vue-cli-service serve --mode micro"
      }
      // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 启动应用,此处演示子应用嵌入所以用子应用模式运行项目
    npm run micro
    
    • 1

    2、配置子应用 app2。配置一个子应用即可,这里两个用来做对比

    和 app1 一样的配置,把 app1 修改成 app2。区别:这里把 app2 配置成 hash 模式

    • 修改路由文件
    // /src/router/index.js
    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import Home from '../views/Home.vue'
    Vue.use(VueRouter)
    const routes = [
      {
        path: '/',
        name: 'Home',
        component: Home,
      },
      {
        path: '/about',
        name: 'About',
        component: () =>
          import(/* webpackChunkName: "about" */ '../views/About.vue'),
      },
    ]
    let newRoutes = routes
    // 子应用模式下动态拼接路由
    if (window.singleSpaNavigate) {
      newRoutes = routes.map((route) => {
        route.path = `${process.env.VUE_APP_BASE_URL}${route.path}`
        return route
      })
    }
    const router = new VueRouter({
      mode: 'hash',
      // history模式下base配置有用,hash模式下不可以,只能动态拼接路由
      // base: process.env.VUE_APP_BASE_URL,
      routes: newRoutes,
    })
    // hash模式下还要动态的在路由守卫地方拼接路径
    router.beforeEach((to, from, next) => {
      if (window.singleSpaNavigate) {
        if (!to.path.includes(`${process.env.VUE_APP_BASE_URL}`)) {
          next({
            ...to,
            path: `${process.env.VUE_APP_BASE_URL}${to.path}`,
          })
        } else {
          next()
        }
      } else {
        next()
      }
    })
    export default router
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    3、配置基座应用 layout

    • 通过脚手架搭建 layout
    • 安装 single-spa
    npm i single-spa -S
    
    • 1
    • 修改入口文件
    // src/main.js
    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    import { registerApplication, start } from 'single-spa'
    Vue.config.productionTip = false
    // 远程加载子应用
    function createScript(url) {
      return new Promise((resolve, reject) => {
        const script = document.createElement('script')
        script.src = url
        script.onload = resolve
        script.onerror = reject
        const firstScript = document.getElementsByTagName('script')[0]
        firstScript.parentNode.insertBefore(script, firstScript)
      })
    }
    // 记载函数,返回一个 promise
    function loadApp(url, globalVar) {
      // 支持远程加载子应用
      return async () => {
        await createScript(url + '/js/chunk-vendors.js')
        await createScript(url + '/js/app.js')
        // 这里的return很重要,需要从这个全局对象中拿到子应用暴露出来的生命周期函数
        return window[globalVar]
      }
    }
    // 子应用列表
    const apps = [
      {
        // 子应用名称
        name: 'app1',
        // 子应用加载函数,是一个promise
        app: loadApp('http://localhost:8091', 'app1'),
        // 当路由满足条件时(返回true),激活(挂载)子应用。
        // 如果基座是hash模式,此处判断要更改为:location.hash.startsWith('#/app1'),
        activeWhen: (location) => location.pathname.startsWith('/app1'),
        // 传递给子应用的对象
        customProps: {},
      },
      {
        name: 'app2',
        app: loadApp('http://localhost:8092', 'app2'),
        activeWhen: (location) => location.pathname.startsWith('/app2'),
        customProps: {},
      },
    ]
    // 注册子应用
    for (let i = apps.length - 1; i >= 0; i--) {
      registerApplication(apps[i])
    }
    new Vue({
      router,
      mounted() {
        // 启动
        start()
      },
      render: (h) => h(App),
    }).$mount('#app')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • App.vue 文件修改
    <template>
      <div id="app">
        <div id="nav">
          <router-link to="/app1">app1router-link> |
          <router-link to="/app2">app2router-link>
        div>
        
        <div id="microApp">
          <router-view />
        div>
      div>
    template>
    <style>
      #app {
        font-family: Avenir, Helvetica, Arial, sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        text-align: center;
        color: #2c3e50;
      }
      #nav {
        padding: 30px;
      }
      #nav a {
        font-weight: bold;
        color: #2c3e50;
      }
      #nav a.router-link-exact-active {
        color: #42b983;
      }
    style>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 路由修改
    import Vue from 'vue'
    import VueRouter from 'vue-router'
    Vue.use(VueRouter)
    const routes = []
    const router = new VueRouter({
      mode: 'history',
      base: process.env.BASE_URL,
      routes,
    })
    export default router
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 启动项目
    npm run serve
    
    • 1

    到这里利用 single-spa 搭建的小型微前端集成功能就好了

    四、项目中使用 qiankun 框架的分享

    1、具体的qiankun框架的安装可以自行看qiankun文档实施

    2、踩坑记录(注意点)

    • 子应用路由必须挂载前缀;子应用是hash模式时候需要动态拼接路由前缀,上面案例也有实现
    • 路由是以主应用为主,所以路由传参可以直接使用;
    • vuex、cookie是共享主应用的,子应用中可以直接使用;
    • 虚拟节点的获取,因为微前端中有沙箱隔离的功能,所有在主应用没有办法直接获取子应用节点(沙箱开启的情况下。当时写了一个方法通过操作原生dom实现获取)
    • 弹框挂载的时候需要自定义挂载的dom节点,还是开启沙箱隔离的问题导致的。
    • 当前平台使用的适配方案是根据根节点的字体大小进行适配,根节点字体在1440下是16px,在其他尺寸的时候是14px,子应用需要统一适配方案,否则就考虑使用iframe嵌套;

    五、 补充

    1、京东的微前端方案

    文档地址:https://zeroing.jd.com/micro-app/docs.html#/

    2、微前端方案比较(图片来源于知乎)

    在这里插入图片描述

  • 相关阅读:
    在一个流量为王的时代,泰山众筹为什么脱颖而出呢?本文深度解析
    ssm毕设项目志愿者活动管理平台zx2tk(java+VUE+Mybatis+Maven+Mysql+sprnig)
    C语言内存函数
    数商云渠道商系统实现全渠道数据精细化管理,助力机械行业打造高效分销渠道
    MySQL实现事务隔离的原理
    FPGA——三速自适应以太网设计(2)GMII与RGMII接口
    round函数四舍五入结果不对
    可观测|时序数据降采样在Prometheus实践复盘
    django自动生成问卷表的软件的设计与实现毕业设计源码291138
    阿里首次公开企业级 SpringBoot 实战进阶笔记
  • 原文地址:https://blog.csdn.net/sinat_38592878/article/details/127774577