react-app-provider
v1.0.7
Published
React application root component generic provider.
Downloads
22
Maintainers
Readme
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 中进行阻塞)
主要提供两大功能:
createAppProvider
提供应用程序根组件。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,
}
注意: 当 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
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
,并与之对应的 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