从0搭建一个微前端项目(Single-spa)

背景

微前端架构模式早就在2016年由ThoughtWorks年提出,微前端是模仿服务端微服务的理念而应用于浏览器端,即将多个单一的单体应用组合起来拼凑成唯一应用,各个单体应用还可以独立开发、独立运行、独立部署,这也是微前端比较重要的特点。

我司要开发一个中后台项目,本身toB的中后台项目因为周期时间长会形成项目难以维护和项目过大的缺点,我是体验过要完成一个小需求,本来只需要半天的时间就可以完成,结果代码查找和打包部署直接教育了我的天真想法,而且最重要的原因我们是多个团队共同开发,为了让不同的团队之间可以独立开发和部署,互相不影响,必须选择微前端。

技术选型

微前端是指一种架构模式,他有很多种实现方式,具体有哪些我就不一一介绍了,建议大家参考前端架构从入门到微前端这本书。

我们选择的是前端微服务化的实现模式,前端微服务化是指每个前端应用完全独立,通过模块化的方式组合应用。

技术栈:single-spa + vue

准备工作

  1. 项目地址
  2. systemjs、相关模块化的知识
  3. single-spa框架
  4. vue相关技术栈

我司基本流程图:

image

主项目

1.安装依赖:

  • Single-spa
  • Systemjs

模块加载器,用来加载子项目初始化相关资源,但是子项目入口文件必须打包成<font color=red>umd</font>格式,也可以配合构建工具webpack中的externals属性,将子项目和主项目公共的模块(vue,vuex,vue-router,loadsh…)提取到主项目中,配置成外链可以实现按需加载,可以减少子项目打包的体积,前提这些公共模块版本必须保持一致

2.子项目注册(microfrontend.js):

import { registerApplication, start } from 'single-spa';

///// 文件的相关路径需要替换成自己项目实际路径
/**
 *
 * 获取子项目app.js文件
 */
function getApplication(path) {
  return window.System.import(`${path}?time=${new Date().getTime()}`).then((res) => {
    if (res.default) {
      return window.System.import(res.default['app.js']).then((ret) => ret.default);
    }
  });
}
/**
 * name: 第一个参数表示应用名称,name必须是string类型
 *
 * app:  加载函数  可以是一个Promise类型的 加载函数,返回的Promise resolve之后的结果必须是一个可以被解析的应用,
 * 这个应用其实是一个包含single-spa各个生命周期函数 的对象(e.g: vue打包 引入后的app.js)。
 *
 * activeWhen: 激活函数 一个纯函数  window.loaction作为第一个参数被调用,只有函数返回的值为true时,应用才会被激活。
 * 通常情况下,activity function会根据 window.location 的path来决定是否需要被激活(我就是这样玩的)
 *
 * single-spa根据顶级路由查找应用,而每个应用会处理自身的子路由。以下场景,single-spa会调用应用的activity funtion
 * 1.hashchange or popstate 事件触发时(vue-router hash或history路由模式都会触发)
 * 2.pushState or replaceState被调用时
 * 3.在single-spa 手动调用 triggerAppChange 方法
 * 4.checkActivityFunctions 方法被调用时
 *
 * customProps
 * 对象 可以表示自定义字段,子应用生命周期函数可以获取
 * 函数 两个参数 应用的名称和window.location
 */
const development = process.env.NODE_ENV === 'development';
/// web 部署根域名
const baseUrl = process.env.VUE_APP_WEB_URL;

const configProject = [
  {
    name: 'app1',
    app: window.System.import('app-demo1').then((res) => res.default), /// 子项目通过插件完成配置的引入方式
    activeWhen: (location) => location.pathname.startsWith('/app1'),
    customProps: {
      // 对象
      everything: 'just do it'
    }
    //   customProps: (name, location) => {
    //     // 函数
    //     return {everything: 'just do it'};
    //   }
  },
  {
    name: 'app2',
    app: getApplication(development ? 'http://localhost:8993/manifest.json' : `${baseUrl}/app2/manifest.json`),
    activeWhen: (location) => location.pathname.startsWith('/app2'),
    customProps: {
      // 对象
      everything: 'just do it'
    }
  }
];

/// 注册子应用
configProject.forEach((element) => {
  registerApplication(element);
});
// 暴露single-spa启动方法 可以控制子应用的激活时机
export function appStart() {
  return start();
}

上面相关的xx.mainifest.json文件是什么?下面子项目模块有介绍
3.修改入口文件(main.js):

相关逻辑代码,全部代码可以查看项目。
// 引入启动方法
import { appStart } from './microfrontend';

// Single-spa启动方法在此处调用,是因为我们项目一般需要前置条件才会容许项目挂载 挂载应用

/// e.g 比如登录
// async function login() {
//   await 相关接口请求
//   appStart();
// }
appStart();

4.修改router.js文件:

vue-router默认路由模式是hash模式,我们需要修改成history模式。

5.import maps:

使用import maps共享依赖 配合webpack externals,使依赖包作为外部依赖,告诉你的应用不在node_modules里寻找,而是去运行时的模块中寻找; Chrome是目前唯一实现支持importmas的浏览器,想要在其他浏览器正常使用,就需要借助systemjs。

<!--  配置导入映射的,模块必须符合以下三种规范之一:
      1. System.register - https://github.com/systemjs/systemjs/blob/master/docs/system-register.md
      2. UMD ( 推荐 )
      3. Global variable
