前端人 从0实现1个微前端

背景

一个新的技术的出现,都能有效或者间接的解决原有技术的痛点。起因我们公司的项目有着大量的重复代码。不同的端中存在大量重复的组件/代码。
我们公司的业务存在两个端,员工端/企业端,首先他们是两个独立分离的项目,导致改动一小点东西,改/提交/合并都是要进行两次重复的操作,包括我们公司的流水线也是独立分开的,导致每次测试的同学也要重新起项目两次。

1. 什么是微前端?

  • 微前端类似于一种微服务的架构(比如:k8s),而k8s是由多个微服务组成的复杂应用程序的绝佳平台。微前端可以将前端应用分解成一些更小、更简单能够独立开发/测试/部署的应用,能够有效的解决代码跨包复用的问题(采用Monorepo),从而提高开发效率,节约时间成本。
  • 实现微前端要解决以下基本问题
    1. js/元素/样式
    2. 数据通信

2. 微前端的实现方式

2.1 iframe

  • HTML 内联框架元素 (<iframe>),它能够将另一个 HTML 页面嵌入到当前页面中,方便快捷成本较低。
  • 为什么说iframe可以实现微前端?微前端的本质就是一个系统集成多个子应用,需要考虑到如何解决css/js相互污染等问题,微前端中很重要的一步就是实现css/js的隔离,而对于iframe来说,有先天的优势,因为这些浏览器提供了原生硬隔离方案,已经统统被浏览器解决了

