微前端与微模块
微前端是一种泛称,Elux项目中使用颗粒度更小的微模块
来实现微前端,参见微模块。
在前面《实例分析》中我们讲解了一个单体工程
的实例,而微模块真正的魅力是可以多Team合作开发、独立上线、实现粒度更细的微前端。
假设我们有3个Team来合作开发这个项目,他们是微模块的生产者:
basic-team
负责开发模块:stagearticle-team
负责开发模块:article、shopuser-team
负责开发模块:admin、my
- 每个Team都是一个独立工程,可以单独上线运行。
- 每个Team都将开发的微模块作为
npm包
发布到公司内部私有NPM仓库中。
另外还有2个Team根据业务需求来组合这些微模块,他们是微模块的消费者:
app-build-team
采用静态编译(node_modules)的方式集成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
,将微模块作为子应用独立部署,与时下流行的微前端类似。这种方式的优点是某子应用中的微模块更新时,依赖该微模块的其它应用无需重新编译,刷新浏览器即可动态获取最新模块;缺点是没有打包工具的整体编译与优化,代码和资源容易重复加载或冲突。
app-build-team工程
我们假设app-build-team使用静态编译
方案来使用微模块。
- 建立app-build-team工程,主要结构如下:
├── src
│ ├── Project.ts //微模块源配置
│ └── index.ts //App入口文件
└── package.json //写入微模块依赖
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"
},
}
- 配置微模块源:
// 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'),
};
- 正常打包运行就好,跟单体工程的唯一区别就是:微模块代码来自于
node_modules
。
app-runtime-team工程
我们假设app-runtime-team使用Module-Federation
方案来使用微模块,先简单回顾一下Webpack5的Module-Federation:
简单来说Module-Federation就是提供了一种Module跨站点实时共享
的手段,它分2个角色:Module提供者和Module消费者,如图所示:
- SiteA作为模块Modulex的提供者。
- SiteB作为模块Modulex的消费者。
当SiteA更新ModuleX
时,SiteB无需重新构建,刷新浏览器即可从SiteA拉取最新的ModuleX
。
同时作为提供者和消费者
站点可以生产某些模块提供给其它站点使用,同时也可以使用其它站点提供的某些模块,如图:
- SiteA提供了ModuleA、ModuleB,同时消费了ModuleD、ModuleE
- SiteB提供了ModuleC、ModuleD,同时消费了ModuleA、ModuleF
- SiteC提供了ModuleE、ModuleF,同时消费了ModuleA、ModuleB、ModuleC
从Module-Federation到微前端
SiteA、SiteB、SiteC三个站点就组合成了我们所谓的“微前端”系统,可以看出其中2个关键点:
- 如何提供和加载Module,这个就是Webpack-Module-Federation所能解决的问题。
- 如何划分Module,这个就是Elux中
微模块
所能解决的问题。
工程结构
app-runtime-team工程结构与app-build-team
基本一致,稍有变动如下:
- 打开
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: '*'},
},
},
}
- 将原
src/index.ts
改名为bootstrap.ts
,并重新建立src/index.ts
// src/index.ts
import bootstrap from './bootstrap';
bootstrap(() => undefined);
- 同时运行article-team、user-team、app-runtime-team,可以看到各微模块的代码来自于线上
remote.js
。
源码示例
- 运行工程向导:
npm create elux@latest
或yarn create elux
- 选择
简单示例模板
- 然后选择
基于Webpack5的微前端 + 微模块方案
? 请选择:平台架构
CSR: 基于浏览器渲染的Web应用
SSR: 基于服务器渲染 + 浏览器渲染的同构应用
❯ Micro: 基于Webpack5的微前端 + 微模块方案
Model: 基于模型驱动,React与Vue跨项目共用Model
Taro: 基于Taro的跨平台应用(各类小程序)
RN: 基于ReactNative的原生APP(开发中...)
示例为了演示方便,将各Team放在一个Monorepo工程中管理,实际中各Team是独立的工程。