为什么需要微前端

设想如果你有一个维护了很久的项目A,有着相当的体量,现在业务需要创建一个新的项目B,并且把A集成到B中,应该怎么设计?在B中把A重构一遍吗?

再设想有一个规划的产品,体量很大,需要拆分成多个子业务,每个子业务之间耦合很少,每个子业务分配给不同的团队负责,这种场景应该怎么设计?

上面场景举例表现了巨石应用业务场景,巨石应用有几个特点:

  1. 体量巨大,一个大应用内含多个子应用。
  2. 子应用之间比较独立。(因为单独的一个应用很难增长为一个巨石的规模,不断有新的独立的子页面加入,就容易形成一个巨石)
  3. 不同子应用之间由不同团队负责。

巨石应用给开发带来一些问题:

  1. 逻辑耦合,牵一发动全身
  2. 整体构建和整体部署很慢,开发体验差
  3. 不同团队维护协作成本高

微前端就是巨石应用的解决方案。主要思想就是把巨石应用拆分。

微前端解决方案

对于巨石应用,微前端的解决方法是:

  1. 拆分成多个子应用。
  2. 主应用根据url路由加载子应用。

并且具有几个特点

  1. 技术栈无关。
  2. 子应用单独部署单独构建。
  3. 运行时隔离。

如何落地微前端

微前端应用开发思路

首先对微前端技术方案技术选型,接入微前端框架

通常使用微前端框架实现一个微前端应用有以下几个步骤:

  1. 创建主应用(也叫基座应用)和子应用
  2. 主应用注册子应用,其实就是告诉框架几个事情,这样主应用就可以在切换路由时候正确加载子应用:
    1. 有哪些子应用
    2. 子应用资源地址
    3. 子应用和路由之间的关系
  3. 子应用生命周期管理:不同框架方案不同,例如qiankun是在子应用导出几个固定的生命周期钩子,主应用在相应时机会调用子应用的生命周期钩子。
  4. 共享状态:不同框架方案不同,如qiankun是通过两种方式来处理不同微前端之间的共享状态:自定义hook函数和props传递。

qiankun

介绍 - qiankun

qiankun上手很容易,主要有3步

  1. 子应用导出生命周期钩子
  1. /**
  2. * 微应用需要在自己的入口 js (通常就是你配置的 webpack 的 entry js) 导出 bootstrap、mount、unmount 三个生命周期钩子,以供主应用在适当的时机调用。
  3. * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
  4. * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
  5. */
  6. export async function bootstrap() {
  7. console.log('react app bootstraped');
  8. }
  9. /**
  10. * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
  11. */
  12. export async function mount(props) {
  13. ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
  14. }
  15. /**
  16. * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
  17. */
  18. export async function unmount(props) {
  19. ReactDOM.unmountComponentAtNode(
  20. props.container ? props.container.querySelector('#root') : document.getElementById('root'),
  21. );
  22. }
  23. /**
  24. * 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
  25. */
  26. export async function update(props) {
  27. console.log('update props', props);
  28. }
  1. 子应用打包

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

  1. // 导出为umd,这样主应用才能访问到子应用导出的信息。
  2. const packageName = require('./package.json').name;
  3. module.exports = {
  4. output: {
  5. library: `${packageName}-[name]`,
  6. libraryTarget: 'umd',
  7. chunkLoadingGlobal: `webpackJsonp_${packageName}`,
  8. },
  9. };
  1. 主应用注册子应用
  1. import { registerMicroApps, start } from 'qiankun';
  2. // 注册子应用
  3. // 主要包括子应用访问地址、挂载容器、路由
  4. // 这样一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑
  5. registerMicroApps([
  6. {
  7. name: 'react app', // app name registered
  8. entry: '//localhost:7100',
  9. container: '#yourContainer',
  10. activeRule: '/yourActiveRule',
  11. },
  12. {
  13. name: 'vue app',
  14. entry: { scripts: ['//localhost:7100/main.js'] },
  15. container: '#yourContainer2',
  16. activeRule: '/yourActiveRule2',
  17. },
  18. ]);
  19. start();

Module Federation和EMP

Module Federation实现微前端

