阅读 194

微前端的一些探索与实践

本文作者:胡亚杰

什么是微前端?

微前端(Micro-Frontends)是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立运行、独立开发、独立部署。微前端不是单纯的前端框架或者工具,而是一套架构体系

可以由多个团队独立开发的现代web应用程序的技术、策略和方案。

可以跟微服务这么对比着去理解:

微服务微前端
一个微服务就是由一组接口构成,接口地址一般是 URL。当微服务收到一个接口的请求时,会进行路由找到相应的逻辑,输出响应内容。一个微前端则是由一组页面构成,页面地址也是 URL。当微前端收到一个页面 URL 的请求时,会进行路由找到相应的组件,渲染页面内容。
后端微服务会有一个网关,作为单一入口接收所有的客户端接口请求,根据接口 URL 与服务的匹配关系,路由到对应的服务。微前端则会有一个加载器,作为单一入口接收所有页面 URL 的访问,根据页面 URL 与微前端的匹配关系,选择加载对应的微前端,由该微前端进行进行路由响应 URL。

微前端的价值

微前端架构具备以下几个核心价值:

技术栈无关:主框架不限制接入应用的技术栈,子应用具备完全自主权

独立开发、独立部署:子应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新

独立运行时:每个子应用之间状态隔离,运行时状态不共享

微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用( Frontend Monolith )后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。

实现微前端有哪些方案

  1. iframe

Web Components
ESM
single-spa、qiankun
EMP

  1. 各解决方案的利弊:
  • iframe可以直接加载其他应用,但无法做到单页导致许多功能无法正常在主应用中展示。

  • web Components

    1. 技术栈无关:Web Components是浏览器原生组件,那即是在任何框架中都可以使用。
    2. 独立开发:使用Web Components开发的应用无需与其他应用间产生任何关联。
    3. 应用间隔离: Shadow DOM的特性,各个引入的微应用间可以达到相互隔离的效果。
  • ESM

    1. 无技术栈限制:ESM加载的只是js内容,无论哪个框架,最终都要编译成js,因此,无论哪种框架,ESM都能加载。
    2. 应用单独开发: ESM只是js的一种规范,不会影响应用的开发模式。
    3. 多应用整合: 只要将微应用以ESM的方式暴露出来,就能正常加载。
    4. 远程加载模块: ESM能够直接请求cdn资源,这是它与生俱来的能力。
  • single-spa、qiankun基本上可以称为单页版的iframe,具有沙箱隔离及资源预加载的特点,几乎无可挑剔。但可能存在以下几点不足:

    1. 对于React 深度定制项目来说,无法做到状态管理很好的传递
    2. 对于非标准的AMD、UMD、SystemJS 等加载方式的库会存在依赖问题(需要针对性改造)
    3. 多框架实现体积过大以及存在一定的调试成本
  • EMP作为最年轻微前端解决方案,也是吸收了许多web优秀特性才诞生的,它在实现微前端的基础上,扩充了跨应用状态共享、跨框架组件调用、远程拉取ts声明文件、动态更新微应用等能力。同时,细心的小伙伴应该已经发现,EMP能做到第三方依赖的共享,使代码尽可能地重复利用,减少加载的内容。

以下表格为各解决方案的总结:

方案描述缺点
iframe天生隔离样式与脚本、多页
不是单页应用,会导致浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用

弹框类的功能无法应用到整个大应用中,只能在对应的窗口内展示

由于可能应用间不是在相同的域内,主应用的 cookie 要透传到根域名都不同的子应中才能实现免登录效果

每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程,占用大量资源的同时也在极大地消耗资源

iframe的特性导致搜索引擎无法获取到其中的内容,进而无法实现应用的seo
Web Components天生隔离样式与脚本无法兼容所有浏览器
ESM远程加载模块无法兼容所有浏览器(但可以通过编译工具解决)
需手动隔离样式(可通过css module解决)
single-spa、qiankunHTML Entry 接入方式
资源预加载
-
EMP每个微应用独立部署运行
动态更新微应用
应用间通信
-

single-spa

single-spa实现原理:

首先对微前端路由进行注册,使用single-spa充当微前端加载器,并作为项目单一入口来接受所有页面URL的访问,根据页面URL与微前端的匹配关系,选择加载对应的微前端模块,再由该微前端模块进行路由响应URL,即微前端模块中路由找到相应的组件,渲染页面内容。

基座(vue)

在基座里我们调用single-spa提供给我们的registerApplication和start的方法 1.registerApplication参数有四个个appNameOrConfig、appOrLoadApp、activeWhen、customProps。分别对应的是注册的子项目名和一些配置。加载子项目时执行的fn、执行的规则、和通信的参数


