多端容器设计

925 阅读9分钟

序言

本文就如何设计多端容器做出实现方案。本文指的多端并非是跨端,多端容器泛指容器提供统一能力,以供下游应用在多种环境下的容器加载并正常运行。而本文的多端指的是下游也无需关心上层实现及依赖,只依赖于抽象接口,遵循依赖反转原则。

小伙伴应该接触过以下这种需求场景:应用既可以在web browser运行,也可以以Hybrid的方式内嵌 webview、三方应用(alipay,wx,微应用等)浏览器。可能有些同学会问:“同是 Browser Environment,提供的上下文能力不都一样吗”,但实际上,会有一些容器对加载内容的能力做一些限制和添加特色功能,端容器需要做的就是将它们对齐。当处于可以访问DOM的环境调用社区的动画库,处于原生应用中时调用JSBridge使用原生应用的交互能力,处于其他端例如小程序时使用三方生态提供的能力 —— 当环境限制无法达到最优解时,可以自动降级到兜底方案。

又比如wx/h5/微应用,会有默认的导航栏,部分应用会开放应用导航栏的定制能力,而当环境处于Web Browser 时自带导航栏应该展现出来。这只是部分差异,当维护的应用涉及多个环境时,问题被放大也就棘手了起来。

总之,多端容器解决的问题是:

  • 提供多端统一API,抽象api约定,下游无感。
  • 多端逻辑隔离,利于维护管理。
  • 依赖反转,解耦提高可替代性。

uni-api 类似,不过此文场景是偏向业务/场景多端,后者是上下文多端,普适性更强。

目标

基于已有的业务代码,从可维护性和成本最小化的综合考虑出发,扩展不同的浏览环境。

适用场景

比如我们组在去年做搭建平台时,考虑到物料组件会被复用在不同的业务,数据源API地址、参数和返回数据结构有很大差异,三条业务线跑H5/小程序/RN环境的都有,人力和时间都比较紧凑,需要保证组件代码开发成本尽可能小,就使用了这套方案。

2023-02-06更新: 目前两条业务线都砍掉了[/doge]

容器能力

容器提供的能力分为以下6层(术语偏向业余,更容易理解些):

  1. 基础层,常用指令API(toast/alert/confirm)和常用工具类(剪切板/本地存储/预览图片/环境区分)。
  2. 网关层,Services,服务网关(根据环境调用对应API调整参数) e.g. fetch/jsonp/自定义注入网关。
  3. 加载层,ComponentLoader,组件加载器,具备预加载功能。
  4. 路由层,NavgationAPI,导航及路由缓存。
  5. 容错层,ErrorBoundary,可定制错误兜底视图自动上报。
  6. 过渡层,定制 React.lazy、componentLoader 加载过渡动画。

使用方式

import { containerContext, Container } from '@iron-man/container-api';
function App(){
    return (
      <Container {...options}>
        <AnyChild />
      </Container>
    )
}
function AnyChild() {
    const containerAPI = useContext(containerContext);
    useEffect(() => {
      // use container ability
    }, [])
    return //...
}

经常在某些文章里看到对 Context 性能和组件间可预测的数据复杂性两方面问题,首先它是跨组件共享数据的首选方式,定义后属性的不可重写代表也不会让跨组件层级之间组件关系变得复杂且不可预测,还有一些文章指出它的诟病是容易造成性能损耗,例如不拆分 Context 充当 Redux 构建单一数据树使用,使组件强行脱离 bailout 逻辑, 然而 React 性能优化的一大关键在于减少不必要的render,这一点可以通过 memo 规避掉。

设计分层

其他层依赖于基础容器提供的基础能力,除基础容器外,每种功能无耦合可组合拆分不影响其他能力,还有像埋点容器、Profiler容器、动画过渡容器等自由发挥。

