1.0.7 • Published 1 year ago

react-app-provider v1.0.7

Weekly downloads
-
License
MIT
Repository
-
Last release
1 year ago

react-app-provider

version dw

基本介绍

React 应用程序根组件构造器,基于泛型抽象设计,理论上适用所有 React 应用环境,设计面向跨 Dom / Native / MiniProgram 多端适用。

  • React 18
  • React 17
  • React Native
  • Taro 小程序
  • Remax 小程序

注:React DOM / React Native 可使用 App.onLaunch 阻塞,Taro 小程序(Remax),基于其机制,无法做到在 App.onLaunch 阻塞(请在 Page 中进行阻塞)

主要提供两大功能:

  1. createAppProvider 提供应用程序根组件。
  2. setupPage 提供应用基础页面组件,为你的所有 React Page 提供全局注入机制。

createAppProvider 使用说明

最基本的使用

index.tsx

import React from 'react';
import { render } from 'react-dom';
import { createAppProvider } from 'react-app-provider';

const [AppRoot, useAppContext] = createAppProvider();

render((
  <React.StrictMode>
    <AppRoot>
      {/* 你的组件加载入口 */}
    </AppRoot>
  </React.StrictMode>
), document.getElementById('root'));

加入应用程序类

我们将部分代码分离到 App.tsx

App.tsx

import { createAppProvider, AppInterface } from 'react-app-provider';

export class YourApp implements AppInterface {
  async onLaunch(): Promise<void> {
    await fetch('http://yourapi/app_launch').then(() => {
      // 程序启动前置
    });
  }
}

export const [AppRoot, useAppContext] = createAppProvider<YourApp>();

注意:这里建议指定 createAppProvider<YourApp> 这个泛型,这样 AppRoot 和 useAppContext 在整个应用环境都将能自动智能感知和提示 YourApp 的方法和属性。

如果觉得泛型的方式太啰嗦,也可以 createAppProvider(new YourApp) ,这样 AppRoot 就无需在指定 app 属性声明。

index.tsx

import React from 'react';
import { render } from 'react-dom';
import { AppRoot, YourApp } from './App.tsx';

render((
  <React.StrictMode>
    <AppRoot app={new YourApp()}>
      {/* 你的组件加载入口 */}
    </AppRoot>
  </React.StrictMode>
), document.getElementById('root'));

也可以是一个静态 Object

App.tsx

import { createAppProvider, AppInterface } from 'react-app-provider';

export const YourApp: AppInterface = {} as const;

export const [AppRoot, useAppContext] = createAppProvider<typeof YourApp>();

createAppProvider

createAppProvider 函数,根据你的应用环境需要,动态创建泛型的 AppContext 。他主要返回三个数据:

export const [
  AppRoot,       // 应用根节点组件
  useAppContext, // 获取 AppContext 的钩子
  AppConsumer,   // AppContext.Consumer 实际上不怎么用的上
] = createAppProvider<YourApp>();

AppRoot

其实你可以给他任何命名都可以,不一定非要叫 AppRoot

作为根节点组件,出于跨端考虑,没有默认绑定 React.StrictMode

AppRoot 内组件挂载,主要如下:

AppRoot
└─AppContext
   └─ErrorFallback
      └─LoaderFallback
         └─children => your code entry
  • AppRoot 为 PureComponent,只有两个 state { error: null, ready: false } ,主要职责
    • 等待 onLuanch 异步完成,更新 ready
    • 如果子组件出错,则捕获错误
  • AppContext 只持有三个属性:app 应用实例,appReady 应用是否准备好(onLaunch 异步完成),appError AppRoot 错误边界所捕获到的错误异常。
  • 如果存在 appError ,则 ErrorFallback 不会继续往下渲染,而是回退到 ErrorDisplay。
  • 如果 appReady 未预备,且指定了 AppRootProps.loader ,则回退到 Loader (应用程序准备中)

AppInterface 接口和 AppRoot 属性

/**
 * 应用程序接口类
 *
 * 该接口类声明了应用程序(管理)实例的接口定义,声明应用程序组件需要那些接口。
 *
 * 应用程序(管理)实例,可以是一个实现了 AppInterface 的类实例,也可以是一个 Object 结构。
 */
export interface AppInterface {

