@mpxjs/webpack-plugin 2.10.12 → 2.10.13

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.
@@ -6,6 +6,7 @@ const toPosix = require('../utils/to-posix')
6
6
  const async = require('async')
7
7
  const parseRequest = require('../utils/parse-request')
8
8
  const hasOwn = require('../utils/has-own')
9
+ const { RetryRuntimeGlobal } = require('../retry-runtime-module')
9
10
 
10
11
  class DynamicEntryDependency extends NullDependency {
11
12
  constructor (range, request, entryType, outputPath = '', packageRoot = '', relativePath = '', context = '', extraOptions = {}) {
@@ -201,9 +202,11 @@ class DynamicEntryDependency extends NullDependency {
201
202
  DynamicEntryDependency.Template = class DynamicEntryDependencyTemplate {
202
203
  apply (dep, source, {
203
204
  module,
204
- chunkGraph
205
+ chunkGraph,
206
+ runtimeRequirements
205
207
  }) {
206
- const { resultPath, range, key, publicPath, extraOptions } = dep
208
+ const { resultPath, key, publicPath, extraOptions } = dep
209
+ let range = dep.range
207
210
 
208
211
  let replaceContent = ''
209
212
 
@@ -214,10 +217,10 @@ DynamicEntryDependency.Template = class DynamicEntryDependencyTemplate {
214
217
  let relativePath = toPosix(path.relative(publicPath + path.dirname(chunkGraph.getModuleChunks(module)[0].name), resultPath))
215
218
  if (!relativePath.startsWith('.')) relativePath = './' + relativePath
216
219
  replaceContent = JSON.stringify(relativePath)
217
- if (extraOptions.retryRequireAsync) {
218
- replaceContent += `).catch(function (e) {
219
- return require.async(${JSON.stringify(relativePath)});
220
- }`
220
+ if (extraOptions.retryRequireAsync && extraOptions.retryRequireAsync.times > 0) {
221
+ range = extraOptions.requireAsyncRange
222
+ runtimeRequirements.add(RetryRuntimeGlobal)
223
+ replaceContent = `${RetryRuntimeGlobal}(function() { return require.async(${JSON.stringify(relativePath)}) }, ${extraOptions.retryRequireAsync.times}, ${extraOptions.retryRequireAsync.interval})`
221
224
  }
222
225
  } else {
223
226
  replaceContent = JSON.stringify(resultPath)
@@ -1,4 +1,6 @@
1
1
  const ModuleDependency = require('webpack/lib/dependencies/ModuleDependency')
2
+ const { RetryRuntimeGlobal } = require('../retry-runtime-module')
3
+ const parseRequest = require('../utils/parse-request')
2
4
 
3
5
  class ImportDependencyTemplate extends (
4
6
  ModuleDependency.Template
@@ -31,6 +33,16 @@ class ImportDependencyTemplate extends (
31
33
  content = content.replace(/(__webpack_require__\.t\.bind\(.+,\s*)(\d+)(\s*\))/, (_, p1, p2, p3) => {
32
34
  return p1 + '9' + p3
33
35
  })
36
+
37
+ const { queryObj } = parseRequest(dep.request)
38
+ const retryRequireAsync = queryObj.retryRequireAsync && JSON.parse(queryObj.retryRequireAsync)
39
+
40
+ // require.async 的场景且配置了重试次数才注入 RetryRuntimeModule
41
+ if (queryObj.isRequireAsync && retryRequireAsync && retryRequireAsync.times > 0) {
42
+ runtimeRequirements.add(RetryRuntimeGlobal)
43
+ content = `${RetryRuntimeGlobal}(function() { return ${content} }, ${retryRequireAsync.times}, ${retryRequireAsync.interval})`
44
+ }
45
+
34
46
  source.replace(dep.range[0], dep.range[1] - 1, content)
35
47
  }
36
48
  }
package/lib/index.js CHANGED
@@ -77,6 +77,7 @@ const VirtualModulesPlugin = require('webpack-virtual-modules')
77
77
  const RuntimeGlobals = require('webpack/lib/RuntimeGlobals')
78
78
  const LoadAsyncChunkModule = require('./react/LoadAsyncChunkModule')
79
79
  const ExternalModule = require('webpack/lib/ExternalModule')
80
+ const { RetryRuntimeModule, RetryRuntimeGlobal } = require('./retry-runtime-module')
80
81
  require('./utils/check-core-version-match')
81
82
 
82
83
  const isProductionLikeMode = options => {
@@ -202,6 +203,12 @@ class MpxWebpackPlugin {
202
203
  options.asyncSubpackageRules = options.asyncSubpackageRules || []
203
204
  options.optimizeRenderRules = options.optimizeRenderRules ? (Array.isArray(options.optimizeRenderRules) ? options.optimizeRenderRules : [options.optimizeRenderRules]) : []
204
205
  options.retryRequireAsync = options.retryRequireAsync || false
206
+ if (options.retryRequireAsync === true) {
207
+ options.retryRequireAsync = {
208
+ times: 1,
209
+ interval: 0
210
+ }
211
+ }
205
212
  options.optimizeSize = options.optimizeSize || false
206
213
  options.dynamicComponentRules = options.dynamicComponentRules || {}// 运行时组件配置
207
214
  this.options = options
@@ -615,6 +622,13 @@ class MpxWebpackPlugin {
615
622
  }
616
623
  })
617
624
 
625
+ compilation.hooks.runtimeRequirementInTree
626
+ .for(RetryRuntimeGlobal)
627
+ .tap('MpxWebpackPlugin', (chunk) => {
628
+ compilation.addRuntimeModule(chunk, new RetryRuntimeModule())
629
+ return true
630
+ })
631
+
618
632
  if (isReact(this.options.mode)) {
619
633
  compilation.hooks.runtimeRequirementInTree
620
634
  .for(RuntimeGlobals.loadScript)
@@ -1436,6 +1450,10 @@ class MpxWebpackPlugin {
1436
1450
  if (mpx.supportRequireAsync) {
1437
1451
  if (isWeb(mpx.mode) || isReact(mpx.mode)) {
1438
1452
  if (isReact(mpx.mode)) tarRoot = transSubpackage(mpx.transSubpackageRules, tarRoot)
1453
+ request = addQuery(request, {
1454
+ isRequireAsync: true,
1455
+ retryRequireAsync: JSON.stringify(this.options.retryRequireAsync)
1456
+ })
1439
1457
  const depBlock = new AsyncDependenciesBlock(
1440
1458
  {
1441
1459
  name: tarRoot + '/index'
@@ -1451,7 +1469,8 @@ class MpxWebpackPlugin {
1451
1469
  const dep = new DynamicEntryDependency(range, request, 'export', '', tarRoot, '', context, {
1452
1470
  isAsync: true,
1453
1471
  isRequireAsync: true,
1454
- retryRequireAsync: !!this.options.retryRequireAsync
1472
+ retryRequireAsync: this.options.retryRequireAsync,
1473
+ requireAsyncRange: expr.range
1455
1474
  })
1456
1475
 
1457
1476
  parser.state.current.addPresentationalDependency(dep)
@@ -1606,7 +1625,7 @@ class MpxWebpackPlugin {
1606
1625
  target = expr.object
1607
1626
  }
1608
1627
 
1609
- if (!matchCondition(resourcePath, this.options.transMpxRules) || resourcePath.indexOf('node_modules/@mpxjs') !== -1 || !target || mode === srcMode) return
1628
+ if (!matchCondition(resourcePath, this.options.transMpxRules) || toPosix(resourcePath).indexOf('node_modules/@mpxjs') !== -1 || !target || mode === srcMode) return
1610
1629
 
1611
1630
  const type = target.name
1612
1631
  const name = type === 'wx' ? 'mpx' : 'createFactory'
@@ -5,7 +5,7 @@ const HelperRuntimeModule = require('webpack/lib/runtime/HelperRuntimeModule')
5
5
  class LoadAsyncChunkRuntimeModule extends HelperRuntimeModule {
6
6
  constructor (timeout) {
7
7
  super('load async chunk')
8
- this.timeout = timeout || 5000
8
+ this.timeout = timeout || 10000
9
9
  }
10
10
 
11
11
  generate () {
@@ -1,6 +1,6 @@
1
1
  const normalize = require('../utils/normalize')
2
2
  const optionProcessorPath = normalize.lib('runtime/optionProcessorReact')
3
- const { buildPagesMap, buildComponentsMap, getRequireScript, buildGlobalParams, stringifyRequest } = require('./script-helper')
3
+ const { buildPagesMap, buildComponentsMap, getRequireScript, buildGlobalParams, stringifyRequest, buildI18n } = require('./script-helper')
4
4
 
5
5
  module.exports = function (script, {
6
6
  loaderContext,
@@ -17,7 +17,7 @@ module.exports = function (script, {
17
17
  componentGenerics,
18
18
  genericsInfo
19
19
  }, callback) {
20
- const { appInfo } = loaderContext.getMpx()
20
+ const { appInfo, i18n } = loaderContext.getMpx()
21
21
 
22
22
  let scriptSrcMode = srcMode
23
23
  if (script) {
@@ -62,6 +62,9 @@ import { getComponent, getAsyncSuspense } from ${stringifyRequest(loaderContext,
62
62
  })
63
63
 
64
64
  output += buildGlobalParams({ moduleId, scriptSrcMode, loaderContext, isProduction, ctorType, jsonConfig, componentsMap, outputPath, genericsInfo, componentGenerics, hasApp })
65
+ if (!hasApp && i18n) {
66
+ output += buildI18n({ loaderContext })
67
+ }
65
68
  output += getRequireScript({ ctorType, script, loaderContext })
66
69
  output += `export default global.__mpxOptionsMap[${JSON.stringify(moduleId)}]\n`
67
70
  }
@@ -0,0 +1,56 @@
1
+ const Template = require('webpack/lib/Template')
2
+ const RuntimeModule = require('webpack/lib/RuntimeModule')
3
+
4
+ const RetryRuntimeGlobal = '__webpack_require__.__retry'
5
+
6
+ class RetryRuntimeModule extends RuntimeModule {
7
+ constructor () {
8
+ super('mpx retry module')
9
+ }
10
+
11
+ generate () {
12
+ const { compilation } = this
13
+ const { runtimeTemplate } = compilation
14
+ return Template.asString([
15
+ `${RetryRuntimeGlobal} = ${runtimeTemplate.basicFunction(
16
+ 'fn, times, interval',
17
+ [
18
+ 'times = times || 1;',
19
+ 'interval = interval || 0;',
20
+ `return new Promise(${runtimeTemplate.basicFunction(
21
+ 'resolve, reject',
22
+ [
23
+ Template.indent([
24
+ 'var _t = 0;',
25
+ `var _retry = ${runtimeTemplate.basicFunction('', [
26
+ Template.indent([
27
+ `fn().then(resolve).catch(${runtimeTemplate.basicFunction('err', [
28
+ Template.indent([
29
+ 'if (_t < times) {',
30
+ Template.indent([
31
+ '++_t;',
32
+ 'interval > 0 ? setTimeout(_retry, interval) : _retry()'
33
+ ]),
34
+ '} else {',
35
+ Template.indent([
36
+ 'reject(err);'
37
+ ]),
38
+ '}'
39
+ ])
40
+ ])})`
41
+ ])
42
+ ])};`,
43
+ '_retry();'
44
+ ])
45
+ ]
46
+ )})`
47
+ ]
48
+ )}`
49
+ ])
50
+ }
51
+ }
52
+
53
+ module.exports = {
54
+ RetryRuntimeModule,
55
+ RetryRuntimeGlobal
56
+ }
@@ -91,10 +91,19 @@ const AsyncSuspense = ({ type, chunkName, moduleId, innerProps, getLoading, getF
91
91
  if (cancelled)
92
92
  return;
93
93
  if (type === 'component') {
94
- global.onLazyLoadError({
95
- type: 'subpackage',
96
- subpackage: [chunkName],
97
- errMsg: `loadSubpackage: ${e.type}`
94
+ global.__mpxAppCbs.lazyLoad.forEach((cb) => {
95
+ // eslint-disable-next-line node/no-callback-literal
96
+ cb({
97
+ type: 'subpackage',
98
+ subpackage: [chunkName],
99
+ errMsg: `loadSubpackage: ${e.type}`
100
+ });
101
+ });
102
+ }
103
+ if (type === 'page' && typeof mpxGlobal.__mpx.config?.rnConfig?.lazyLoadPageErrorHandler === 'function') {
104
+ mpxGlobal.__mpx.config.rnConfig.lazyLoadPageErrorHandler({
105
+ subpackage: chunkName,
106
+ errType: e.type
98
107
  });
99
108
  }
100
109
  loadChunkPromise.current = null;
@@ -117,14 +117,13 @@ const SwiperWrapper = forwardRef((props, ref) => {
117
117
  // 记录元素的偏移量
118
118
  const offset = useSharedValue(getOffset(props.current || 0, initStep));
119
119
  const strAbso = 'absolute' + dir.toUpperCase();
120
+ const strVelocity = 'velocity' + dir.toUpperCase();
120
121
  // 标识手指触摸和抬起, 起点在onBegin
121
122
  const touchfinish = useSharedValue(true);
122
123
  // 记录上一帧的绝对定位坐标
123
124
  const preAbsolutePos = useSharedValue(0);
124
125
  // 记录从onBegin 到 onTouchesUp 时移动的距离
125
126
  const moveTranstion = useSharedValue(0);
126
- // 记录从onBegin 到 onTouchesUp 的时间
127
- const moveTime = useSharedValue(0);
128
127
  const timerId = useRef(0);
129
128
  const intervalTimer = props.interval || 500;
130
129
  const simultaneousHandlers = flatGesture(originSimultaneousHandlers);
@@ -405,7 +404,11 @@ const SwiperWrapper = forwardRef((props, ref) => {
405
404
  }
406
405
  }, [children.length]);
407
406
  useEffect(() => {
408
- updateCurrent(props.current || 0, step.value);
407
+ // 1. 如果用户在touch的过程中, 外部更新了current以外部为准(小程序表现)
408
+ // 2. 手指滑动过程中更新索引,外部会把current再传入进来,导致offset直接更新,增加判断不同才更新
409
+ if (props.current !== currentIndex.value) {
410
+ updateCurrent(props.current || 0, step.value);
411
+ }
409
412
  }, [props.current]);
410
413
  useEffect(() => {
411
414
  autoplayShared.value = autoplay;
@@ -427,7 +430,7 @@ const SwiperWrapper = forwardRef((props, ref) => {
427
430
  function getTargetPosition(eventData) {
428
431
  'worklet';
429
432
  // 移动的距离
430
- const { translation } = eventData;
433
+ const { transdir } = eventData;
431
434
  let resetOffsetPos = 0;
432
435
  let selectedIndex = currentIndex.value;
433
436
  // 是否临界点
@@ -435,9 +438,9 @@ const SwiperWrapper = forwardRef((props, ref) => {
435
438
  // 真实滚动到的偏移量坐标
436
439
  let moveToTargetPos = 0;
437
440
  const tmp = !circularShared.value ? 0 : preMarginShared.value;
438
- const currentOffset = translation < 0 ? offset.value - tmp : offset.value + tmp;
441
+ const currentOffset = transdir < 0 ? offset.value - tmp : offset.value + tmp;
439
442
  const computedIndex = Math.abs(currentOffset) / step.value;
440
- const moveToIndex = translation < 0 ? Math.ceil(computedIndex) : Math.floor(computedIndex);
443
+ const moveToIndex = transdir < 0 ? Math.ceil(computedIndex) : Math.floor(computedIndex);
441
444
  // 实际应该定位的索引值
442
445
  if (!circularShared.value) {
443
446
  selectedIndex = moveToIndex;
@@ -470,14 +473,18 @@ const SwiperWrapper = forwardRef((props, ref) => {
470
473
  }
471
474
  function canMove(eventData) {
472
475
  'worklet';
473
- const { translation } = eventData;
474
- const currentOffset = Math.abs(offset.value);
476
+ // 旧版:如果在快速多次滑动时,只根据当前的offset判断,会出现offset没超出,加上translation后越界的场景(如在倒数第二个元素快速滑动)
477
+ // 新版:会加上translation
478
+ const { translation, transdir } = eventData;
479
+ const gestureMovePos = offset.value + translation;
475
480
  if (!circularShared.value) {
476
- if (translation < 0) {
477
- return currentOffset < step.value * (childrenLength.value - 1);
481
+ // 如果只判断区间,中间非滑动状态(handleResistanceMove)向左滑动,突然改为向右滑动,但是还在非滑动态,本应该可滑动判断为了不可滑动
482
+ const posEnd = -step.value * (childrenLength.value - 1);
483
+ if (transdir < 0) {
484
+ return gestureMovePos > posEnd;
478
485
  }
479
486
  else {
480
- return currentOffset > 0;
487
+ return gestureMovePos < 0;
481
488
  }
482
489
  }
483
490
  else {
@@ -511,25 +518,16 @@ const SwiperWrapper = forwardRef((props, ref) => {
511
518
  });
512
519
  }
513
520
  }
514
- function handleBackInit() {
515
- 'worklet';
516
- // 微信的效果
517
- // 1. 只有一个元素,即使设置了circular,也不会产生循环的效果,2. 可以响应手势,但是会有回弹的效果
518
- offset.value = withTiming(0, {
519
- duration: easeDuration,
520
- easing: easeMap[easeingFunc]
521
- });
522
- }
523
521
  function handleBack(eventData) {
524
522
  'worklet';
525
- const { translation } = eventData;
523
+ const { transdir } = eventData;
526
524
  // 向右滑动的back:trans < 0, 向左滑动的back: trans < 0
527
525
  let currentOffset = Math.abs(offset.value);
528
526
  if (circularShared.value) {
529
- currentOffset += translation < 0 ? preMarginShared.value : -preMarginShared.value;
527
+ currentOffset += transdir < 0 ? preMarginShared.value : -preMarginShared.value;
530
528
  }
531
529
  const curIndex = currentOffset / step.value;
532
- const moveToIndex = (translation < 0 ? Math.floor(curIndex) : Math.ceil(curIndex)) - patchElmNumShared.value;
530
+ const moveToIndex = (transdir < 0 ? Math.floor(curIndex) : Math.ceil(curIndex)) - patchElmNumShared.value;
533
531
  const targetOffset = -(moveToIndex + patchElmNumShared.value) * step.value + (circularShared.value ? preMarginShared.value : 0);
534
532
  offset.value = withTiming(targetOffset, {
535
533
  duration: easeDuration,
@@ -541,65 +539,111 @@ const SwiperWrapper = forwardRef((props, ref) => {
541
539
  }
542
540
  });
543
541
  }
544
- function handleLongPress() {
542
+ // 当前的offset和index多对应的offset进行对比,判断是否超过一半
543
+ function computeHalf(eventData) {
545
544
  'worklet';
545
+ const { transdir } = eventData;
546
546
  const currentOffset = Math.abs(offset.value);
547
547
  let preOffset = (currentIndex.value + patchElmNumShared.value) * step.value;
548
548
  if (circularShared.value) {
549
549
  preOffset -= preMarginShared.value;
550
550
  }
551
- // 正常事件中拿到的transition值(正向滑动<0,倒着滑>0)
551
+ // 正常事件中拿到的translation值(正向滑动<0,倒着滑>0)
552
552
  const diffOffset = preOffset - currentOffset;
553
553
  const half = Math.abs(diffOffset) > step.value / 2;
554
+ const isTriggerUpdateHalf = (transdir < 0 && currentOffset < preOffset) || (transdir > 0 && currentOffset > preOffset);
555
+ return {
556
+ diffOffset,
557
+ half,
558
+ isTriggerUpdateHalf
559
+ };
560
+ }
561
+ function handleLongPress(eventData) {
562
+ 'worklet';
563
+ const { diffOffset, half, isTriggerUpdateHalf } = computeHalf(eventData);
554
564
  if (+diffOffset === 0) {
555
565
  runOnJS(resumeLoop)();
556
566
  }
567
+ else if (isTriggerUpdateHalf) {
568
+ // 如果触发了onUpdate时的索引变更,则直接以update时的index为准
569
+ const targetIndex = !circularShared.value ? currentIndex.value : currentIndex.value + patchElmNumShared.value - 1;
570
+ offset.value = withTiming(-targetIndex * step.value, {
571
+ duration: easeDuration,
572
+ easing: easeMap[easeingFunc]
573
+ }, () => {
574
+ if (touchfinish.value !== false) {
575
+ currentIndex.value = targetIndex;
576
+ runOnJS(resumeLoop)();
577
+ }
578
+ });
579
+ }
557
580
  else if (half) {
558
- handleEnd({ translation: diffOffset });
581
+ handleEnd(eventData);
559
582
  }
560
583
  else {
561
- handleBack({ translation: diffOffset });
584
+ handleBack(eventData);
562
585
  }
563
586
  }
564
587
  function reachBoundary(eventData) {
565
588
  'worklet';
566
- // 移动的距离
589
+ // 1. 基于当前的offset和translation判断是否超过当前边界值
567
590
  const { translation } = eventData;
568
- const elementsLength = step.value * childrenLength.value;
591
+ const boundaryStart = -patchElmNumShared.value * step.value;
592
+ const boundaryEnd = -(childrenLength.value + patchElmNumShared.value) * step.value;
593
+ const moveToOffset = offset.value + translation;
569
594
  let isBoundary = false;
570
595
  let resetOffset = 0;
571
- // Y轴向下滚动, transDistance > 0, 向上滚动 < 0 X轴向左滚动, transDistance > 0
572
- const currentOffset = offset.value;
573
- const moveStep = Math.ceil(translation / elementsLength);
574
- if (translation < 0) {
575
- const posEnd = (childrenLength.value + patchElmNumShared.value + 1) * step.value;
576
- const posReverseEnd = (patchElmNumShared.value - 1) * step.value;
577
- if (currentOffset < -posEnd + step.value) {
578
- isBoundary = true;
579
- resetOffset = Math.abs(moveStep) === 0 ? patchElmNumShared.value * step.value + translation : moveStep * elementsLength;
580
- }
581
- if (currentOffset > -posReverseEnd) {
582
- isBoundary = true;
583
- resetOffset = moveStep * elementsLength;
584
- }
596
+ if (moveToOffset < boundaryEnd) {
597
+ isBoundary = true;
598
+ // 超过边界的距离
599
+ const exceedLength = Math.abs(moveToOffset) - Math.abs(boundaryEnd);
600
+ // 计算对标正常元素所在的offset
601
+ resetOffset = patchElmNumShared.value * step.value + exceedLength;
585
602
  }
586
- else if (translation > 0) {
587
- const posEnd = (patchElmNumShared.value - 1) * step.value;
588
- const posReverseEnd = (patchElmNumShared.value + childrenLength.value) * step.value;
589
- if (currentOffset > -posEnd) {
590
- isBoundary = true;
591
- resetOffset = moveStep * elementsLength + step.value + (moveStep === 1 ? translation : 0);
592
- }
593
- if (currentOffset < -posReverseEnd) {
594
- isBoundary = true;
595
- resetOffset = moveStep * elementsLength + patchElmNumShared.value * step.value;
596
- }
603
+ if (moveToOffset > boundaryStart) {
604
+ isBoundary = true;
605
+ // 超过边界的距离
606
+ const exceedLength = Math.abs(boundaryStart) - Math.abs(moveToOffset);
607
+ // 计算对标正常元素所在的offset
608
+ resetOffset = (patchElmNumShared.value + childrenLength.value - 1) * step.value + (step.value - exceedLength);
597
609
  }
598
610
  return {
599
611
  isBoundary,
600
612
  resetOffset: -resetOffset
601
613
  };
602
614
  }
615
+ // 非循环超出边界,应用阻力; 开始滑动少阻力小,滑动越长阻力越大
616
+ function handleResistanceMove(eventData) {
617
+ 'worklet';
618
+ const { translation, transdir } = eventData;
619
+ const moveToOffset = offset.value + translation;
620
+ const maxOverDrag = Math.floor(step.value / 2);
621
+ const maxOffset = translation < 0 ? -(childrenLength.value - 1) * step.value : 0;
622
+ let resistance = 0.1;
623
+ let overDrag = 0;
624
+ let finalOffset = 0;
625
+ // 向右向下小于0, 向左向上大于0;
626
+ if (transdir < 0) {
627
+ overDrag = Math.abs(moveToOffset - maxOffset);
628
+ }
629
+ else {
630
+ overDrag = Math.abs(moveToOffset);
631
+ }
632
+ // 滑动越多resistance越小
633
+ resistance = 1 - overDrag / maxOverDrag;
634
+ // 确保阻力在合理范围内
635
+ resistance = Math.min(0.5, resistance);
636
+ // 限制在最大拖拽范围内
637
+ if (transdir < 0) {
638
+ const adjustOffset = offset.value + translation * resistance;
639
+ finalOffset = Math.max(adjustOffset, maxOffset - maxOverDrag);
640
+ }
641
+ else {
642
+ const adjustOffset = offset.value + translation * resistance;
643
+ finalOffset = Math.min(adjustOffset, maxOverDrag);
644
+ }
645
+ return finalOffset;
646
+ }
603
647
  const gesturePan = Gesture.Pan()
604
648
  .onBegin((e) => {
605
649
  'worklet';
@@ -610,21 +654,42 @@ const SwiperWrapper = forwardRef((props, ref) => {
610
654
  runOnJS(pauseLoop)();
611
655
  preAbsolutePos.value = e[strAbso];
612
656
  moveTranstion.value = e[strAbso];
613
- moveTime.value = new Date().getTime();
614
657
  })
615
- .onTouchesMove((e) => {
658
+ .onUpdate((e) => {
616
659
  'worklet';
617
660
  if (touchfinish.value)
618
661
  return;
619
- const touchEventData = e.changedTouches[0];
620
- const moveDistance = touchEventData[strAbso] - preAbsolutePos.value;
662
+ const moveDistance = e[strAbso] - preAbsolutePos.value;
621
663
  const eventData = {
622
- translation: moveDistance
664
+ translation: moveDistance,
665
+ transdir: moveDistance !== 0 ? moveDistance : e[strAbso] - moveTranstion.value
623
666
  };
624
- // 处理用户一直拖拽到临界点的场景, 不会执行onEnd
625
- if (!circularShared.value && !canMove(eventData)) {
667
+ // 1. 支持滑动中超出一半更新索引的能力:只更新索引并不会影响onFinalize依据当前offset计算的索引
668
+ const { half } = computeHalf(eventData);
669
+ if (childrenLength.value > 1 && half) {
670
+ const { selectedIndex } = getTargetPosition(eventData);
671
+ currentIndex.value = selectedIndex;
672
+ }
673
+ // 2. 非循环: 处理用户一直拖拽到临界点的场景,如果放到onFinalize无法阻止offset.value更新为越界的值
674
+ if (!circularShared.value) {
675
+ if (canMove(eventData)) {
676
+ offset.value = moveDistance + offset.value;
677
+ }
678
+ else {
679
+ const finalOffset = handleResistanceMove(eventData);
680
+ offset.value = finalOffset;
681
+ }
682
+ preAbsolutePos.value = e[strAbso];
626
683
  return;
627
684
  }
685
+ // 3. 循环更新: 只有一个元素时可滑动,加入阻力
686
+ if (circularShared.value && childrenLength.value === 1) {
687
+ const finalOffset = handleResistanceMove(eventData);
688
+ offset.value = finalOffset;
689
+ preAbsolutePos.value = e[strAbso];
690
+ return;
691
+ }
692
+ // 4. 循环更新:正常
628
693
  const { isBoundary, resetOffset } = reachBoundary(eventData);
629
694
  if (childrenLength.value > 1 && isBoundary && circularShared.value) {
630
695
  offset.value = resetOffset;
@@ -632,28 +697,43 @@ const SwiperWrapper = forwardRef((props, ref) => {
632
697
  else {
633
698
  offset.value = moveDistance + offset.value;
634
699
  }
635
- preAbsolutePos.value = touchEventData[strAbso];
700
+ preAbsolutePos.value = e[strAbso];
636
701
  })
637
- .onTouchesUp((e) => {
702
+ .onFinalize((e) => {
638
703
  'worklet';
639
704
  if (touchfinish.value)
640
705
  return;
641
- const touchEventData = e.changedTouches[0];
642
- const moveDistance = touchEventData[strAbso] - moveTranstion.value;
643
706
  touchfinish.value = true;
707
+ // 触发过onUpdate正常情况下e[strAbso] - preAbsolutePos.value=0; 未触发过onUpdate的情况下e[strAbso] - preAbsolutePos.value 不为0
708
+ const moveDistance = e[strAbso] - preAbsolutePos.value;
644
709
  const eventData = {
645
- translation: moveDistance
710
+ translation: moveDistance,
711
+ transdir: moveDistance !== 0 ? moveDistance : e[strAbso] - moveTranstion.value
646
712
  };
713
+ // 1. 只有一个元素:循环 和 非循环状态,都走回弹效果
647
714
  if (childrenLength.value === 1) {
648
- return handleBackInit();
715
+ offset.value = withTiming(0, {
716
+ duration: easeDuration,
717
+ easing: easeMap[easeingFunc]
718
+ });
719
+ return;
649
720
  }
650
- // 用户手指按下起来, 需要计算正确的位置, 比如在滑动过程中突然按下然后起来,需要计算到正确的位置
721
+ // 2.非循环状态不可移动态:最后一个元素 第一个元素
722
+ // 非循环支持最后元素可滑动能力后,向左快速移动未超过最大可移动范围一半,因为offset为正值,向左滑动handleBack,默认向上取整
723
+ // 但是在offset大于0时,取0。[-100, 0](back取0), [0, 100](back取1), 所以handleLongPress里的处理逻辑需要兼容支持,因此这里直接单独处理,不耦合下方公共的判断逻辑。
651
724
  if (!circularShared.value && !canMove(eventData)) {
725
+ if (eventData.transdir < 0) {
726
+ handleBack(eventData);
727
+ }
728
+ else {
729
+ handleEnd(eventData);
730
+ }
652
731
  return;
653
732
  }
654
- const strVelocity = moveDistance / (new Date().getTime() - moveTime.value) * 1000;
655
- if (Math.abs(strVelocity) < longPressRatio) {
656
- handleLongPress();
733
+ // 3. 非循环状态可移动态、循环状态, 正常逻辑处理
734
+ const velocity = e[strVelocity];
735
+ if (Math.abs(velocity) < longPressRatio) {
736
+ handleLongPress(eventData);
657
737
  }
658
738
  else {
659
739
  handleEnd(eventData);
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useMemo, useRef } from 'react';
2
2
  import { Easing, useSharedValue, withTiming, useAnimatedStyle, withSequence, withDelay, makeMutable, cancelAnimation, runOnJS } from 'react-native-reanimated';
3
3
  import { error, hasOwn, collectDataset } from '@mpxjs/utils';
4
+ import { useRunOnJSCallback } from './utils';
4
5
  // 微信 timingFunction 和 RN Easing 对应关系
5
6
  const EasingKey = {
6
7
  linear: Easing.linear,
@@ -182,13 +183,19 @@ export default function useAnimationHooks(props) {
182
183
  timeStamp: Date.now()
183
184
  });
184
185
  }
186
+ // eslint-disable-next-line react-hooks/rules-of-hooks
187
+ const runOnJSCallbackRef = useRef({
188
+ withTimingCallback
189
+ });
190
+ // eslint-disable-next-line react-hooks/rules-of-hooks
191
+ const runOnJSCallback = useRunOnJSCallback(runOnJSCallbackRef);
185
192
  // 创建单个animation
186
193
  function getAnimation({ key, value }, { delay, duration, easing }, callback) {
187
194
  const animation = typeof callback === 'function'
188
195
  ? withTiming(value, { duration, easing }, (finished, current) => {
189
196
  callback(finished, current);
190
197
  if (transitionend && finished) {
191
- runOnJS(withTimingCallback)(finished, current, duration);
198
+ runOnJS(runOnJSCallback)('withTimingCallback', finished, current, duration);
192
199
  }
193
200
  })
194
201
  : withTiming(value, { duration, easing });
@@ -5,7 +5,6 @@ import { VarContext, ScrollViewContext, RouteContext } from './context';
5
5
  import { ExpressionParser, parseFunc, ReplaceSource } from './parser';
6
6
  import { initialWindowMetrics } from 'react-native-safe-area-context';
7
7
  import FastImage from '@d11/react-native-fast-image';
8
- import { runOnJS } from 'react-native-reanimated';
9
8
  import { Gesture } from 'react-native-gesture-handler';
10
9
  export const TEXT_STYLE_REGEX = /color|font.*|text.*|letterSpacing|lineHeight|includeFontPadding|writingDirection/;
11
10
  export const PERCENT_REGEX = /^\s*-?\d+(\.\d+)?%\s*$/;
@@ -663,13 +662,11 @@ export function useHover({ enableHover, hoverStartTime, hoverStayTime, disabled
663
662
  const gesture = useMemo(() => {
664
663
  return Gesture.Pan()
665
664
  .onTouchesDown(() => {
666
- 'worklet';
667
- runOnJS(setStartTimer)();
665
+ setStartTimer();
668
666
  })
669
667
  .onTouchesUp(() => {
670
- 'worklet';
671
- runOnJS(setStayTimer)();
672
- });
668
+ setStayTimer();
669
+ }).runOnJS(true);
673
670
  }, []);
674
671
  if (gestureRef) {
675
672
  gesture.simultaneousWithExternalGesture(gestureRef);
@@ -679,3 +676,17 @@ export function useHover({ enableHover, hoverStartTime, hoverStayTime, disabled
679
676
  gesture
680
677
  };
681
678
  }
679
+ export function useRunOnJSCallback(callbackMapRef) {
680
+ const invokeCallback = useCallback((key, ...args) => {
681
+ const callback = callbackMapRef.current[key];
682
+ // eslint-disable-next-line node/no-callback-literal
683
+ if (isFunction(callback))
684
+ return callback(...args);
685
+ }, []);
686
+ useEffect(() => {
687
+ return () => {
688
+ callbackMapRef.current = {};
689
+ };
690
+ }, []);
691
+ return invokeCallback;
692
+ }
@@ -1,6 +1,7 @@
1
1
  import { useState, ComponentType, useEffect, useCallback, useRef, ReactNode, createElement } from 'react'
2
2
  import { View, Image, StyleSheet, Text, TouchableOpacity } from 'react-native'
3
3
  import FastImage from '@d11/react-native-fast-image'
4
+ import { AnyFunc } from './types/common'
4
5
 
5
6
  const asyncChunkMap = new Map()
6
7
 
@@ -136,10 +137,19 @@ const AsyncSuspense: React.FC<AsyncSuspenseProps> = ({
136
137
  .catch((e) => {
137
138
  if (cancelled) return
138
139
  if (type === 'component') {
139
- global.onLazyLoadError({
140
- type: 'subpackage',
141
- subpackage: [chunkName],
142
- errMsg: `loadSubpackage: ${e.type}`
140
+ global.__mpxAppCbs.lazyLoad.forEach((cb: AnyFunc) => {
141
+ // eslint-disable-next-line node/no-callback-literal
142
+ cb({
143
+ type: 'subpackage',
144
+ subpackage: [chunkName],
145
+ errMsg: `loadSubpackage: ${e.type}`
146
+ })
147
+ })
148
+ }
149
+ if (type === 'page' && typeof mpxGlobal.__mpx.config?.rnConfig?.lazyLoadPageErrorHandler === 'function') {
150
+ mpxGlobal.__mpx.config.rnConfig.lazyLoadPageErrorHandler({
151
+ subpackage: chunkName,
152
+ errType: e.type
143
153
  })
144
154
  }
145
155
  loadChunkPromise.current = null
@@ -1,5 +1,5 @@
1
1
  import { View, NativeSyntheticEvent, LayoutChangeEvent } from 'react-native'
2
- import { GestureDetector, Gesture, PanGesture } from 'react-native-gesture-handler'
2
+ import { GestureDetector, Gesture, PanGesture, GestureStateChangeEvent, PanGestureHandlerEventPayload } from 'react-native-gesture-handler'
3
3
  import Animated, { useAnimatedStyle, useSharedValue, withTiming, Easing, runOnJS, useAnimatedReaction, cancelAnimation } from 'react-native-reanimated'
4
4
 
5
5
  import React, { JSX, forwardRef, useRef, useEffect, ReactNode, ReactElement, useMemo, createElement } from 'react'
@@ -26,8 +26,12 @@ import Portal from './mpx-portal'
26
26
  */
27
27
  type EaseType = 'default' | 'linear' | 'easeInCubic' | 'easeOutCubic' | 'easeInOutCubic'
28
28
  type StrAbsoType = 'absoluteX' | 'absoluteY'
29
+ type StrVelocityType = 'velocityX' | 'velocityY'
29
30
  type EventDataType = {
31
+ // 和上一帧offset值的对比
30
32
  translation: number
33
+ // onUpdate时根据上一个判断方向,onFinalize根据transformStart判断
34
+ transdir: number
31
35
  }
32
36
 
33
37
  interface SwiperProps {
@@ -46,7 +50,7 @@ interface SwiperProps {
46
50
  vertical?: boolean
47
51
  style: {
48
52
  [key: string]: any
49
- };
53
+ }
50
54
  'easing-function'?: EaseType
51
55
  'previous-margin'?: string
52
56
  'next-margin'?: string
@@ -54,7 +58,7 @@ interface SwiperProps {
54
58
  'enable-var': boolean
55
59
  'parent-font-size'?: number
56
60
  'parent-width'?: number
57
- 'parent-height'?: number;
61
+ 'parent-height'?: number
58
62
  'external-var-context'?: Record<string, any>
59
63
  'wait-for'?: Array<GestureHandler>
60
64
  'simultaneous-handlers'?: Array<GestureHandler>
@@ -199,14 +203,13 @@ const SwiperWrapper = forwardRef<HandlerRef<View, SwiperProps>, SwiperProps>((pr
199
203
  // 记录元素的偏移量
200
204
  const offset = useSharedValue(getOffset(props.current || 0, initStep))
201
205
  const strAbso = 'absolute' + dir.toUpperCase() as StrAbsoType
206
+ const strVelocity = 'velocity' + dir.toUpperCase() as StrVelocityType
202
207
  // 标识手指触摸和抬起, 起点在onBegin
203
208
  const touchfinish = useSharedValue(true)
204
209
  // 记录上一帧的绝对定位坐标
205
210
  const preAbsolutePos = useSharedValue(0)
206
211
  // 记录从onBegin 到 onTouchesUp 时移动的距离
207
212
  const moveTranstion = useSharedValue(0)
208
- // 记录从onBegin 到 onTouchesUp 的时间
209
- const moveTime = useSharedValue(0)
210
213
  const timerId = useRef(0 as number | ReturnType<typeof setTimeout>)
211
214
  const intervalTimer = props.interval || 500
212
215
 
@@ -505,7 +508,11 @@ const SwiperWrapper = forwardRef<HandlerRef<View, SwiperProps>, SwiperProps>((pr
505
508
  }, [children.length])
506
509
 
507
510
  useEffect(() => {
508
- updateCurrent(props.current || 0, step.value)
511
+ // 1. 如果用户在touch的过程中, 外部更新了current以外部为准(小程序表现)
512
+ // 2. 手指滑动过程中更新索引,外部会把current再传入进来,导致offset直接更新,增加判断不同才更新
513
+ if (props.current !== currentIndex.value) {
514
+ updateCurrent(props.current || 0, step.value)
515
+ }
509
516
  }, [props.current])
510
517
 
511
518
  useEffect(() => {
@@ -529,7 +536,7 @@ const SwiperWrapper = forwardRef<HandlerRef<View, SwiperProps>, SwiperProps>((pr
529
536
  function getTargetPosition (eventData: EventDataType) {
530
537
  'worklet'
531
538
  // 移动的距离
532
- const { translation } = eventData
539
+ const { transdir } = eventData
533
540
  let resetOffsetPos = 0
534
541
  let selectedIndex = currentIndex.value
535
542
  // 是否临界点
@@ -537,9 +544,9 @@ const SwiperWrapper = forwardRef<HandlerRef<View, SwiperProps>, SwiperProps>((pr
537
544
  // 真实滚动到的偏移量坐标
538
545
  let moveToTargetPos = 0
539
546
  const tmp = !circularShared.value ? 0 : preMarginShared.value
540
- const currentOffset = translation < 0 ? offset.value - tmp : offset.value + tmp
547
+ const currentOffset = transdir < 0 ? offset.value - tmp : offset.value + tmp
541
548
  const computedIndex = Math.abs(currentOffset) / step.value
542
- const moveToIndex = translation < 0 ? Math.ceil(computedIndex) : Math.floor(computedIndex)
549
+ const moveToIndex = transdir < 0 ? Math.ceil(computedIndex) : Math.floor(computedIndex)
543
550
  // 实际应该定位的索引值
544
551
  if (!circularShared.value) {
545
552
  selectedIndex = moveToIndex
@@ -569,13 +576,17 @@ const SwiperWrapper = forwardRef<HandlerRef<View, SwiperProps>, SwiperProps>((pr
569
576
  }
570
577
  function canMove (eventData: EventDataType) {
571
578
  'worklet'
572
- const { translation } = eventData
573
- const currentOffset = Math.abs(offset.value)
579
+ // 旧版:如果在快速多次滑动时,只根据当前的offset判断,会出现offset没超出,加上translation后越界的场景(如在倒数第二个元素快速滑动)
580
+ // 新版:会加上translation
581
+ const { translation, transdir } = eventData
582
+ const gestureMovePos = offset.value + translation
574
583
  if (!circularShared.value) {
575
- if (translation < 0) {
576
- return currentOffset < step.value * (childrenLength.value - 1)
584
+ // 如果只判断区间,中间非滑动状态(handleResistanceMove)向左滑动,突然改为向右滑动,但是还在非滑动态,本应该可滑动判断为了不可滑动
585
+ const posEnd = -step.value * (childrenLength.value - 1)
586
+ if (transdir < 0) {
587
+ return gestureMovePos > posEnd
577
588
  } else {
578
- return currentOffset > 0
589
+ return gestureMovePos < 0
579
590
  }
580
591
  } else {
581
592
  return true
@@ -607,25 +618,16 @@ const SwiperWrapper = forwardRef<HandlerRef<View, SwiperProps>, SwiperProps>((pr
607
618
  })
608
619
  }
609
620
  }
610
- function handleBackInit () {
611
- 'worklet'
612
- // 微信的效果
613
- // 1. 只有一个元素,即使设置了circular,也不会产生循环的效果,2. 可以响应手势,但是会有回弹的效果
614
- offset.value = withTiming(0, {
615
- duration: easeDuration,
616
- easing: easeMap[easeingFunc]
617
- })
618
- }
619
621
  function handleBack (eventData: EventDataType) {
620
622
  'worklet'
621
- const { translation } = eventData
623
+ const { transdir } = eventData
622
624
  // 向右滑动的back:trans < 0, 向左滑动的back: trans < 0
623
625
  let currentOffset = Math.abs(offset.value)
624
626
  if (circularShared.value) {
625
- currentOffset += translation < 0 ? preMarginShared.value : -preMarginShared.value
627
+ currentOffset += transdir < 0 ? preMarginShared.value : -preMarginShared.value
626
628
  }
627
629
  const curIndex = currentOffset / step.value
628
- const moveToIndex = (translation < 0 ? Math.floor(curIndex) : Math.ceil(curIndex)) - patchElmNumShared.value
630
+ const moveToIndex = (transdir < 0 ? Math.floor(curIndex) : Math.ceil(curIndex)) - patchElmNumShared.value
629
631
  const targetOffset = -(moveToIndex + patchElmNumShared.value) * step.value + (circularShared.value ? preMarginShared.value : 0)
630
632
  offset.value = withTiming(targetOffset, {
631
633
  duration: easeDuration,
@@ -637,64 +639,108 @@ const SwiperWrapper = forwardRef<HandlerRef<View, SwiperProps>, SwiperProps>((pr
637
639
  }
638
640
  })
639
641
  }
640
- function handleLongPress () {
642
+ // 当前的offset和index多对应的offset进行对比,判断是否超过一半
643
+ function computeHalf (eventData: EventDataType) {
641
644
  'worklet'
645
+ const { transdir } = eventData
642
646
  const currentOffset = Math.abs(offset.value)
643
647
  let preOffset = (currentIndex.value + patchElmNumShared.value) * step.value
644
648
  if (circularShared.value) {
645
649
  preOffset -= preMarginShared.value
646
650
  }
647
- // 正常事件中拿到的transition值(正向滑动<0,倒着滑>0)
651
+ // 正常事件中拿到的translation值(正向滑动<0,倒着滑>0)
648
652
  const diffOffset = preOffset - currentOffset
649
653
  const half = Math.abs(diffOffset) > step.value / 2
654
+ const isTriggerUpdateHalf = (transdir < 0 && currentOffset < preOffset) || (transdir > 0 && currentOffset > preOffset)
655
+ return {
656
+ diffOffset,
657
+ half,
658
+ isTriggerUpdateHalf
659
+ }
660
+ }
661
+ function handleLongPress (eventData: EventDataType) {
662
+ 'worklet'
663
+ const { diffOffset, half, isTriggerUpdateHalf } = computeHalf(eventData)
650
664
  if (+diffOffset === 0) {
651
665
  runOnJS(resumeLoop)()
666
+ } else if (isTriggerUpdateHalf) {
667
+ // 如果触发了onUpdate时的索引变更,则直接以update时的index为准
668
+ const targetIndex = !circularShared.value ? currentIndex.value : currentIndex.value + patchElmNumShared.value - 1
669
+ offset.value = withTiming(-targetIndex * step.value, {
670
+ duration: easeDuration,
671
+ easing: easeMap[easeingFunc]
672
+ }, () => {
673
+ if (touchfinish.value !== false) {
674
+ currentIndex.value = targetIndex
675
+ runOnJS(resumeLoop)()
676
+ }
677
+ })
652
678
  } else if (half) {
653
- handleEnd({ translation: diffOffset })
679
+ handleEnd(eventData)
654
680
  } else {
655
- handleBack({ translation: diffOffset })
681
+ handleBack(eventData)
656
682
  }
657
683
  }
658
684
  function reachBoundary (eventData: EventDataType) {
659
685
  'worklet'
660
- // 移动的距离
686
+ // 1. 基于当前的offset和translation判断是否超过当前边界值
661
687
  const { translation } = eventData
662
- const elementsLength = step.value * childrenLength.value
688
+ const boundaryStart = -patchElmNumShared.value * step.value
689
+ const boundaryEnd = -(childrenLength.value + patchElmNumShared.value) * step.value
690
+ const moveToOffset = offset.value + translation
663
691
  let isBoundary = false
664
692
  let resetOffset = 0
665
- // Y轴向下滚动, transDistance > 0, 向上滚动 < 0 X轴向左滚动, transDistance > 0
666
- const currentOffset = offset.value
667
- const moveStep = Math.ceil(translation / elementsLength)
668
- if (translation < 0) {
669
- const posEnd = (childrenLength.value + patchElmNumShared.value + 1) * step.value
670
- const posReverseEnd = (patchElmNumShared.value - 1) * step.value
671
- if (currentOffset < -posEnd + step.value) {
672
- isBoundary = true
673
- resetOffset = Math.abs(moveStep) === 0 ? patchElmNumShared.value * step.value + translation : moveStep * elementsLength
674
- }
675
- if (currentOffset > -posReverseEnd) {
676
- isBoundary = true
677
- resetOffset = moveStep * elementsLength
678
- }
679
- } else if (translation > 0) {
680
- const posEnd = (patchElmNumShared.value - 1) * step.value
681
- const posReverseEnd = (patchElmNumShared.value + childrenLength.value) * step.value
682
- if (currentOffset > -posEnd) {
683
- isBoundary = true
684
- resetOffset = moveStep * elementsLength + step.value + (moveStep === 1 ? translation : 0)
685
- }
686
- if (currentOffset < -posReverseEnd) {
687
- isBoundary = true
688
- resetOffset = moveStep * elementsLength + patchElmNumShared.value * step.value
689
- }
693
+ if (moveToOffset < boundaryEnd) {
694
+ isBoundary = true
695
+ // 超过边界的距离
696
+ const exceedLength = Math.abs(moveToOffset) - Math.abs(boundaryEnd)
697
+ // 计算对标正常元素所在的offset
698
+ resetOffset = patchElmNumShared.value * step.value + exceedLength
699
+ }
700
+ if (moveToOffset > boundaryStart) {
701
+ isBoundary = true
702
+ // 超过边界的距离
703
+ const exceedLength = Math.abs(boundaryStart) - Math.abs(moveToOffset)
704
+ // 计算对标正常元素所在的offset
705
+ resetOffset = (patchElmNumShared.value + childrenLength.value - 1) * step.value + (step.value - exceedLength)
690
706
  }
691
707
  return {
692
708
  isBoundary,
693
709
  resetOffset: -resetOffset
694
710
  }
695
711
  }
712
+ // 非循环超出边界,应用阻力; 开始滑动少阻力小,滑动越长阻力越大
713
+ function handleResistanceMove (eventData: EventDataType) {
714
+ 'worklet'
715
+ const { translation, transdir } = eventData
716
+ const moveToOffset = offset.value + translation
717
+ const maxOverDrag = Math.floor(step.value / 2)
718
+ const maxOffset = translation < 0 ? -(childrenLength.value - 1) * step.value : 0
719
+ let resistance = 0.1
720
+ let overDrag = 0
721
+ let finalOffset = 0
722
+ // 向右向下小于0, 向左向上大于0;
723
+ if (transdir < 0) {
724
+ overDrag = Math.abs(moveToOffset - maxOffset)
725
+ } else {
726
+ overDrag = Math.abs(moveToOffset)
727
+ }
728
+ // 滑动越多resistance越小
729
+ resistance = 1 - overDrag / maxOverDrag
730
+ // 确保阻力在合理范围内
731
+ resistance = Math.min(0.5, resistance)
732
+ // 限制在最大拖拽范围内
733
+ if (transdir < 0) {
734
+ const adjustOffset = offset.value + translation * resistance
735
+ finalOffset = Math.max(adjustOffset, maxOffset - maxOverDrag)
736
+ } else {
737
+ const adjustOffset = offset.value + translation * resistance
738
+ finalOffset = Math.min(adjustOffset, maxOverDrag)
739
+ }
740
+ return finalOffset
741
+ }
696
742
  const gesturePan = Gesture.Pan()
697
- .onBegin((e) => {
743
+ .onBegin((e: GestureStateChangeEvent<PanGestureHandlerEventPayload>) => {
698
744
  'worklet'
699
745
  if (!step.value) return
700
746
  touchfinish.value = false
@@ -702,47 +748,81 @@ const SwiperWrapper = forwardRef<HandlerRef<View, SwiperProps>, SwiperProps>((pr
702
748
  runOnJS(pauseLoop)()
703
749
  preAbsolutePos.value = e[strAbso]
704
750
  moveTranstion.value = e[strAbso]
705
- moveTime.value = new Date().getTime()
706
751
  })
707
- .onTouchesMove((e) => {
752
+ .onUpdate((e: GestureStateChangeEvent<PanGestureHandlerEventPayload>) => {
708
753
  'worklet'
709
754
  if (touchfinish.value) return
710
- const touchEventData = e.changedTouches[0]
711
- const moveDistance = touchEventData[strAbso] - preAbsolutePos.value
755
+ const moveDistance = e[strAbso] - preAbsolutePos.value
712
756
  const eventData = {
713
- translation: moveDistance
757
+ translation: moveDistance,
758
+ transdir: moveDistance !== 0 ? moveDistance : e[strAbso] - moveTranstion.value
714
759
  }
715
- // 处理用户一直拖拽到临界点的场景, 不会执行onEnd
716
- if (!circularShared.value && !canMove(eventData)) {
760
+ // 1. 支持滑动中超出一半更新索引的能力:只更新索引并不会影响onFinalize依据当前offset计算的索引
761
+ const { half } = computeHalf(eventData)
762
+ if (childrenLength.value > 1 && half) {
763
+ const { selectedIndex } = getTargetPosition(eventData)
764
+ currentIndex.value = selectedIndex
765
+ }
766
+ // 2. 非循环: 处理用户一直拖拽到临界点的场景,如果放到onFinalize无法阻止offset.value更新为越界的值
767
+ if (!circularShared.value) {
768
+ if (canMove(eventData)) {
769
+ offset.value = moveDistance + offset.value
770
+ } else {
771
+ const finalOffset = handleResistanceMove(eventData)
772
+ offset.value = finalOffset
773
+ }
774
+ preAbsolutePos.value = e[strAbso]
717
775
  return
718
776
  }
777
+ // 3. 循环更新: 只有一个元素时可滑动,加入阻力
778
+ if (circularShared.value && childrenLength.value === 1) {
779
+ const finalOffset = handleResistanceMove(eventData)
780
+ offset.value = finalOffset
781
+ preAbsolutePos.value = e[strAbso]
782
+ return
783
+ }
784
+ // 4. 循环更新:正常
719
785
  const { isBoundary, resetOffset } = reachBoundary(eventData)
720
786
  if (childrenLength.value > 1 && isBoundary && circularShared.value) {
721
787
  offset.value = resetOffset
722
788
  } else {
723
789
  offset.value = moveDistance + offset.value
724
790
  }
725
- preAbsolutePos.value = touchEventData[strAbso]
791
+ preAbsolutePos.value = e[strAbso]
726
792
  })
727
- .onTouchesUp((e) => {
793
+ .onFinalize((e: GestureStateChangeEvent<PanGestureHandlerEventPayload>) => {
728
794
  'worklet'
729
795
  if (touchfinish.value) return
730
- const touchEventData = e.changedTouches[0]
731
- const moveDistance = touchEventData[strAbso] - moveTranstion.value
732
796
  touchfinish.value = true
797
+ // 触发过onUpdate正常情况下e[strAbso] - preAbsolutePos.value=0; 未触发过onUpdate的情况下e[strAbso] - preAbsolutePos.value 不为0
798
+ const moveDistance = e[strAbso] - preAbsolutePos.value
733
799
  const eventData = {
734
- translation: moveDistance
800
+ translation: moveDistance,
801
+ transdir: moveDistance !== 0 ? moveDistance : e[strAbso] - moveTranstion.value
735
802
  }
803
+ // 1. 只有一个元素:循环 和 非循环状态,都走回弹效果
736
804
  if (childrenLength.value === 1) {
737
- return handleBackInit()
805
+ offset.value = withTiming(0, {
806
+ duration: easeDuration,
807
+ easing: easeMap[easeingFunc]
808
+ })
809
+ return
738
810
  }
739
- // 用户手指按下起来, 需要计算正确的位置, 比如在滑动过程中突然按下然后起来,需要计算到正确的位置
811
+ // 2.非循环状态不可移动态:最后一个元素 第一个元素
812
+ // 非循环支持最后元素可滑动能力后,向左快速移动未超过最大可移动范围一半,因为offset为正值,向左滑动handleBack,默认向上取整
813
+ // 但是在offset大于0时,取0。[-100, 0](back取0), [0, 100](back取1), 所以handleLongPress里的处理逻辑需要兼容支持,因此这里直接单独处理,不耦合下方公共的判断逻辑。
740
814
  if (!circularShared.value && !canMove(eventData)) {
815
+ if (eventData.transdir < 0) {
816
+ handleBack(eventData)
817
+ } else {
818
+ handleEnd(eventData)
819
+ }
741
820
  return
742
821
  }
743
- const strVelocity = moveDistance / (new Date().getTime() - moveTime.value) * 1000
744
- if (Math.abs(strVelocity) < longPressRatio) {
745
- handleLongPress()
822
+ // 3. 非循环状态可移动态、循环状态, 正常逻辑处理
823
+ const velocity = e[strVelocity]
824
+ if (Math.abs(velocity) < longPressRatio) {
825
+ handleLongPress(eventData)
746
826
  } else {
747
827
  handleEnd(eventData)
748
828
  }
@@ -40,6 +40,8 @@ declare let global: {
40
40
  __formatValue (value: string): string | number
41
41
  } & Record<string, any>
42
42
 
43
+ declare let mpxGlobal: Record<string, any>
44
+
43
45
  declare module '@react-navigation/native' {
44
46
  export function useNavigation (): Record<string, any>
45
47
  export function usePreventRemove(
@@ -14,6 +14,7 @@ import {
14
14
  } from 'react-native-reanimated'
15
15
  import type { AnimationCallback, WithTimingConfig, SharedValue, AnimatableValue } from 'react-native-reanimated'
16
16
  import { error, hasOwn, collectDataset } from '@mpxjs/utils'
17
+ import { useRunOnJSCallback } from './utils'
17
18
  import { ExtendedViewStyle } from './types/common'
18
19
  import type { _ViewProps } from './mpx-view'
19
20
 
@@ -218,13 +219,19 @@ export default function useAnimationHooks<T, P> (props: _ViewProps & { enableAni
218
219
  timeStamp: Date.now()
219
220
  })
220
221
  }
222
+ // eslint-disable-next-line react-hooks/rules-of-hooks
223
+ const runOnJSCallbackRef = useRef({
224
+ withTimingCallback
225
+ })
226
+ // eslint-disable-next-line react-hooks/rules-of-hooks
227
+ const runOnJSCallback = useRunOnJSCallback(runOnJSCallbackRef)
221
228
  // 创建单个animation
222
229
  function getAnimation ({ key, value }: { key: string, value: string|number }, { delay, duration, easing }: ExtendWithTimingConfig, callback?: AnimationCallback) {
223
230
  const animation = typeof callback === 'function'
224
231
  ? withTiming(value, { duration, easing }, (finished, current) => {
225
232
  callback(finished, current)
226
233
  if (transitionend && finished) {
227
- runOnJS(withTimingCallback)(finished, current, duration)
234
+ runOnJS(runOnJSCallback)('withTimingCallback', finished, current, duration)
228
235
  }
229
236
  })
230
237
  : withTiming(value, { duration, easing })
@@ -1,4 +1,4 @@
1
- import { useEffect, useCallback, useMemo, useRef, ReactNode, ReactElement, isValidElement, useContext, useState, Dispatch, SetStateAction, Children, cloneElement, createElement } from 'react'
1
+ import { useEffect, useCallback, useMemo, useRef, ReactNode, ReactElement, isValidElement, useContext, useState, Dispatch, SetStateAction, Children, cloneElement, createElement, MutableRefObject } from 'react'
2
2
  import { LayoutChangeEvent, TextStyle, ImageProps, Image } from 'react-native'
3
3
  import { isObject, isFunction, isNumber, hasOwn, diffAndCloneA, error, warn } from '@mpxjs/utils'
4
4
  import { VarContext, ScrollViewContext, RouteContext } from './context'
@@ -787,13 +787,11 @@ export function useHover ({ enableHover, hoverStartTime, hoverStayTime, disabled
787
787
  const gesture = useMemo(() => {
788
788
  return Gesture.Pan()
789
789
  .onTouchesDown(() => {
790
- 'worklet'
791
- runOnJS(setStartTimer)()
790
+ setStartTimer()
792
791
  })
793
792
  .onTouchesUp(() => {
794
- 'worklet'
795
- runOnJS(setStayTimer)()
796
- })
793
+ setStayTimer()
794
+ }).runOnJS(true)
797
795
  }, [])
798
796
 
799
797
  if (gestureRef) {
@@ -805,3 +803,19 @@ export function useHover ({ enableHover, hoverStartTime, hoverStayTime, disabled
805
803
  gesture
806
804
  }
807
805
  }
806
+
807
+ export function useRunOnJSCallback (callbackMapRef: MutableRefObject<Record<string, AnyFunc>>) {
808
+ const invokeCallback = useCallback((key: string, ...args: any) => {
809
+ const callback = callbackMapRef.current[key]
810
+ // eslint-disable-next-line node/no-callback-literal
811
+ if (isFunction(callback)) return callback(...args)
812
+ }, [])
813
+
814
+ useEffect(() => {
815
+ return () => {
816
+ callbackMapRef.current = {}
817
+ }
818
+ }, [])
819
+
820
+ return invokeCallback
821
+ }
@@ -49,7 +49,7 @@ import { processAppOption, getComponent } from ${stringifyRequest(loaderContext,
49
49
  Vue.use(VueRouter)\n`
50
50
 
51
51
  if (i18n) {
52
- output += buildI18n({ i18n, loaderContext })
52
+ output += buildI18n({ i18n, isMain: true, loaderContext })
53
53
  }
54
54
 
55
55
  output += buildGlobalParams({
@@ -7,7 +7,8 @@ const {
7
7
  buildComponentsMap,
8
8
  getRequireScript,
9
9
  buildGlobalParams,
10
- stringifyRequest
10
+ stringifyRequest,
11
+ buildI18n
11
12
  } = require('./script-helper')
12
13
 
13
14
  module.exports = function (script, {
@@ -24,7 +25,7 @@ module.exports = function (script, {
24
25
  wxsModuleMap,
25
26
  localComponentsMap
26
27
  }, callback) {
27
- const { projectRoot, appInfo, webConfig } = loaderContext.getMpx()
28
+ const { projectRoot, appInfo, webConfig, i18n } = loaderContext.getMpx()
28
29
 
29
30
  let output = '/* script */\n'
30
31
 
@@ -70,6 +71,9 @@ module.exports = function (script, {
70
71
  }
71
72
 
72
73
  content += buildGlobalParams({ moduleId, scriptSrcMode, loaderContext, isProduction, webConfig, hasApp })
74
+ if (!hasApp && i18n) {
75
+ content += buildI18n({ i18n, loaderContext })
76
+ }
73
77
  content += getRequireScript({ ctorType, script, loaderContext })
74
78
  content += `
75
79
  export default processComponentOption({
@@ -186,13 +186,17 @@ function buildGlobalParams ({
186
186
  return content
187
187
  }
188
188
 
189
- function buildI18n ({ i18n, loaderContext }) {
189
+ function buildI18n ({ i18n, isMain, loaderContext }) {
190
190
  let i18nContent = ''
191
191
  const i18nObj = Object.assign({}, i18n)
192
- i18nContent += `
193
- import VueI18n from 'vue-i18n'
192
+ if (!isMain) {
193
+ i18nContent += `import Vue from 'vue'
194
+ import Mpx from '@mpxjs/core'\n`
195
+ }
196
+ i18nContent += `import VueI18n from 'vue-i18n'
194
197
  import { createI18n } from 'vue-i18n-bridge'
195
- Vue.use(VueI18n , { bridge: true })\n`
198
+ if (!Mpx.i18n) {
199
+ Vue.use(VueI18n , { bridge: true })\n`
196
200
  const requestObj = {}
197
201
  const i18nKeys = ['messages', 'dateTimeFormats', 'numberFormats']
198
202
  i18nKeys.forEach((key) => {
@@ -201,15 +205,16 @@ function buildI18n ({ i18n, loaderContext }) {
201
205
  delete i18nObj[`${key}Path`]
202
206
  }
203
207
  })
204
- i18nContent += ` var i18nCfg = ${JSON.stringify(i18nObj)}\n`
208
+ i18nContent += ` var i18nCfg = ${JSON.stringify(i18nObj)}\n`
205
209
  Object.keys(requestObj).forEach((key) => {
206
- i18nContent += ` i18nCfg.${key} = require(${requestObj[key]})\n`
210
+ i18nContent += ` i18nCfg.${key} = require(${requestObj[key]})\n`
207
211
  })
208
212
  i18nContent += `
209
- i18nCfg.legacy = false
210
- var i18n = createI18n(i18nCfg, VueI18n)
211
- Vue.use(i18n)
212
- Mpx.i18n = i18n\n`
213
+ i18nCfg.legacy = false
214
+ var i18n = createI18n(i18nCfg, VueI18n)
215
+ Vue.use(i18n)
216
+ Mpx.i18n = i18n
217
+ }\n`
213
218
  return i18nContent
214
219
  }
215
220
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpxjs/webpack-plugin",
3
- "version": "2.10.12",
3
+ "version": "2.10.13",
4
4
  "description": "mpx compile core",
5
5
  "keywords": [
6
6
  "mpx"
@@ -28,7 +28,7 @@
28
28
  "@better-scroll/wheel": "^2.5.1",
29
29
  "@better-scroll/zoom": "^2.5.1",
30
30
  "@mpxjs/template-engine": "^2.8.7",
31
- "@mpxjs/utils": "^2.10.8",
31
+ "@mpxjs/utils": "^2.10.13",
32
32
  "acorn": "^8.11.3",
33
33
  "acorn-walk": "^7.2.0",
34
34
  "async": "^2.6.0",
@@ -83,7 +83,7 @@
83
83
  },
84
84
  "devDependencies": {
85
85
  "@d11/react-native-fast-image": "^8.6.12",
86
- "@mpxjs/api-proxy": "^2.10.8",
86
+ "@mpxjs/api-proxy": "^2.10.13",
87
87
  "@types/babel-traverse": "^6.25.4",
88
88
  "@types/babel-types": "^7.0.4",
89
89
  "@types/react": "^18.2.79",
@@ -100,5 +100,5 @@
100
100
  "engines": {
101
101
  "node": ">=14.14.0"
102
102
  },
103
- "gitHead": "430dd358e9737dc543d3395fee3c304d19a98702"
103
+ "gitHead": "1b4a2d4765341ef6c6b74e501f72a0f856f247f9"
104
104
  }