image.png

  • 基础层
    • 1.2.3 偏向于容器api,没有其他依赖,适合放在基础容器层内或为下游层提供能力。
      • 通用API提供上下文常用的指令工具方法。
      • 服务网关提供服务调用能力。
      • ComponentLoader 过渡动画依赖 Suspense, 容错依赖 ErrorBoundary,这里并不是直接依赖容器层的 ErrorBoundary,而是复用基础组件,它只作为一个基础组件为加载器单独提供功能。
  • 路由层
    • 依赖基础容器的 ComponentLoader, 导航路由在上篇文章里已经介绍了实现 传送门,本章不重复介绍。
  • ErrorBoundary 层
    • 复用 ErrorBoundary 组件,目的是将 ComponentLoader 加载发生的错误和运行时的错误隔离区别展示。
  • Suspense 层
    • 与 ErrorBoundary 相同,也是为了将 ComponentLoader 加载时的过渡动画和 React.lazy 触发的区分开来。

api 约定

为了保证多端实现的API统一,需要定义一个抽象接口中心,把抽象API接口暴露出来,由各端容器的真正实现引入作为核心规范约束。

erDiagram
abstract-api ||--|{ web-impl : web
abstract-api ||--|{ app-impl : app
abstract-api ||--|{ xxx-impl : xxx

最终应用入口根据判断环境通过DI (Dependency Injection) 方式注入到下游应用,完成对下游应用的多端兼容。

和钢铁侠很像,他能够根据敌人的特性去装载适合对战的机甲,也能够适应各种环境,故取名@iron-man/container-api,各端实现则为 @iron-man/container-${platform}-impl。 然后小程序限制太大,即便有好的跨端框架,方案也并非完全适用,主要体现在路由和脚本加载方面。

产物最终将提供两种方式接入,两种方式的差别不大,取决于平台的架构模式。

  1. DI,类 requirejs,各端统一引入@iron-man/container-api ,平台容器根据环境判断自动注入对应平台的依赖文件。
  2. 二方包,类 npm 模块,各端直接引入对应端环境的包。

为了避免在npm上发布测试包,本文章容器抽象&web-impl部分使用 Learn 管理,demo 使用 webpack external + requirejs 完成对各端业务mock实现。

接口定义

根据设计分层可以得到以下结构,基础容器层细分归纳为 Layer,而其他层分类到 Wrapper。

  • Layers
    • Basic // 基础层
    • Scheme // 协议层
    • Service // 网关层
  • Wrappers
    • Navigation // 路由层
    • ErrorBoundary // 容错层
    • Suspense // 加载层

Basic

一些常用的工具方法

export interface TipUtils {
  alert: () => void;
  confirm: () => void;
  toast: () => void;
}

export interface StorageApi {
  get: (key: string) => Promise<string | null>;
  set: (key: string, value: string) => Promise<void>;
  del: (key: string) => Promise<void>;
  getJSON: <T = unknown>(key: string) => Promise<T | null>;
  setJSON: <T = unknown>(key: string, value: T) => Promise<void>;
}

export interface PracticalUtils {
  /**
   * 预览图片
   */
  previewImage: (text: string) => void,
   /**
   * 复制到剪切板
   */
  copyToClipboard: (text: string) => Promise<void>
};

export interface BasicLayerAbility extends TipUtils {
   //...
}

Schema

协议层,主要通过 componentURI 解析生成实际组件/脚本引用地址提供组件渲染和预加载功能。

import { ComponentType } from "react";
import { SuspenseAbility } from '../wrapper/suspense';

export interface ComponentLoaderProps extends SuspenseAbility {
  /**
   * 组件协议URI
   */
  componentURI: string;
  /**
   * 组件 inner Props
   */
  props?: Record<string, any>;
}

export type SchemaLayerAbility = {
  ComponentLoader: ComponentType<ComponentLoaderProps>;
  preLoadComponent: (componentName: string) => Promise<any>;
}

Suspense & ErrorBoundary

提供自定义加装方法和自定义错误边界,两者共有 onError、renderError 方法,由容器侧传入以支持动态配置。

export type ErrorBoundaryAbility = {
  /**
   * 出错的事件(包括加载出错和渲染出错)
   */
  onError?: (error: any, type: 'load' | 'render') => void;
  /**
   * 自定义失败页面
   */
  renderError?: (error: any, type: 'load' | 'render') => React.ReactNode;
}

// Suspense
export interface SuspenseAbility extends ErrorBoundaryAbility {
  /**
   * 自定义加载界面
   */
   renderLoading?: () => ReactElement;
   /**
    * 加载成功的事件
    */
   onLoad?: (componentClass: any) => void;
}

Navigator

运行时的实现思路是使用 history API 模拟一个页面栈,所有导航方法的操作都是基于 PageStack,搭配 popstate event 响应返回事件,路由导航能力基于 ComponentLoader。并在其基础上添加了单路由match多副本和路由回调功能,这块内容上篇文章叙述过了,传送门

export interface NavigationAbility {
  navAPI: {
    navigateTo(page: string, params?: Record<string, any>): void;
    back(): void;
  
    navigateAndWaitBack(page: string, params?: Record<string, any>): Promise<any>;
    backWithResponse(data: any): void;
  
    replace(page: string, params?: Record<string, any>): void;
  
    open(page: string, params?: Record<string, any>): void;
    reload(): void;
  }
}

容器侧配置

部分配置, 具体配置请查看 container-api

export interface ContainerProps {
  children?: ReactNode;
  /**
   * 环境注入
   */
  envType?: EnvTypeEnum;
  /**
   * 单路由多副本缓存规则
   */
  cacheOptions?: Array<RegExp>;
  /**
   * 自定义解析
   */
  customResolveModuleRule?: (componentURI: string) => ModuleType | null;
  /**
   * 预置组件
   */
  modules?: {
    readonly [moduleURI: string]: RemoteModule | LazyModule | LocalModule;
  };
  /**
   * 自定义加装动画
   */
  readonly withSuspense?: boolean | React.ComponentType<any>;
  /**
   * 自定义错误边界
   */
  readonly withErrorBoundary?: boolean | React.ComponentType<any>;
}

Mock实现

除了要定义接口,还需要对部分功能做初步实现,这样就可以直接打 container-api 的包,在 demo 里引入该声明文件。

import type * as ContainerAPI from '../../context/index';
declare module '@iron-man/container-api' {
  export = ContainerAPI;
}

Web-implement

初始化上下文

web端实现,可以使用各种开放能力,但在实现之前,首先需要初始化Context对各API提供初步的mock实现。

import { BasicLayerAbility, ServiceLayerAbility, SchemaLayerAbility, NavigationAbility, ContainerAbility } from '@iron-man/container-api';

import { unimplementedAsyncFunction, unimplementedFunction, unimplementedComponent } from './utils/initialFunction';

const defaultBasicAbility = {
  alert: unimplementedFunction,
  // ...
}
const defaultSchema = {
  componentLoader: unimplementedComponent,
  // ...
}
// other ...
export const containerContext: Context<ContainerAbility> = createContext({
  ...defaultBasicAbility,
  ...defaultSchema,
  ...defaultService,
  ...defaultNavigation
});

Basic

下一步是初始化基础容器, createContainer 负责整合基础容器层内的layer,提供迭代方法初始化。

工具层

// layers
import { ContainerOptions, ContainerAbility } from '@iron-man/container-api';
import BasicLayer from './basic';
import SchemaLayer from './schema/index';
import ServiceLayer from './service/index';
import { LayerIteration } from './interface';

const layers: LayerIteration[] = [BasicLayer, SchemaLayer, ServiceLayer];
// 初始化基础上下文
function createContainer(originContainer: ContainerAbility, options: ContainerOptions) {
  return layers.reduce((preContainer, layer) => layer(preContainer, options), originContainer);
}

import { ContainerProps } from '@iron-man/container-api';
import { containerContext as ContainerContext } from '@/containerContext';
/**
 * 基础容器层
 */
export default function BasicContainer(props: ContainerProps) {
  const { children, ...restProps } = props;
  const propsRef = useSafeTrackingRef(restProps);
  const originContext = useContext(ContainerContext);
  const value = useMemo(() => createContainer(originContext, propsRef.current), []);
  return <ContainerContext.Provider value={value}>{children}</ContainerContext.Provider>;
}
function BasicLayer(origin: ContainerAbility, options: ContainerOptions): ContainerAbility {
  const basic = createBasicLayer(origin, options)
  return {
    ...origin,
    ...basic
  };
}
function createBasicLayer() {
  return { alert: AM.alert, toast: AM.toast, ... }
}

项目需求里无线端 antd-mobile 用的比较多,这里就提供它的实现了。需要注意的是适配接口参数类型。再照猫画虎定义好其他工具API,基础容器层工具部分就实现完了。

服务网关层

主要包含服务调用和服务注入,服务调用包含基本的request/jsonp,每个公司都可能有自己的一套网关,也可以根据 natty-fetch 去定制团队协同规范,注入服务提供自定义服务,但必须固化参数。

function ServiceLayer(origin: ContainerAbility, options: ContainerOptions) {
  return {
    ...origin,
    service: {
      ...createServicePortal(origin, options), // 注册服务调用
      ...createCustomServicePortal(origin, options), // 提供注册自定义服务
    },
  };
}

根据上下文注入的domain&判断环境加载服务,这里request就用fetch做简单实现。createCustomServicePortal 包装自定义服务。

function createServicePortal(
  origin: ContainerAbility,
  options: ContainerOptions
): Pick<ServiceLayerAbility['service'], 'jsonp' | 'request' | 'getService'> {
  const Request = createRequestService(options.envType || 'online'); // 创建request服务
  const Jsonp = createJsonpService(options.envType || 'online');

  const presetServices = {
    request: Request,
    jsonp: Jsonp,
  };
	// 提供统一获取服务的入口
  const getService = (name: string): TypeRequest | CustomFetcher | null => {
    if (Object.prototype.hasOwnProperty.call(presetServices, name)) {
      return presetServices[name as keyof typeof presetServices];
    } else if (typeof name === 'string') {
      return origin.service.getCustomService(name)
    }else {
      return null
    }
  };

  return {
    ...presetServices,
    getService,
  };
}

协议层(组件/模块加载器)

比较麻烦的是协议层,它支持三种模块引入,远程模块、本地模块、懒加载模块,本地模块很容器理解,懒加载模块大致上和远程模块类似,不同点是额外提供懒加载能力,远程模块可以引入AMD、CMD、UMD、ESM模块。

除此之外,还需要考虑几个问题

  • 协议解析,如何根据协议解析判断你到底是想要哪种模块,并且可以自定义解析模块
    • 远程模块 => URL
    • 本地模块 => 内置/预置组件
  • 协议缓存与组件预加载。
  • 自定义加装动画和错误边界
协议规范

无论是任何类型的组件,都会有其自身的“身份信息”,这些信息在加载过程中仅提供给加载器使用,除此之外,对外有统一的加装方法供调用。加载和渲染区分开来,预加载实际意义上即是只调用了加载方法。

如何生成组件的“身份信息”,componentLoader 接受一个 componentURI 参数,该参数根据协议解析中的协议头、协议体、协议参数,默认协议支持如下三种格式。流程如下

"group://chat.list#default" // 团队仓库模块
"internal://Loading" // 本地/内置模块
"https://xxx.com/group/repo_name/version/index.js#Home" // 远程协议模块
graph TD
Loader --ComponentRegister--> URI-Parse
URI-Parse --GroupRepoModule--> 团队其他仓库通用模块 --根据Map返回URI--> LoadScript
URI-Parse --RemoteModule--> 远程脚本资源 --> LoadScript --cache--> Render
URI-Parse --LocalModule --> 本地模块  --> LazyModule 

LazyModule --Y--> LoadScript
LazyModule --N--> Render

通过解析协议拿到模块的地址,这个地址可能是外链也可能是本地,也有可能是本地+外链资源的混合 —— lazyModule,拿到组件身份信息后,根据模块类型加载脚本,加装动画和错误边界后渲染出来,过程中还需要考虑缓存和避免重复加载的问题。预加载的区别只是没有最后Render那一步。

export const createComponentLoader = (origin: ContainerAbility, options: ContainerOptions): Pick<ContainerAbility, 'ComponentLoader' | 'preLoadComponent'> => {
  const { getComponentInfo } = createComponentRegister(options, INTERNAL_MODULES);
  // 加载组件
	const componentLoader = (loaderProps: ComponentLoaderProps) => {
    // ...
    return <ComponentRender componentURI={componentURI} innerProps={innerProps} />
  };
  // 渲染组件
	const ComponentRender: React.FC<ComponentRenderProps> = (props) => {
    const { componentURI, innerProps = {} } = props;
    // 获取组件身份信息
    const componentInfo = useMemo(() => getComponentInfo(componentURI), [componentURI]);
    if (!componentInfo) {
      // 踢出不符合协议格式的消息数据
      throw new Error(`unknown componentURI ${componentURI}`);
    }
    // Fragment规避掉ts报错
    return <>{componentInfo.render(innerProps)}</>;
  };
  return {
    componentLoader,
    preLoadComponent: (componentURI: string) => {
      const componentInfo = getComponentInfo(componentURI);
      if (!componentInfo) {
        throw new CantGetModuleInfo(componentURI);
      }
			// 仅调用 load 方法
      return componentInfo.load();
    }
  }
}

getComponentInfo(componentURI) 主要做三件事情

  1. 解析协议,提供对外加载的API。
  2. 提供协议解析与组件的缓存。
  3. 提供自定义协议解析流程。
const createComponentRegister = (options: ContainerOptions, internalModule: ContainerOptions['modules']) => {
  const registerMap = useMemo(() => new Map(), []);
  function getComponentInfo(componentURI: string) {
    // 缓存
    if(registerMap.has(componentURI)) {
      return registerMap.get(componentURI);
    }
    // 自解析协议 > 内置模块 > 协议解析
    const info: ModuleType = options.customResolveModuleRule?.call(null, componentURI) || 
      internalModule && internalModule[componentURI] ||
      resolveModule(componentURI, options);
      if(!info) {
        throw new CantGetModuleInfo(componentURI);
      }
    	
      return registerComponentFromModuleInfo(componentURI, info);
    }
  return {
    registerMap,
    getComponentInfo
  }
}
协议解析

协议解析就比较好处理了,只可能存在三种类型。

export type RemoteModule = {
  type: 'remote';
  url: string;
  format?: 'AMD' | 'UMD' | 'CMD';
  /** 组件的导出名 */
  exportName?: string;
};

export type LocalModule = {
  type: 'local';
  module: unknown;
};
export type LazyModule = {
  type: 'lazy';
  module: () => Promise<unknown>;
};
export type ModuleType = LocalModule | RemoteModule | LazyModule;

然后通过parseURI 解析协议URI,得到以下几种字段

type moduleURIInfo = {
  protocol: string; // 协议
  path: string; // 文件路径
  subPath: string; // 模块路径
  // cacheId: string; // 缓存, 路由层需要用到,可先忽略
  componentURI: string; // 完整路径
};

根据 protocol 字段命中对应的协议体解析逻辑

enum ProtocolEnum {
  http = 'http',
  https = 'https',
  group = 'group',
  internal = 'internal'
}

export default function resolveModule(componentURI: string, options: ContainerOptions): ModuleType {
  const uriInfo = parseURI(componentURI, options);

  switch (uriInfo.protocol) {
    case ProtocolEnum.http:
    case ProtocolEnum.https: { // 远程协议模块
      return parseRemoteModule(uriInfo, options);
    }
    case ProtocolEnum.group: { // 团队仓库模块
      return parseGroupModule(uriInfo, options);
    }
    case ProtocolEnum.internal: { // 内置模块
      return Reflect.get(options.modules || {}, uriInfo.path)
    }
    default: {
			throw new InvalidComponentURIProtocol(uriInfo.protocol);
    }
  }
}
function parseGroupModule(uriInfo: moduleURIInfo, options: ContainerOptions): ModuleType {
  // custom rule for group ...
  return {
    type: 'remote',
    url: `//xxx.com/${uriInfo.protocol}/${uriInfo.path}.js`,
    exportName: uriInfo.subPath,
    format: 'UMD',
  };
}

其他命中逻辑类似,此处还应该细化组件的版本信息和协议头控制,例如在模板内注入全局组件版本映射表,以及更为灵动的协议头match规则,group => hotline-group。。。

协议加载

接下来就是加载过程,可自行注入全局的事件钩子,e.g. registed、beforeLoad 、loaded。

function registerComponentFromModuleInfo(componentURI: string, info: ModuleType) {
    let _component: any = null;
    // 包装器,等待注入加载请求逻辑
    function createComponentInfo(fetchComponent: Fetcher<any>): ComponentInfo {
      const load = () => {
        const Component = fetchComponent();
        if (!Component) {
          throw new InvalidComponentError(componentURI, getModuleName(info));
        }
        return _component = Component;
      }
      return {
        id: componentURI,
        render: (props: any) => {
          const Component = load();
          return React.createElement(Component, props);
        },
        load,
        get component() {
          return _component;
        }
      };
    }

    if(info.type === 'local') { // 本地模块
      return register(componentURI, createComponentInfo(() => info.module));
    }

    if(info.type === 'lazy') { // 懒加载模块
      return register(componentURI, createComponentInfo(createFetcher(info.module)));
    }

    if(info.type === 'remote') {
      const fetcher = createFetcher(async () => loadModule({
          name: info.name,
          url: info.url,
          exportName: info.exportName || '',
          format: info.format || 'UMD'
        })
      );
      return register(componentURI, createComponentInfo(fetcher));
    }
    throw new InvalidModuleURIError(componentURI);
  }

都知道除了 hooks 之外的函数在函数组件里都会执行多次,为了组件只加载一次,需要利用闭包函数。loadModule 是脚本加载函数,屏蔽了部分细节,例如处理可能存在的沙盒、requirejs、环节限制不能注入脚本(fetch内容eval掉,注意跨域),web直接使用了 url-package-loader ,这个包内置了AMD兼容方案。如果环境不支持 amd,需要额外配置 libraryName 降低到 UMD。

export async function loadModule<T = any>(config: LoadModuleOptions): Promise<T> {
  const { url, name, format = 'UMD', exportName } = config;
  let module;
	// PackageLoader 内置了 amd 判断,暂时先写成一样的
  if (format === 'UMD' || format === 'AMD') {
    module = await new PackageLoader({ name, url }).loadScript();
  }

  if (module && exportName) {
    return get(module, exportName);
  }

  if (module && module.__esModule && !exportName) { // es module
    return module.default;
  }
  return module;
}

到此,基础容器就可以跑起来了。

const App: FC<ContainerProps> = (props) => {
  <BasicContainer {...props}>
    <Demo />
  </BasicContainer>
}

const Demo = () => {
  const container = useContext(containerContext);
  const { ComponentLoader } = container;
  useEffect(() => {
    // container.toast('xxx')
  }, [])
  return (
      <div>
    	<ComponentLoader
        componentURI="https://0.0.0.0:8082/todoList"
        props={{
          title: '蔬菜',
          itemList: ['🥒', '🥔', '🎃'],
        }}
      />
      <ComponentLoader
        componentURI="group://todoList#default"
        props={{
          title: '水果',
          itemList: ['🍌', '🍊', '🍐', '🍉'],
        }}
      />
    </div>
  )
}

img.png

Navigator

虽然之前文章已经详细介绍过,还是要唠叨一嘴,路由层解决的问题是

  1. 无刷新切换路由,与上层路由不耦合、不冲突,可并用。
  2. 路由多副本缓存,例如多个聊天界面。
  3. 静默路由不更新。
  4. 提供路由钩子和路由回调。

数据管控路由实现状态持久化

ErrorBoundary

本质是控制错误的边界,不至于让整个应用崩溃,同时也提供兜底视图,除了提升用户体感外还可以附加其他功能,比如“点击重试”、“错误展示/上报”等,在此基础上,提供了自定义边界的接口。

const ErrorBoundaryContainer: React.FC<ContainerProps> = props => {
  const { children, withErrorBoundary } = props;
  if (withErrorBoundary === true || withErrorBoundary === undefined) {
    return <ErrorBoundaryWrapper>{children}</ErrorBoundaryWrapper>;
  }
  if (typeof withErrorBoundary === 'function') {
    return createElement(withErrorBoundary, {}, children);
  }
  return children as ReactElement;
};

Suspense

它的原理是通过捕捉子组件抛出的 Promise 状态来判断完成加载渲染,配合 React.lazy + dynamic import 可以达到 code-splitting 的效果,与 ErrorBoundary 同样要为其提供自定义 Suspense 的接口。注意某些不支持Suspense的情况,e.g. 没有 动态import,自定义Suspense 需要借助 ErrorBoundaryWrapper 来完成。

const MockSuspense = () => {
  // ...other
    const handleError = useCallback((boundaryError: any) => {
        // Error or not Promise, continue throw
        if (boundaryError instanceof Error || !isPromiseAlike(boundaryError)) {
          throw boundaryError;
        }
        const thePromise = boundaryError;
        promiseIdRef.current = Date.now() + Math.random();
        const thePromiseId = promiseIdRef.current;

        // promise only
        thePromise.then(() => {
            // success 
          },
          (err: any) => {
            if (thePromiseId === promiseIdRef.current) {
              // fail
            }
          }
        );
  }, []);
  return (
    <ErrorBoundaryWrapper onError={handleError} renderError={renderFallback}>
      {children}
    </ErrorBoundaryWrapper>
  )
}

// SuspenseContainer.tsx
const SuspenseContainer: React.FC<ContainerProps> = (props: ContainerProps) => {
  const { children, withSuspense } = props;

  if (withSuspense === true || withSuspense === undefined) {
    return <Suspense fallback={<Loading />}>{children}</Suspense>;
  }

  if (typeof withSuspense === 'function') {
    return createElement(withSuspense, { fallback: <Loading /> }, children);
  }
  return children as ReactElement;
};

至此,完成了web-impl,使用起来非常简单,因为是使用 Context API,只需要在应用最外层包裹即可,像 react-route 一样,提供了 withContext 的注入方式和 Context 自带的 Hooks API。

webpack.js.org/configurati…

搭配 requirejs requirejs.config({ paths: { "@iron-man/container-web-api": "//0.0.0.0:7105/index" } })

const Container: FC<ContainerProps> = ({ children, ...props }) => (
  <BasicContainer {...props}>
    <NavigatorContainer {...props}>
      <ErrorBoundaryContainer {...props}>
        <SuspenseContainer {...props}>{children}</SuspenseContainer>
      </ErrorBoundaryContainer>
    </NavigatorContainer>
  </BasicContainer>
);

const App = () => (
  <Container {...options}>
  	<Demo />
  </Container>
)

const Demo = () => {
  const container = useContext(containerContext);
}
// or
const Demo = withContext<DemoProps>((props) => {
  const { navAPI } = props;
})

1620281708889.gif

其他端容器大部分实现相似,只需要对不同业务体系和端环境区别实现即可。