# react-app-provider
**Repository Path**: janpoem/react-app-provider
## Basic Information
- **Project Name**: react-app-provider
- **Description**: React 应用程序根组件构造器,基于泛型抽象实际,无任何实体 AppClass,理论上适用所有 React 应用环境,设计面向跨 Dom / Native / MiniProgram 多端适用。
- **Primary Language**: TypeScript
- **License**: MIT
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2022-03-22
- **Last Updated**: 2022-12-11
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# react-app-provider
[](https://www.npmjs.com/package/react-app-provider)

### 基本介绍
React 应用程序根组件构造器,基于泛型抽象设计,理论上适用所有 React 应用环境,设计面向跨 Dom / Native / MiniProgram 多端适用。
- [x] React 18
- [x] React 17
- [x] React Native
- [x] Taro 小程序
- [x] Remax 小程序
注:React DOM / React Native 可使用 `App.onLaunch` 阻塞,Taro 小程序(Remax),基于其机制,无法做到在 `App.onLaunch` 阻塞(请在 Page 中进行阻塞)
主要提供两大功能:
1. `createAppProvider` 提供应用程序根组件。
2. `setupPage` 提供应用基础页面组件,为你的所有 React Page 提供全局注入机制。
### `createAppProvider` 使用说明
#### 最基本的使用
**index.tsx**
```tsx
import React from 'react';
import { render } from 'react-dom';
import { createAppProvider } from 'react-app-provider';
const [AppRoot, useAppContext] = createAppProvider();
render((
{/* 你的组件加载入口 */}
), document.getElementById('root'));
```
#### 加入应用程序类
我们将部分代码分离到 App.tsx
**App.tsx**
```tsx
import { createAppProvider, AppInterface } from 'react-app-provider';
export class YourApp implements AppInterface {
async onLaunch(): Promise {
await fetch('http://yourapi/app_launch').then(() => {
// 程序启动前置
});
}
}
export const [AppRoot, useAppContext] = createAppProvider();
```
**注意:**这里建议指定 `createAppProvider` 这个泛型,这样 AppRoot 和 useAppContext 在整个应用环境都将能自动智能感知和提示 YourApp 的方法和属性。
如果觉得泛型的方式太啰嗦,也可以 `createAppProvider(new YourApp)` ,这样 `AppRoot` 就无需在指定 `app` 属性声明。
**index.tsx**
```tsx
import React from 'react';
import { render } from 'react-dom';
import { AppRoot, YourApp } from './App.tsx';
render((
{/* 你的组件加载入口 */}
), document.getElementById('root'));
```
#### 也可以是一个静态 Object
**App.tsx**
```tsx
import { createAppProvider, AppInterface } from 'react-app-provider';
export const YourApp: AppInterface = {} as const;
export const [AppRoot, useAppContext] = createAppProvider();
```
#### createAppProvider
`createAppProvider` 函数,根据你的应用环境需要,动态创建泛型的 `AppContext` 。他主要返回三个数据:
```tsx
export const [
AppRoot, // 应用根节点组件
useAppContext, // 获取 AppContext 的钩子
AppConsumer, // AppContext.Consumer 实际上不怎么用的上
] = createAppProvider();
```
#### 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 属性
```ts
/**
* 应用程序接口类
*
* 该接口类声明了应用程序(管理)实例的接口定义,声明应用程序组件需要那些接口。
*
* 应用程序(管理)实例,可以是一个实现了 AppInterface 的类实例,也可以是一个 Object 结构。
*/
export interface AppInterface {
/**
* 应用程序启动接口,运行在 App 组件 componentDidMount
*/
onLaunch?(): void | Promise;
/**
* 当组件渲染接收到错误时的处理接口
*
* @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?: App,
/**
* 应用启动接口
*/
onLaunch?: () => void | Promise,
/**
* 错误异常处理接口
* @param error - 错误实例
*/
onError?: (error?: ErrorLike) => void,
/**
* 错误异常回退组件声明
* 可以是一个 Element 实例(自动注入 Error 实例),也可以是一个组件
*/
errorFallback?: ErrorFallbackComponent,
/**
* 错误发生时,是否回退,默认值为 `true`
*/
shouldErrorFallback?: boolean | ((error: ErrorLike) => boolean),
/**
* 应用程序准备中的加载器
*/
loader?: LoaderComponent,
}
```
**注意:** 当 `AppInterface` 和 `AppRootProps` 某个属性或接口同时存在,如 `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`
```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`
```tsx
import { App } from './services/AppService';
export default App;
```
Taro 小程序启动,App 和 Page 两者是同步并发的,所以阻塞 App.onLaunch 是无意义的。App 层级的错误边界也是无用的,应该要在 Page 进行错误捕获。
#### React Native
```tsx
import { createAppProvider } from 'react-app-provider';
const Loader: React.FC = () => {
return (
App Loading...
);
};
const [AppRoot, useAppContext] = createAppProvider({
onLaunch(): Promise {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, 3000);
});
},
Loader,
});
const App = () => {
return (
{/* ...... */}
)
}
export default App;
```
### `setupPage` 使用说明
`setupPage` 提供一个根据你的 React 应用环境,自行注入所需 props 到标准页面组件的注入机制,具体注入什么,你自己决定,基础页面布局,也由你自己决定。
```tsx
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> {
state = {
error: undefined,
}
render() {
return (
{isValidElementType(render) ? createElement(render, props) : null}
);
}
}
// 这里得到一个 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**
```tsx
// 你原来的首页,可以不去改变他
const IndexPage = () => {
return (
{/* 首页的代码 */}
);
};
// 页面初始化的配置,非必要
const pageOpts = {};
export default page(IndexPage, pageOpts);
```
实际应用中,我们往往会在 BasePage 加入一个 PageContext,以便于相关的页面内的所有组件,可以共享得到当前页面的上下文。
也不局限于一套页面机制,你可以定义多个,比如 `userPage` `adminPage`,并与之对应的 `useUserPage`,`useAdminPage` 等等。
`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`