模块联邦(Module Federation)出现的动机:多个独立的构建可以组成一个应用程序,这些独立的构建之间不应该存在依赖关系,因此可以单独开发和部署它们。module federation —— webpack 官方

Webpack模块联邦(Webpack Module Federation)是 Webpack 5 中引入的一项新功能,它允许不同的 Webpack构建 之间共享代码并动态加载依赖项。具体来说,它允许将应用程序拆分成多个独立的 Webpack构建(或称为远程应用程序),这些构建可以在运行时共享代码和依赖项。

MF支持打包一个子应用并暴露模块,然后主应用打包时候注册子应用信息,这样主应用内部就可以访问子应用暴露的模块了。

除此之外MF还支持共享模块,类似externals,多个应用共享的模块可以单独生成,提升加载性能。

MF最核心的是ModuleFederationPlugin插件,不管是主应用还是子应用,都用该插件打包。

使用MF构建微前端应用步骤:

  1. 打包子应用,暴露主应用需要的模块
  2. 打包主应用,注册子应用信息
  3. 主应用中导入子应用并调用其暴露的模块

示例

  1. // 子应用打包
  2. // webpack.config.js
  3. const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
  4. module.exports = {
  5. // ...
  6. plugins: [
  7. new ModuleFederationPlugin({
  8. // 子应用名称,主应用注册子应用时候会用到
  9. name: "microFrontEnd1",
  10. // 打包出的文件名
  11. filename: "remoteEntry.js",
  12. // 暴露出的模块,<访问路径>:<文件路径>
  13. exposes: {
  14. "./MicroFrontEnd1Index": "./src/index",
  15. },
  16. }),
  17. ],
  18. };
  1. // 主应用打包
  2. // webpack.config.js
  3. const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
  4. module.exports = {
  5. // ...
  6. plugins: [
  7. new ModuleFederationPlugin({
  8. // 主应用名称
  9. name: "container",
  10. // 注册子应用
  11. // <主应用中使用子应用的别名>: <子应用名称(子应用使用MF打包时候指定的)>@<子应用部署地址>
  12. remotes: {
  13. microFrontEnd1: "microFrontEnd1@http://localhost:8081/remoteEntry.js",
  14. },
  15. }),
  16. ],
  17. };
  1. // 主应用导入子应用,会异步加载子应用并执行
  2. import "microFrontEnd1/MicroFrontEnd1Index";

刚刚提到MF还支持共享模块

  1. plugins: [
  2. new ModuleFederationPlugin({
  3. // 声明共享模块
  4. shared: {
  5. react: { eager: true, singleton: true },
  6. "react-dom": { eager: true, singleton: true },
  7. "place-my-order-assets": {eager: true, singleton: true},
  8. }
  9. })
  10. ]

缺点

  • 不提供路由控制等微前端方案,只是一个代码共享方案。
  • 对环境要求略高,需要使用 webpack5,旧项目改造成本大。
  • 远程模块 typing 的问题。
  • 对代码封闭性高的项目,依旧需要做npm那一套管理和额外的拉取代码,还不如npm复用方便。
  • 拆分粒度需要权衡,虽然能做到依赖共享,但是被共享的lib不能做tree-shaking,也就是说如果共享了一个lodash,那么整个lodash库都会被打包到shared-chunk中。虽然依赖共享能解决传统微前端的externals的版本一致性问题。
  • webpack为了支持加载remote模块对runtime做了大量改造,在运行时要做的事情也因此陡然增加,可能会对我们页面的运行时性能造成负面影响。
  • 运行时共享也是一把双刃剑,如何去做版本控制以及控制共享模块的影响是需要去考虑的问题。

EMP

EMP是基于Module Federation的微前端框架。

EMP

参考文章

微前端到底是什么?
2020年你必须要会的微前端 -(实战篇)_微前端有必要学吗?-CSDN博客
微前端的6种方式

介绍 - qiankun

从阿里QianKun看前端沙箱隔离_qiankun legacysandbox-CSDN博客

理解微前端技术原理

你想要的【微前端】都在这里了!

面试官:你真的懂微前端吗? 手写一个 mini qiankun!

微前端面试题集

【微前端】手把手教你从0到1实现基于Webpack5 模块联邦(Module Federation)的微前端~ - 掘金

EMP