微前端框架 之 single-spa 从入门到精通

当学习成为了习惯,知识也就变成了常识。感谢各位的 点赞收藏评论

新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn

文章已收录到 github,欢迎 Watch 和 Star。

简介

从基本使用 -> 部署 -> 框架源码分析 -> 手写框架,带你全方位刨析 single-spa 框架

前序

目的

  • 会使用single-spa开发项目,然后打包部署上线

  • 刨析single-spa的源码原理

  • 手写一个自己的single-spa框架

过程

  • 编写示例项目

  • 打包部署

  • 框架源码解读

  • 手写框架

关于微前端的介绍这里就不再赘述了,网上有很多的文章,本文的重点在于刨析微前端框架single-spa的实现原理。

single-spa是一个很好的微前端基础框架,qiankun框架就是基于single-spa来实现的,在single-spa的基础上做了一层封装,也解决了single-spa的一些缺陷。

因为single-spa是一个基础的微前端框架,了解了它的实现原理,再去看其它的微前端框架,就会非常容易了。

提示

  • 先熟悉基本使用,熟悉常用的API,可通过示例项目 + 官网相结合来达成

  • 如果基础比较好,可以先读后面的手写 single-spa 框架部分,再回来阅读源码,效果可能会更好

  • 文章中涉及到的所有代码都在 github(示例项目 + single-spa源码分析 + 手写single-spa框架 + single-spa-vue源码分析)

示例项目

新建项目目录,接下来的所有代码都会在该目录中完成

mkdir micro-frontend && cd micro-frontend

示例代码都是通过vue来编写的,当然也可以采用其它的,比如react或者原生JS

子应用 app1

新建子应用
vue create app1

按图选择,去除一切项目不需要的干扰项,后面一路回车,等待应用创建完毕

微前端框架 之 single-spa 从入门到精通_第1张图片
微前端框架 之 single-spa 从入门到精通_第2张图片
配置子应用

以下所有的操作都在项目根目录/micro-frontend/app1下完成

vue.config.js

在项目根目录下新建vue.config.js文件

const package = require('./package.json')
module.exports = {
   
  // 告诉子应用在这个地址加载静态资源,否则会去基座应用的域名下加载
  publicPath: '//localhost:8081',
  // 开发服务器
  devServer: {
   
    port: 8081
  },
  configureWebpack: {
   
    // 导出umd格式的包,在全局对象上挂载属性package.name,基座应用需要通过这个全局对象获取一些信息,比如子应用导出的生命周期函数
    output: {
   
      // library的值在所有子应用中需要唯一
      library: package.name,
      libraryTarget: 'umd'
    }
  }
}

安装single-spa-vue
npm i single-spa-vue -S

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

改造入口文件
// /src/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',
  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(() => {
   })
}

更改视图文件





环境配置文件
.env

应用独立运行时的开发环境配置

NODE_ENV=development
VUE_APP_BASE_URL=/
.env.micro

作为子应用运行时的开发环境配置

NODE_ENV=development
VUE_APP_BASE_URL=/app1
.env.buildMicro

作为子应用构建生产环境bundle时的环境配置,但这里的NODE_ENVdevelopment,而不是production,是为了方便,这个方便其实single-spa带来的弊端(js entry的弊端)

NODE_ENV=development
VUE_APP_BASE_URL=/app1
修改路由文件
// /src/router/index.js
// ...
const router = new VueRouter({
   
  mode: 'history',
  // 通过环境变量来配置路由的 base url
  base: process.env.VUE_APP_BASE_URL,
  routes
})
// ...

修改package.json中的script
{
   
  "name": "app1",
  // ...
  "scripts": {
   
    // 独立运行
    "serve": "vue-cli-service serve",
    // 作为子应用运行
    "serve:micro": "vue-cli-service serve --mode micro",
    // 构建子应用
    "build": "vue-cli-service build --mode buildMicro"
  },
 	// ...
}

启动应用
应用独立运行
npm run serve

