微前端与微模块

微前端是一种泛称,Elux项目中使用颗粒度更小的微模块来实现微前端,参见微模块

在前面《实例分析》中我们讲解了一个单体工程的实例,而微模块真正的魅力是可以多Team合作开发、独立上线、实现粒度更细的微前端。

假设我们有3个Team来合作开发这个项目,他们是微模块的生产者

  1. basic-team 负责开发模块:stage
  2. article-team 负责开发模块:article、shop
  3. user-team 负责开发模块:admin、my
  • 每个Team都是一个独立工程,可以单独上线运行。
  • 每个Team都将开发的微模块作为npm包发布到公司内部私有NPM仓库中。

另外还有2个Team根据业务需求来组合这些微模块,他们是微模块的消费者

  1. app-build-team 采用静态编译(node_modules)的方式集成
  2. app-runtime-team 采用动态注入(module_federation)的方式集成

将微模块定义成NPM包

为了跨工程使用“微模块”,我们将微模块定义成NPM包。
方法很简单:在每个微模块下面创建一个package.json

src
├── modules
│      ├── article
│      │     ├── ...
│      │     └── package.json
│      ├── shop
│      │     ├── ...
│      │     └── package.json
│      ├── my
│      │     ├── ...
│      │     └── package.json
│      ├── admin
│      │     ├── ...
│      │     └── package.json
│      └── stage
│            ├── ...
│            └── package.json
//src/modules/article/package.json
{
  "name": "@test-project/article",
  "version": "1.0.0",
  "main": "index.ts",
  "peerDependencies": {
    "@test-project/stage": "*",
    "@elux/react-web": "*",
    "react": "*",
    "path-to-regexp": "*"
  }
}

使用Lerna+Monorepo管理

一个工程可以生产或消费多个“微模块”,我们使用Lerna+Monorepo工程结构来管理。各微模块开发好之后,使用Lerna来统一发布到私有NPM仓库。

注意,可以直接将各模块的源码发布,无需编译打包。

//lerna.json
{
  "version": "1.0.0",
  "npmClient": "yarn",
  "useWorkspaces": true,
  "packages": [
    "src/modules/*"
  ]
}

修改Import路径

因为src/modules/下面的微模块都将发布到npm,所以在import路径上必需注意:

  • 跨微模块import请使用真实的npm包名,不要使用相对路径或者alias
  • 微模块内部的相互import可以使用相对路径
//跨微模块import使用`npm包名`,不使用`相对路径`或者`alias`
//import {mergeDefaultParams} from '@modules/stage/utils/tools';
import {mergeDefaultParams} from '@test-project/stage/utils/tools';

basic-team工程

src
├── modules
│      └── stage   //基座模块
├── Project.ts  //微模块源配置
└── index.ts    //App入口文件

article-team工程

src
├── modules
│      ├── shop    //商品模块
│      └── article //文章模块
├── Project.ts  //微模块源配置
└── index.ts    //App入口文件

user-team工程

src
├── modules
│      ├── my    //个人中心模块
│      └── admin //鉴权模块
├── Project.ts  //微模块源配置
└── index.ts    //App入口文件

组合微模块

以上3个Team负责生产微模块(积木),下面我们来看如何消费微模块(搭积木)。

在前文《微模块》中我们说过,微模块的使用有2种方案:

  • 静态编译:微模块作为一个NPM包被安装到工程中,通过打包工具(如webpack)正常编译打包即可。这种方式的优点是代码产物得到打包工具的各种去重和优化;缺点是当某个模块更新时,需要整体重新打包。
  • 动态注入:利用ModuleFederation,将微模块作为子应用独立部署,与时下流行的微前端类似。这种方式的优点是某子应用中的微模块更新时,依赖该微模块的其它应用无需重新编译,刷新浏览器即可动态获取最新模块;缺点是没有打包工具的整体编译与优化,代码和资源容易重复加载或冲突。

micro-install.png

app-build-team工程

我们假设app-build-team使用静态编译方案来使用微模块。

  1. 建立app-build-team工程,主要结构如下:
