关于single-spa的理解和小实战

single-spa

起因是看了一下掘金的这篇链接; [每日优鲜供应链前端团队微前端改造](https://juejin.im/post/5d7f702ce51d4561f777e258, 讲最终将多个独立的小项目,利用single-spa框架的方法(即l类似于微服务)) 整合成一个大项目。讲的很不错

  • 适用场景:项目庞大,多个子项目整合在一个大的项目中。即使子项目的所用的技术栈不同,比如vue,react, angular有相应的single-spa的轮子,可以进行整合。
  • 不适用于:

    (1)< ie9 以下的版本

    (2)项目不大型,不需要切分

    (3)项目应用之间没有必然的联系


  • 优点:

    (1) 若多个应用之间公用一些共有的部分,可将其抽离出来,在最终的打包过程中明显的体积缩小。运行加快。

    (2) 不同的应用之间,由一个跳转到另一个时,在不涉及应用之间状态交互时,不用考虑重写之类的问题,只需配置好对应的spa,就可以进行切换


  • 我对于single-spa的理解:

    在娘胎里,熊大、熊二、熊三...熊n 都是熊妈妈的宝宝。做b超时都能看到各个熊宝宝的表现和各自的行为,但是总体上,它们都在熊妈妈的肚子里。

    而熊妈妈就属于 这个single-spa 包装后的平台, 而每个熊宝宝都是独立的,有各自的行为状态

  • 更好的理解,可以参考这篇链接:

    1. 前端:你要懂的单页面应用和多页面应用

    2. single-spa github官网

注: 英文不错的同学可以好好读读官网的教程文章,写的超级好,有详细的教程、api和参数文档

还可以看下这个官方的实例,single-spa-vue的例子该例中是全部以vue为单应用,组合起来的一个single-spa场景, 建议clone下来后跑一遍,并参考文档深刻理解其中含义


以下是我clone官网的例子后,一些理解和体会,有补充的欢迎补充。

如下所示:

(1)黑色部分每一个都是一个vue单页应用,最外层橙色的包裹器将他们注册开启包裹进来

(2)navbar 应用通过vue-router的方式,可以跳转到app1应用或者app2应用

整个demo的结构理解

分析一下单应用navbar, app1, app2。因为他们文件结构都是相同的,所以就看下app1 和app2好啦,如下:

目录结构图

按照官网例子安装好对应的依赖后,查看文件夹中的内容。

1.先看package.json

除了name和监听的端口不一样,其他都一样

package.json图

由于navbar、app1、app2的目录结构基本一致,就只说app1的吧,

app1 的目录结构如下:

app1 的目录结构

2.vue.config.js是这样写的:


module.exports = {

  chainWebpack: config => {

    config.devServer.set('inline', false)

    config.devServer.set('hot', false)

    config.externals(['vue', 'vue-router'])

  },

  filenameHashing: false,

}

上述配置表明:

  • 热加载配置中: 关闭当前应用的代码改变就加载的功能,关闭热更新,

  • 先将vue和vue-router的依赖暴露出去,再在最终的root-html-file的index.html中进行配置,目的是 为了多个单应用之间相同的依赖,它们的版本号应该保持一致,vue实例应该保持一致

  • 关闭最终打包生成的文件后面带有hash码

3. babel.config.js,

写成这样是为了让浏览器支持最新的es语法,比如扩展运算符,动态import,转化vue jsx 或者转化generator和async/await语法糖


module.exports = {

  presets: [

    '@vue/app'

  ]

}

4.入口文件main.js 改变

一般的vue应用, 入口文件main.js 配置如下:


import Vue from 'vue'

import App from './App'

import router from './router'

Vue.config.productionTip = false

new Vue({

  el: '#app',

  router,

  components: { App },

  template: '<App/>'

})

src/main.js ,此文件为应用的入口文件配置如下:


import './set-public-path'

import Vue from 'vue';

import App from './App.vue';

import router from './router';

import singleSpaVue from 'single-spa-vue';

Vue.config.productionTip = false;

const vueLifecycles = singleSpaVue({

  Vue,

  appOptions: {

    render: (h) => h(App),

    router,

  },

});

//应用初始

export const bootstrap = vueLifecycles.bootstrap;

//应用挂载

export const mount = vueLifecycles.mount;

//应用卸载

export const unmount = vueLifecycles.unmount;

其中这里的singleSpaVue(opts)里面的参数和表示的含义分别是:

  • Vue,(必须参数),Vue对象,通过import暴露或者通过require('vue')的方式

  • appOptions,(必须参数),是用于创建你的vue应用的一个实例对象,通过new Vue(appOptions)的方式创建。

注意:如果没有给appOptions提供一个挂载的节点el元素上,那它创建一个div,作为一个默认的容器插入到你的vue应用dom中

  • loadRootComponent(可选参数,可替代appOptions中的render),解析你的根组件并返回一个promise对象。这个适合于懒加载

5.src/set-public-path.js书写


__webpack_public_path__ = window.getPublicPath('app1')

webpack_public_path是webpack的output配置中的publickPath参数的另外一种配置,它是webpack暴露的一个全局变量,表示设置运行的publicPath

这里window.getPublicPath,是个方法,定义在最外层的root-html-file下的html的script标签中,用来获取当前应用的publicPath是什么。

可能有同学很好奇了,为啥上述的每个vue应用,都没有webpack的配置文件,也要设置上述的publicPath路径?

这是因为使用了vue/cli-service 插件,它里面封装了webpack, 启动每个应用的命令是这样写的:

"serve": "vue-cli-service serve --port 8081",

实际上:

image

6. router.js、App.vue、views/ 等文件都跟我们写vue文件保持一致,不做细讲。

再看我们最终融合的应用root-html-file, 目录文件如下:

root-html-file目录图
7.package.json很简单,代码如下,serve是个node服务插件,将我们的项目展示在浏览器上,

{

  "name": "root-html-file",

  "scripts": {

    "serve": "serve -s -l 5000"

  },

  "devDependencies": {

    "serve": "^11.1.0"

  }

}

8. root-html-file中的index.html

主要的是这个index.html文件,里面这些东西都很重要,

下面来挑主要的一一说明,


<script type="systemjs-importmap">

      {

        "imports": {

          "navbar": "http://localhost:8080/app.js",

          "app1": "http://localhost:8081/app.js",

          "app2": "http://localhost:8082/app.js",

          "single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js",

          "vue": "https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js",

          "vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue-router.min.js"

        }

      }

</script>

这里是使用systemjs,是一个运行于浏览器端的模块加载器,将我们整个应用所需要的js文件,都以imports的形式引入进来

再来看body中的script代码,这里面比较核心。一一分析下,总体代码如下:

核心代码
第一部分:

var originalResolve = System.resolve

var moduleMap = {}

System.resolve = function(name) {

    return originalResolve.apply(this, arguments).then(resolved => {

    moduleMap[name] = resolved;

    return resolved;

  });

}

上述代码是将导入的模块文件,通过system.resolve解析出来,形成一个 moduleMap对象

这里由于默认输入localhost: 5000,加载的只有navbar应用和single-spa插件,vue插件、vue-router插件。

图示

app1应用和app2应用是点击上述链接,通过vue-router的形式进入,并加载对应的应用,所以

结果得到验证,如下图所示,一开始的localhost:5000时,打印其中的moduleMap信息,显示如下

结果

进入app1应用时, 由于vue-router配置的作用,默认地址栏后加了个/app1,但实际加载的app1是跑在localhost: 8081上的,

图示

localhost:5000/app1时的moduleMap信息:

结果

进入app2应用时,同理地址栏后加了个/app2,但实际加载的app2是跑在localhost: 8082上的,

图示

localhost:5000/app2时的moduleMap信息:

结果

即个人理解: 这样做到了访问不同的应用,对应的模块按需加载,节省多余的请求和带宽,提高加载效率。

第二部分:

window.getPublicPath = function(name) {

  const url = moduleMap[name]

  console.log('url ---?',url)

  if (url) {

    let index = url.lastIndexOf('/js')

    if (index < 0) {

      index = url.lastIndexOf('/')

    }

    index++

    return url.slice(0, index);

  } else {

  throw Error(`Could not find url for module '${name}'`)

}

}

这里是通过拿到moduleMap键值对中的值,即现在请求访问的url,用来设置当前应用运行时的publicPath!不得不说是很巧妙的一种方式啊~

第三部分:

Promise.all([System.import('single-spa'), System.import('vue'), System.import('vue-router')]).then(function (modules) {

      var singleSpa = modules[0];

      var Vue = modules[1];

      var VueRouter = modules[2];

      Vue.use(VueRouter)

      singleSpa.registerApplication(

        'navbar',

        () => System.import('navbar'),

        location => true

      );

      singleSpa.registerApplication(

        'app1',

        () => System.import('app1'),

        location => location.pathname.startsWith('/app1')

      )

      singleSpa.registerApplication(

        'app2',

        () => System.import('app2'),

        location => location.pathname.startsWith('/app2')

      )

    singleSpa.start();

})

注: 由于System.import('xxx') 这种方式,获得是个promise对象,所以检测所需模块是否都引入完全,使用Promise.all方法~

这里是引入所有应用所需的模块后,注册single-spa应用,下面这个方法用来注册应用。具体参数解释如下:

registerApplication(name, howToLoad, activityFunction)

  • name: (required),为application的name

  • howToload: (required),是一个function ,表示怎样加载,上述代码是通过System.import, 加载各个项目的入口js文件 (模块加载)

  • activityFunction: (required) 也是一个function, 当url符合时,返回true

最后再使用 singleSpa.start() 进行启动

啦啦啦~完结撒花 <( ̄▽ ̄)/

通过对于demo的分析,自己更好的理解了single-spa的使用场景和如何进行的。不得不说,single-spa的设计和想法,令人眼界大开~希望未来有机会也能在大型多个应用上一展身手

后记:
后来自己参考网上的实例,参考网站:react+vue single-spa实战
也写了个以react, vue分别为单应用的 简单的single-spa, 例子详戳:模仿的react+vue single-spa实战

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容

  • 本文首发于TalkingCoder,一个有逼格的程序员社区。转载请注明出处和作者。 写在前面 本文为系列文章,总共...
    Aresn阅读 9,508评论 0 42
  • UI组件 element - 饿了么出品的Vue2的web UI工具套件 Vux - 基于Vue和WeUI的组...
    鲁大师666阅读 43,362评论 5 97
  • UI组件 element- 饿了么出品的Vue2的web UI工具套件 Vux- 基于Vue和WeUI的组件库 m...
    柴东啊阅读 15,846评论 2 140
  • 简说Vue (组件库) https://github.com/ElemeFE/element" 饿了么出品的VUE...
    Estrus丶阅读 1,533评论 0 1
  • 乖乖,亲爱的你,我们808的姐妹,在端午节遇见了小小的你。 我没有见过你的第一眼,不曾体会你蜷缩着的小小身体保存着...
    山鹛阅读 744评论 7 5