当然下面的启动方式也可以,只不过会在pathname的开头加了/app1前缀

npm run serve:micro
作为子应用运行
npm run serve:micro
作为独立应用访问

微前端框架 之 single-spa 从入门到精通_第3张图片

子应用 app2

/micro-frontend目录下新建子应用app2,步骤同app1,只需把过程中出现的'app1'字样改成'app2'即可,vue.config.js中的8081改成8082`

启动应用,作为独立应用访问

微前端框架 之 single-spa 从入门到精通_第4张图片

子应用 app3(react)

这部分内容于2020/08/30添加,为什么后来添加这部分内容呢?是因为有同学希望增加一个react项目的示例,他们在集成react项目时遇到了一些困难,于是找时间就加了这部分内容;发现网上single-spa集成react的示例非常少,仅有的几个看了下也是对官网示例的抄写。

示例项目是基于react脚手架cra创建的,整个集成的过程中难点有两个:

  • webpack的配置,这部分内容官网有提供

  • 子应用入口的配置,单纯看官方文档的示例项目根本跑不起来,或者即使跑起来也有问题,reactvue的集成还不一样,react需要在主项目的配置中也加一点东西,这部分官网配置没说,是通过single-spa-react源码看出来的

接下来就开始吧,在/micro-frontend目录下通过cra脚手架新建子应用app3

安装 app3
create-react-app app3

以下所有操作都在/micro-frontend/app3目录下进行

安装react-router-domsingle-spa-react
npm i react-router-dom single-spa-react -S
打散配置

打散项目的配置,方便更改webpack的配置内容,当然通过react-app-rewired覆写默认配置应该也是可以的,官网也有提到,不过我这里没试,采用的是直接打散配置

npm run eject
更改 webpack 配置文件
/config/webpack.config.js,官网
  • 删掉optimization部分,这部分配置和chunk有关,有动态生成的异步chunk存在,会导致主应用无法配置,因为chunk的名字会变,其实这也是single-spa的缺陷,或者说采用JS entry的缺陷,JS entry建议将所有内容都打成一个bundle - app.js

  • 更改entryoutput部分

{
   
  ...
  entry: [
      paths.appIndexJs,
    ].filter(Boolean),
  output: {
   
    path: isEnvProduction ? paths.appBuild : undefined,
    filename: 'js/app.js',
    publicPath: '//localhost:3000',
    jsonpFunction: `webpackJsonp${
     appPackageJson.name}`,
    library: 'app3',
    libraryTarget: 'umd'
  },
  ...
}
项目入口文件改造

我这里将无关紧要的内容都删了,只留了/src/index.js/src/index.css

/src/index.js

由于文章内容太多,字数超出限制,这部分代码就通过图片的形式来展示了,如果需要拷贝可去 github

微前端框架 之 single-spa 从入门到精通_第5张图片

微前端框架 之 single-spa 从入门到精通_第6张图片

微前端框架 之 single-spa 从入门到精通_第7张图片

/src/index.css
body {
   
  text-align: center;
}
启动子应用
npm run start

浏览器访问localhost:3000

基座应用 layout

/micro-frontend目录下新建基座应用,为了简洁明了,新建项目时选择的配置项和子应用一样;在本示例中基座应用采用了vue来实现,用别的方式或者框架实现也可以,比如自己用webpack构建一个项目。

以下操作都在/micro-frontend/layout目录下进行

安装single-spa
npm i single-spa -S
改造基座项目
入口文件
// 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:8081', 'app1'),
    // 当路由满足条件时(返回true),激活(挂载)子应用
    activeWhen: location => location.pathname.startsWith('/app1'),
    // 传递给子应用的对象
    customProps: {
   }
  },
  {
   
    name: 'app2',
    app: loadApp('http://localhost:8082', 'app2'),
    activeWhen: location => location.pathname.startsWith('/app2'),
    customProps: {
   }
  },
  {
   
    // 子应用名称
    name: 'app3',
    // 子应用加载函数,是一个promise
    app: loadApp('http://localhost:3000', 'app3'),
    // 当路由满足条件时(返回true),激活(挂载)子应用
    activeWhen: location => location.pathname.startsWith('/app3'),
    // 传递给子应用的对象,这个很重要,该配置告诉react子应用自己的容器元素是什么,这块儿和vue子应用的集成不一样,官网并没有说这部分,或者我没找到,是通过看single-spa-react源码知道的
    customProps: {
   
      domElement: document.getElementById('microApp'),
      // 添加 name 属性是为了兼容自己写的lyn-single-spa,原生的不需要,当然加了也不影响
      name: 'app3'
    }
  }
]

// 注册子应用
for (let i = apps.length - 1; i >= 0; i--) {
   
  registerApplication(apps[i])
}

new Vue({
   
  router,
  mounted() {
   
    // 启动
    start()
  },
  render: h => h(App)
}).$mount('#app')

App.vue




路由
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

启动基座应用
npm run serve
浏览器访问基座应用
微前端框架 之 single-spa 从入门到精通_第8张图片 微前端框架 之 single-spa 从入门到精通_第9张图片

终于看到了结果。

小技巧

有时候single-spa可能会报一些我们现在无法理解的错误,我们可能需要去做代码调试,阅读源码时碰到不理解的地方也需要编写示例 + 单步调试,但是默认的是已经打包压缩后的代码,不太方便做这些,大家可以在node_modules目录找到single-spa目录,把目录下的package.json中的module字段的值改为lib/single-spa.dev.js,这是一个未压缩的bundle,利于代码的阅读的调试,当然需要重启应用。

子应用也是一样类似的技巧,因为single-spa-vue就一个文件,可以直接拷贝出来放到项目的/src目录下,将main.js中的引入的single-spa-vue改成当前目录即可。

打包部署

打包

在各个项目的根目录下分别执行

npm run build

部署

可以将打包后的bundle发布到nginx服务器上,这个nginx服务器可以是单独的服务器、或者虚拟机、亦或是docker容器都行,这里采用serve在本地模拟部署

如果你有条件部署到nginx上,需要注意nginx的代理配置

  • 对于子应用静态资源的加载只需要拦截相应的前缀将请求转发到对应子应用的目录下即可
  • 页面刷新只需要拦截到主应用即可,主应用内部自己根据activeWhen去挂载对应的子应用

全局安装 serve

npm i serve -g

在各个项目的根目录下启动 serve

serve ./dist -p port

在浏览器访问基座应用的地址,发现得到和刚才一样的结果

single-spa 源码分析

整个阅读过程以示例项目为例,阅读源码时一定要多动手写注释、做笔记,遇到不理解的地方编写示例代码 + console.log + 单步调试,切记不要只看不动手

single-spa 源码阅读思维导图

微前端框架 之 single-spa 从入门到精通_第10张图片

这是我在阅读时整理的一个思维导图,源码中也写了大量的注释,大家可以参照着进行阅读。Ok !!这就开始吧

微前端框架 之 single-spa 从入门到精通_第11张图片

从源码目录中可以看到,single-spa是使用rollup来打包的,从rollup.config.js中可以发现入口是single-spa.js
打开会发现里面导出了一大堆东西,有我们非常熟悉的各个方法,我们就从registerApplication方法开始

registerApplication 注册子应用

single-spa/src/applications/apps.js

/**
 * 注册应用,两种方式
 * registerApplication('app1', loadApp(url), activeWhen('/app1'), customProps)
 * registerApplication({
 *    name: 'app1',
 *    app: loadApp(url),
 *    activeWhen: activeWhen('/app1'),
 *    customProps: {}
 * })
 * @param {*} appNameOrConfig 应用名称或者应用配置对象
 * @param {*} appOrLoadApp 应用的加载方法,是一个 promise
 * @param {*} activeWhen 判断应用是否激活的一个方法,方法返回 true or false
 * @param {*} customProps 传递给子应用的 props 对象
 */
export function registerApplication(
  appNameOrConfig,
  appOrLoadApp,
  activeWhen,
  customProps
) {
   
  /**
   * 格式化用户传递的应用配置参数
   * registration = {
   *    name: 'app1',
   *    loadApp: 返回promise的函数,
   *    activeWhen: 返回boolean值的函数,
   *    customProps: {},
   * }
   */
  const registration = sanitizeArguments(
    appNameOrConfig,
    appOrLoadApp,
    activeWhen,
    customProps
  );

  // 判断应用是否重名
  if (getAppNames().indexOf(registration.name) !== -1)
    throw Error(
      formatErrorMessage(
        21,
        __DEV__ &&
          `There is already an app registered with name ${
     registration.name}`,
        registration.name
      )
    );

  // 将各个应用的配置信息都存放到 apps 数组中
  apps.push(
    // 给每个应用增加一个内置属性
    assign(
      {
   
        loadErrorTime: null,
        // 最重要的,应用的状态
        status: NOT_LOADED,
        parcels: {
   },
        devtools: {
   
          overlays: {
   
            options: {
   },
            selectors: [],
          },
        },
      },
      registration
    )
  );

  // 浏览器环境运行
  if (isInBrowser) {
   
    // https://zh-hans.single-spa.js.org/docs/api#ensurejquerysupport
    // 如果页面中使用了jQuery,则给jQuery打patch
    ensureJQuerySupport();
    reroute();
  }
}

sanitizeArguments 格式化用户传递的子应用配置参数

single-spa/src/applications/apps.js

// 返回处理后的应用配置对象
function sanitizeArguments(
  appNameOrConfig,
  appOrLoadApp,
  activeWhen,
  customProps
) {
   
  // 判断第一个参数是否为对象
  const usingObjectAPI = typeof appNameOrConfig === "object";

  // 初始化应用配置对象
  const registration = {
   
    name: null,
    loadApp: null,
    activeWhen: null,
    customProps: null,
  };

  if (usingObjectAPI) {
   
    // 注册应用的时候传递的参数是对象
    validateRegisterWithConfig(appNameOrConfig);
    registration.name = appNameOrConfig.name;
    registration.loadApp = appNameOrConfig.app;
    registration.activeWhen = appNameOrConfig.activeWhen;
    registration.customProps = appNameOrConfig.customProps;
  } else {
   
    // 参数列表
    validateRegisterWithArguments(
      appNameOrConfig,
      appOrLoadApp,
      activeWhen,
      customProps
    );
    registration.name = appNameOrConfig;
    registration.loadApp = appOrLoadApp;
    registration.activeWhen = activeWhen;
    registration.customProps = customProps;
  }

  // 如果第二个参数不是一个函数,比如是一个包含已经生命周期的对象,则包装成一个返回 promise 的函数
  registration.loadApp = sanitizeLoadApp(registration.loadApp);
  // 如果用户没有提供 props 对象,则给一个默认的空对象
  registration.customProps = sanitizeCustomProps(registration.customProps);
  // 保证activeWhen是一个返回boolean值的函数
  registration.activeWhen = sanitizeActiveWhen(registration.activeWhen);

  // 返回处理后的应用配置对象
  return registration;
}

validateRegisterWithConfig

single-spa/src/applications/apps.js

/**
 * 验证应用配置对象的各个属性是否存在不合法的情况,存在则抛出错误
 * @param {*} config = { name: 'app1', app: function, activeWhen: function, customProps: {} }
 */
export function validateRegisterWithConfig(config) {
   
  // 异常判断,应用的配置对象不能是数组或者null
  if (Array.isArray(config) || config === null)
    throw Error(
      formatErrorMessage(
        39,
        __DEV__ && "Configuration object can't be an Array or null!"
      )
    );
  // 配置对象只能包括这四个key
  const validKeys = ["name", "app", "activeWhen", "customProps"];
  // 找到配置对象存在的无效的key
  const invalidKeys = Object.keys(config).reduce(
    (i

你可能感兴趣的:(微前端,前端框架,vue.js,前端)