什么是 React 中的远程组件?

22 阅读2分钟

React 中的 远程组件 是指从远程源(如 CDN 或服务器)加载和执行的组件,而不是与你的应用程序捆绑在一起。这样可以:

  1. 运行时组件的动态加载
  2. 跨不同应用程序共享组件
  3. 更新组件,无需重新构建主应用程序
  4. 微前端架构实现

如何实现远程组件

在 React 中实现远程组件的方法有多种:

1.模块联合 (Webpack 5)

这是主流用法

使用者/消费者(host主应用程序):

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('@module-federation/webpack');

module.exports = {
  // ...other config
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
      },
      shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

远程应用程序(provider提供者)

// webpack.config.js
const { ModuleFederationPlugin } = require('@module-federation/webpack');

module.exports = {
  // ...other config
  plugins: [
    new ModuleFederationPlugin({
      name: 'remoteApp',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/components/Button',
      },
      shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
    }),
  ],
};

在主应用程序中使用:

import React, { Suspense } from 'react';

const RemoteButton = React.lazy(() => import('remoteApp/Button'));

function App() {
  return (
    <div>
      <h1>Host Application</h1>
      <Suspense fallback="Loading Remote Component...">
        <RemoteButton />
      </Suspense>
    </div>
  );
}

2.使用 CDN 动态导入

从 CDN 加载组件:

import React, { useState, useEffect } from 'react';

function RemoteComponent({ url }) {
  const [Component, setComponent] = useState(null);

  useEffect(() => {
    const loadComponent = async () => {
      try {
        const module = await import(/* webpackIgnore: true */ url);
        setComponent(() => module.default);
      } catch (error) {
        console.error('Failed to load remote component:', error);
      }
    };

    loadComponent();
  }, [url]);

  if (!Component) {
    return <div>Loading...</div>;
  }

  return <Component />;
}

3.自定义远程组件加载器

创建更复杂的远程组件系统:

// RemoteComponentLoader.js
import React, { useState, useEffect, useRef } from 'react';

class RemoteComponentLoader {
  constructor() {
    this.cache = new Map();
  }

  async loadComponent(url, scope, module) {
    const key = `${url}-${scope}-${module}`;
    
    if (this.cache.has(key)) {
      return this.cache.get(key);
    }

    try {
      // Load the remote entry
      await this.loadScript(url);
      
      // Get the module from the remote container
      const factory = await window[scope].get(module);
      const Module = factory();
      
      this.cache.set(key, Module);
      return Module;
    } catch (error) {
      console.error('Error loading remote component:', error);
      throw error;
    }
  }

  loadScript(url) {
    return new Promise((resolve, reject) => {
      if (document.querySelector(`script[src="${url}"]`)) {
        resolve();
        return;
      }

      const script = document.createElement('script');
      script.src = url;
      script.onload = resolve;
      script.onerror = reject;
      document.head.appendChild(script);
    });
  }
}

const loader = new RemoteComponentLoader();

export function RemoteComponent({ url, scope, module, fallback = 'Loading...' }) {
  const [Component, setComponent] = useState(null);
  const [error, setError] = useState(null);
  const mountedRef = useRef(true);

  useEffect(() => {
    const loadRemoteComponent = async () => {
      try {
        const LoadedComponent = await loader.loadComponent(url, scope, module);
        
        if (mountedRef.current) {
          setComponent(() => LoadedComponent);
        }
      } catch (err) {
        if (mountedRef.current) {
          setError(err);
        }
      }
    };

    loadRemoteComponent();

    return () => {
      mountedRef.current = false;
    };
  }, [url, scope, module]);

  if (error) {
    return <div>Error loading component: {error.message}</div>;
  }

  if (!Component) {
    return <div>{fallback}</div>;
  }

  return <Component />;
}

用法

function App() {
  return (
    <div>
      <h1>My App</h1>
      <RemoteComponent 
        url="http://localhost:3001/remoteEntry.js"
        scope="remoteApp"
        module="./Button"
        fallback="Loading button..."
      />
    </div>
  );
}

4. 使用systemJs

import React, { useState, useEffect } from 'react';
import { importMap } from './importMap';

function SystemJSRemoteComponent({ componentName }) {
  const [Component, setComponent] = useState(null);

  useEffect(() => {
    const loadComponent = async () => {
      try {
        // Configure SystemJS with import map
        System.addImportMap(importMap);
        
        // Import the remote component
        const module = await System.import(componentName);
        setComponent(() => module.default || module);
      } catch (error) {
        console.error('Failed to load remote component:', error);
      }
    };

    loadComponent();
  }, [componentName]);

  if (!Component) {
    return <div>Loading remote component...</div>;
  }

  return <Component />;
}
// importMap.js
export const importMap = {
  imports: {
    // npm packages
    "react": "https://cdn.skypack.dev/react",
    "react-dom": "https://cdn.skypack.dev/react-dom",
    "lodash": "https://cdn.skypack.dev/lodash",
    
    // Remote components
    "my-button": "https://my-cdn.com/components/button.js",
    "my-header": "https://my-cdn.com/components/header.js",
    "shared-ui-library": "https://shared-components.company.com/library.js",
    
    // Scoped packages
    "@material-ui/core": "https://cdn.skypack.dev/@material-ui/core",
    "@myorg/shared-components": "https://myorg-cdn.com/shared-components/index.js"
  },
  scopes: {
    // Scoped mappings for version management
    "https://my-cdn.com/components/": {
      "utils": "https://my-cdn.com/utils/v2.0.0/index.js"
    }
  }
};

注意点

  1. 安全性:始终验证和清理远程组件以防止 XSS 攻击
  2. 版本控制:为远程组件实施适当的版本控制策略
  3. 错误处理:优雅地处理网络故障和组件加载错误