import * as singleSpa from 'single-spa' //导入single-spa
import axios from 'axios'

/*
 * 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)
  })
}

/*
 * getManifest:远程加载manifest.json 文件,解析需要加载的js
 * */
const getManifest = (url, bundle) =>
  new Promise(async (resolve) => {
    const { data } = await axios.get(url)
    const { entrypoints, publicPath } = data
    const assets = entrypoints[bundle].assets
    for (let i = 0; i < assets.length; i++) {
      await runScript(publicPath + assets[i]).then(() => {
        if (i === assets.length - 1) {
          resolve()
        }
      })
    }
  })

singleSpa.registerApplication(
  //注册微前端服务
  'app1',
  async () => {
    // 注册用函数,
    // return 一个singleSpa 模块对象,模块对象来自于要加载的js导出
    // 如果这个函数不需要在线引入,只需要本地引入一块加载:
    // () => import('xxx/main.js')
    let singleVue = null
    await getManifest('http://127.0.0.1:9000/manifest.json', 'app').then(() => {
      singleVue = window.singleVue
    })
    return singleVue
  },
  (location) => location.pathname.startsWith('/app1') // 配置微前端模块前缀
)

singleSpa.registerApplication(
  //注册微前端服务
  'app2',
  async () => {
    // 注册用函数,
    // return 一个singleSpa 模块对象,模块对象来自于要加载的js导出
    // 如果这个函数不需要在线引入,只需要本地引入一块加载:
    // () => import('xxx/main.js')
    let singleVue = null
    await getManifest('http://127.0.0.1:9001/manifest.json', 'app').then(() => {
      singleVue = window.singleVue
    })
    return singleVue
  },
  (location) => location.pathname.startsWith('/app2') // 配置微前端模块前缀
)

singleSpa.start() // 启动


复制代码

子项目

子项目最重要的就是提供三个方法 bootstrap、mount、unmount 和 打包格式

这里借助了社区提供的single-spa-vue、react就使用single-spa-react。它会默认导出三个方法。


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 vueOptions = {
  el: "#vue",
  router,
  store,
  render: h => h(App)
};

// 判断当前页面使用singleSpa应用,不是就渲染
if (!window.singleSpaNavigate) {
  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;

复制代码

配置manifest.json 让组项目自动导入项目


  ...
  output: {
        library: "singleVue",
        libraryTarget: "umd",
    },
    plugins: [
        new StatsPlugin('manifest.json', {
            chunkModules: false,
            entrypoints: true,
            source: false,
            chunks: false,
            modules: false,
            assets: false,
            children: false,
            exclude: [/node_modules/]
        }),
    ],
  .../
复制代码

qiankun

核心设计理念

  1. 简单

由于主应用微应用都能做到技术栈无关,qiankun 对于用户而言只是一个类似 jQuery 的库,你需要调用几个 qiankun 的 API 即可完成应用的微前端改造。同时由于 qiankun 的 HTML entry 及沙箱的设计,使得微应用的接入像使用 iframe 一样简单。

  1. 解耦/技术栈无关

微前端的核心目标是将巨石应用拆解成若干可以自治的松耦合微应用,而 qiankun 的诸多设计均是秉持这一原则,如 HTML entry、沙箱、应用间通信等。这样才能确保微应用真正具备 独立开发、独立运行 的能力。

特性

  1. 基于 single-spa 封装,提供了更加开箱即用的 API。
  2. 技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
  3. HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
  4. 样式隔离,确保微应用之间样式互相不干扰。
  5. JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
  6. 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。

基座

注册微应用

当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。


import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
  {
    name: 'react app', // app name registered
    entry: '//localhost:7100',
    container: '#yourContainer',
    activeRule: '/yourActiveRule',
  },
  {
    name: 'vue app',
    entry: { scripts: ['//localhost:7100/main.js'] },
    container: '#yourContainer2',
    activeRule: '/yourActiveRule2',
  },
]);
start();


复制代码

子应用

导出相应的生命周期钩子

微应用需要在自己的入口 js (通常就是你配置的 webpack 的 entry js) 导出 bootstrap、mount、unmount 三个生命周期钩子,以供主应用在适当的时机调用。


/**
 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
 */
export async function bootstrap() {
  console.log('react app bootstraped');
}
/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props) {
  ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
}
/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount(props) {
  ReactDOM.unmountComponentAtNode(props.container ? props.container.querySelector('#root') : document.getElementById('root'));
}


复制代码

子应用的webpack 配置

除了代码中暴露出相应的生命周期钩子之外,为了让主应用能正确识别微应用暴露出来的一些信息,微应用的打包工具需要增加如下配置:


