微前端比你想象的要简单

2,087 阅读7分钟

前言

前段时间我接到老板的一个需求,内容是:把杂乱的多个活动的项目整理,抽离公用组件、模块、插件等文件为一个公共资源库 base 。这...,原本是单独的项目,需要共用相同组件库,只能把活动项目整合成一个多页面应用了,可是这不就又饶回去了吗?

当初拆开就是希望每个应用可以独自选技术栈、独自开发、独自部署。难道真的要开启 npm 私有库吗?可是私有 npm 有很大的一个缺点是版本维护,每个组件更新,若旧项目需要使用新的功能必须要同步更新,而且要搭一个私有库也挺麻烦的!

那么如何实现既能实现应用独立管理、开发和部署,又可以共享一些公共模块?答案就是这个文章的主题微前端技术架构!

什么是微前端?

微前端,灵感来源于后端的微服务,而前端模块越来越多,代码变得越来越难打理,为了便于维护而产生一种微前端架构。 是一种由独立交付的多个前端应用组成整体的架构风格。 具体的,将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的小块,而在用户看来仍然是内聚的单个产品。

常见微前端实现方案

  1. iframe
  • 优点:实现简单,能够实现css、js等资源隔离,具备独立开发、维护、部署等微前端的特征。
  • 缺点:应用上下文无法被共享,应用之间不能共享状态,公共资源文件也无法共享。
  1. web Components
  • 优点:不限技术栈、具备应用隔离、独立开发等特点。
  • 缺点:兼容性不好。
  1. ESM(ES Module)
  • 优点:不限技术栈、具备独立开发、远程加载模块等特点。
  • 缺点:需要结合打包工具解决兼容性问题。
  1. qiankun
  • 优点:不限技术栈、css、js资源隔离、HTML Entry 的接入方式、资源预加载等特点。
  • 缺点:必须要有基座作为底盘,适合大项目拆分小应用的场景。
  1. EMP
  • 优点:完全满足了微前端、独立开发/部署特性,同时具备去中心化,应用间共享状态,跨技术栈组件调用,共享第三方依赖等特点。
  • 缺点:基本没有缺点,但需要一些学习和入手成本。

发现宝藏 - module federation

经过上文微前端实现方案对比,得出结论: EMP 的技术方案是最符合预期的,对 EMP 做深一步发研究现,其实它是利用 webpack5 的新功能 module federation 封装了一个可以自选技术栈模版的 cli。没错,在这里我发现了实现微前端的宝藏工具 module federation!

module federation 是什么?

module federation 使 JavaScript 应用得以在客户端或服务器上动态运行另一个 bundle 的代码。 其有两个重要的概念:

  • Remote,被 Host 消费的 Webpack 构建;
  • Host,消费其他 Remote 的 Webpack 构建; module federation 配置示例:
new ModuleFederationPlugin({
 name: "app1",
 library: { type: "var", name: "app1" },
 filename: "remoteEntry.js",
 remotes: {
    app2: 'app2',
},
  exposes: {
    './button': './src/button',  
},
  shared: ['vue', 'vuex'],
}),

配置属性:

name,必须,唯一 ID,作为输出的模块名,使用的时通过 name/{name}/{expose} 的方式使用;
library,必须,其中这里的 name 为作为 umd 的 name;
remotes,可选,表示作为 Host 时,去消费哪些 Remote;
exposes,可选,表示作为 Remote 时,export 哪些属性被消费;
shared,可选,优先用 Host 的依赖,如果 Host 没有,再用自己的;

module federation 的原理

传送门 三大应用场景调研,Webpack 新功能 Module Federation 深入解析

基于 module federation 实战微前端

本次实战是我实现前言说的老板需求:抽离公用组件、模块、插件等文件为一个公共资源库 base 的例子。

实现思路是利用 module federation 技术把 base 资源抽离一个 base 应用 activity-base,activity-base 通过 exposes 暴露公共文件给其他应用使用,同时又可以通过 shared 共享一些工具插件比如 vue、vuex、axios。