2.2 Module Federation

  • webpack5提供的 Module Federation(联邦模块,多个独立的构建可以组成一个应用程序,这些独立的构建之间不应该存在依赖关系,因此可以单独开发和部署它们。

2.3 其他

  • microApp
  • qiankun
  • 等其他微前端框架

总结:无论是iframe,还是联邦模块,还是其他微前端框架等,没有最好的框架,各有优劣。只有最适合业务,最适合当下团队的框架,接下来就重点讲解iframe/联邦模块,这种较“轻”就能实现微前端的方案,

3. 项目架构图

前端人 从0实现1个微前端

  • 我们带着解决业务的角度去衡量采用哪种最合适的方案,前者它们是独立分开的包,后者采用Monorepo实现单个仓库中管理多个项目。

3. iframe

3.1 起手式

<!-- 员工端 -->
<style>
  .staff {
    height: 100px;
    background: pink;
  }
</style>
<body>
    <div class="staff">staff</div>
</body>

<!-- 企业端 -->
<style>
  .enterprise {
    height: 100px;
    background: skyblue;
  }
</style>
</head>
<body>
<div class="wrap">
  <iframe width="100%" src="http://127.0.0.1:5500/1.iframe/staff.html" frameborder="0"></iframe>
  <div class="enterprise">enterprise</div>
</div>
</body>
  • 自己本地开启一个服务,直接应用iframe标签src属性引入即可,此时我们把员工端的代码嵌入到企业端中
  • 此时我们就能简单的实现,代码复用的问题

3.2 父子应用如何通信

此时我们把staff,当作“子应用”,而应用enterprise的我们称之为“父应用

3.2.1 staff

  <script>
    const staffEl = document.querySelector(".staff");

    // 监听消息
    window.addEventListener("message", (e) => {
      const data = e.data;
      const divEl = document.createElement("div");
      divEl.innerHTML = `姓名:${data.name},年龄:${data.age}`;

      staffEl.appendChild(divEl);
    });
  </script>

3.2.2 enterprise

<script>
  const btnEl = document.querySelector(".send");
  const iframeEl = document.querySelector("iframe");

  // 发送数据
  btnEl.addEventListener("click", () => {
    const info = { name: "ice", age: 23 };
    const targetOrigin = "http://127.0.0.1:5500";
    
    iframeEl.contentWindow.postMessage(info, targetOrigin);
  });
</script>
  • postMessage/message 父应用发送数据/子组件接受数据,具体API用法查看MDN文档

4. iframe的特点

4.1 优点

  1. iframe有天然解决css/js/元素隔离问题的优势,我们无需关心。
  2. 学习成本低,难度低

4.2 缺点

  1. ui样式问题
    前端人 从0实现1个微前端

    • 样式存在差异,会有空白区域,如果要求居中显示,还要使用resize自动居中,比较麻烦
  2. 丢失了返后退/前进的按钮

    • 每次进入,浏览器都要重新加载资源,还会导致一些不必要的代码执行

4.3 总结

  • 如果业务只是单独toB,针对企业的后台管理系统,其实采用iframe无伤大雅,不需要追求极致的ui/性能等问题,就是一个不断取舍的过程

5. Module Federation

它允许多个 webpack 构建一起工作。 从运行时的角度来看,多个构建的模块将表现得像一个巨大的连接模块图。 从开发者的角度来看,模块可以从指定的远程构建中导入,并以最小的限制来使用。

5.1 前置知识

Monorepo

在版本控制系统中,Monorepo(“mono”意为“单一”,“repo”是“存储库”的缩写),单个仓库中管理多个项目
将多个项目集成到一个仓库下,共享工程配置,同时又快捷地共享模块代码,成为趋势,这种代码管理方式称之为 MonoRepo。

Learn

  • lerna是可以实现Monorepo的开发工具,用于管理包含多个软件包的js项目
  • lerna可以 单包/多包 运行打包
  • 将大型代码仓库分割成多个独立版本化的 软件包(package)对于代码共享来说非常有用。
  • 优势:
    • 代码共享,自动Link
    • 安装依赖,如果存在相同版本包,优先安装到根目录

5.2 Learn介绍

命令介绍

// 初始化项目
npx lerna init

// 输出目录结构
├── packages //存放源代码
├── lerna.json
├── package.json

其中packages中存放多个子包(多应用),即上方提到的staff,enterprise两个端的代码,为了更接近企业项目,这里采用creare-react-app + craco创建这两个项目,实现组件的复用/通信

// 目录输出
.
├── lerna.json
├── package-lock.json
├── package.json
└── packages
    ├── enterprise
    │   ├── README.md
    │   ├── craco.config.js
    │   ├── package-lock.json
    │   ├── package.json
    │   ├── public
    │   │   ├── favicon.ico
    │   │   └── index.html
    │   ├── src
    │   │   ├── ...
    │   └── tsconfig.json
    └── staff
        ├── README.md
        ├── craco.config.js
        ├── package-lock.json
        ├── package.json
        ├── public
        │   ├── favicon.ico
        │   └── index.html
        ├── src
        │   ├── ...
        └── tsconfig.json

创建项目,可以直接采用进入到packages文件夹中,直接采用cli命令,也可以采用lerna create staff创建文件夹,再组织你的目录结构
如何运行/打包 项目

  // package.json
  "scripts": {
    "start:staff": "lerna run start --scope staff",
    "start:enterprise": "lerna run start --scope enterprise",
    "start": "lerna run --parallel start",
    "build:staff": "lerna run build --scope staff",
    "build:enterprise": "lerna run build --scope enterprise",
    "build": "lerna run --parallel build"
  }
  • 定义脚本,在scripts中的脚本,会去node_modules/.bin 中寻找可执行文件,运行/打包项目,相当于npx lerna run ...
  • parallel并行的执行脚本

5.3 起手式

首先要了解两个概念

  1. host:可以使用别的模块,可以把它想象成消费者
  2. remote:可以被别的模块所使用,可以把它想象成商家
  3. 一个模块,可以成为host/remote/两者兼任

本质上,它就是一个plugin,ModuleFederationPlugin,插件中有几个重要的参数

  1. name 应用的名称/唯一,(消费的时候会使用到)
  2. filename 文件到名称(chunk的名称)
  3. exposes 导出的模块,只有导出的模块才会被打包到chunk中
  4. remotes 加载远程模块
  5. shared 用来共享依赖,比如host中采用react,远程的模块也是react,那么配置该属性,则优先采用本地的包,否则采用远程应用的包

staff 员工端 (商家)

  • 我们以员工端为基座,把它当作remote(商家),可以让项目enterprise所使用(即它为消费者)
// craco.config.js
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
  // 端口号
  devServer: {
    port: 3002,
  },

  webpack: {
    configure: {
      output: {
        // 极为重要
        publicPath: "auto",
      },
    },
    plugins: {
      add: [
        new ModuleFederationPlugin({
          name: "staff",
          filename: "staffEntry.js",
          exposes: {
            "./Button": "./src/Button.tsx",
            "./Report": "./src/Report.tsx",
          },
          shared: ["react", "react-dom"],
        }),
      ],
    },
  },
};
  • publicPath
    • 对于外部加载的文件/资源,如果指定了一个错误的值,则在加载这些资源时会收到 404 错误。
  • ModuleFederationPlugin的属性,其中导出了ButtonReport组件,其表现形式为,一个独立chunk(即staffEntry),n个expose chunk,供其他消费者消费,本质上就是通过类似于CDN的方式,动态加载该模块,热插拔效果,其表现形式如图所示