  /**
   * 应用程序启动接口,运行在 App 组件 componentDidMount
   */
  onLaunch?(): void | Promise<void>;

  /**
   * 当组件渲染接收到错误时的处理接口
   *
   * @param error - 错误实例
   */
  onError?(error: ErrorLike): void;

  /**
   * 当错误发生时,App 渲染是否切换至 ErrorFallback
   *
   * @param error - 错误实例
   */
  shouldErrorFallback?(error: ErrorLike): boolean;

  /**
   * 错误异常回退组件声明,不指定,则使用默认的 ErrorDisplay
   *
   * 可以是一个 Element 实例(自动注入 Error 实例),也可以是一个组件
   */
  readonly ErrorFallback?: ErrorFallbackComponent;

  /**
   * 应用预备中的加载器
   * 可以是一个 Element 实例(自动注入 ready),也可以是一个组件
   */
  readonly Loader?: LoaderComponent;

  // fallback
  [key: string]: unknown;
}

export type AppRootProps<App extends AppInterface> = {
  /**
   * 传入的应用程序实例
   */
  app?: App,
  /**
   * 应用启动接口
   */
  onLaunch?: () => void | Promise<void>,
  /**
   * 错误异常处理接口
   * @param error - 错误实例
   */
  onError?: (error?: ErrorLike) => void,
  /**
   * 错误异常回退组件声明
   * 可以是一个 Element 实例(自动注入 Error 实例),也可以是一个组件
   */
  errorFallback?: ErrorFallbackComponent,
  /**
   * 错误发生时,是否回退,默认值为 `true`
   */
  shouldErrorFallback?: boolean | ((error: ErrorLike) => boolean),
  /**
   * 应用程序准备中的加载器
   */
  loader?: LoaderComponent,
}

注意:AppInterfaceAppRootProps 某个属性或接口同时存在,如 onLaunch ,必优先 AppRootProps.onLaunch > AppInterface.onLaunch ,其他同理。

ErrorDisplay 和 setDefaultErrorDisplay

目前全部组件无任何具体的标签、样式渲染,唯独除了 ErrorDisplay ,所以额外提供了一个 setDefaultErrorDisplay 方法,允许因环境不同(如 MiniProgram 或 Native ),对默认的错误现实进行重载。

ErrorFallback

该组件根据传入的 error: ErrorLike 参数,决定是否回退(只有成功才现实 children)。

如果不指定 fallback 参数,则使用 ErrorDisplay 来显示错误。

该组件设计就是为了被复用的,其实常常需要用到这个组件。

LoaderFallback

该组件根据传入的 ready: boolean 参数,决定是否回退,但他和 ErrorFallback 不同的地方在于,必须同时指定 loader 属性。

即必须 ready === true && loader != null 时,才会回退显示加载中的界面。

该组件设计就是为了被复用的,其实常常需要用到这个组件。

小程序中使用(Taro)

src/services/AppService.ts

import { AppInterface, createAppProvider } from 'react-app-provider';

class AppService implements AppInterface {
  onLaunch(): void {
    // 小程序的 onLaunch 是无法被阻塞的
  }
}

export const [App, useAppContext] = createAppProvider(new AppService);

export const useApp = () => {
  const { app } = useAppContext();
  return app;
};

src/app.tsx

import { App } from './services/AppService';

export default App;

Taro 小程序启动,App 和 Page 两者是同步并发的,所以阻塞 App.onLaunch 是无意义的。App 层级的错误边界也是无用的,应该要在 Page 进行错误捕获。

React Native

import { createAppProvider } from 'react-app-provider';

const Loader: React.FC = () => {
  return (
    <SafeAreaView style={{ backgroundColor: Colors.darker }}>
      <View style={{ display: 'flex', height: '100%', justifyContent: 'center', alignItems: 'center' }}>
        <Text style={{ color: 'white' }}>App Loading...</Text>
      </View>
    </SafeAreaView>
  );
};

const [AppRoot, useAppContext] = createAppProvider({
  onLaunch(): Promise<void> {
    return new Promise<void>(resolve => {
      setTimeout(() => {
        resolve();
      }, 3000);
    });
  },
  Loader,
});

const App = () => {
  return (
    <AppRoot>
      <SafeAreaView>
        {/* ...... */}
      </SafeAreaView>
    </AppRoot>
  )
}