然后每个活动应用可以通过 remote 导入 activity-base 的公共组件,这样达到了共享资源的能力,同时又不会重复打包这些组件和公共依赖。

创建基础资源应用

安装最新的版本的 webpack,引入 ModuleFederationPlugin 插件,具体 activity-base 的 webpack 以下配置:

const path = require('path')
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin')
const packageJsonDeps = require('./package.json').dependencies
module.exports = {
  .....
  plugins: [
    new ModuleFederationPlugin({
      name: 'activityBase',
      library: { type: 'var', name: 'activityBase' },
      filename: 'activityBaseEntry.js',
      // 暴露的公共文件
      exposes: {
        './Toast': './src/components/Toast.vue',
        './utils':'./src/global/utils.js',
        './analyse':'./src/global/analyse.js',
        './api':'./src/global/api/api.js',
      },
      shared: {
        ...packageJsonDeps,
        vue: {
          singleton: true,
          eager: true,
          requiredVersion: packageJsonDeps.vue
        }
      }
    }),
   .....
  ]
}

应用基础资源

activtiy 应用 vue.config.js 配置

const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin')
module.exports = {
  productionSourceMap:false,
  publicPath: process.env.BASE_URL || './',
  chainWebpack: (config) => {
    config
    .plugin('module-federation-plugin')
    .use(ModuleFederationPlugin, [{
      name: 'activityProject',
      library: { type: 'var', name: 'activityProject' },
      filename: 'activityProjectEntry.js',
      remotes:{'activityBase':'activityBase'} // 导入远程资源
  }])   
}
}

html 引用资源

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
    <title>活动 app</title>
    <script src="//localhost:8080/activityBaseEntry.js"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

组件、模块在项目中引入

导入 toast 组件

<template>
  <div id="app">
    <Toast v-if="$store.state.toast.show"/>
    <router-view />
  </div>
</template>
<script>
import Toast from 'activityBase/Toast' 
export default {
  components:{
    Toast
  }
}
</script>
<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

</style>

实战结果

使用 ModuleFederation Plugin 前,文件大小约为 1.4M image.png 使用 ModuleFederation Plugin 后,文件大小约为 359KB

image.png

文章参考

帮你对比多种微前端方案

三大应用场景调研,Webpack 新功能 Module Federation 深入解析

补充

上面抽离的公共资源库,其实跟SDK的作用很像,但它们在使用方面的目标和实践方法上有一些关键区别:

  1. SDK 抽离公共资源:

    SDK(Software Development Kit)是一种包含了一组工具、库、相关文档和代码示例的软件包,可以帮助开发者更方便地创建应用程序、软件或者进行其他类型的开发。

    当我们谈论“SDK 抽离公共资源”,这通常意味着通过创建一个 SDK 来封装那些在很多不同应用中都会被用到的公共组件、功能或服务等。这样,开发者可以在不同的项目中重复使用这些公共资源,而无需每次都从零开始开发。这种方式的一个优点是,每当公共资源更新或发生变化时,只需要更新一次 SDK,然后在所有需要这些资源的项目中部署这个新的 SDK。

  2. Module Federation 抽离公共资源:

    Module Federation 是 Webpack 5 的一个新功能,它允许多个独立构建的 JavaScript 应用在运行时共享 JavaScript 模块。在 Webpack 的早期版本中,开发者可以在应用中通过外部脚本标签或其他类型的加载器来共享一些公共库,但这个过程经常需要手动操作,而且不够灵活。

    使用 Module Federation,你可以把一些公共的 JavaScript 模块展示为 "federated" modules,其他的应用在运行时可以动态地从你的应用中加载并使用这些模块。这意味着,你可以不需要重新打包和发布你的应用,就能实时地更新和共享你的代码。这在需要频繁更新代码或者在微前端架构中是非常有用的工具。

总的来说,"SDK 抽离公共资源" 是一种更传统、更静态的方式用于共享代码和资源,适用于不需要频繁改动的公共资源。而 "Module Federation 抽离公共资源" 是一种适用于需要动态和实时共享代码的场景,尤其在微前端架构中更具优势。两者各有适用场景,选择哪种方式主要取决于你的具体需求和情况。