├── .npmrc ├── src ├── index.ts ├── component │ ├── index.ts │ ├── TryAgainPage.tsx │ └── index.less ├── .DS_Store ├── types.ts ├── lazyEntrance.tsx ├── loadable.tsx ├── retry.ts ├── withErrorBoundary.tsx └── assets │ └── page-crash.svg ├── .gitignore ├── demo ├── WechatIMG591.png ├── app.tsx ├── error.tsx └── index.tsx ├── babel.config.js ├── typings └── global.d.ts ├── .fatherrc.ts ├── .prettierrc ├── tsconfig.node.json ├── .editorconfig ├── vite.config.ts ├── index.html ├── tsconfig.json ├── LICENSE ├── README.md └── package.json /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lazyEntrance'; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | dist 4 | -------------------------------------------------------------------------------- /src/component/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TryAgainPage'; 2 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hawx1993/lazy-import-with-error-boundary/main/src/.DS_Store -------------------------------------------------------------------------------- /demo/WechatIMG591.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hawx1993/lazy-import-with-error-boundary/main/demo/WechatIMG591.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript', 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /demo/app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const App = () => { 4 | const a = 1; 5 | a += 1; 6 | return
123
; 7 | }; 8 | 9 | export default App; 10 | -------------------------------------------------------------------------------- /typings/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | declare module '*.less'; 3 | declare module '*.svg'; 4 | declare module '*.jpg'; 5 | declare module '*.png'; 6 | declare module '*.gif'; 7 | declare module '*.scss'; 8 | -------------------------------------------------------------------------------- /.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father'; 2 | 3 | export default defineConfig({ 4 | esm: {}, 5 | cjs: {}, 6 | umd: { 7 | entry: { 8 | 'src/index': {}, 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 80, 5 | "overrides": [ 6 | { 7 | "files": ".prettierrc", 8 | "options": { "parser": "json" } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type State = { 4 | hasError: boolean; 5 | }; 6 | 7 | type ErrorProps = { 8 | retry: () => void; 9 | }; 10 | 11 | type Options = { 12 | Error?: React.ElementType; 13 | }; 14 | 15 | export { State, ErrorProps, Options }; 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | css: { 8 | preprocessorOptions: { 9 | less: { 10 | javascriptEnabled: true, 11 | }, 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vite + React + TS 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /demo/error.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | // import './index.less'; 3 | 4 | const CustomError = ({ onReload }) => { 5 | const handleClick = useCallback(async () => { 6 | await onReload(); 7 | }, [onReload]); 8 | return ( 9 |
10 |

页面崩溃了,无法正确显示,(自定义文案)

11 | 14 |
15 | ); 16 | }; 17 | export { CustomError }; 18 | -------------------------------------------------------------------------------- /src/component/TryAgainPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import PAGE_CRASH from '../assets/page-crash.svg'; 3 | import './index.less'; 4 | 5 | const TryAgainPage = ({ onReload }) => { 6 | const handleClick = useCallback(async () => { 7 | await onReload(); 8 | }, [onReload]); 9 | return ( 10 |
11 | 12 |

页面崩溃了,无法正确显示

13 | 16 |
17 | ); 18 | }; 19 | export { TryAgainPage }; 20 | -------------------------------------------------------------------------------- /src/lazyEntrance.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { loadableComponent } from './loadable'; 3 | import { TryAgainPage } from './component/TryAgainPage'; 4 | import { ErrorProps } from './types'; 5 | 6 | interface DefaultImportedComponent

{ 7 | default: React.ElementType

; 8 | } 9 | type DefaultComponent

= React.ElementType

| DefaultImportedComponent