const packageName = require('./package.json').name;
module.exports = {
  output: {
    library: `${packageName}-[name]`,
    libraryTarget: 'umd',
    jsonpFunction: `webpackJsonp_${packageName}`,
  },
};


复制代码

module-Federation

  1. Module Federation是什么

Module Federation中文直译为“模块联邦”,而在webpack官方文档中,其实并未给出其真正含义,但给出了使用该功能的motivation, 即动机,原文如下

::: tip Multiple separate builds should form a single application. These separate builds should not have dependencies between each other, so they can be developed and deployed individually.

This is often known as Micro-Frontends, but is not limited to that.
::: 翻译成中文即
::: tip 多个独立的构建可以形成一个应用程序。这些独立的构建不会相互依赖,因此可以单独开发和部署它们。 这通常被称为微前端,但并不仅限于此。

:::

结合以上,不难看出,mf实际想要做的事,便是把多个无相互依赖、单独部署的应用合并为一个。通俗点讲,即mf提供了能在当前应用中远程加载其他服务器上应用的能力。对此,可以引出下面两个概念:

  • host:引用了其他应用的应用
  • remote:被其他应用所使用的应用
mixureSecure

鉴于mf的能力,我们可以完全实现一个去中心化的应用部署群:每个应用是单独部署在各自的服务器,每个应用都可以引用其他应用,也能被其他应用所引用,即每个应用可以充当host的角色,亦可以作为remote出现,无中心应用的概念。

mixureSecure

Module Federation如何使用

配置示例:


const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

module.exports = {
  // 其他webpack配置...
  plugins: [
    new ModuleFederationPlugin({
        name: 'Base',
        library: { type: 'var', name: 'Base' },
        filename: 'sec.js',
        remotes: {
          app_two: "app_two_remote",
          app_three: "app_three_remote"
        },
        exposes: {
          './Component1': 'src/components/Component1',
          './Component2': 'src/components/Component2',
        },
        shared: ["react", "react-dom","react-router-dom"]
      })
  ]
}


复制代码

通过以上配置,我们对mf有了一个初步的认识,即如果要使用mf,需要配置好几个重要的属性:

字段名类型含义
namestring必传值,即输出的模块名,被远程引用时路径为name/{name}/{expose}
libraryobject声明全局变量的方式,name为umd的name
filenamestring构建输出的文件名
remotesobject远程引用的应用名及其别名的映射,使用时以key值作为name
exposesobject被远程引用时可暴露的资源路径及其别名
sharedobject与其他应用之间可以共享的第三方依赖,使你的代码中不用重复加载同一份依赖
  1. Module Federation的构建解析

var moduleMap = {
	"./Content": () => {
		return Promise.all([__webpack_require__.e(252), __webpack_require__.e(230)]).then(() => () => (__webpack_require__(230)));
	},
	"./Button": () => {
		return Promise.all([__webpack_require__.e(252), __webpack_require__.e(347)]).then(() => () => (__webpack_require__(347)));
	}
};
var get = (module, getScope) => {
	__webpack_require__.R = getScope;
	getScope = (
		__webpack_require__.o(moduleMap, module)
			? moduleMap[module]()
			: Promise.resolve().then(() => {
				throw new Error('Module "' + module + '" does not exist in container.');
			})
	);
	__webpack_require__.R = undefined;
	return getScope;
};
var init = (shareScope, initScope) => {
	if (!__webpack_require__.S) return;
	var oldScope = __webpack_require__.S["default"];
	var name = "default"
	if(oldScope && oldScope !== shareScope) throw new Error("Container initialization failed as it has already been initialized with a different share scope");
	__webpack_require__.S[name] = shareScope;
	return __webpack_require__.I(name, initScope);
};

复制代码

代码中包括三个部分:

  • moduleMap:通过exposes生成的模块集合
  • get: host通过该函数,可以拿到remote中的组件
  • init:host通过该函数将依赖注入remote中

如下总结:

  • 首先,mf会让webpack以filename作为文件名生成文件
  • 其次,文件中以var的形式暴露了一个名为name的全局变量,其中包含了exposes以及shared中配置的内容
  • 最后,作为host时,先通过remote的init方法将自身shared写入remote中,再通过get获取remote中expose的组件,而作为remote时,判断host中是否有可用的共享依赖,若有,则加载host的这部分依赖,若无,则加载自身依赖。
  1. Module Federation的应用场景有哪些
  • 微前端:通过shared以及exposes可以将多个应用引入同一应用中进行管理
  • 资源复用,减少编译体积:可以将多个应用都用到的通用组件单独部署,通过mf的功能在runtime时引入到其他项目中,这样组件代码就不会编译到项目中,同时亦能满足多个项目同时使用的需求,一举两得。
文章分类
前端
文章标签