@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.
@@ -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
@@ -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
- // todo 未来添加分包处理后相对地址不一定是./开头的,需要考虑通过dependency的方式在sourceModule时通过最终的chunkName得到准确的相对路径
39
- if (isRN) publicPath = `__non_webpack_require__(${JSON.stringify(`_mpx_rn_img_relative_path_/${url}`)})`
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: 'bundle/index', // web 输出 chunk 路径和 rn 输出分包格式拉齐
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 getAsyncComponent (componentName, componentRequest, chunkName, fallback) {
28
- return `getComponent(memo(forwardRef(function(props, ref) {
29
- return createElement(
30
- getComponent(require(${mpxAsyncSuspense})),
31
- {
32
- type: 'component',
33
- props: Object.assign({}, props, { ref }),
34
- chunkName: ${JSON.stringify(chunkName)},
35
- request: ${JSON.stringify(componentRequest)},
36
- loading: getComponent(require(${fallback})),
37
- getChildren: () => import(${getAsyncChunkName(chunkName)}${componentRequest})
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
- pagesMap[pagePath] = getAsyncPage(pagePath, pageRequest, pageCfg.async, rnConfig.asyncChunk && rnConfig.asyncChunk.fallback, rnConfig.asyncChunk && rnConfig.asyncChunk.loading)
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] = getAsyncComponent(componentName, componentRequest, componentCfg.async, placeholderRequest)
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] = getAsyncComponent(componentName, componentRequest, componentCfg.async, getMpxComponentRequest(tag))
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] = getAsyncComponent(componentName, componentRequest, componentCfg.async)
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 { DefaultFallback, DefaultLoading, PageWrapper } from './AsyncContainer'
3
- import type { DefaultFallbackProps } from './AsyncContainer'
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
- interface props {
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
- request: string
11
- props: any,
115
+ moduleId: string
116
+ innerProps: any,
12
117
  loading: ComponentType<unknown>
13
118
  fallback: ComponentType<unknown>
14
- getChildren: () => Promise<unknown>
119
+ getChildren: () => Promise<AsyncModule>
15
120
  }
16
121
 
17
- const AsyncSuspense: React.FC<props> = ({ type, props, chunkName, request, loading, fallback, getChildren }) => {
18
- const [status, setStatus] = useState('pending')
19
- const loaded = asyncChunkMap.has(request)
20
- const [, setKey] = useState(0)
21
- const chunkPromise = useRef<null | Promise<unknown>>(null)
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
- if (!loaded && status === 'pending') {
31
- // todo 清楚副作用?
32
- console.log('the current :', chunkPromise.current)
33
- chunkPromise.current!
34
- .then((m: any) => {
35
- console.log('[mpxAsyncSuspense]: load sucess')
36
- asyncChunkMap.set(request, m.__esModule ? m.default : m)
37
- setStatus('loaded')
38
- })
39
- .catch((e) => {
40
- if (type === 'component') {
41
- console.log(11111, e)
42
- global.onLazyLoadError({
43
- type: 'subpackage',
44
- subpackage: [chunkName],
45
- errMsg: `loadSubpackage: ${e.type}`
46
- })
47
- }
48
- console.log('[mpxAsyncSuspense]: load eror', e)
49
- chunkPromise.current = null
50
- setStatus('error')
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 (loaded) {
56
- const Comp = asyncChunkMap.get(request)
57
- return <Comp {...props}></Comp>
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 = fallback as ComponentType<DefaultFallbackProps> || DefaultFallback
62
- return <><PageWrapper><Fallback onReload={reloadPage}></Fallback></PageWrapper></>
176
+ const Fallback =
177
+ (fallback as ComponentType<DefaultFallbackProps>) || DefaultFallback
178
+ return createElement(LayoutView, null, createElement(Fallback, { onReload: reloadPage }))
63
179
  } else {
64
- const Fallback = loading
65
- return <Fallback {...props}></Fallback>
180
+ return createElement(fallback, innerProps)
66
181
  }
67
182
  } else {
68
- if (!chunkPromise.current) {
69
- chunkPromise.current = getChildren()
183
+ if (!loadChunkPromise.current) {
184
+ loadChunkPromise.current = getChildren()
70
185
  }
71
186
  if (type === 'page') {
72
- const Fallback = loading || DefaultLoading
73
- return <PageWrapper><Fallback /></PageWrapper>
187
+ return createElement(LayoutView, null, createElement(loading || DefaultLoading))
74
188
  } else {
75
- const Fallback = loading
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 { DefaultFallback, DefaultLoading, PageWrapper } from './AsyncContainer';
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 AsyncSuspense = ({ type, props, chunkName, request, loading, fallback, getChildren }) => {
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 loaded = asyncChunkMap.has(request);
7
- const [, setKey] = useState(0);
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
- if (!loaded && status === 'pending') {
16
- // todo 清楚副作用?
17
- console.log('the current :', chunkPromise.current);
18
- chunkPromise.current
19
- .then((m) => {
20
- console.log('[mpxAsyncSuspense]: load sucess');
21
- asyncChunkMap.set(request, m.__esModule ? m.default : m);
22
- setStatus('loaded');
23
- })
24
- .catch((e) => {
25
- if (type === 'component') {
26
- console.log(11111, e);
27
- global.onLazyLoadError({
28
- type: 'subpackage',
29
- subpackage: [chunkName],
30
- errMsg: `loadSubpackage: ${e.type}`
31
- });
32
- }
33
- console.log('[mpxAsyncSuspense]: load eror', e);
34
- chunkPromise.current = null;
35
- setStatus('error');
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
- if (loaded) {
40
- const Comp = asyncChunkMap.get(request);
41
- return <Comp {...props}></Comp>;
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 <><PageWrapper><Fallback onReload={reloadPage}></Fallback></PageWrapper></>;
119
+ return createElement(LayoutView, null, createElement(Fallback, { onReload: reloadPage }));
48
120
  }
49
121
  else {
50
- const Fallback = loading;
51
- return <Fallback {...props}></Fallback>;
122
+ return createElement(fallback, innerProps);
52
123
  }
53
124
  }
54
125
  else {
55
- if (!chunkPromise.current) {
56
- chunkPromise.current = getChildren();
126
+ if (!loadChunkPromise.current) {
127
+ loadChunkPromise.current = getChildren();
57
128
  }
58
129
  if (type === 'page') {
59
- const Fallback = loading || DefaultLoading;
60
- return <PageWrapper><Fallback /></PageWrapper>;
130
+ return createElement(LayoutView, null, createElement(loading || DefaultLoading));
61
131
  }
62
132
  else {
63
- const Fallback = loading;
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpxjs/webpack-plugin",
3
- "version": "2.10.6-beta.4",
3
+ "version": "2.10.6-beta.5",
4
4
  "description": "mpx compile core",
5
5
  "keywords": [
6
6
  "mpx"
@@ -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
- }