微前端起源
微前端的概念最早由 thoughtworks 在 2016 年提出。其核心思路是借鉴后端微服务架构理念,将一个单体的庞大的前端应用拆分为多个简单独立的前端工程。每个前端工程可以独立开发、测试、部署。最终再由一个容器应用,将拆分后的微前端工程组合为一个整体,面向用户提供服务
微前端的价值
- 技术栈无关
主框架不限制接入应用的技术栈,子应用具备完全自主权 - 独立开发、独立部署
子应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新 - 独立运行时
每个子应用之间状态隔离,运行时状态不共享
微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。
解决方案:
MPA: 多页面应用(Multi page web application)
SPA: 单页面应用(Single page web appliction)
MPA:
- 优点: 部署简单、各应用之间硬隔离,天生具备技术栈无关、独立开发、独立部署的特性。
- 缺点则也很明显,应用之间切换会造成浏览器重刷,由于产品域名之间相互跳转,流程体验上会存在断点。
SPA
- 优点: 则天生具备体验上的优势,应用直接无刷新切换,能极大的保证多产品之间流程操作串联时的流程性。
- 缺点则在于各应用技术栈之间是强耦合的。
常见的实现方式
- 路由分发式。通过 HTTP 服务器的反向代理功能,来将请求路由到对应的应用上。
- 前端微服务化。在不同的框架之上设计通讯、加载机制,以在一个页面内加载对应的应用。
- 微应用。通过软件工程的方式,在部署构建环境中,组合多个独立应用成一个单体应用。
- 微件化。开发一个新的构建系统,将部分业务功能构建成一个独立的 chunk 代码,使用时只需要远程加载即可。
- 前端容器化。通过将 iFrame 作为容器,来容纳其它前端应用。
- 应用组件化。借助于 Web Components 技术,来构建跨框架的前端应用。
路由分发式
路由分发式微前端,即通过路由将不同的业务分发到不同的、独立前端应用上。其通常可以通过 HTTP 服务器的反向代理来实现,又或者是应用框架自带的路由来解决。
前端微服务化
前端微服务化,是微服务架构在前端的实施,每个前端应用都是完全独立(技术栈、开发、部署、构建独立)、自主运行的,最后通过模块化的方式组合出完整的前端应用。其
组合式集成:微应用化
微应用化,即在开发时,应用都是以单一、微小应用的形式存在,而在运行时,则通过构建系统合并这些应用,组合成一个新的应用。
微件化
微件(widget),指的是一段可以直接嵌入在应用上运行的代码,它由开发人员预先编译好,在加载时不需要再做任何修改或者编译。
前端容器化
前端容器 iframe 或 web components
几种实现方式对比
Systemjs模块化解决方案
https://github.com/systemjs/systemjs
systemjs 是一个最小系统加载工具,用来创建插件来处理可替代的场景加载过程,包括加载 CSS 场景和图片,主要运行在浏览器和 NodeJS 中。它是 ES6 浏览器加载程序的的扩展,将应用在本地浏览器中。通常创建的插件名称是模块本身,要是没有特意指定用途,则默认插件名是模块的扩展名称。
通常它支持创建的插件种类有:
1 | // CSS |
示例
1 | <script src="system.js"></script> |
webpack5 Module Federation
https://indepth.dev/posts/1173/webpack-5-module-federation-a-game-changer-in-javascript-architecture
1、模块联邦是什么
简单来说就是允许运行时动态决定代码的引入和加载。
1 | app1 |
2、代码结构
1 | /** app1 **/ |
暂时不用关心app2的代码,问题关键是: app1是如何引入app2的代码的?
3、Module federation的配置
1 | /** |
- 配置:exposes/remotes
app1项目引入 app2 的 News组件 User组件我们重点关注1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/**
* app2/webpack.config.js
**/
new Mfp({
filename:'app2.js',// 对外提供打包后的文件名,导入时会使用
name:'app2',// 微应用的名字
exposes:{ // 暴露外部的组件
'./News':'./src/News.js', // 名字:具体那个一个组件
'./User':'./src/User.js',
},
})
/**
* app1/webpack.config.js
**/
new Mfp({
filename:'app1.js',// 对外提供打包后的文件名,导入时会使用
name:'app1',// 微应用的名字
remotes: { // 引用外部的组件
app2: "app2@http://localhost:3001/app2.js",
},
})exposes/remotes
:
- 提供了
exposes
选项的表示当前应用是一个Remote
,exposes
内的模块可以被其他的Host
引用,引用方式为import(${name}/${expose})
。 - 提供了
remotes
选项的表示当前应用是一个Host
,可以引用remote
中expose
的模块。
项目中如何使用
1 | /** |
- 配置:shared
除了前面提到的模块引入和模块暴露相关的配置外,还有个shared
配置,主要是用来避免项目出现多个公共依赖。
例如,我们当前的项目 app1,已经引入了一个react/react-dom
,而项目 app2 暴露的User组件也依赖了react/react-dom
。如果不解决这个问题,项目 app1 就会加载两个react
库。
- remotes的代码自己不打包,类似external,例如app2/button就是加载app2打包的代码
- shared的代码自己是有打包的
- 问题及解决方案
1、配置shared后报错: Shared module is not available for eager consumption
解决方案:
增加bootstrap.js
通过 index.js
异步加载页面
1 | /** |
主要原因是 remote
暴露的 js 文件需要优先加载,如果 bootstrap.js
不是一个异步逻辑,在 import User
的时候,会依赖 app2
的 app2.js
,如果直接在 index.js
执行,app2
的 app2.js
根本没有加载,所以会有问题。
双向共享
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40/**
* app1/webpack.config.js
**/
new Mfp({
filename:'app1.js',
name:'app1',
exposes:{
// 名字:具体那个一个组件
'./Button':'./src/Button.js',
},
})
/**
* app2/webpack.config.js
**/
new Mfp({
filename:'app2.js',
name:'app2',
// 引用外部的组件
remotes: {
app1: "app1@http://localhost:3000/app1.js",
},
})
/**
* app2/News.js
**/
import React from 'react'
const Button = React.lazy(() => import("app1/Button"))
const News = () => (
<div>
App2 News组件
<React.Suspense fallback="loading app1">
<Button />
</React.Suspense>
</div>
)
export default News加载逻辑
这里有一个点需要特别注意,就是入口文件 index.js 本身没有什么逻辑,反而将逻辑放在了 bootstrap.js 中,index.js 去动态加载 bootstrap.js。