@mpxjs/webpack-plugin 2.10.6-beta.4 → 2.10.6-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/dependencies/RecordFileUrlDependency.js +58 -0
- package/lib/file-loader.js +3 -2
- package/lib/index.js +15 -11
- package/lib/react/processScript.js +2 -2
- package/lib/react/script-helper.js +22 -44
- package/lib/runtime/components/react/AsyncSuspense.tsx +164 -51
- package/lib/runtime/components/react/dist/AsyncSuspense.jsx +113 -44
- package/lib/runtime/optionProcessorReact.d.ts +18 -0
- package/lib/runtime/optionProcessorReact.js +24 -0
- package/package.json +1 -1
- package/lib/runtime/components/react/AsyncContainer.tsx +0 -217
- package/lib/runtime/components/react/dist/AsyncContainer.jsx +0 -160
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const NullDependency = require('webpack/lib/dependencies/NullDependency')
|
|
3
|
+
const makeSerializable = require('webpack/lib/util/makeSerializable')
|
|
4
|
+
|
|
5
|
+
class RecordFileUrlDependency extends NullDependency {
|
|
6
|
+
constructor (range, url) {
|
|
7
|
+
super()
|
|
8
|
+
this.range = range
|
|
9
|
+
this.url = url
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
get type () {
|
|
13
|
+
return 'mpx record file url'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
mpxAction (module, compilation, callback) {
|
|
17
|
+
return callback()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
updateHash (hash, context) {
|
|
21
|
+
hash.update('' + (+new Date()) + Math.random())
|
|
22
|
+
super.updateHash(hash, context)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
serialize (context) {
|
|
26
|
+
const { write } = context
|
|
27
|
+
write(this.url)
|
|
28
|
+
super.serialize(context)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
deserialize (context) {
|
|
32
|
+
const { read } = context
|
|
33
|
+
this.url = read()
|
|
34
|
+
super.deserialize(context)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
RecordFileUrlDependency.Template = class RecordFileUrlDependencyTemplate {
|
|
39
|
+
apply (dependency, source, { module, chunkGraph, runtimeTemplate }) {
|
|
40
|
+
const { range } = dependency
|
|
41
|
+
const compliation = runtimeTemplate.compilation
|
|
42
|
+
const publicPath = compliation.outputOptions.publicPath
|
|
43
|
+
const chunks = chunkGraph.getModuleChunks(module)
|
|
44
|
+
const chunk = chunks[0]
|
|
45
|
+
const chunkPath = path.dirname(publicPath + chunk.name)
|
|
46
|
+
const imgPath = publicPath + dependency.url
|
|
47
|
+
let relativePath = path.relative(chunkPath, imgPath)
|
|
48
|
+
if (!relativePath.startsWith('.')) {
|
|
49
|
+
relativePath = './' + relativePath
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
source.replace(range[0], range[1] - 1, JSON.stringify(relativePath))
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
makeSerializable(RecordFileUrlDependency, '@mpxjs/webpack-plugin/lib/dependencies/RecordFileUrlDependency')
|
|
57
|
+
|
|
58
|
+
module.exports = RecordFileUrlDependency
|
package/lib/file-loader.js
CHANGED
|
@@ -35,8 +35,9 @@ module.exports = function loader (content, prevOptions) {
|
|
|
35
35
|
|
|
36
36
|
let publicPath = `__webpack_public_path__ + ${JSON.stringify(url)}`
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
if (isRN) {
|
|
39
|
+
publicPath = `__non_webpack_require__(__mpx_rn_resolve_url_path_(${JSON.stringify(url)}))`
|
|
40
|
+
}
|
|
40
41
|
|
|
41
42
|
if (options.publicPath) {
|
|
42
43
|
if (typeof options.publicPath === 'function') {
|
package/lib/index.js
CHANGED
|
@@ -44,6 +44,7 @@ const FlagPluginDependency = require('./dependencies/FlagPluginDependency')
|
|
|
44
44
|
const RemoveEntryDependency = require('./dependencies/RemoveEntryDependency')
|
|
45
45
|
const RecordLoaderContentDependency = require('./dependencies/RecordLoaderContentDependency')
|
|
46
46
|
const RecordRuntimeInfoDependency = require('./dependencies/RecordRuntimeInfoDependency')
|
|
47
|
+
const RecordFileUrlDependency = require('./dependencies/RecordFileUrlDependency')
|
|
47
48
|
const SplitChunksPlugin = require('webpack/lib/optimize/SplitChunksPlugin')
|
|
48
49
|
const fixRelative = require('./utils/fix-relative')
|
|
49
50
|
const parseRequest = require('./utils/parse-request')
|
|
@@ -672,6 +673,9 @@ class MpxWebpackPlugin {
|
|
|
672
673
|
compilation.dependencyFactories.set(RecordRuntimeInfoDependency, new NullFactory())
|
|
673
674
|
compilation.dependencyTemplates.set(RecordRuntimeInfoDependency, new RecordRuntimeInfoDependency.Template())
|
|
674
675
|
|
|
676
|
+
compilation.dependencyFactories.set(RecordFileUrlDependency, new NullFactory())
|
|
677
|
+
compilation.dependencyTemplates.set(RecordFileUrlDependency, new RecordFileUrlDependency.Template())
|
|
678
|
+
|
|
675
679
|
compilation.dependencyTemplates.set(ImportDependency, new ImportDependencyTemplate())
|
|
676
680
|
})
|
|
677
681
|
|
|
@@ -1222,7 +1226,7 @@ class MpxWebpackPlugin {
|
|
|
1222
1226
|
if (isWeb(mpx.mode) && !hasOwn(splitChunksOptions.cacheGroups, 'main')) {
|
|
1223
1227
|
splitChunksOptions.cacheGroups.main = {
|
|
1224
1228
|
chunks: 'initial',
|
|
1225
|
-
name: '
|
|
1229
|
+
name: 'lib/index', // web 输出 chunk 路径和 rn 输出分包格式拉齐
|
|
1226
1230
|
test: /[\\/]node_modules[\\/]/
|
|
1227
1231
|
}
|
|
1228
1232
|
needInit = true
|
|
@@ -1230,7 +1234,7 @@ class MpxWebpackPlugin {
|
|
|
1230
1234
|
if (!hasOwn(splitChunksOptions.cacheGroups, 'async')) {
|
|
1231
1235
|
splitChunksOptions.cacheGroups.async = {
|
|
1232
1236
|
chunks: 'async',
|
|
1233
|
-
name: 'async/index',
|
|
1237
|
+
name: 'async-common/index',
|
|
1234
1238
|
minChunks: 2
|
|
1235
1239
|
}
|
|
1236
1240
|
needInit = true
|
|
@@ -1333,15 +1337,6 @@ class MpxWebpackPlugin {
|
|
|
1333
1337
|
compilation.hooks.processAssets.tap({
|
|
1334
1338
|
name: 'MpxWebpackPlugin'
|
|
1335
1339
|
}, (assets) => {
|
|
1336
|
-
if (isReact(mpx.mode)) {
|
|
1337
|
-
Object.keys(assets).forEach((chunkName) => {
|
|
1338
|
-
if (/\.js$/.test(chunkName)) {
|
|
1339
|
-
let val = assets[chunkName].source()
|
|
1340
|
-
val = val.replace(/_mpx_rn_img_relative_path_/g, chunkName === 'app.js' ? '.' : '..')
|
|
1341
|
-
compilation.assets[chunkName] = new RawSource(val)
|
|
1342
|
-
}
|
|
1343
|
-
})
|
|
1344
|
-
}
|
|
1345
1340
|
try {
|
|
1346
1341
|
const dynamicAssets = {}
|
|
1347
1342
|
for (const packageName in mpx.runtimeInfo) {
|
|
@@ -1387,6 +1382,15 @@ class MpxWebpackPlugin {
|
|
|
1387
1382
|
}
|
|
1388
1383
|
})
|
|
1389
1384
|
|
|
1385
|
+
parser.hooks.call.for('__mpx_rn_resolve_url_path_').tap('MpxWebpackPlugin', (expr) => {
|
|
1386
|
+
const args = expr.arguments.map((i) => i.value)
|
|
1387
|
+
args.unshift(expr.range)
|
|
1388
|
+
|
|
1389
|
+
const dep = new RecordFileUrlDependency(...args)
|
|
1390
|
+
parser.state.current.addPresentationalDependency(dep)
|
|
1391
|
+
return true
|
|
1392
|
+
})
|
|
1393
|
+
|
|
1390
1394
|
parser.hooks.call.for('__mpx_dynamic_entry__').tap('MpxWebpackPlugin', (expr) => {
|
|
1391
1395
|
const args = expr.arguments.map((i) => i.value)
|
|
1392
1396
|
args.unshift(expr.range)
|
|
@@ -36,7 +36,7 @@ module.exports = function (script, {
|
|
|
36
36
|
output += "import { lazy, createElement, memo, forwardRef } from 'react'\n"
|
|
37
37
|
if (ctorType === 'app') {
|
|
38
38
|
output += `
|
|
39
|
-
import { getComponent } from ${stringifyRequest(loaderContext, optionProcessorPath)}
|
|
39
|
+
import { getComponent, getAsyncSuspense } from ${stringifyRequest(loaderContext, optionProcessorPath)}
|
|
40
40
|
\n`
|
|
41
41
|
const { pagesMap, firstPage } = buildPagesMap({
|
|
42
42
|
localPagesMap,
|
|
@@ -53,7 +53,7 @@ import { getComponent } from ${stringifyRequest(loaderContext, optionProcessorPa
|
|
|
53
53
|
output += getRequireScript({ ctorType, script, loaderContext })
|
|
54
54
|
output += `export default global.__mpxOptionsMap[${JSON.stringify(moduleId)}]\n`
|
|
55
55
|
} else {
|
|
56
|
-
output += `import { getComponent } from ${stringifyRequest(loaderContext, optionProcessorPath)}\n`
|
|
56
|
+
output += `import { getComponent, getAsyncSuspense } from ${stringifyRequest(loaderContext, optionProcessorPath)}\n`
|
|
57
57
|
// 获取组件集合
|
|
58
58
|
const componentsMap = buildComponentsMap({
|
|
59
59
|
localComponentsMap,
|
|
@@ -4,7 +4,6 @@ const parseRequest = require('../utils/parse-request')
|
|
|
4
4
|
const shallowStringify = require('../utils/shallow-stringify')
|
|
5
5
|
const normalize = require('../utils/normalize')
|
|
6
6
|
const addQuery = require('../utils/add-query')
|
|
7
|
-
const path = require('path')
|
|
8
7
|
const { isBuildInReactTag } = require('../utils/dom-tag-config')
|
|
9
8
|
|
|
10
9
|
function stringifyRequest (loaderContext, request) {
|
|
@@ -15,8 +14,6 @@ function getMpxComponentRequest (component) {
|
|
|
15
14
|
return JSON.stringify(addQuery(`@mpxjs/webpack-plugin/lib/runtime/components/react/dist/${component}`, { isComponent: true }))
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
const mpxAsyncSuspense = getMpxComponentRequest('AsyncSuspense')
|
|
19
|
-
|
|
20
17
|
function getAsyncChunkName (chunkName) {
|
|
21
18
|
if (chunkName && typeof chunkName !== 'boolean') {
|
|
22
19
|
return `/* webpackChunkName: "${chunkName}/index" */`
|
|
@@ -24,49 +21,33 @@ function getAsyncChunkName (chunkName) {
|
|
|
24
21
|
return ''
|
|
25
22
|
}
|
|
26
23
|
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
})))`
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function getAsyncPage (componentName, componentRequest, chunkName, fallback, loading) {
|
|
44
|
-
fallback = fallback && `getComponent(require('${fallback}?isComponent=true'))`
|
|
45
|
-
loading = loading && `getComponent(require('${loading}?isComponent=true'))`
|
|
46
|
-
return `getComponent(function(props) {
|
|
47
|
-
return createElement(
|
|
48
|
-
getComponent(require(${mpxAsyncSuspense})),
|
|
49
|
-
{
|
|
50
|
-
type: 'page',
|
|
51
|
-
props: props,
|
|
52
|
-
chunkName: ${JSON.stringify(chunkName)},
|
|
53
|
-
request: ${JSON.stringify(componentRequest)},
|
|
54
|
-
fallback: ${fallback},
|
|
55
|
-
loading: ${loading},
|
|
56
|
-
getChildren: () => import(${getAsyncChunkName(chunkName)}${componentRequest})
|
|
57
|
-
}
|
|
58
|
-
)
|
|
59
|
-
})`
|
|
24
|
+
function getAsyncSuspense (type, moduleId, componentRequest, chunkName, fallback, loading) {
|
|
25
|
+
fallback = fallback && `getComponent(require(${fallback}))`
|
|
26
|
+
loading = loading && `getComponent(require(${loading}))`
|
|
27
|
+
return `
|
|
28
|
+
getAsyncSuspense({
|
|
29
|
+
type: ${JSON.stringify(type)},
|
|
30
|
+
moduleId: ${JSON.stringify(moduleId)},
|
|
31
|
+
chunkName: ${JSON.stringify(chunkName)},
|
|
32
|
+
loading: ${loading},
|
|
33
|
+
fallback: ${fallback},
|
|
34
|
+
getChildren: () => import(${getAsyncChunkName(chunkName)}${componentRequest})
|
|
35
|
+
})
|
|
36
|
+
`
|
|
60
37
|
}
|
|
61
38
|
|
|
62
39
|
function buildPagesMap ({ localPagesMap, loaderContext, jsonConfig, rnConfig }) {
|
|
63
40
|
let firstPage = ''
|
|
64
41
|
const pagesMap = {}
|
|
42
|
+
const mpx = loaderContext.getMpx()
|
|
65
43
|
Object.keys(localPagesMap).forEach((pagePath) => {
|
|
66
44
|
const pageCfg = localPagesMap[pagePath]
|
|
67
45
|
const pageRequest = stringifyRequest(loaderContext, pageCfg.resource)
|
|
68
46
|
if (pageCfg.async) {
|
|
69
|
-
|
|
47
|
+
const moduleId = mpx.getModuleId(pageCfg.resource)
|
|
48
|
+
const fallback = rnConfig.asyncChunk && rnConfig.asyncChunk.fallback && JSON.stringify(addQuery(rnConfig.asyncChunk.fallback, { isComponent: true }))
|
|
49
|
+
const loading = rnConfig.asyncChunk && rnConfig.asyncChunk.loading && JSON.stringify(addQuery(rnConfig.asyncChunk.loading, { isComponent: true }))
|
|
50
|
+
pagesMap[pagePath] = getAsyncSuspense('page', moduleId, pageRequest, pageCfg.async, fallback, loading)
|
|
70
51
|
} else {
|
|
71
52
|
// 为了保持小程序中app->page->component的js执行顺序,所有的page和component都改为require引入
|
|
72
53
|
pagesMap[pagePath] = `getComponent(require(${pageRequest}), {__mpxPageRoute: ${JSON.stringify(pagePath)}, displayName: "Page"})`
|
|
@@ -92,6 +73,7 @@ function buildComponentsMap ({ localComponentsMap, builtInComponentsMap, loaderC
|
|
|
92
73
|
const componentCfg = localComponentsMap[componentName]
|
|
93
74
|
const componentRequest = stringifyRequest(loaderContext, componentCfg.resource)
|
|
94
75
|
if (componentCfg.async) {
|
|
76
|
+
const moduleId = mpx.getModuleId(componentCfg.resource)
|
|
95
77
|
const placeholder = jsonConfig.componentPlaceholder && jsonConfig.componentPlaceholder[componentName]
|
|
96
78
|
if (placeholder) {
|
|
97
79
|
if (localComponentsMap[placeholder]) {
|
|
@@ -102,11 +84,7 @@ function buildComponentsMap ({ localComponentsMap, builtInComponentsMap, loaderC
|
|
|
102
84
|
new Error(`[json processor][${loaderContext.resource}]: componentPlaceholder ${placeholder} should not be a async component, please check!`)
|
|
103
85
|
)
|
|
104
86
|
}
|
|
105
|
-
componentsMap[componentName] =
|
|
106
|
-
} else if (mpx.globalComponents[placeholder]) {
|
|
107
|
-
const { queryObj, rawResourcePath } = parseRequest(mpx.globalComponents[placeholder])
|
|
108
|
-
const placeholderRequest = JSON.stringify(path.resolve(queryObj.context, rawResourcePath))
|
|
109
|
-
componentsMap[componentName] = getAsyncComponent(componentName, componentRequest, componentCfg.async, placeholderRequest)
|
|
87
|
+
componentsMap[componentName] = getAsyncSuspense('component', moduleId, componentRequest, componentCfg.async, placeholderRequest)
|
|
110
88
|
} else {
|
|
111
89
|
const tag = `mpx-${placeholder}`
|
|
112
90
|
if (!isBuildInReactTag(tag)) {
|
|
@@ -114,13 +92,13 @@ function buildComponentsMap ({ localComponentsMap, builtInComponentsMap, loaderC
|
|
|
114
92
|
new Error(`[json processor][${loaderContext.resource}]: componentPlaceholder ${placeholder} is not built-in component, please check!`)
|
|
115
93
|
)
|
|
116
94
|
}
|
|
117
|
-
componentsMap[componentName] =
|
|
95
|
+
componentsMap[componentName] = getAsyncSuspense('component', moduleId, componentRequest, componentCfg.async, getMpxComponentRequest(tag))
|
|
118
96
|
}
|
|
119
97
|
} else {
|
|
120
98
|
loaderContext.emitError(
|
|
121
99
|
new Error(`[json processor][${loaderContext.resource}]: ${componentName} has no componentPlaceholder, please check!`)
|
|
122
100
|
)
|
|
123
|
-
componentsMap[componentName] =
|
|
101
|
+
componentsMap[componentName] = getAsyncSuspense('component', moduleId, componentRequest, componentCfg.async)
|
|
124
102
|
}
|
|
125
103
|
} else {
|
|
126
104
|
componentsMap[componentName] = `getComponent(require(${componentRequest}), {displayName: ${JSON.stringify(componentName)}})`
|
|
@@ -1,79 +1,192 @@
|
|
|
1
|
-
import { useState, ComponentType, useEffect, useCallback, useRef } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import { useState, ComponentType, useEffect, useCallback, useRef, ReactNode, createElement } from 'react'
|
|
2
|
+
import { View, Image, StyleSheet, Text, TouchableOpacity } from 'react-native'
|
|
3
|
+
import FastImage from '@d11/react-native-fast-image'
|
|
4
4
|
|
|
5
5
|
const asyncChunkMap = new Map()
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
const styles = StyleSheet.create({
|
|
8
|
+
container: {
|
|
9
|
+
flex: 1,
|
|
10
|
+
padding: 20,
|
|
11
|
+
backgroundColor: '#fff'
|
|
12
|
+
},
|
|
13
|
+
loadingImage: {
|
|
14
|
+
width: 100,
|
|
15
|
+
height: 100,
|
|
16
|
+
marginTop: 220,
|
|
17
|
+
alignSelf: 'center'
|
|
18
|
+
},
|
|
19
|
+
buttonText: {
|
|
20
|
+
color: '#fff',
|
|
21
|
+
fontSize: 16,
|
|
22
|
+
fontWeight: '500',
|
|
23
|
+
textAlign: 'center'
|
|
24
|
+
},
|
|
25
|
+
errorImage: {
|
|
26
|
+
marginTop: 80,
|
|
27
|
+
width: 220,
|
|
28
|
+
aspectRatio: 1,
|
|
29
|
+
alignSelf: 'center'
|
|
30
|
+
},
|
|
31
|
+
errorText: {
|
|
32
|
+
fontSize: 16,
|
|
33
|
+
textAlign: 'center',
|
|
34
|
+
color: '#333',
|
|
35
|
+
marginBottom: 20
|
|
36
|
+
},
|
|
37
|
+
retryButton: {
|
|
38
|
+
position: 'absolute',
|
|
39
|
+
bottom: 54,
|
|
40
|
+
left: 20,
|
|
41
|
+
right: 20,
|
|
42
|
+
backgroundColor: '#fff',
|
|
43
|
+
paddingVertical: 15,
|
|
44
|
+
borderRadius: 30,
|
|
45
|
+
marginTop: 40,
|
|
46
|
+
borderWidth: 1,
|
|
47
|
+
borderColor: '#FF5F00'
|
|
48
|
+
},
|
|
49
|
+
retryButtonText: {
|
|
50
|
+
color: '#FF5F00',
|
|
51
|
+
fontSize: 16,
|
|
52
|
+
fontWeight: '500',
|
|
53
|
+
textAlign: 'center'
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
interface LayoutViewProps {
|
|
58
|
+
children: ReactNode
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface AsyncModule {
|
|
62
|
+
__esModule: boolean
|
|
63
|
+
default: ReactNode
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface DefaultFallbackProps {
|
|
67
|
+
onReload: () => void
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const DefaultFallback = ({ onReload }: DefaultFallbackProps) => {
|
|
71
|
+
return (
|
|
72
|
+
<View style={styles.container}>
|
|
73
|
+
<Image
|
|
74
|
+
source={{
|
|
75
|
+
uri: 'https://dpubstatic.udache.com/static/dpubimg/Vak5mZvezPpKV5ZJI6P9b_drn-fallbak.png'
|
|
76
|
+
}}
|
|
77
|
+
style={styles.errorImage}
|
|
78
|
+
resizeMode="contain"
|
|
79
|
+
/>
|
|
80
|
+
<Text style={styles.errorText}>网络出了点问题,请查看网络环境</Text>
|
|
81
|
+
<TouchableOpacity
|
|
82
|
+
style={styles.retryButton}
|
|
83
|
+
onPress={onReload}
|
|
84
|
+
activeOpacity={0.7}
|
|
85
|
+
>
|
|
86
|
+
<Text style={styles.retryButtonText}>点击重试</Text>
|
|
87
|
+
</TouchableOpacity>
|
|
88
|
+
</View>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const LayoutView = (props: LayoutViewProps) => {
|
|
93
|
+
return (
|
|
94
|
+
<View style={{ flex: 1 }} collapsable={false}>{props.children}</View>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const DefaultLoading = () => {
|
|
99
|
+
return (
|
|
100
|
+
<View style={styles.container}>
|
|
101
|
+
<FastImage
|
|
102
|
+
style={styles.loadingImage}
|
|
103
|
+
source={{
|
|
104
|
+
uri: 'https://dpubstatic.udache.com/static/dpubimg/439jiCVOtNOnEv9F2LaDs_loading.gif'
|
|
105
|
+
}}
|
|
106
|
+
resizeMode={FastImage.resizeMode.contain}
|
|
107
|
+
></FastImage>
|
|
108
|
+
</View>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface AsyncSuspenseProps {
|
|
8
113
|
type: 'component' | 'page'
|
|
9
114
|
chunkName: string
|
|
10
|
-
|
|
11
|
-
|
|
115
|
+
moduleId: string
|
|
116
|
+
innerProps: any,
|
|
12
117
|
loading: ComponentType<unknown>
|
|
13
118
|
fallback: ComponentType<unknown>
|
|
14
|
-
getChildren: () => Promise<
|
|
119
|
+
getChildren: () => Promise<AsyncModule>
|
|
15
120
|
}
|
|
16
121
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
122
|
+
type ComponentStauts = 'pending' | 'error' | 'loaded'
|
|
123
|
+
|
|
124
|
+
const AsyncSuspense: React.FC<AsyncSuspenseProps> = ({
|
|
125
|
+
type,
|
|
126
|
+
innerProps,
|
|
127
|
+
chunkName,
|
|
128
|
+
moduleId,
|
|
129
|
+
loading,
|
|
130
|
+
fallback,
|
|
131
|
+
getChildren
|
|
132
|
+
}) => {
|
|
133
|
+
const [status, setStatus] = useState<ComponentStauts>('pending')
|
|
134
|
+
const chunkLoaded = asyncChunkMap.has(moduleId)
|
|
135
|
+
const loadChunkPromise = useRef<null | Promise<AsyncModule>>(null)
|
|
22
136
|
|
|
23
137
|
const reloadPage = useCallback(() => {
|
|
24
|
-
setKey((preV) => preV + 1)
|
|
25
|
-
console.log('[mpxAsyncSuspense]: reload page')
|
|
26
138
|
setStatus('pending')
|
|
27
139
|
}, [])
|
|
28
140
|
|
|
29
141
|
useEffect(() => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
142
|
+
let cancelled = false
|
|
143
|
+
if (!chunkLoaded && status === 'pending') {
|
|
144
|
+
if (loadChunkPromise.current) {
|
|
145
|
+
loadChunkPromise
|
|
146
|
+
.current.then((m: AsyncModule) => {
|
|
147
|
+
if (cancelled) return
|
|
148
|
+
asyncChunkMap.set(moduleId, m.__esModule ? m.default : m)
|
|
149
|
+
setStatus('loaded')
|
|
150
|
+
})
|
|
151
|
+
.catch((e) => {
|
|
152
|
+
if (cancelled) return
|
|
153
|
+
if (type === 'component') {
|
|
154
|
+
global.onLazyLoadError({
|
|
155
|
+
type: 'subpackage',
|
|
156
|
+
subpackage: [chunkName],
|
|
157
|
+
errMsg: `loadSubpackage: ${e.type}`
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
loadChunkPromise.current = null
|
|
161
|
+
setStatus('error')
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return () => {
|
|
167
|
+
cancelled = true
|
|
52
168
|
}
|
|
53
|
-
})
|
|
169
|
+
}, [status])
|
|
54
170
|
|
|
55
|
-
if (
|
|
56
|
-
const Comp = asyncChunkMap.get(
|
|
57
|
-
return
|
|
171
|
+
if (chunkLoaded) {
|
|
172
|
+
const Comp = asyncChunkMap.get(moduleId)
|
|
173
|
+
return createElement(Comp, innerProps)
|
|
58
174
|
} else if (status === 'error') {
|
|
59
|
-
console.log('the status is:', status)
|
|
60
175
|
if (type === 'page') {
|
|
61
|
-
const Fallback =
|
|
62
|
-
|
|
176
|
+
const Fallback =
|
|
177
|
+
(fallback as ComponentType<DefaultFallbackProps>) || DefaultFallback
|
|
178
|
+
return createElement(LayoutView, null, createElement(Fallback, { onReload: reloadPage }))
|
|
63
179
|
} else {
|
|
64
|
-
|
|
65
|
-
return <Fallback {...props}></Fallback>
|
|
180
|
+
return createElement(fallback, innerProps)
|
|
66
181
|
}
|
|
67
182
|
} else {
|
|
68
|
-
if (!
|
|
69
|
-
|
|
183
|
+
if (!loadChunkPromise.current) {
|
|
184
|
+
loadChunkPromise.current = getChildren()
|
|
70
185
|
}
|
|
71
186
|
if (type === 'page') {
|
|
72
|
-
|
|
73
|
-
return <PageWrapper><Fallback /></PageWrapper>
|
|
187
|
+
return createElement(LayoutView, null, createElement(loading || DefaultLoading))
|
|
74
188
|
} else {
|
|
75
|
-
|
|
76
|
-
return <Fallback {...props}></Fallback>
|
|
189
|
+
return createElement(fallback, innerProps)
|
|
77
190
|
}
|
|
78
191
|
}
|
|
79
192
|
}
|
|
@@ -1,67 +1,136 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
-
import {
|
|
1
|
+
import { useState, useEffect, useCallback, useRef, createElement } from 'react';
|
|
2
|
+
import { View, Image, StyleSheet, Text, TouchableOpacity } from 'react-native';
|
|
3
|
+
import FastImage from '@d11/react-native-fast-image';
|
|
3
4
|
const asyncChunkMap = new Map();
|
|
4
|
-
const
|
|
5
|
+
const styles = StyleSheet.create({
|
|
6
|
+
container: {
|
|
7
|
+
flex: 1,
|
|
8
|
+
padding: 20,
|
|
9
|
+
backgroundColor: '#fff'
|
|
10
|
+
},
|
|
11
|
+
loadingImage: {
|
|
12
|
+
width: 100,
|
|
13
|
+
height: 100,
|
|
14
|
+
marginTop: 220,
|
|
15
|
+
alignSelf: 'center'
|
|
16
|
+
},
|
|
17
|
+
buttonText: {
|
|
18
|
+
color: '#fff',
|
|
19
|
+
fontSize: 16,
|
|
20
|
+
fontWeight: '500',
|
|
21
|
+
textAlign: 'center'
|
|
22
|
+
},
|
|
23
|
+
errorImage: {
|
|
24
|
+
marginTop: 80,
|
|
25
|
+
width: 220,
|
|
26
|
+
aspectRatio: 1,
|
|
27
|
+
alignSelf: 'center'
|
|
28
|
+
},
|
|
29
|
+
errorText: {
|
|
30
|
+
fontSize: 16,
|
|
31
|
+
textAlign: 'center',
|
|
32
|
+
color: '#333',
|
|
33
|
+
marginBottom: 20
|
|
34
|
+
},
|
|
35
|
+
retryButton: {
|
|
36
|
+
position: 'absolute',
|
|
37
|
+
bottom: 54,
|
|
38
|
+
left: 20,
|
|
39
|
+
right: 20,
|
|
40
|
+
backgroundColor: '#fff',
|
|
41
|
+
paddingVertical: 15,
|
|
42
|
+
borderRadius: 30,
|
|
43
|
+
marginTop: 40,
|
|
44
|
+
borderWidth: 1,
|
|
45
|
+
borderColor: '#FF5F00'
|
|
46
|
+
},
|
|
47
|
+
retryButtonText: {
|
|
48
|
+
color: '#FF5F00',
|
|
49
|
+
fontSize: 16,
|
|
50
|
+
fontWeight: '500',
|
|
51
|
+
textAlign: 'center'
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
const DefaultFallback = ({ onReload }) => {
|
|
55
|
+
return (<View style={styles.container}>
|
|
56
|
+
<Image source={{
|
|
57
|
+
uri: 'https://dpubstatic.udache.com/static/dpubimg/Vak5mZvezPpKV5ZJI6P9b_drn-fallbak.png'
|
|
58
|
+
}} style={styles.errorImage} resizeMode="contain"/>
|
|
59
|
+
<Text style={styles.errorText}>网络出了点问题,请查看网络环境</Text>
|
|
60
|
+
<TouchableOpacity style={styles.retryButton} onPress={onReload} activeOpacity={0.7}>
|
|
61
|
+
<Text style={styles.retryButtonText}>点击重试</Text>
|
|
62
|
+
</TouchableOpacity>
|
|
63
|
+
</View>);
|
|
64
|
+
};
|
|
65
|
+
const LayoutView = (props) => {
|
|
66
|
+
return (<View style={{ flex: 1 }} collapsable={false}>{props.children}</View>);
|
|
67
|
+
};
|
|
68
|
+
const DefaultLoading = () => {
|
|
69
|
+
return (<View style={styles.container}>
|
|
70
|
+
<FastImage style={styles.loadingImage} source={{
|
|
71
|
+
uri: 'https://dpubstatic.udache.com/static/dpubimg/439jiCVOtNOnEv9F2LaDs_loading.gif'
|
|
72
|
+
}} resizeMode={FastImage.resizeMode.contain}></FastImage>
|
|
73
|
+
</View>);
|
|
74
|
+
};
|
|
75
|
+
const AsyncSuspense = ({ type, innerProps, chunkName, moduleId, loading, fallback, getChildren }) => {
|
|
5
76
|
const [status, setStatus] = useState('pending');
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const chunkPromise = useRef(null);
|
|
77
|
+
const chunkLoaded = asyncChunkMap.has(moduleId);
|
|
78
|
+
const loadChunkPromise = useRef(null);
|
|
9
79
|
const reloadPage = useCallback(() => {
|
|
10
|
-
setKey((preV) => preV + 1);
|
|
11
|
-
console.log('[mpxAsyncSuspense]: reload page');
|
|
12
80
|
setStatus('pending');
|
|
13
81
|
}, []);
|
|
14
82
|
useEffect(() => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
83
|
+
let cancelled = false;
|
|
84
|
+
if (!chunkLoaded && status === 'pending') {
|
|
85
|
+
if (loadChunkPromise.current) {
|
|
86
|
+
loadChunkPromise
|
|
87
|
+
.current.then((m) => {
|
|
88
|
+
if (cancelled)
|
|
89
|
+
return;
|
|
90
|
+
asyncChunkMap.set(moduleId, m.__esModule ? m.default : m);
|
|
91
|
+
setStatus('loaded');
|
|
92
|
+
})
|
|
93
|
+
.catch((e) => {
|
|
94
|
+
if (cancelled)
|
|
95
|
+
return;
|
|
96
|
+
if (type === 'component') {
|
|
97
|
+
global.onLazyLoadError({
|
|
98
|
+
type: 'subpackage',
|
|
99
|
+
subpackage: [chunkName],
|
|
100
|
+
errMsg: `loadSubpackage: ${e.type}`
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
loadChunkPromise.current = null;
|
|
104
|
+
setStatus('error');
|
|
105
|
+
});
|
|
106
|
+
}
|
|
37
107
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
108
|
+
return () => {
|
|
109
|
+
cancelled = true;
|
|
110
|
+
};
|
|
111
|
+
}, [status]);
|
|
112
|
+
if (chunkLoaded) {
|
|
113
|
+
const Comp = asyncChunkMap.get(moduleId);
|
|
114
|
+
return createElement(Comp, innerProps);
|
|
42
115
|
}
|
|
43
116
|
else if (status === 'error') {
|
|
44
|
-
console.log('the status is:', status);
|
|
45
117
|
if (type === 'page') {
|
|
46
118
|
const Fallback = fallback || DefaultFallback;
|
|
47
|
-
return
|
|
119
|
+
return createElement(LayoutView, null, createElement(Fallback, { onReload: reloadPage }));
|
|
48
120
|
}
|
|
49
121
|
else {
|
|
50
|
-
|
|
51
|
-
return <Fallback {...props}></Fallback>;
|
|
122
|
+
return createElement(fallback, innerProps);
|
|
52
123
|
}
|
|
53
124
|
}
|
|
54
125
|
else {
|
|
55
|
-
if (!
|
|
56
|
-
|
|
126
|
+
if (!loadChunkPromise.current) {
|
|
127
|
+
loadChunkPromise.current = getChildren();
|
|
57
128
|
}
|
|
58
129
|
if (type === 'page') {
|
|
59
|
-
|
|
60
|
-
return <PageWrapper><Fallback /></PageWrapper>;
|
|
130
|
+
return createElement(LayoutView, null, createElement(loading || DefaultLoading));
|
|
61
131
|
}
|
|
62
132
|
else {
|
|
63
|
-
|
|
64
|
-
return <Fallback {...props}></Fallback>;
|
|
133
|
+
return createElement(fallback, innerProps);
|
|
65
134
|
}
|
|
66
135
|
}
|
|
67
136
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ReactNode, ComponentType } from 'react'
|
|
1
2
|
declare global {
|
|
2
3
|
namespace NodeJS {
|
|
3
4
|
interface Global {
|
|
@@ -7,3 +8,20 @@ declare global {
|
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export function getComponent (...args: any): object
|
|
11
|
+
|
|
12
|
+
interface AsyncModule {
|
|
13
|
+
__esModule: boolean
|
|
14
|
+
default: ReactNode
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface AsyncSuspenseProps {
|
|
18
|
+
type: 'component' | 'page'
|
|
19
|
+
chunkName: string
|
|
20
|
+
moduleId: string
|
|
21
|
+
innerProps: any
|
|
22
|
+
loading: ComponentType<unknown>
|
|
23
|
+
fallback: ComponentType<unknown>
|
|
24
|
+
getChildren: () => Promise<AsyncModule>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getAsyncSuspense(props: AsyncSuspenseProps): ReactNode
|
|
@@ -1,6 +1,30 @@
|
|
|
1
|
+
import AsyncSuspense from '@mpxjs/webpack-plugin/lib/runtime/components/react/dist/AsyncSuspense'
|
|
2
|
+
import { memo, forwardRef, createElement } from 'react'
|
|
3
|
+
import { extend } from './utils'
|
|
4
|
+
|
|
1
5
|
export function getComponent (component, extendOptions) {
|
|
2
6
|
component = component.__esModule ? component.default : component
|
|
3
7
|
// eslint-disable-next-line
|
|
4
8
|
if (extendOptions) Object.assign(component, extendOptions)
|
|
5
9
|
return component
|
|
6
10
|
}
|
|
11
|
+
|
|
12
|
+
export function getAsyncSuspense (commonProps) {
|
|
13
|
+
if (commonProps.type === 'component') {
|
|
14
|
+
return memo(forwardRef(function (props, ref) {
|
|
15
|
+
return createElement(AsyncSuspense,
|
|
16
|
+
extend(commonProps, {
|
|
17
|
+
innerProps: Object.assign({}, props, { ref })
|
|
18
|
+
})
|
|
19
|
+
)
|
|
20
|
+
}))
|
|
21
|
+
} else {
|
|
22
|
+
return function (props) {
|
|
23
|
+
return createElement(AsyncSuspense,
|
|
24
|
+
extend(commonProps, {
|
|
25
|
+
innerProps: props
|
|
26
|
+
})
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
import { ComponentType, ReactNode, Component, Suspense } from 'react'
|
|
2
|
-
import { View, Text, Image, StyleSheet, TouchableOpacity } from 'react-native'
|
|
3
|
-
import FastImage from '@d11/react-native-fast-image'
|
|
4
|
-
import Animated, { useAnimatedStyle } from 'react-native-reanimated'
|
|
5
|
-
|
|
6
|
-
type PageWrapper = {
|
|
7
|
-
children: ReactNode
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const PageWrapper = ({ children }: PageWrapper) => {
|
|
11
|
-
const animatedStyle = useAnimatedStyle(() => ({
|
|
12
|
-
transform: [{ translateY: -0 }],
|
|
13
|
-
flexBasis: 'auto',
|
|
14
|
-
flex: 1
|
|
15
|
-
}))
|
|
16
|
-
|
|
17
|
-
return (
|
|
18
|
-
<Animated.View
|
|
19
|
-
style={[
|
|
20
|
-
animatedStyle
|
|
21
|
-
]}
|
|
22
|
-
>
|
|
23
|
-
{children}
|
|
24
|
-
</Animated.View>
|
|
25
|
-
)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const styles = StyleSheet.create({
|
|
29
|
-
container: {
|
|
30
|
-
flex: 1,
|
|
31
|
-
padding: 20,
|
|
32
|
-
backgroundColor: '#fff'
|
|
33
|
-
},
|
|
34
|
-
loadingImage: {
|
|
35
|
-
width: 100,
|
|
36
|
-
height: 100,
|
|
37
|
-
marginTop: 220,
|
|
38
|
-
alignSelf: 'center'
|
|
39
|
-
},
|
|
40
|
-
buttonText: {
|
|
41
|
-
color: '#fff',
|
|
42
|
-
fontSize: 16,
|
|
43
|
-
fontWeight: '500',
|
|
44
|
-
textAlign: 'center'
|
|
45
|
-
},
|
|
46
|
-
errorImage: {
|
|
47
|
-
marginTop: 80,
|
|
48
|
-
width: 220,
|
|
49
|
-
aspectRatio: 1,
|
|
50
|
-
alignSelf: 'center'
|
|
51
|
-
},
|
|
52
|
-
errorText: {
|
|
53
|
-
fontSize: 16,
|
|
54
|
-
textAlign: 'center',
|
|
55
|
-
color: '#333',
|
|
56
|
-
marginBottom: 20
|
|
57
|
-
},
|
|
58
|
-
retryButton: {
|
|
59
|
-
position: 'absolute',
|
|
60
|
-
bottom: 54,
|
|
61
|
-
left: 20,
|
|
62
|
-
right: 20,
|
|
63
|
-
backgroundColor: '#fff',
|
|
64
|
-
paddingVertical: 15,
|
|
65
|
-
borderRadius: 30,
|
|
66
|
-
marginTop: 40,
|
|
67
|
-
borderWidth: 1,
|
|
68
|
-
borderColor: '#FF5F00'
|
|
69
|
-
},
|
|
70
|
-
retryButtonText: {
|
|
71
|
-
color: '#FF5F00',
|
|
72
|
-
fontSize: 16,
|
|
73
|
-
fontWeight: '500',
|
|
74
|
-
textAlign: 'center'
|
|
75
|
-
}
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
type AsyncType = 'page' | 'component'
|
|
79
|
-
|
|
80
|
-
interface PropsType<T extends AsyncType> {
|
|
81
|
-
type: T
|
|
82
|
-
props: object
|
|
83
|
-
loading: ComponentType<unknown>
|
|
84
|
-
fallback: ComponentType<unknown>
|
|
85
|
-
children: (props: any) => ReactNode | ReactNode
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
interface StateType {
|
|
89
|
-
hasError: boolean,
|
|
90
|
-
key: number
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
interface ComponentError extends Error {
|
|
94
|
-
request?: string
|
|
95
|
-
type: 'timeout' | 'fail'
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export const DefaultLoading = () => {
|
|
99
|
-
return (
|
|
100
|
-
<View style={styles.container}>
|
|
101
|
-
<FastImage
|
|
102
|
-
style={styles.loadingImage}
|
|
103
|
-
source={{ uri: 'https://dpubstatic.udache.com/static/dpubimg/439jiCVOtNOnEv9F2LaDs_loading.gif' }}
|
|
104
|
-
resizeMode={FastImage.resizeMode.contain}></FastImage>
|
|
105
|
-
</View>
|
|
106
|
-
)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export interface DefaultFallbackProps {
|
|
110
|
-
onReload: () => void
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export const DefaultFallback = ({ onReload }: DefaultFallbackProps) => {
|
|
114
|
-
return (
|
|
115
|
-
<View style={styles.container}>
|
|
116
|
-
<Image
|
|
117
|
-
source={{ uri: 'https://dpubstatic.udache.com/static/dpubimg/Vak5mZvezPpKV5ZJI6P9b_drn-fallbak.png' }}
|
|
118
|
-
style={styles.errorImage}
|
|
119
|
-
resizeMode="contain"
|
|
120
|
-
/>
|
|
121
|
-
<Text style={styles.errorText}>网络出了点问题,请查看网络环境</Text>
|
|
122
|
-
<TouchableOpacity
|
|
123
|
-
style={styles.retryButton}
|
|
124
|
-
onPress={onReload}
|
|
125
|
-
activeOpacity={0.7}
|
|
126
|
-
>
|
|
127
|
-
<Text style={styles.retryButtonText}>点击重试</Text>
|
|
128
|
-
</TouchableOpacity>
|
|
129
|
-
</View>
|
|
130
|
-
)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export default class AsyncContainer extends Component<PropsType<AsyncType>, StateType> {
|
|
134
|
-
private suspenseFallback: ReactNode
|
|
135
|
-
private errorFallback: ReactNode
|
|
136
|
-
|
|
137
|
-
constructor (props: PropsType<AsyncType>) {
|
|
138
|
-
super(props)
|
|
139
|
-
this.state = {
|
|
140
|
-
hasError: false,
|
|
141
|
-
key: 0
|
|
142
|
-
}
|
|
143
|
-
this.suspenseFallback = this.getSuspenseFallback()
|
|
144
|
-
this.errorFallback = this.getErrorFallback()
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// render 阶段收集到的错误
|
|
148
|
-
static getDerivedStateFromError (error: ComponentError): StateType | undefined {
|
|
149
|
-
if (error.name === 'ChunkLoadError') {
|
|
150
|
-
return {
|
|
151
|
-
hasError: true,
|
|
152
|
-
key: 0
|
|
153
|
-
}
|
|
154
|
-
} else {
|
|
155
|
-
// 被外层捕获
|
|
156
|
-
throw error
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
componentDidCatch (error: ComponentError): void {
|
|
161
|
-
if (error.name === 'ChunkLoadError' && this.props.type === 'component') {
|
|
162
|
-
const request = error.request || ''
|
|
163
|
-
const subpackage = request.split('/').filter((i: string) => !!i)[0]
|
|
164
|
-
global.onLazyLoadError({
|
|
165
|
-
type: 'subpackage',
|
|
166
|
-
subpackage: [subpackage],
|
|
167
|
-
errMsg: `loadSubpackage: ${error.type}`
|
|
168
|
-
})
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
reloadPage () {
|
|
173
|
-
this.setState((prevState) => {
|
|
174
|
-
return {
|
|
175
|
-
hasError: false,
|
|
176
|
-
key: prevState.key + 1
|
|
177
|
-
}
|
|
178
|
-
})
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
getSuspenseFallback () {
|
|
182
|
-
if (this.props.type === 'page') {
|
|
183
|
-
const Fallback = this.props.loading || DefaultLoading
|
|
184
|
-
return <Fallback />
|
|
185
|
-
} else {
|
|
186
|
-
const Fallback = this.props.loading
|
|
187
|
-
return <Fallback {...this.props.props}></Fallback>
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
getErrorFallback () {
|
|
192
|
-
if (this.props.type === 'page') {
|
|
193
|
-
const Fallback = this.props.fallback as ComponentType<DefaultFallbackProps> || DefaultFallback
|
|
194
|
-
return <Fallback onReload={this.reloadPage.bind(this)}></Fallback>
|
|
195
|
-
} else {
|
|
196
|
-
const Fallback = this.props.loading
|
|
197
|
-
return <Fallback {...this.props.props}></Fallback>
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
render () {
|
|
202
|
-
if (this.state.hasError) {
|
|
203
|
-
if (this.props.type === 'component') {
|
|
204
|
-
return this.errorFallback
|
|
205
|
-
} else {
|
|
206
|
-
return (<PageWrapper>{this.errorFallback}</PageWrapper>)
|
|
207
|
-
}
|
|
208
|
-
} else {
|
|
209
|
-
return (
|
|
210
|
-
<Suspense fallback={this.suspenseFallback} key={this.state.key}>
|
|
211
|
-
{typeof this.props.children === 'function' ? this.props.children(this.props.props) : this.props.children}
|
|
212
|
-
{/* {this.props.children(this.props.props)} */}
|
|
213
|
-
</Suspense>
|
|
214
|
-
)
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { Component, Suspense } from 'react';
|
|
2
|
-
import { View, Text, Image, StyleSheet, TouchableOpacity } from 'react-native';
|
|
3
|
-
import FastImage from '@d11/react-native-fast-image';
|
|
4
|
-
import Animated, { useAnimatedStyle } from 'react-native-reanimated';
|
|
5
|
-
export const PageWrapper = ({ children }) => {
|
|
6
|
-
const animatedStyle = useAnimatedStyle(() => ({
|
|
7
|
-
transform: [{ translateY: -0 }],
|
|
8
|
-
flexBasis: 'auto',
|
|
9
|
-
flex: 1
|
|
10
|
-
}));
|
|
11
|
-
return (<Animated.View style={[
|
|
12
|
-
animatedStyle
|
|
13
|
-
]}>
|
|
14
|
-
{children}
|
|
15
|
-
</Animated.View>);
|
|
16
|
-
};
|
|
17
|
-
const styles = StyleSheet.create({
|
|
18
|
-
container: {
|
|
19
|
-
flex: 1,
|
|
20
|
-
padding: 20,
|
|
21
|
-
backgroundColor: '#fff'
|
|
22
|
-
},
|
|
23
|
-
loadingImage: {
|
|
24
|
-
width: 100,
|
|
25
|
-
height: 100,
|
|
26
|
-
marginTop: 220,
|
|
27
|
-
alignSelf: 'center'
|
|
28
|
-
},
|
|
29
|
-
buttonText: {
|
|
30
|
-
color: '#fff',
|
|
31
|
-
fontSize: 16,
|
|
32
|
-
fontWeight: '500',
|
|
33
|
-
textAlign: 'center'
|
|
34
|
-
},
|
|
35
|
-
errorImage: {
|
|
36
|
-
marginTop: 80,
|
|
37
|
-
width: 220,
|
|
38
|
-
aspectRatio: 1,
|
|
39
|
-
alignSelf: 'center'
|
|
40
|
-
},
|
|
41
|
-
errorText: {
|
|
42
|
-
fontSize: 16,
|
|
43
|
-
textAlign: 'center',
|
|
44
|
-
color: '#333',
|
|
45
|
-
marginBottom: 20
|
|
46
|
-
},
|
|
47
|
-
retryButton: {
|
|
48
|
-
position: 'absolute',
|
|
49
|
-
bottom: 54,
|
|
50
|
-
left: 20,
|
|
51
|
-
right: 20,
|
|
52
|
-
backgroundColor: '#fff',
|
|
53
|
-
paddingVertical: 15,
|
|
54
|
-
borderRadius: 30,
|
|
55
|
-
marginTop: 40,
|
|
56
|
-
borderWidth: 1,
|
|
57
|
-
borderColor: '#FF5F00'
|
|
58
|
-
},
|
|
59
|
-
retryButtonText: {
|
|
60
|
-
color: '#FF5F00',
|
|
61
|
-
fontSize: 16,
|
|
62
|
-
fontWeight: '500',
|
|
63
|
-
textAlign: 'center'
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
export const DefaultLoading = () => {
|
|
67
|
-
return (<View style={styles.container}>
|
|
68
|
-
<FastImage style={styles.loadingImage} source={{ uri: 'https://dpubstatic.udache.com/static/dpubimg/439jiCVOtNOnEv9F2LaDs_loading.gif' }} resizeMode={FastImage.resizeMode.contain}></FastImage>
|
|
69
|
-
</View>);
|
|
70
|
-
};
|
|
71
|
-
export const DefaultFallback = ({ onReload }) => {
|
|
72
|
-
return (<View style={styles.container}>
|
|
73
|
-
<Image source={{ uri: 'https://dpubstatic.udache.com/static/dpubimg/Vak5mZvezPpKV5ZJI6P9b_drn-fallbak.png' }} style={styles.errorImage} resizeMode="contain"/>
|
|
74
|
-
<Text style={styles.errorText}>网络出了点问题,请查看网络环境</Text>
|
|
75
|
-
<TouchableOpacity style={styles.retryButton} onPress={onReload} activeOpacity={0.7}>
|
|
76
|
-
<Text style={styles.retryButtonText}>点击重试</Text>
|
|
77
|
-
</TouchableOpacity>
|
|
78
|
-
</View>);
|
|
79
|
-
};
|
|
80
|
-
export default class AsyncContainer extends Component {
|
|
81
|
-
suspenseFallback;
|
|
82
|
-
errorFallback;
|
|
83
|
-
constructor(props) {
|
|
84
|
-
super(props);
|
|
85
|
-
this.state = {
|
|
86
|
-
hasError: false,
|
|
87
|
-
key: 0
|
|
88
|
-
};
|
|
89
|
-
this.suspenseFallback = this.getSuspenseFallback();
|
|
90
|
-
this.errorFallback = this.getErrorFallback();
|
|
91
|
-
}
|
|
92
|
-
// render 阶段收集到的错误
|
|
93
|
-
static getDerivedStateFromError(error) {
|
|
94
|
-
if (error.name === 'ChunkLoadError') {
|
|
95
|
-
return {
|
|
96
|
-
hasError: true,
|
|
97
|
-
key: 0
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
// 被外层捕获
|
|
102
|
-
throw error;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
componentDidCatch(error) {
|
|
106
|
-
if (error.name === 'ChunkLoadError' && this.props.type === 'component') {
|
|
107
|
-
const request = error.request || '';
|
|
108
|
-
const subpackage = request.split('/').filter((i) => !!i)[0];
|
|
109
|
-
global.onLazyLoadError({
|
|
110
|
-
type: 'subpackage',
|
|
111
|
-
subpackage: [subpackage],
|
|
112
|
-
errMsg: `loadSubpackage: ${error.type}`
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
reloadPage() {
|
|
117
|
-
this.setState((prevState) => {
|
|
118
|
-
return {
|
|
119
|
-
hasError: false,
|
|
120
|
-
key: prevState.key + 1
|
|
121
|
-
};
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
getSuspenseFallback() {
|
|
125
|
-
if (this.props.type === 'page') {
|
|
126
|
-
const Fallback = this.props.loading || DefaultLoading;
|
|
127
|
-
return <Fallback />;
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
const Fallback = this.props.loading;
|
|
131
|
-
return <Fallback {...this.props.props}></Fallback>;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
getErrorFallback() {
|
|
135
|
-
if (this.props.type === 'page') {
|
|
136
|
-
const Fallback = this.props.fallback || DefaultFallback;
|
|
137
|
-
return <Fallback onReload={this.reloadPage.bind(this)}></Fallback>;
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
const Fallback = this.props.loading;
|
|
141
|
-
return <Fallback {...this.props.props}></Fallback>;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
render() {
|
|
145
|
-
if (this.state.hasError) {
|
|
146
|
-
if (this.props.type === 'component') {
|
|
147
|
-
return this.errorFallback;
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
return (<PageWrapper>{this.errorFallback}</PageWrapper>);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
return (<Suspense fallback={this.suspenseFallback} key={this.state.key}>
|
|
155
|
-
{typeof this.props.children === 'function' ? this.props.children(this.props.props) : this.props.children}
|
|
156
|
-
{/* {this.props.children(this.props.props)} */}
|
|
157
|
-
</Suspense>);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|