-->
e.g: 
<script type="systemjs-importmap">
      {
        "imports": {
          "vue": "https://cdn.jsdelivr.net/npm/vue@2.6.11",
          "vue-router": "https://unpkg.com/vue-router@3.2.0/dist/vue-router.js",
          "vuex": "https://unpkg.com/vuex@3.4.0",
          "lodash": "https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js",
          "dayjs": "https://unpkg.com/dayjs@1.8.21/dayjs.min.js",
          "app-demo1": "http://localhost:8992/js/app.js?time=<%=version%>"
        }
      }
</script>

子项目

子项目(vue)的改造有两种方法,第一个是通过插件去修改相关配置(不推荐),第二个是自己动手修改相关配置,两种方式都在项目中使用了,分别代表的项目是app-demo1和app-demo2。

不推荐通过插件去修改相关配置,除了通过插件配置会让我们弱化对single-spa相关配置的认知,还有插件会修改一些必要配置之外,还会添加一些非必需配置(e.g:standalone-single-spa-webpack-plugin...)。

手动配置

1.安装相关依赖:

  • single-spa-vue

single-spa-vue是一个针对vue项目的初始化、挂载、卸载的库,可以实现single—spa注册的应用、生命周期函数等功能。

  • webpack-manifest-plugin

通过webpack-manifest-plugin在打包时自动生成资源列表json文件,内容是项目资源清单。这样主应用可以通过这个json文件获取打包后生成的app.hash.js入口文件。
2.修改入口文件(main.js):

import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
// 新增
import singleSpaVue from 'single-spa-vue';

Vue.config.productionTip = false;

const appOptions = {
  render: (h) => h(App),
  router, // 路由
  store // vuex
};
// 判断是微前端加载还是独立运行
if (!window.singleSpaNavigate) {
  new Vue(appOptions).$mount('#app');
}
const vueLifecycles = singleSpaVue({
  Vue, //(必传项) 主Vue对象
  appOptions
});

// single-spa 生命周期函数 三个生命周期函数必须都有
export const bootstrap = vueLifecycles.bootstrap;
export const mount = vueLifecycles.mount;
export const unmount = vueLifecycles.unmount;

3.修改vue.config.js文件(修改webpack配置):

const packageName = require('./package.json').name;
const { WebpackManifestPlugin } = require('webpack-manifest-plugin'); // 资源清单 协助主应用加载资源

module.exports = {
  // 设定好publicPath, 端口最好是一个固定值
  publicPath: process.env.NODE_ENV === 'production' ? '/app-demo2' : 'http://localhost:8993',
  configureWebpack: (config) => {
    config.output.libraryTarget = 'umd'; // 打包格式为umd 配合模块加载工具加载项目
    config.output.library = packageName;
    config.output.jsonpFunction = `webpackJsonp_${packageName}`;// 防止全局变量webpackJsonp冲突
    // 删除chunk-vendors.js文件,公共三方模块打包进app.js文件
    config.optimization.splitChunks.cacheGroups = {};
    //config.optimization.delete('splitChunks');

    // 打包时移除这些通用库,配合systemjs从root加载  
    //  注意⚠️ 和上文importmap相关联
    config.externals = ['vue', 'vue-router', 'vuex', 'lodash', 'dayjs'];

    /// 主项目加载的xxx.mainifest.json文件
    config.plugins.push(
      new WebpackManifestPlugin({
        fileName: 'manifest.json'
        // filter: function(option) {
        //   return option.isInitial;
        // }
      })
    );
  },
  devServer: {
    headers: {
      // 开发模式下解决微前端加载时跨域的问题
      'Access-Control-Allow-Origin': '*'
    }
  },
  css: {
    // css不单独打包成一个文件,和js打包进一个文件 这个也会造成css污染问题,通过其他方式避免。
    extract: false
  }
};
插件
前提是通过vuecli创建的项目;通过插件修改的前提是子项目最好是新项目,因为插件会修改main.js文件,会造成代码丢失。

1.终端执行:

Vue add single-spa

这个插件帮助我们做了哪些事情呢???

  • 安装single-spa-vue包
  • 改造main.js文件
  • 修改webpack相关配置, 至于具体修改了哪些配置,基本上和我们上面在vue.config.js文件修改的东西差不多,一些关键设置都是存在的,还有其他的一些不同之处。具体配置可以参考(配置)。

注意事项

single-spa虽然实现了完整的应用加载逻辑,但是应用之间的隔离需要我们自己去解决,在这方面上qiankun作为一个基于single-spa实现的框架,替我们解决了这部分的问题,后续会研究qiankun。

1.css样式隔离:

对于css污染来说,只要保证样式不会互相覆盖,可以借助打包工具或其他方式为每个子应用添加前缀的目的来规避。

2.js隔离:

项目基于打包工具进行模块化的打包方式,基本可以避免大多数的全局冲突,因为大多数都被编译成了闭包、内部变量和方法。
但是挂载到window上的变量还是有风险,为了降低风险还是需要制定开发规范:尽量避免挂载变量到window,不容许修改原始方法或对象。也可以参考一下qiankun 隔离的做法(通过快照的方式,具体可以自行查找)。

3.应用之间通信:

主应用和子应用,或者子应用和子应用之间如何通信? 要让两个隔离的应用做到通信,需要借助全局对象。我找到了一个非常好用的三方事件库eventemitter3,eventemitter3实例化后挂载到window下,应用之间派发/监听,就可以愉快的通信了。

参考

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

推荐阅读更多精彩内容