single-spa微前端简单实践与优化思路

微前端简单实践

什么是single-spa或者说什么是微前端

微前端是指存在于浏览器中的微服务

  • 微服务大家应该都听过
    微服务是指后端服务,把一个大型的单个应用程序和服务拆分为数个甚至数十个的支持微服务,它可扩展单个组件而不是整个的应用程序堆栈,从而满足服务等级协议。它们在自己的操作系统中运行,管理自己的数据库并通过网络进行彼此间的通信。

  • 微前端作为用户界面的一部分,通常由许多组件组成,并使用类似于React、Vue和Angular等框架来渲染组件。每个微前端可以由不同的团队进行管理,并可以自主选择框架。每个微前端都拥有独立的git仓库、package.json和构建工具配置。

  • 共同点
    独立的构建和部署。将DOM视为微前端使用的共享资源。一个微前端的DOM不能够被其他微前端触及,类似于一个微服务的数据库不应该被其他没有权限的微服务触及。

主应用构建
——mian主应用
  |-public
  |-src
     |-router
     |-app.vue//应用主入口
     |-main.js
  |-single-spa-config.js
  |-vue.config.js
  1. 使用@vue/cli 4.x以上版本构建应用,输入vue create main利用cli进行项目初始化
  2. 安装single-spa和antui依赖npm install ant-design-vue single-spa --save -d并在mian.js中引入
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import Ant from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css';
import '../single-spa-config.js'
Vue.use(Ant);
Vue.config.productionTip = false
new Vue({
  router,
  render: h => h(App),
}).$mount('#app')
  1. 在路由中注册统一路由,我们注册一个子服务路由,不填写component字段。
  {
    path: '/single-router',
    name: 'single-router',
  }
  1. 初始化界面设置2个a标签,此时设置主应用的路由模式为history
 <a href="/vue-antd#"/>
 <a href="/vue-element#"/>
初始化demo界面
  1. 配置single-spa-config.js
  • singleSpa.registerApplication这是注册子应用的方法。
    接受appName: 子应用名称,applicationOrLoadingFn: 子应用注册函数,子应用需要返回 single-spa 的生命周期对象。
    activityFn: 回调函数入参 location 对象,可以写自定义匹配路由加载规则。
// single-spa-config.js
import * as singleSpa from 'single-spa'; //导入single-spa
/*
* runScript:一个promise同步方法。可以代替创建一个script标签,然后加载
*/
const runScript = async (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);
    });
};
singleSpa.registerApplication( //注册微前端服务
    'singleVue', 
    async () => {//异步加载本地3000端口下的chunk
            await runScript('http://127.0.0.1:3000/js/chunk-vendors.js');
            await runScript('http://127.0.0.1:3000/js/app.js'); 
            return window.singleVue;
        }
    },
    location => location.pathname.startsWith('/vue-antd') 
    // 配置微前端模块前缀,对应刚才链接的`/vue-antd`二者名字相同即可
);
singleSpa.start(); // 加载所有配置后调用则启动
子应用构建
——child子应用
  |-public
  |-src
     |-router
     |-view
     |-app.vue//应用主入口
     |-main.js
  |-single-spa-config.js
  |-vue.config.js
  1. 使用@vue/cli构建应用,输入vue create child1利用cli进行项目初始化
  2. 安装single-spa-vue依赖npm install single-spa-vue --save -d并在mian.js中引入
import Vue from 'vue'
import App from './App.vue'
import singleSpaVue from "single-spa-vue"
import router from './router'
Vue.config.productionTip = false
const vueOptions = {//single-spa模式挂载在主应用的vue节点上
  el: "#vue",
  router,
  render: h => h(App),
}
if (!window.singleSpaNavigate) { // 如果不是single-spa模式则挂载在自身的app节点上
  delete vueOptions.el;
  new Vue(vueOptions).$mount('#app');
}
// singleSpaVue包装一个vue微前端服务对象
const vueLifecycles = singleSpaVue({
    Vue,
    appOptions: vueOptions
});
// 导出生命周期对象
export const bootstrap = vueLifecycles.bootstrap; // 启动时
export const mount = vueLifecycles.mount; // 挂载时
export const unmount = vueLifecycles.unmount; // 卸载时
export default vueLifecycles;
  1. vue.config.js设置安装stats-webpack-plugin插件npm install stats-webpack-plugin --save -d
const StatsPlugin = require('stats-webpack-plugin');
const path = require('path');
module.exports = {
    publicPath: "//localhost:3000/",//子应用打包端口,需要和主应用引入端口相同
    css: {
        extract: false
    },
    configureWebpack: {
        devtool: 'none', // 不打包sourcemap
        output: {
            library: "singleVue", // 导出名称
            libraryTarget: "window", //挂载目标
        },
        resolve: {
            alias: {
              "~": path.resolve(__dirname, 'src/'),
              "moment$": "moment/moment.js"
            }
          },
//在每次打包结束后,都生成一个manifest.json 文件,里面存放着本次打包的 
//public_path bundle list chunk list 文件大小依赖等等信息。
        plugins: [
            new StatsPlugin('manifest.json', {
                chunkModules: false,
                entrypoints: true,
                source: false,
                chunks: false,
                modules: false,
                assets: false,
                children: false,
                exclude: [/node_modules/]
            }),
        ]
    },
    devServer: {
        contentBase: './',
        compress: true,
    }
};
  1. 子应用启动vue-cli-service serve --port 3000就可以在主项目中看到
    子应用1
  2. 后续子应用的添加只需在主应用 main.js中调singleSpa.registerApplication新增即可
    此时简单demo

