npm package: @ice/mf-runtime
Description: ice mf runtime
Latest version: 1.0.5

# @ice/mf-runtime

基于 Module Federation 的运行时工具，用于在 ice.js 应用中加载和管理远程模块。支持跨版本 React 组件加载

## 特性

- 基于 [Module Federation 2.0](https://module-federation.io/index.html)
- 支持跨版本 React 组件加载
- 内置冲突检测和降级渲染

## 安装

```bash
$ npm i @ice/mf-runtime --save
```

## 使用

### 1. 初始化配置

在应用入口处初始化 Module Federation 配置：

```tsx
import { init } from '@ice/mf-runtime';

init({
  name: 'host_app', // 当前应用名称
  remotes: [
    {
      name: 'remote_app', // 远程应用名称
      entry: 'http://localhost:3000/remoteEntry.js', // 远程应用入口文件，也支持新的 mf-manifest.json 结构
    }
  ]
});
```

### 2. 加载远程模块

使用 `RemoteModule` 组件加载远程模块：

```tsx
import { RemoteModule } from '@ice/mf-runtime';

function App() {
  return (
    <div>
      <h1>Host Application</h1>
      <RemoteModule 
        module="Widget"  // 远程模块名称
        scope="remote_app"  // 远程应用名称
        LoadingComponent={<div>Loading...</div>}  // 可选的加载状态组件
      />
    </div>
  );
}
```

### 3. 高级配置

#### 自定义 Public Path

如果远程模块的静态资源需要使用特定的 CDN 地址，可以通过 `publicPath` 属性指定：

```tsx
<RemoteModule 
  module="Widget"
  scope="remote_app"
  publicPath="https://cdn.example.com/remote-app/"
/>
```

#### 远程模块高级配置

通过 `init` 方法的 `extraInfo` 配置，可以实现更多高级特性：

##### 类型定义

```ts
interface ExtraInfo {
  // 配置外部依赖
  external?: {
    react?: string;      // 外部 React 全局变量名
    'react-dom'?: string; // 外部 ReactDOM 全局变量名
  };
  // 是否为旧版本模块（用于兼容性处理）
  legacy?: boolean;
}
```

##### 1. 使用外部依赖

当远程模块需要使用 CDN 加载的 React 时，可以通过 `external` 配置指定：

```tsx
init({
  name: 'host_app',
  remotes: [
    {
      name: 'remote_app',
      entry: 'http://localhost:3000/remoteEntry.js',
      extraInfo: {
        external: {
          react: 'React',       // 使用全局的 React
          'react-dom': 'ReactDOM' // 使用全局的 ReactDOM
        }
      }
    }
  ]
});
```

当使用模块的 host 应用没有启用 MF 规范时，你可能会遇到 React 多实例引起的问题，对于这类问题，推荐使用外部依赖配置来保证 host 应用和 remote 应用使用同一个环境的 React 实例。

此时需要保证环境中存在指定的全局 React。runtime 会自动处理全局多个 React 的兼容性，优先使用兼容的 React 版本渲染。

##### 2. 兼容旧版本模块

对于一些旧版本的远程模块，可能需要特殊的兼容性处理，此时可以启用 `legacy` 模式：

目前 legacy 模式下，会做以下处理：
1. 兼容 remote 模块 expose name 不以 `./` 开头的情况。

```tsx
init({
  name: 'host_app',
  remotes: [
    {
      name: 'remote_app',
      entry: 'http://localhost:3000/remoteEntry.js',
      extraInfo: {
        legacy: true  // 启用兼容模式
      }
    }
  ]
});
```

##### 3. 混合配置示例

在实际项目中，可能需要同时配置多个远程模块，每个模块使用不同的配置：

```tsx
init({
  name: 'host_app',
  remotes: [
    {
      name: 'remote_app',
      entry: 'http://localhost:3000/remoteEntry.js',
      extraInfo: {
        external: {
          react: 'React',
          'react-dom': 'ReactDOM'
        }
      }
    },
    {
      name: 'remote_app_2',
      entry: 'http://localhost:3002/remoteEntry.js',
      extraInfo: {
        legacy: true
      }
    }
  ]
});
```

#### 为某个模块指定 React 版本渲染

当远程模块使用不同版本的 React 时，可以通过 `runtime` 属性指定运行时：

```tsx
import { RemoteModule } from '@ice/mf-runtime';
import * as React from 'react';
import * as ReactDOM from 'react-dom';

function App() {
  return (
    <RemoteModule 
      module="Widget"
      scope="remote_app"
      runtime={{
        react: React,
        reactDOM: ReactDOM
      }}
    />
  );
}
```
## API

### init(options)

初始化配置项：

| 参数 | 说明 | 类型 | 默认值 |
|------|------|------|--------|
| name | 当前应用名称 | `string` | - |
| remotes | 远程应用配置数组 | `Array<RemoteConfig>` | `[]` |

RemoteConfig 配置：

| 参数 | 说明 | 类型 | 默认值 |
|------|------|------|--------|
| name | 远程应用名称 | `string` | - |
| entry | 远程应用入口文件地址 | `string` | - |
| extraInfo | 额外配置信息，详见[远程模块高级配置](#远程模块高级配置) | `ExtraInfo` | - |

### RemoteModule

RemoteModule 组件支持泛型，可以用来约束远程组件的 props 类型：

```tsx
// 定义远程组件的 props 类型
interface RemoteButtonProps {
  onClick: () => void;
  text: string;
}

// 使用带类型的 RemoteModule
<RemoteModule<RemoteButtonProps>
  module="Button"
  scope="remote_app"
  componentProps={{
    onClick: () => console.log('clicked'),
    text: '点击我'
  }}
/>
```

组件属性：

| 参数 | 说明 | 类型 | 默认值 |
|------|------|------|--------|
| module | 远程模块名称 | `string` | - |
| scope | 远程应用名称 | `string` | - |
| runtime | React 运行时配置 | `{ react: React, reactDOM: ReactDOM }` | - |
| publicPath | 远程模块静态资源地址 | `string` | - |
| LoadingComponent | 加载状态组件 | `ReactNode` | `<div>Loading...</div>` |
| ErrorComponent | 错误回退组件 | `ReactNode` | `<div>远程模块加载失败: {error.message}</div>` |
| onError | 错误处理回调函数 | `(error: Error, info: { componentStack: string }) => void` | - |
| componentProps | 传递给远程模块的属性 | `T extends object` | `{}` |
| children | 传递给远程模块的子元素 | `ReactNode` | `null` |

类型定义：

```tsx
// RemoteModule 的类型定义
interface RemoteModuleOptions<T = any> {
  module: string;
  scope: string;
  runtime?: {
    react: typeof import('react');
    reactDOM: typeof import('react-dom');
  };
  publicPath?: string;
  LoadingComponent?: React.ReactNode;
  ErrorComponent?: React.ReactNode;
  onError?: (error: Error, info: { componentStack: string }) => void;
  componentProps?: T;
  children?: React.ReactNode;
}

// 组件声明
const RemoteModule = <T extends object = any>(props: RemoteModuleOptions<T>) => { ... }
```


## 注意事项

1. host name 请保证全局唯一。
2. remote name 的 package.json name 请保证全局唯一，否则会出现意料之外的缓存问题。

## 使用示例

### 传递属性和子元素

当远程模块需要接收属性或子元素时，可以使用 `componentProps` 和 `children`：

```tsx
import { RemoteModule } from '@ice/mf-runtime';

interface UserProfileProps {
  userId: string;
  showAvatar: boolean;
  onUserClick: (user: any) => void;
}

interface CardProps {
  title: string;
  children?: React.ReactNode;
}

function App() {
  return (
    <div>
      {/* 传递属性示例 */}
      <RemoteModule<UserProfileProps>
        module="UserProfile"
        scope="remote_app"
        componentProps={{
          userId: "123",
          showAvatar: true,
          onUserClick: (user) => console.log('用户点击:', user)
        }}
      />

      {/* 传递子元素示例 */}
      <RemoteModule<CardProps>
        module="Card"
        scope="remote_app"
        componentProps={{
          title: "我的卡片"
        }}
      >
        <h2>这是卡片内容</h2>
        <p>可以传入任意的子元素</p>
      </RemoteModule>
    </div>
  );
}
```

在远程模块中的使用方式：

```tsx
// 远程模块: UserProfile.tsx
interface UserProfileProps {
  userId: string;
  showAvatar: boolean;
  onUserClick: (user: any) => void;
}

export default function UserProfile(props: UserProfileProps) {
  const { userId, showAvatar, onUserClick } = props;
  // ... 组件实现
}

// 远程模块: Card.tsx
interface CardProps {
  title: string;
  children?: React.ReactNode;
}

export default function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      <div className="card-header">{title}</div>
      <div className="card-body">
        {children}
      </div>
    </div>
  );
}
```