export default App;

setupPage 使用说明

setupPage 提供一个根据你的 React 应用环境,自行注入所需 props 到标准页面组件的注入机制,具体注入什么,你自己决定,基础页面布局,也由你自己决定。

import React, { ComponentType, createElement } from 'react';
import { isValidElementType } from 'react-is';
import { useLocation } from 'react-router-dom';
import { SetupPageProps, ErrorBoundary, ErrorFallback } from 'react-app-provider';
import { setupPage } from './setupPage';

// 要注入到页面的属性,
// 比如你的应用环境使用了 react-router-dom,你可能希望为每个页面注入 path, query 两个参数
type YourAppBasePageProps = {
  path: string,
  query: URLSearchParams,
}

// 你的页面初始化时,给定的一些额外的配置属性
type YourAppPageInitOptions = {
  
}

// 这里构建你的应用程序的基准页面,不一定非要用 class 模式,这里只是为了捕获页面错误
class YourAppBasePage extends ErrorBoundary<SetupPageProps<YourAppBasePageProps>> {
  state = {
    error: undefined,
  }

  render() {
    return (
      <ErrorFallback error={this.state.error}>
        {isValidElementType(render) ? createElement(render, props) : null}
      </ErrorFallback>
    );
  }
}

// 这里得到一个 page 函数,用来包装你既有的 Page 组件。
export const page = setupPage(
  (opts?: YourAppPageInitOptions): YourAppBasePageProps => {
    const { pathname, search } = useLocation();
    const query = React.useMemo(() => new URLSearchParams(search), [search]);
    return { path: pathname, query };
  },
  YourAppBasePage
);

比如你可能有一个 index.tsx

// 你原来的首页,可以不去改变他
const IndexPage = () => {
  return (
    <div>
      {/* 首页的代码 */}
    </div>
  );
};

// 页面初始化的配置,非必要
const pageOpts = {};

export default page(IndexPage, pageOpts);

实际应用中,我们往往会在 BasePage 加入一个 PageContext,以便于相关的页面内的所有组件,可以共享得到当前页面的上下文。

也不局限于一套页面机制,你可以定义多个,比如 userPage adminPage,并与之对应的 useUserPageuseAdminPage 等等。

setupPage 只为你提供最最基础的实现机制,具体要如何实现,完全取决于你的应用环境。

之所以这么设计,另一个重点在于为了同时兼顾 DOM / Native / MiniProgram 三端。因为这三端的严重差异性,几乎很难一言以概之,这样反而不如提供一种一样的可能性,各端再根据实际情况去定制底层页面,而应用层的页面声明,则可采用同样的方法。

安装

pnpm add react-app-provider

珍惜生命,爱惜电脑硬盘,请使用 pnpm

测试覆盖率

--------------------|---------|----------|---------|---------|-------------------
File                | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
--------------------|---------|----------|---------|---------|-------------------
All files           |     100 |       96 |     100 |     100 |                  
 AppRoot.tsx        |     100 |    94.73 |     100 |     100 | 135,138          
 ErrorBoundary.tsx  |     100 |      100 |     100 |     100 |                  
 ErrorFallback.tsx  |     100 |      100 |     100 |     100 |                  
 LoaderFallback.tsx |     100 |    88.88 |     100 |     100 | 19               
 setupPage.tsx      |     100 |      100 |     100 |     100 |                  
--------------------|---------|----------|---------|---------|-------------------

版本历史

1.0.7

  • 根据 react 18 ,增加相关组件的 children 属性声明

1.0.6

  • 增加 setupPage

1.0.5

  • 调整 rollup 配置,不提供 esm 版本

1.0.3

  • 兼容 React 18 ,测试代码 render 改为 createRoot
  • 拆分出 ErrorBoundary 组件

1.0.2

  • AppRoot 删除 async,省去生成的代码 __awaiter

1.0.1

2022/03/26

  • 修正 AppInterface 的属性声明,改为 [key: string]: any
1.0.7

1 year ago

1.0.6

2 years ago

1.0.5

2 years ago

1.0.4

2 years ago

1.0.3

2 years ago

1.0.2

2 years ago

1.0.1

2 years ago

1.0.0

2 years ago