前端人 从0实现1个微前端

enterprise 企业端 (消费者)

// craco.config.js
const { ModuleFederationPlugin } = require("webpack").container;

module.exports = {
  devServer: {
    port: 3001,
  },

  webpack: {
    plugins: {
      add: [
        new ModuleFederationPlugin({
          name: "enterprise",
          filename: "enterpriseEntry.js",
          remotes: {
            staff: "staff@http://localhost:3002/staffEntry.js",
          },
        }),
      ],
    },
  },
};

  • remotes, 即引入该地址下的chunk
import React from "react";

import Button from "staff/Button";
import Report from "staff/Report";

function App() {
  return (
    <div style={{ height: "300px", backgroundColor: "pink" }}>
      <h1>enterprise</h1>
      <Button />
      <Report />
    </div>
  );
}

export default App;

  • 其中Button/Report组件完美的被服复用到enterprise中。
  • 加载远程模块被认为是异步操作。当使用远程模块时,这些异步操作将被放置在远程模块和入口之间的下一个 chunk 的加载操作中。如果没有 chunk 加载操作,就不能使用远程模块,所以需要动态加载根组件(具体实现查看项目源代码)

实现效果如下:
前端人 从0实现1个微前端

  • 我们可以清楚的看见,我在enterprise中使用了staff中的组件,而其本质就是动态加载了staff中的资源,其资源路径为http://localhost:3002/static/js/...,3002端口正是staff的资源

6. Module Federation特点

6.1 优点

  • 采用Monorepo可以方便/快捷/高效的共享代码块,可以让跨应用间真正做到模块共享
  • 相同版本依赖提升到顶层只安装一次,节省磁盘内存
  • 部署可以一次性并行,部署多个项目
  • 只是组件的复用,ui样式/数据通信 可以动态的传入,可以通过props进行定制化
  • webpack5的内置模块,更加轻量级

6.2 缺点

  • git clone下来,速度会比单个的慢,版本控制也会比原先繁琐一些
  • 多个项目代码都在一个仓库中,remote出错,会影响到其他host
  • 相比iframe来说,有一定学习成本,但是基于 Webpack 的生态,学习成本、改造成本、实施成本都比较低

7. 结语

停更了一段时间,也非常庆幸有人私信我催我更新,995的工作有一些疲惫,在闲暇的时刻也给自己充充电,希望看到这篇文章的你们也是,热爱生活,努力工作。
最后附上github项目链接,希望可以帮助到大家~

原文链接:https://juejin.cn/post/7235237072439935031 作者:上山人

(0)
上一篇 2023年5月21日 上午10:05
下一篇 2023年5月21日 上午10:15

相关推荐

发表回复

登录后才能评论