# 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 [![version](https://img.shields.io/npm/v/react-app-provider?style=for-the-badge)](https://www.npmjs.com/package/react-app-provider) ![dw](https://img.shields.io/npm/dw/react-app-provider?style=for-the-badge) ### 基本介绍 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`