; 10 | 11 | const ErrorComponent = ({ retry }: ErrorProps) => ( 12 | 13 | ); 14 | 15 | export function lazyEntrance( 16 | loadFn: (props: T) => Promise>, 17 | customErrorComponent?: React.ElementType, 18 | ) { 19 | return loadableComponent(loadFn, { 20 | Loading: null, 21 | Error: customErrorComponent || ErrorComponent, 22 | }); 23 | } 24 | export { ErrorComponent }; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "jsx": "react", 7 | "declaration": true, 8 | "pretty": true, 9 | "rootDir": "src", 10 | "sourceMap": false, 11 | "strict": true, 12 | "esModuleInterop": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "noImplicitAny": false, 17 | "noFallthroughCasesInSwitch": true, 18 | "allowSyntheticDefaultImports": true, 19 | "outDir": "dist", 20 | "lib": [ 21 | "es2018", 22 | "dom" 23 | ], 24 | "importHelpers": true 25 | }, 26 | "include": ["src/**/*"], 27 | "files": ["./typings/global.d.ts"], 28 | "exclude": [ 29 | "node_modules", 30 | "lib", 31 | "esm", 32 | "tests", 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /src/loadable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import loadable, { 3 | DefaultComponent, 4 | Options, 5 | LoadableComponentOptions, 6 | } from '@loadable/component'; 7 | import withErrorBoundary from './withErrorBoundary'; 8 | import { retry, MaxRetriesError } from './retry'; 9 | import { ErrorProps } from './types'; 10 | 11 | type LoadableComponentOptions = { 12 | timeout?: number; 13 | delay?: number; 14 | Error?: React.ElementType; 15 | retries?: number; 16 | } & Options; 17 | 18 | const RETRIES = 2; 19 | 20 | function loadableComponent( 21 | loadFn: (props: T) => Promise>, 22 | options: LoadableComponentOptions = {}, 23 | ) { 24 | const { retries = RETRIES, Error } = options; 25 | const newLoadFn = (props: T) => retry(loadFn, retries, props); 26 | return withErrorBoundary(loadable(newLoadFn), { 27 | Error, 28 | }); 29 | } 30 | 31 | export { loadableComponent, retry, MaxRetriesError }; 32 | -------------------------------------------------------------------------------- /src/retry.ts: -------------------------------------------------------------------------------- 1 | export class MaxRetriesError { 2 | readonly name = 'MaxRetriesError'; 3 | 4 | readonly stack?: string; 5 | 6 | readonly message: string; 7 | 8 | constructor(message?: string) { 9 | const error = new Error(message); 10 | this.stack = error.stack; 11 | this.message = error.message; 12 | } 13 | } 14 | 15 | export async function retry( 16 | fn: (props?: any) => Promise, 17 | retriesLeft: number = 2, 18 | props?: P, 19 | interval: number = 500, 20 | exponential: boolean = true, 21 | ): Promise { 22 | try { 23 | const val = await fn(props); 24 | return val; 25 | } catch (error) { 26 | if (retriesLeft) { 27 | await new Promise((r) => setTimeout(r, interval)); 28 | return retry( 29 | fn, 30 | retriesLeft - 1, 31 | props, 32 | exponential ? interval * 2 : interval, 33 | exponential, 34 | ); 35 | } 36 | throw new MaxRetriesError(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 trigkit4 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/component/index.less: -------------------------------------------------------------------------------- 1 | .error-wrap { 2 | position: relative; 3 | top: 50%; 4 | transform: translate(0, 100%); 5 | bottom: 0; 6 | left: 0; 7 | right: 0; 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: center; 11 | align-items: center; 12 | img { 13 | width: 220px; 14 | height: 280px; 15 | } 16 | .title { 17 | font-size: 14px; 18 | color: #343434; 19 | line-height: 22px; 20 | font-weight: 600; 21 | } 22 | .refresh-btn{ 23 | min-width: 72px; 24 | padding-right: 16px; 25 | padding-left: 16px; 26 | color: #fff; 27 | border-color: #346fff; 28 | background: #346fff; 29 | outline: 0; 30 | position: relative; 31 | display: inline-block; 32 | font-weight: 400; 33 | white-space: nowrap; 34 | text-align: center; 35 | user-select: none; 36 | touch-action: manipulation; 37 | height: 32px; 38 | padding: 4px 15px; 39 | font-size: 14px; 40 | border-radius: 4px; 41 | border: 1px solid #dae0f0; 42 | cursor: pointer; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /demo/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { lazyEntrance } from '../src'; 4 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 5 | import { CustomError } from './error'; 6 | 7 | enum PATH_NAME { 8 | ROOT = '/', 9 | CUSTOM_ERROR_COMPONENT = '/custom-error-component', 10 | } 11 | 12 | const CustomErrorComponent = ({ retry }: any) => ( 13 | 14 | ); 15 | const Apps = lazyEntrance( 16 | () => 17 | import( 18 | /* webpackChunkName: "page.onlineScripting" */ 19 | /* webpackPrefetch: true */ 20 | /* webpackPreload: true */ './app' 21 | ), 22 | ); 23 | 24 | const CustomErrorPage = lazyEntrance( 25 | () => 26 | import( 27 | /* webpackChunkName: "page.onlineScripting" */ 28 | /* webpackPrefetch: true */ 29 | /* webpackPreload: true */ './app' 30 | ), 31 | CustomErrorComponent, 32 | ); 33 | 34 | const DemoComponent = () => { 35 | return ( 36 | 37 | 38 | 39 | 43 | 44 | 45 | ); 46 | }; 47 | export default DemoComponent; 48 | 49 | render(, document.getElementById('root')); 50 | -------------------------------------------------------------------------------- /src/withErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LoadableComponent } from '@loadable/component'; 3 | import { MaxRetriesError } from './retry'; 4 | import { State, Options } from './types'; 5 | 6 | export async function retry( 7 | fn: (props?: P) => Promise, 8 | retriesLeft: number = 2, 9 | props?: P, 10 | interval: number = 500, 11 | exponential: boolean = true, 12 | ): Promise { 13 | try { 14 | const val = await fn(props); 15 | return val; 16 | } catch (error) { 17 | if (retriesLeft) { 18 | await new Promise((r) => setTimeout(r, interval)); 19 | return retry( 20 | fn, 21 | retriesLeft - 1, 22 | props, 23 | exponential ? interval * 2 : interval, 24 | exponential, 25 | ); 26 | } 27 | throw new MaxRetriesError(); 28 | } 29 | } 30 | 31 | export default function withErrorBoundary

( 32 | Loadable: LoadableComponent

, 33 | options: Options, 34 | ) { 35 | return class ErrorBoundary extends React.PureComponent { 36 | state = { 37 | hasError: false, 38 | }; 39 | 40 | retry = () => { 41 | this.setState({ hasError: false }); 42 | Loadable.load(this.props); 43 | }; 44 | 45 | componentDidCatch(error: Error) { 46 | if (error) { 47 | this.setState({ 48 | hasError: true, 49 | }); 50 | } 51 | } 52 | 53 | render() { 54 | if (this.state.hasError && options.Error) { 55 | const { Error } = options; 56 | return ; 57 | } 58 | 59 | return ; 60 | } 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lazy-import-with-error-boundary 2 | 3 |

4 | 5 | 6 | 7 | 8 |

9 | 10 | Features are as below: 11 | 12 | - Support code splitting 13 | - support load on-demand 14 | - support hooks component error boundary. 15 | - Support refresh function on bottom page 16 | 17 | ### Usages 18 | 19 | 1、install 20 | ```bash 21 | $ yarn add lazy-import-with-error-boundary 22 | ``` 23 | 24 | import lazyEntrance in your react router entrance: 25 | 26 | 2、Usage 27 | 28 | ```tsx 29 | import { lazyEntrance } from 'lazy-import-with-error-boundary'; 30 | 31 | export const Entrance = lazyEntrance(() => 32 | import( 33 | /* webpackChunkName: "contact.entrance" */ /* webpackPrefetch: true */ './container/Entrance' 34 | ), 35 | ); 36 | ``` 37 | 38 | 3、custom error component 39 | 40 | ```tsx 41 | import { CustomError } from './error'; 42 | 43 | const CustomErrorComponent = ({ retry }: any) => ( 44 | 45 | ); 46 | 47 | const CustomErrorPage = lazyEntrance( 48 | () => 49 | import( 50 | /* webpackChunkName: "page.onlineScripting" */ 51 | /* webpackPrefetch: true */ 52 | /* webpackPreload: true */ './app' 53 | ), 54 | CustomErrorComponent, 55 | ); 56 | ``` 57 | more details: run demo, open: http://localhost:5173/custom-error-component 58 | ### Demo 59 | 60 | ```bash 61 | $ yarn run dev 62 | ``` 63 | 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lazy-import-with-error-boundary", 3 | "version": "0.1.8", 4 | "description": "lazy import react hooks component with error boundary", 5 | "sideEffects": false, 6 | "files": [ 7 | "dist" 8 | ], 9 | "module": "dist/esm/index.js", 10 | "types": "dist/esm/index.d.ts", 11 | "main": "dist/cjs/index.js", 12 | "typings": "dist/esm/index.d.ts", 13 | "scripts": { 14 | "dev": "vite", 15 | "build": "npx father build", 16 | "version": "npm run build", 17 | "prepublishOnly": "npm version patch && git push", 18 | "release": "npm run build && npm publish" 19 | }, 20 | "husky": { 21 | "hooks": { 22 | "pre-commit": "lint-staged", 23 | "pre-push": "yarn lint && yarn clean && yarn build" 24 | } 25 | }, 26 | "author": "@hawx1993", 27 | "license": "MIT", 28 | "repository": { 29 | "type": "git" 30 | }, 31 | "dependencies": { 32 | "@loadable/component": "5.12.0" 33 | }, 34 | "peerDependencies": { 35 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0", 36 | "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" 37 | }, 38 | "devDependencies": { 39 | "@babel/core": "7.19.6", 40 | "@babel/preset-env": "7.19.4", 41 | "@babel/preset-typescript": "7.18.6", 42 | "@types/react": "^18.0.25", 43 | "@types/react-dom": "^18.0.9", 44 | "@vitejs/plugin-react": "^2.2.0", 45 | "father": "^4.1.0", 46 | "husky": "^8.0.2", 47 | "less": "^4.1.3", 48 | "less-loader": "^11.1.0", 49 | "lint-staged": "^13.0.3", 50 | "react": "^18.2.0", 51 | "react-dom": "^18.2.0", 52 | "react-router": "^5.2.0", 53 | "react-router-dom": "^5.2.0", 54 | "vite": "^3.2.4" 55 | }, 56 | "config": { 57 | "commitizen": { 58 | "path": "git-cz" 59 | } 60 | }, 61 | "lint-staged": { 62 | "src/**/**/*.{ts,tsx}": [ 63 | "eslint --fix", 64 | "git add" 65 | ] 66 | }, 67 | "volta": { 68 | "node": "14.18.2", 69 | "yarn": "1.22.17" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/assets/page-crash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 编组 8 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | --------------------------------------------------------------------------------