├── src
│    ├── Project.ts //微模块源配置
│    └── index.ts   //App入口文件
└── package.json    //写入微模块依赖
  1. npm install所需的微模块:
{
    "name": "app-build-team",
    "dependencies": {
        ...
        "@test-project/stage": "^1.0.0",
        "@test-project/article": "^1.0.0",
        "@test-project/shop": "^1.0.0",
        "@test-project/admin": "^1.0.0",
        "@test-project/my": "^1.0.0"
    },
}
  1. 配置微模块源:
// src/Project.ts

import stage from '@test-project/stage';

export const ModuleGetter = {
  stage: () => stage, //通常stage为根模块,使用同步加载
  article: () => import('@test-project/article'),
  shop: () => import('@test-project/shop'),
  admin: () => import('@test-project/admin'),
  my: () => import('@test-project/my'),
};

  1. 正常打包运行就好,跟单体工程的唯一区别就是:微模块代码来自于node_modules

app-runtime-team工程

我们假设app-runtime-team使用Module-Federation方案来使用微模块,先简单回顾一下Webpack5的Module-Federation:

mfd-site.png

简单来说Module-Federation就是提供了一种Module跨站点实时共享的手段,它分2个角色:Module提供者Module消费者,如图所示:

  • SiteA作为模块Modulex的提供者。
  • SiteB作为模块Modulex的消费者。

当SiteA更新ModuleX时,SiteB无需重新构建,刷新浏览器即可从SiteA拉取最新的ModuleX

同时作为提供者和消费者

站点可以生产某些模块提供给其它站点使用,同时也可以使用其它站点提供的某些模块,如图:

mfd-site3.png

  • SiteA提供了ModuleA、ModuleB,同时消费了ModuleD、ModuleE
  • SiteB提供了ModuleC、ModuleD,同时消费了ModuleA、ModuleF
  • SiteC提供了ModuleE、ModuleF,同时消费了ModuleA、ModuleB、ModuleC

从Module-Federation到微前端

SiteA、SiteB、SiteC三个站点就组合成了我们所谓的“微前端”系统,可以看出其中2个关键点:

  1. 如何提供和加载Module,这个就是Webpack-Module-Federation所能解决的问题。
  2. 如何划分Module,这个就是Elux中微模块所能解决的问题。

工程结构

app-runtime-team工程结构与app-build-team基本一致,稍有变动如下:

  1. 打开elux.config.json,配置ModuleFederation:
// elux.config.json
{
  moduleFederation: {
    name: 'app-runtime',
    modules: {
      '@test-project/article': '@article-team/modules/article',
      '@test-project/shop': '@article-team/modules/shop',
      '@test-project/admin': '@user-team/modules/admin',
      '@test-project/my': '@user-team/modules/my',
    },
    remotes: {
      '@article-team': 'articleTeam@http://localhost:4001/client/remote.js',
      '@user-team': 'userTeam@http://localhost:4002/client/remote.js',
    },
    shared: {
      'react': {singleton: true, eager: true, requiredVersion: '*'},
      'react-dom': {singleton: true, eager: true, requiredVersion: '*'},
      '@elux/react-web': {singleton: true, eager: true, requiredVersion: '*'},
    },
  },
}
  1. 将原src/index.ts改名为bootstrap.ts,并重新建立src/index.ts
// src/index.ts
import bootstrap from './bootstrap';

bootstrap(() => undefined);
  1. 同时运行article-team、user-team、app-runtime-team,可以看到各微模块的代码来自于线上remote.js

源码示例

  • 运行工程向导:npm create elux@latestyarn create elux
  • 选择简单示例模板
  • 然后选择基于Webpack5的微前端 + 微模块方案
? 请选择:平台架构 
  CSR: 基于浏览器渲染的Web应用 
  SSR: 基于服务器渲染 + 浏览器渲染的同构应用 
❯ Micro: 基于Webpack5的微前端 + 微模块方案 
  Model: 基于模型驱动,React与Vue跨项目共用Model 
  Taro: 基于Taro的跨平台应用(各类小程序) 
  RN: 基于ReactNative的原生APP(开发中...) 

示例为了演示方便,将各Team放在一个Monorepo工程中管理,实际中各Team是独立的工程。