微前端优化相关思路

基于iframe的微前端因为不使用所以不在本文中出现具体表现为每一个子系统的子页面均是由iframe加载的,不同模块的前端应用之间可以相互独立运行
一开始就引入了多个应用的js。是把子应用直接加载到页面中。所有的子应用都运行在同一个内存空间。


simple-single-spa-webpack-example

导入建议

  1. 导航区域在项目中充当调度者的角色,由它来决定在不同的条件下激活不同的子应用。 因此则仅仅是:导航路由 + 资源加载框架


    整体布局
  2. 由于single-spa,是所有子应用共享一个html文件的。子应用包装器接管了子应用的入口组件render行为,所以主应用的html可以动态添加一个dom节点再将子项目入口组件渲染到这个dom节点上主应用需要在子应用加载之前构建好相应的容器节点 (比如 “#vue” 节点),避免子应用挂在节点找不到而报错。


    主应用

    子应用
  1. 直接将子应用打包出来后 HTML 作为入口,子应用可以通过 fetch html 的方式获取子应用的静态资源,同时将 HTML document。 作为子节点塞到主框架的容器中。减少主应用的接入成本,子应用的开发方式及打包方式基本上也不需要调整,而且可以天然的解决子应用之间样式隔离的问题。在用HTML作为入口的 方案下,主应用注册子应用的方式则变成registerApp('subApp1', { entry: '//test/index.html'})

  2. 在请求HTML的情况下,将 HTML入口的改成对配置文件的读取,从而减少一次请求,如:registerApp('App1', { html: '', scripts: ['//abc.test.com/index.js'], css: ['//abc.test.com/index.css']})

  3. 通过gulp合并amd模块的减小重复打包体积,这么多同类型的vue项目,有大量的重复代码、重复引用,可以进行优化,webpack打包后,externals配置的模块不会打包进bundle,会被摘出来按umd规范通过requre方式去加载。相关依赖可以选择尽量相同的版本

       const gulp = require('gulp');
       const concat = require('gulp-concat');
       gulp.task('storeConcat', function () {
           gulp.src('project/**/Store.js')
               .pipe(concat('Store.js')) //合并后的文件名
               .pipe(gulp.dest('project/'));
       });

通过配置externals可以减小子项目打包出来的体积。webpack外部扩展

   // 每个子项目自己的webpack.config.js,根据使用情况设置externals
    externals: {
         'axios': 'axios',
         'vue': 'Vue',
         'vue-router': 'VueRouter',
         'vuex': 'Vuex',
         'moment': 'moment',
         ...
     }

通过system.js优化资源加载

  // index.html 整个微前端的唯一入口
  <script src="system.js"></script>
  <script>
    SystemJS.config({
      map: {
        "Vue": "//xxx.cdn.cn/static/vue/2.5.17/vue.min.js",
        "Vuex": "//xxx.cdn.cn/static/vuex/3.0.1/vuex.min.js",
        "VueRouter": "//xxx.cdn.cn/static/vueRouter/3.0.1/vue-router.min.js",
        "moment": "//xxx.cdn.cn/static/moment/2.22.2/moment.min.js",
        "axios": "//xxx.cdn.cn/static/axios/0.15.3/axios.min.js",
      }
    })
  </script>

入口index.html只有一个,不一次性引入所有CDN资源,可能子项目A使用而B不使用导致重复引用systemjs只是在加载index.html时注册了这些CDN地址,不会直接去加载,当子项目里用到的时候,systemjs会接管模块引入,再动态去加载资源。避免不同子项多余加载。 参考demo地址

  1. 页面切换优化性能加载,在页面切换时候依旧需要获取页面数据时,可能会在数据返回前有短暂的白屏。
    • 切换前:在确保组件&数据加载完毕前,可保证页面可交互性,路由跳转前进行拦截,数据处理后再进行跳转,减少阻塞感。如果需要重新请求就写在activated钩子里
    • 添加转场动画:组件&数据已经完全加载,在切换至新页面瞬间,依旧需要页面渲染时间,大多数页面保证在转场动画完毕之后依然渲染完毕。
    • 为了让页面切换不刷新,使用了keep-alive去缓存页面,在关闭页面时通过keep-alive的exclude属性去除了keep-alive缓存或者用include,把要换存的页面的name放在状态管理,把一些复杂重复调用接口或者没有必要缓存的模块剔除不进行keep-alive缓存
    • 由于我们的子应用加载后就不对其进行卸载,主要是处理缓存,防止堆内存溢出,还有项目间切换时路由钩子接管的处理。
      keep-alive 缓存页面demo
  1. 让子项目使用 stats-webpack-plugin 插件,每次打包后都输出一个 只包含重要信息的manifest.json文件。父项目先ajax 请求 这个json文件,从中读取出需要加载的js目录,然后同步加载。

  2. 借鉴qiankun 框架,路由系统基于 Single-SPA 实现,在应用的加载和管理层引入了 jsSandowBox,其他项目的css和js的我们在子应用切换时并没有去除,只能通过规范避免相互污染,或者通过CustomEvent来进行页面通信
    项目加载流程应该为

      浏览器访问/main/app1=>
      加载main主应用=>
      加载子应用app1=>
      请求app1config.js=>
      加载app1的相关静态资源=>
      main主应用接管路由相应路由变化=>
      main加载对应页面

在获取子应用的配置信息时,我们可以按照约定 path 的规则,Single-SPA 对应 entry js/html 配置可以减少加载。

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