@mpxjs/core 2.9.70 → 2.9.72

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpxjs/core",
3
- "version": "2.9.70",
3
+ "version": "2.9.72",
4
4
  "description": "mpx runtime core",
5
5
  "keywords": [
6
6
  "miniprogram",
@@ -24,17 +24,16 @@
24
24
  "miniprogram-api-typings": "^3.10.0"
25
25
  },
26
26
  "peerDependencies": {
27
- "@ant-design/react-native": "^5.1.3",
28
27
  "@d11/react-native-fast-image": "^8.6.12",
29
28
  "@mpxjs/api-proxy": "^2.9.0",
30
29
  "@mpxjs/store": "^2.9.0",
31
30
  "@react-navigation/native": "^7.0.3",
32
31
  "@react-navigation/stack": "^7.0.4",
33
- "promise": "^8.3.0",
34
32
  "react": "*",
35
33
  "react-native": "*",
36
34
  "react-native-gesture-handler": "^2.19.0",
37
35
  "react-native-linear-gradient": "^2.8.3",
36
+ "react-native-reanimated": "^3.15.2",
38
37
  "react-native-safe-area-context": "^4.14.0",
39
38
  "react-native-screens": "^4.1.0",
40
39
  "react-native-webview": "^13.10.5",
@@ -62,7 +61,7 @@
62
61
  "react-native": {
63
62
  "optional": true
64
63
  },
65
- "promise": {
64
+ "react-native-reanimated": {
66
65
  "optional": true
67
66
  },
68
67
  "@react-navigation/native": {
@@ -71,9 +70,6 @@
71
70
  "@react-navigation/stack": {
72
71
  "optional": true
73
72
  },
74
- "@ant-design/react-native": {
75
- "optional": true
76
- },
77
73
  "react-native-safe-area-context": {
78
74
  "optional": true
79
75
  },
@@ -109,5 +105,5 @@
109
105
  "url": "https://github.com/didi/mpx/issues"
110
106
  },
111
107
  "sideEffects": false,
112
- "gitHead": "ad8f04de95865ffc2b50b8c0a7f87b5952fb8647"
108
+ "gitHead": "5ceca0430a79a41456260d87a81760f2cd31fad1"
113
109
  }
@@ -5,7 +5,7 @@ import {
5
5
  import { implemented } from '../core/implement'
6
6
 
7
7
  // 暂不支持的wx选项,后期需要各种花式支持
8
- const unsupported = ['relations', 'moved', 'definitionFilter']
8
+ const unsupported = ['moved', 'definitionFilter']
9
9
 
10
10
  function convertErrorDesc (key) {
11
11
  error(`Options.${key} is not supported in runtime conversion from wx to react native.`, global.currentResource)
@@ -122,9 +122,10 @@ function extractObservers (options) {
122
122
  Object.keys(props).forEach(key => {
123
123
  const prop = props[key]
124
124
  if (prop && prop.observer) {
125
+ let callback = prop.observer
126
+ delete prop.observer
125
127
  mergeWatch(key, {
126
128
  handler (...rest) {
127
- let callback = prop.observer
128
129
  if (typeof callback === 'string') {
129
130
  callback = this[callback]
130
131
  }
@@ -168,11 +169,7 @@ function extractObservers (options) {
168
169
  cb = this[cb]
169
170
  }
170
171
  if (typeof cb === 'function') {
171
- if (keyPathArr.length < 2) {
172
- val = [val]
173
- old = [old]
174
- }
175
- cb.call(this, ...val, ...old)
172
+ Array.isArray(val) ? cb.call(this, ...val) : cb.call(this, val)
176
173
  }
177
174
  },
178
175
  deep,
package/src/core/proxy.js CHANGED
@@ -1,4 +1,4 @@
1
- import { reactive } from '../observer/reactive'
1
+ import { reactive, defineReactive } from '../observer/reactive'
2
2
  import { ReactiveEffect, pauseTracking, resetTracking } from '../observer/effect'
3
3
  import { effectScope } from '../platform/export/index'
4
4
  import { watch } from '../observer/watch'
@@ -111,7 +111,7 @@ export default class MpxProxy {
111
111
  this.uid = uid++
112
112
  this.name = options.name || ''
113
113
  this.options = options
114
- this.ignoreReactivePattern = this.options.options?.ignoreReactivePattern
114
+ this.shallowReactivePattern = this.options.options?.shallowReactivePattern
115
115
  // beforeCreate -> created -> mounted -> unmounted
116
116
  this.state = BEFORECREATE
117
117
  this.ignoreProxyMap = makeMap(Mpx.config.ignoreProxyWhiteList)
@@ -145,10 +145,12 @@ export default class MpxProxy {
145
145
  this.initApi()
146
146
  }
147
147
 
148
- processIgnoreReactive (obj) {
149
- if (this.ignoreReactivePattern && isObject(obj)) {
148
+ processShallowReactive (obj) {
149
+ if (this.shallowReactivePattern && isObject(obj)) {
150
150
  Object.keys(obj).forEach((key) => {
151
- if (this.ignoreReactivePattern.test(key)) {
151
+ if (this.shallowReactivePattern.test(key)) {
152
+ // 命中shallowReactivePattern的属性将其设置为 shallowReactive
153
+ defineReactive(obj, key, obj[key], true)
152
154
  Object.defineProperty(obj, key, {
153
155
  enumerable: true,
154
156
  // set configurable to false to skip defineReactive
@@ -169,6 +171,8 @@ export default class MpxProxy {
169
171
  // web中BEFORECREATE钩子通过vue的beforeCreate钩子单独驱动
170
172
  this.callHook(BEFORECREATE)
171
173
  setCurrentInstance(this)
174
+ this.parent = this.resolveParent()
175
+ this.provides = this.parent ? this.parent.provides : Object.create(null)
172
176
  // 在 props/data 初始化之前初始化 inject
173
177
  this.initInject()
174
178
  this.initProps()
@@ -193,6 +197,18 @@ export default class MpxProxy {
193
197
  }
194
198
  }
195
199
 
200
+ resolveParent () {
201
+ if (isReact) {
202
+ return {
203
+ provides: this.target.__parentProvides
204
+ }
205
+ }
206
+ if (isFunction(this.target.selectOwnerComponent)) {
207
+ const parent = this.target.selectOwnerComponent()
208
+ return parent ? parent.__mpxProxy : null
209
+ }
210
+ }
211
+
196
212
  createRenderTask (isEmptyRender) {
197
213
  if ((!this.isMounted() && this.currentRenderTask) || (this.isMounted() && isEmptyRender)) {
198
214
  return
@@ -232,16 +248,6 @@ export default class MpxProxy {
232
248
  // 页面/组件销毁清除上下文的缓存
233
249
  contextMap.remove(this.uid)
234
250
  }
235
- if (!isWeb && this.options.__type__ === 'page') {
236
- // 小程序页面销毁时移除对应的 provide
237
- if (isFunction(this.target.getPageId)) {
238
- const pageId = this.target.getPageId()
239
- const providesMap = global.__mpxProvidesMap
240
- if (providesMap.__pages[pageId]) {
241
- delete providesMap.__pages[pageId]
242
- }
243
- }
244
- }
245
251
  this.callHook(BEFOREUNMOUNT)
246
252
  this.scope?.stop()
247
253
  if (this.update) this.update.active = false
@@ -290,10 +296,10 @@ export default class MpxProxy {
290
296
  if (isReact) {
291
297
  // react模式下props内部对象透传无需深clone,依赖对象深层的数据响应触发子组件更新
292
298
  this.props = this.target.__getProps()
293
- reactive(this.processIgnoreReactive(this.props))
299
+ reactive(this.processShallowReactive(this.props))
294
300
  } else {
295
301
  this.props = diffAndCloneA(this.target.__getProps(this.options)).clone
296
- reactive(this.processIgnoreReactive(this.props))
302
+ reactive(this.processShallowReactive(this.props))
297
303
  }
298
304
  proxy(this.target, this.props, undefined, false, this.createProxyConflictHandler('props'))
299
305
  }
@@ -333,7 +339,7 @@ export default class MpxProxy {
333
339
  if (isFunction(dataFn)) {
334
340
  Object.assign(this.data, callWithErrorHandling(dataFn.bind(this.target), this, 'data function'))
335
341
  }
336
- reactive(this.processIgnoreReactive(this.data))
342
+ reactive(this.processShallowReactive(this.data))
337
343
  proxy(this.target, this.data, undefined, false, this.createProxyConflictHandler('data'))
338
344
  this.collectLocalKeys(this.data)
339
345
  }
@@ -514,7 +520,7 @@ export default class MpxProxy {
514
520
  if (hasOwn(renderData, key)) {
515
521
  const data = renderData[key]
516
522
  const firstKey = getFirstKey(key)
517
- if (!this.localKeysMap[firstKey] || (this.ignoreReactivePattern && this.ignoreReactivePattern.test(firstKey))) {
523
+ if (!this.localKeysMap[firstKey]) {
518
524
  continue
519
525
  }
520
526
  // 外部clone,用于只需要clone的场景
@@ -22,7 +22,8 @@ export default function getBuiltInMixins ({ type, rawOptions = {} }) {
22
22
  directiveHelperMixin(),
23
23
  styleHelperMixin(),
24
24
  refsMixin(),
25
- i18nMixin()
25
+ i18nMixin(),
26
+ relationsMixin(type)
26
27
  ]
27
28
  } else if (__mpx_mode__ === 'web') {
28
29
  bulitInMixins = [
@@ -1,4 +1,5 @@
1
- import { setByPath } from '@mpxjs/utils'
1
+ import { setByPath, error, parseDataset } from '@mpxjs/utils'
2
+ import Mpx from '../../index'
2
3
 
3
4
  export default function proxyEventMixin () {
4
5
  return {
@@ -19,11 +20,58 @@ export default function proxyEventMixin () {
19
20
  const value = filterMethod ? (innerFilter[filterMethod] ? innerFilter[filterMethod](originValue) : typeof this[filterMethod] === 'function' && this[filterMethod]) : originValue
20
21
  setByPath(this, expr, value)
21
22
  },
22
- __invokeHandler (eventName, $event) {
23
- const handler = this[eventName]
24
- if (handler && typeof handler === 'function') {
25
- handler.call(this, $event)
23
+ __invoke (rawEvent, eventConfig = []) {
24
+ if (typeof Mpx.config.proxyEventHandler === 'function') {
25
+ try {
26
+ Mpx.config.proxyEventHandler(rawEvent)
27
+ } catch (e) {}
26
28
  }
29
+ const location = this.__mpxProxy.options.mpxFileResource
30
+
31
+ if (rawEvent.target && !rawEvent.target._datasetProcessed) {
32
+ const originalDataset = rawEvent.target.dataset
33
+ Object.defineProperty(rawEvent.target, 'dataset', {
34
+ get: () => parseDataset(originalDataset),
35
+ configurable: true,
36
+ enumerable: true
37
+ })
38
+ rawEvent.target._datasetProcessed = true
39
+ }
40
+ if (rawEvent.currentTarget && !rawEvent.currentTarget._datasetProcessed) {
41
+ const originalDataset = rawEvent.currentTarget.dataset
42
+ Object.defineProperty(rawEvent.currentTarget, 'dataset', {
43
+ get: () => parseDataset(originalDataset),
44
+ configurable: true,
45
+ enumerable: true
46
+ })
47
+ rawEvent.currentTarget._datasetProcessed = true
48
+ }
49
+
50
+ let returnedValue
51
+ eventConfig.forEach((item) => {
52
+ const callbackName = item[0]
53
+ if (callbackName) {
54
+ const params =
55
+ item.length > 1
56
+ ? item.slice(1).map((item) => {
57
+ if (item === '__mpx_event__') {
58
+ return rawEvent
59
+ } else {
60
+ return item
61
+ }
62
+ })
63
+ : [rawEvent]
64
+ if (typeof this[callbackName] === 'function') {
65
+ returnedValue = this[callbackName].apply(this, params)
66
+ } else {
67
+ error(
68
+ `Instance property [${callbackName}] is not function, please check.`,
69
+ location
70
+ )
71
+ }
72
+ }
73
+ })
74
+ return returnedValue
27
75
  }
28
76
  }
29
77
  }
@@ -0,0 +1,67 @@
1
+ import { BEFORECREATE, MOUNTED, BEFOREUNMOUNT } from '../../core/innerLifecycle'
2
+
3
+ const relationTypeMap = {
4
+ parent: 'child',
5
+ ancestor: 'descendant'
6
+ }
7
+
8
+ export default function relationsMixin (mixinType) {
9
+ if (mixinType === 'component') {
10
+ return {
11
+ [BEFORECREATE] () {
12
+ this.__relationNodesMap = {}
13
+ },
14
+ [MOUNTED] () {
15
+ this.__mpxExecRelations('linked')
16
+ },
17
+ [BEFOREUNMOUNT] () {
18
+ this.__mpxExecRelations('unlinked')
19
+ this.__relationNodesMap = {}
20
+ },
21
+ methods: {
22
+ getRelationNodes (path) {
23
+ return this.__relationNodesMap[path] || null
24
+ },
25
+ __mpxExecRelations (type) {
26
+ const relations = this.__mpxProxy.options.relations
27
+ const relationContext = this.__relation
28
+ const currentPath = this.__componentPath
29
+ if (relations && relationContext) {
30
+ Object.keys(relations).forEach((path) => {
31
+ const relation = relations[path]
32
+ const relationType = relation.type
33
+ if ((relationType === 'parent' || relationType === 'ancestor') && relationContext[path]) {
34
+ const target = relationContext[path]
35
+ const targetRelation = target.__mpxProxy.options.relations?.[currentPath]
36
+ if (targetRelation && targetRelation.type === relationTypeMap[relationType] && target.__componentPath === path) {
37
+ if (type === 'linked') {
38
+ this.__mpxLinkRelationNodes(target, currentPath)
39
+ } else if (type === 'unlinked') {
40
+ this.__mpxRemoveRelationNodes(target, currentPath)
41
+ }
42
+ if (typeof targetRelation[type] === 'function') {
43
+ targetRelation[type].call(target, this)
44
+ }
45
+ if (typeof relation[type] === 'function') {
46
+ relation[type].call(this, target)
47
+ }
48
+ this.__relationNodesMap[path] = [target]
49
+ }
50
+ }
51
+ })
52
+ }
53
+ },
54
+ __mpxLinkRelationNodes (target, path) {
55
+ target.__relationNodesMap[path] = target.__relationNodesMap[path] || [] // 父级绑定子级
56
+ target.__relationNodesMap[path].push(this)
57
+ },
58
+ __mpxRemoveRelationNodes (target, path) {
59
+ const relationNodesMap = target.__relationNodesMap
60
+ const arr = relationNodesMap[path] || []
61
+ const index = arr.indexOf(this)
62
+ if (index !== -1) arr.splice(index, 1)
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
@@ -1,13 +1,13 @@
1
1
  import transferOptions from '../core/transferOptions'
2
2
  import builtInKeysMap from './patch/builtInKeysMap'
3
- import { makeMap, spreadProp, getFocusedNavigation, hasOwn, extend } from '@mpxjs/utils'
3
+ import { makeMap, spreadProp, getFocusedNavigation, hasOwn } from '@mpxjs/utils'
4
4
  import { mergeLifecycle } from '../convertor/mergeLifecycle'
5
5
  import { LIFECYCLE } from '../platform/patch/lifecycle/index'
6
6
  import Mpx from '../index'
7
7
  import { createElement, memo, useRef, useEffect } from 'react'
8
8
  import * as ReactNative from 'react-native'
9
9
  import { Image } from 'react-native'
10
- import { ref } from '../observer/ref'
10
+ import { initAppProvides } from './export/inject'
11
11
 
12
12
  const appHooksMap = makeMap(mergeLifecycle(LIFECYCLE).app)
13
13
 
@@ -30,22 +30,28 @@ function filterOptions (options, appData) {
30
30
  return newOptions
31
31
  }
32
32
 
33
- function createAppInstance (appData) {
34
- return extend({}, Mpx.prototype, appData)
35
- }
36
-
37
- export default function createApp (option, config = {}) {
33
+ export default function createApp (options) {
38
34
  const appData = {}
39
35
 
40
36
  const { NavigationContainer, createStackNavigator, SafeAreaProvider } = global.__navigationHelper
41
37
  // app选项目前不需要进行转换
42
- const { rawOptions, currentInject } = transferOptions(option, 'app', false)
38
+ const { rawOptions, currentInject } = transferOptions(options, 'app', false)
39
+ initAppProvides(rawOptions.provide, rawOptions)
43
40
  const defaultOptions = filterOptions(spreadProp(rawOptions, 'methods'), appData)
44
- defaultOptions.onAppInit && defaultOptions.onAppInit()
45
41
  // 在页面script执行前填充getApp()
46
42
  global.getApp = function () {
47
43
  return appData
48
44
  }
45
+
46
+ // 模拟小程序appInstance在热启动时不会重新创建的行为,在外部创建跟随js context的appInstance
47
+ const appInstance = Object.assign({}, appData, Mpx.prototype)
48
+
49
+ defaultOptions.onShow && global.__mpxAppCbs.show.push(defaultOptions.onShow.bind(appInstance))
50
+ defaultOptions.onHide && global.__mpxAppCbs.hide.push(defaultOptions.onHide.bind(appInstance))
51
+ defaultOptions.onError && global.__mpxAppCbs.error.push(defaultOptions.onError.bind(appInstance))
52
+ defaultOptions.onUnhandledRejection && global.__mpxAppCbs.rejection.push(defaultOptions.onUnhandledRejection.bind(appInstance))
53
+ defaultOptions.onAppInit && defaultOptions.onAppInit()
54
+
49
55
  const pages = currentInject.getPages() || {}
50
56
  const firstPage = currentInject.firstPage
51
57
  const Stack = createStackNavigator()
@@ -82,55 +88,50 @@ export default function createApp (option, config = {}) {
82
88
  }
83
89
 
84
90
  global.__mpxAppLaunched = false
85
-
86
- global.__mpxAppFocusedState = ref('show')
87
91
  global.__mpxOptionsMap[currentInject.moduleId] = memo((props) => {
88
- const instanceRef = useRef(null)
89
- if (!instanceRef.current) {
90
- instanceRef.current = createAppInstance(appData)
91
- }
92
- const instance = instanceRef.current
92
+ const firstRef = useRef(true)
93
93
  const initialRouteRef = useRef({
94
94
  initialRouteName: firstPage,
95
95
  initialParams: {}
96
96
  })
97
-
98
- if (!global.__mpxAppLaunched) {
97
+ if (firstRef.current) {
98
+ // 热启动情况下,app会被销毁重建,将__mpxAppHotLaunched重置保障路由等初始化逻辑正确执行
99
+ global.__mpxAppHotLaunched = false
100
+ // 热启动情况下重置__mpxPagesMap避免页面销毁函数未及时执行时错误地引用到之前的navigation
101
+ global.__mpxPagesMap = {}
102
+ firstRef.current = false
103
+ }
104
+ if (!global.__mpxAppHotLaunched) {
99
105
  const { initialRouteName, initialParams } = Mpx.config.rnConfig.parseAppProps?.(props) || {}
100
106
  initialRouteRef.current.initialRouteName = initialRouteName || initialRouteRef.current.initialRouteName
101
107
  initialRouteRef.current.initialParams = initialParams || initialRouteRef.current.initialParams
102
108
 
103
109
  global.__mpxAppOnLaunch = (navigation) => {
104
- global.__mpxAppLaunched = true
105
110
  const state = navigation.getState()
106
111
  Mpx.config.rnConfig.onStateChange?.(state)
107
112
  const current = state.routes[state.index]
108
- global.__mpxEnterOptions = {
113
+ const options = {
109
114
  path: current.name,
110
115
  query: current.params,
111
116
  scene: 0,
112
117
  shareTicket: '',
113
- referrerInfo: {}
118
+ referrerInfo: {},
119
+ isLaunch: true
120
+ }
121
+ global.__mpxEnterOptions = options
122
+ if (!global.__mpxAppLaunched) {
123
+ global.__mpxLaunchOptions = options
124
+ defaultOptions.onLaunch && defaultOptions.onLaunch.call(appInstance, options)
114
125
  }
115
- defaultOptions.onLaunch && defaultOptions.onLaunch.call(instance, global.__mpxEnterOptions)
116
- defaultOptions.onShow && defaultOptions.onShow.call(instance, global.__mpxEnterOptions)
126
+ global.__mpxAppCbs.show.forEach((cb) => {
127
+ cb(options)
128
+ })
129
+ global.__mpxAppLaunched = true
130
+ global.__mpxAppHotLaunched = true
117
131
  }
118
132
  }
119
133
 
120
134
  useEffect(() => {
121
- if (defaultOptions.onShow) {
122
- global.__mpxAppCbs.show.push(defaultOptions.onShow.bind(instance))
123
- }
124
- if (defaultOptions.onHide) {
125
- global.__mpxAppCbs.hide.push(defaultOptions.onHide.bind(instance))
126
- }
127
- if (defaultOptions.onError) {
128
- global.__mpxAppCbs.error.push(defaultOptions.onError.bind(instance))
129
- }
130
- if (defaultOptions.onUnhandledRejection) {
131
- global.__mpxAppCbs.rejection.push(defaultOptions.onUnhandledRejection.bind(instance))
132
- }
133
-
134
135
  const changeSubscription = ReactNative.AppState.addEventListener('change', (currentState) => {
135
136
  if (currentState === 'active') {
136
137
  let options = global.__mpxEnterOptions
@@ -24,19 +24,29 @@ function filterOptions (options, appData) {
24
24
  return newOptions
25
25
  }
26
26
 
27
- export default function createApp (option, config = {}) {
28
- // 在App中挂载mpx对象供周边工具访问,如e2e测试
27
+ export default function createApp (options, config = {}) {
28
+ const appData = {}
29
+ // app选项目前不需要进行转换
30
+ const { rawOptions, currentInject } = transferOptions(options, 'app', false)
29
31
  const builtInMixins = [{
32
+ // 在App中挂载mpx对象供周边工具访问,如e2e测试
30
33
  getMpx () {
31
34
  return Mpx
32
35
  }
33
36
  }]
34
- const appData = {}
35
37
  if (__mpx_mode__ === 'web') {
36
38
  builtInMixins.push({
39
+ beforeCreate () {
40
+ // for vue provide vm access
41
+ Object.assign(this, appData, Mpx.prototype)
42
+ if (isBrowser) {
43
+ rawOptions.onShow && global.__mpxAppCbs.show.push(rawOptions.onShow.bind(this))
44
+ rawOptions.onHide && global.__mpxAppCbs.hide.push(rawOptions.onHide.bind(this))
45
+ rawOptions.onError && global.__mpxAppCbs.error.push(rawOptions.onError.bind(this))
46
+ rawOptions.onUnhandledRejection && global.__mpxAppCbs.rejection.push(rawOptions.onUnhandledRejection.bind(this))
47
+ }
48
+ },
37
49
  created () {
38
- Object.assign(this, Mpx.prototype)
39
- Object.assign(this, appData)
40
50
  const current = this.$root.$options?.router?.currentRoute || {}
41
51
  const options = {
42
52
  path: current.path && current.path.replace(/^\//, ''),
@@ -45,48 +55,36 @@ export default function createApp (option, config = {}) {
45
55
  shareTicket: '',
46
56
  referrerInfo: {}
47
57
  }
58
+ // web不分冷启动和热启动
48
59
  global.__mpxEnterOptions = options
49
- this.$options.onLaunch && this.$options.onLaunch.call(this, options)
50
- if (isBrowser) {
51
- if (this.$options.onShow) {
52
- this.$options.onShow.call(this, options)
53
- global.__mpxAppCbs.show.push(this.$options.onShow.bind(this))
54
- }
55
- if (this.$options.onHide) {
56
- global.__mpxAppCbs.hide.push(this.$options.onHide.bind(this))
57
- }
58
- if (this.$options.onError) {
59
- global.__mpxAppCbs.error.push(this.$options.onError.bind(this))
60
- }
61
- if (this.$options.onUnhandledRejection) {
62
- global.__mpxAppCbs.rejection.push(this.$options.onUnhandledRejection.bind(this))
63
- }
64
- }
60
+ global.__mpxLaunchOptions = options
61
+ rawOptions.onLaunch && rawOptions.onLaunch.call(this, options)
62
+ global.__mpxAppCbs.show.forEach((cb) => {
63
+ cb(options)
64
+ })
65
65
  }
66
66
  })
67
67
  } else {
68
68
  builtInMixins.push({
69
69
  onLaunch () {
70
70
  Object.assign(this, Mpx.prototype)
71
+ initAppProvides(rawOptions.provide, this)
71
72
  }
72
73
  })
73
74
  }
74
- // app选项目前不需要进行转换
75
- const { rawOptions, currentInject } = transferOptions(option, 'app', false)
76
75
  rawOptions.mixins = builtInMixins
77
76
  const defaultOptions = filterOptions(spreadProp(mergeOptions(rawOptions, 'app', false), 'methods'), appData)
78
77
 
79
78
  if (__mpx_mode__ === 'web') {
80
- global.__mpxOptionsMap = global.__mpxOptionsMap || {}
81
- global.__mpxOptionsMap[currentInject.moduleId] = defaultOptions
82
79
  global.getApp = function () {
83
80
  if (!isBrowser) {
84
81
  console.error('[Mpx runtime error]: Dangerous API! global.getApp method is running in non browser environments')
85
82
  }
86
83
  return appData
87
84
  }
85
+ global.__mpxOptionsMap = global.__mpxOptionsMap || {}
86
+ global.__mpxOptionsMap[currentInject.moduleId] = defaultOptions
88
87
  } else {
89
- initAppProvides(rawOptions)
90
88
  defaultOptions.onAppInit && defaultOptions.onAppInit()
91
89
  const ctor = config.customCtor || global.currentCtor || App
92
90
  ctor(defaultOptions)
@@ -11,57 +11,60 @@ function extendEvent (e, extendObj = {}) {
11
11
  })
12
12
  }
13
13
 
14
- function MpxEvent (layer) {
15
- this.targetElement = null
16
- this.touches = []
17
- this.touchStartX = 0
18
- this.touchStartY = 0
19
- this.startTimer = null
20
- this.needTap = true
21
- this.isTouchDevice = document && ('ontouchstart' in document.documentElement)
14
+ function createMpxEvent (layer) {
15
+ let startTimer = null
16
+ let needTap = true
17
+ let touchStartX = 0
18
+ let touchStartY = 0
19
+ let targetElement = null
20
+ const isTouchDevice = document && 'ontouchstart' in document.documentElement
22
21
 
23
- this.onTouchStart = (event) => {
22
+ const onTouchStart = (event) => {
24
23
  if (event.targetTouches?.length > 1) {
25
24
  return true
26
25
  }
27
- this.touches = event.targetTouches
28
- this.targetElement = event.target
29
- this.needTap = true
30
- this.startTimer = null
31
- this.touchStartX = this.touches[0].pageX
32
- this.touchStartY = this.touches[0].pageY
33
- this.startTimer = setTimeout(() => {
34
- this.needTap = false
35
- this.sendEvent(this.targetElement, 'longpress', event)
36
- this.sendEvent(this.targetElement, 'longtap', event)
26
+ const touches = event.targetTouches
27
+ targetElement = event.target
28
+ needTap = true
29
+ startTimer = null
30
+ touchStartX = touches[0].pageX
31
+ touchStartY = touches[0].pageY
32
+ startTimer = setTimeout(() => {
33
+ needTap = false
34
+ sendEvent(targetElement, 'longpress', event)
35
+ sendEvent(targetElement, 'longtap', event)
37
36
  }, 350)
38
37
  }
39
38
 
40
- this.onTouchMove = (event) => {
39
+ const onTouchMove = (event) => {
41
40
  const touch = event.changedTouches[0]
42
- if (Math.abs(touch.pageX - this.touchStartX) > 1 || Math.abs(touch.pageY - this.touchStartY) > 1) {
43
- this.needTap = false
44
- this.startTimer && clearTimeout(this.startTimer)
45
- this.startTimer = null
41
+ if (
42
+ Math.abs(touch.pageX - touchStartX) > 1 ||
43
+ Math.abs(touch.pageY - touchStartY) > 1
44
+ ) {
45
+ needTap = false
46
+ startTimer && clearTimeout(startTimer)
47
+ startTimer = null
46
48
  }
47
49
  }
48
50
 
49
- this.onTouchEnd = (event) => {
51
+ const onTouchEnd = (event) => {
50
52
  if (event.targetTouches?.length > 1) {
51
53
  return true
52
54
  }
53
- this.startTimer && clearTimeout(this.startTimer)
54
- this.startTimer = null
55
- if (this.needTap) {
56
- this.sendEvent(this.targetElement, 'tap', event)
55
+ startTimer && clearTimeout(startTimer)
56
+ startTimer = null
57
+ if (needTap) {
58
+ sendEvent(targetElement, 'tap', event)
57
59
  }
58
60
  }
59
61
 
60
- this.onClick = (event) => {
61
- this.targetElement = event.target
62
- this.sendEvent(this.targetElement, 'tap', event)
62
+ const onClick = (event) => {
63
+ targetElement = event.target
64
+ sendEvent(targetElement, 'tap', event)
63
65
  }
64
- this.sendEvent = (targetElement, type, event) => {
66
+
67
+ const sendEvent = (targetElement, type, event) => {
65
68
  const touchEvent = new CustomEvent(type, {
66
69
  bubbles: true,
67
70
  cancelable: true
@@ -72,7 +75,6 @@ function MpxEvent (layer) {
72
75
  changedTouches,
73
76
  touches: changedTouches,
74
77
  detail: {
75
- // pc端点击事件可能没有changedTouches,所以直接从 event中取
76
78
  x: changedTouches[0]?.pageX || event.pageX || 0,
77
79
  y: changedTouches[0]?.pageY || event.pageY || 0
78
80
  }
@@ -80,28 +82,23 @@ function MpxEvent (layer) {
80
82
  targetElement && targetElement.dispatchEvent(touchEvent)
81
83
  }
82
84
 
83
- this.addListener = () => {
84
- if (this.isTouchDevice) {
85
- layer.addEventListener('touchstart', this.onTouchStart, true)
86
- layer.addEventListener('touchmove', this.onTouchMove, true)
87
- layer.addEventListener('touchend', this.onTouchEnd, true)
88
- } else {
89
- layer.addEventListener('click', this.onClick, true)
90
- }
85
+ if (isTouchDevice) {
86
+ layer.addEventListener('touchstart', onTouchStart, true)
87
+ layer.addEventListener('touchmove', onTouchMove, true)
88
+ layer.addEventListener('touchend', onTouchEnd, true)
89
+ } else {
90
+ layer.addEventListener('click', onClick, true)
91
91
  }
92
- this.addListener()
93
92
  }
94
93
 
95
94
  export function initEvent () {
96
95
  if (isBrowser && !global.__mpxCreatedEvent) {
97
96
  global.__mpxCreatedEvent = true
98
97
  if (document.readyState === 'complete' || document.readyState === 'interactive') {
99
- // eslint-disable-next-line no-new
100
- new MpxEvent(document.body)
98
+ createMpxEvent(document.body)
101
99
  } else {
102
100
  document.addEventListener('DOMContentLoaded', function () {
103
- // eslint-disable-next-line no-new
104
- new MpxEvent(document.body)
101
+ createMpxEvent(document.body)
105
102
  }, false)
106
103
  }
107
104
  }
@@ -1,3 +1,4 @@
1
+ import { isFunction, isNumber, isString } from '@mpxjs/utils'
1
2
  import { createI18n } from '../builtInMixins/i18nMixin'
2
3
 
3
4
  export function init (Mpx) {
@@ -30,21 +31,30 @@ function initGlobalErrorHandling () {
30
31
  })
31
32
  }
32
33
 
34
+ function onUnhandledRejection (event) {
35
+ if (global.__mpxAppCbs && global.__mpxAppCbs.rejection && global.__mpxAppCbs.rejection.length) {
36
+ global.__mpxAppCbs.rejection.forEach((cb) => {
37
+ cb(event)
38
+ })
39
+ } else {
40
+ console.warn(`UNHANDLED PROMISE REJECTION ${(isNumber(event.id) || isString(event.id)) ? '(id:' + event.id + ')' : ''}: ${event.reason}\n`)
41
+ }
42
+ }
33
43
  const rejectionTrackingOptions = {
34
44
  allRejections: true,
35
45
  onUnhandled (id, error) {
36
- if (global.__mpxAppCbs && global.__mpxAppCbs.rejection && global.__mpxAppCbs.rejection.length) {
37
- global.__mpxAppCbs.rejection.forEach((cb) => {
38
- cb(error, id)
39
- })
40
- } else {
41
- console.warn(`UNHANDLED PROMISE REJECTION (id: ${id}): ${error}\n`)
42
- }
46
+ onUnhandledRejection({ id, reason: error, promise: null })
43
47
  }
44
48
  }
45
49
 
46
- if (global?.HermesInternal?.hasPromise?.()) {
47
- global.HermesInternal?.enablePromiseRejectionTracker?.(rejectionTrackingOptions)
50
+ // 支持 core-js promise polyfill
51
+ const oldOnUnhandledRejection = global.onunhandledrejection
52
+ global.onunhandledrejection = function onunhandledrejection (event) {
53
+ onUnhandledRejection(event)
54
+ isFunction(oldOnUnhandledRejection) && oldOnUnhandledRejection.call(this, event)
55
+ }
56
+ if (global.HermesInternal?.hasPromise?.()) {
57
+ global.HermesInternal.enablePromiseRejectionTracker?.(rejectionTrackingOptions)
48
58
  } else {
49
59
  require('promise/setimmediate/rejection-tracking').enable(rejectionTrackingOptions)
50
60
  }
@@ -38,7 +38,7 @@ function initGlobalErrorHandling () {
38
38
  window.addEventListener('unhandledrejection', (event) => {
39
39
  if (global.__mpxAppCbs && global.__mpxAppCbs.rejection && global.__mpxAppCbs.rejection.length) {
40
40
  global.__mpxAppCbs.rejection.forEach((cb) => {
41
- cb(event.reason, event.promise)
41
+ cb(event)
42
42
  })
43
43
  } else {
44
44
  console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}\n`)
@@ -60,17 +60,23 @@ export default function install (Vue) {
60
60
  data: {
61
61
  get () {
62
62
  return Object.assign({}, this.$props, this.$data)
63
- }
63
+ },
64
+ enumerable: true,
65
+ configurable: true
64
66
  },
65
67
  dataset: {
66
68
  get () {
67
69
  return collectDataset(this.$attrs, true)
68
- }
70
+ },
71
+ enumerable: true,
72
+ configurable: true
69
73
  },
70
74
  id: {
71
75
  get () {
72
76
  return this.$attrs.id || ''
73
- }
77
+ },
78
+ enumerable: true,
79
+ configurable: true
74
80
  }
75
81
  })
76
82
 
@@ -1,39 +1,30 @@
1
1
  import { callWithErrorHandling, isFunction, isObject, warn } from '@mpxjs/utils'
2
2
  import { currentInstance } from '../../core/proxy'
3
3
 
4
- const providesMap = {
5
- /** 全局 scope */
6
- __app: Object.create(null),
7
- /** 页面 scope */
8
- __pages: Object.create(null)
9
- }
10
-
11
- global.__mpxProvidesMap = providesMap
4
+ /** 全局 scope */
5
+ let appProvides = Object.create(null)
12
6
 
13
7
  /** @internal createApp() 初始化应用层 scope provide */
14
- export function initAppProvides (appOptions) {
15
- const provideOpt = appOptions.provide
8
+ export function initAppProvides (provideOpt, instance) {
16
9
  if (provideOpt) {
17
10
  const provided = isFunction(provideOpt)
18
- ? callWithErrorHandling(provideOpt.bind(appOptions), appOptions, 'createApp provide function')
11
+ ? callWithErrorHandling(provideOpt.bind(instance), instance, 'createApp provide function')
19
12
  : provideOpt
20
13
  if (isObject(provided)) {
21
- providesMap.__app = provided
14
+ appProvides = provided
22
15
  } else {
23
16
  warn('App provides must be an object or a function that returns an object.')
24
17
  }
25
18
  }
26
19
  }
27
20
 
28
- function resolvePageId (context) {
29
- if (context && isFunction(context.getPageId)) {
30
- return context.getPageId()
21
+ function resolveProvides (vm) {
22
+ const provides = vm.provides
23
+ const parentProvides = vm.parent && vm.parent.provides
24
+ if (parentProvides === provides) {
25
+ return (vm.provides = Object.create(parentProvides))
31
26
  }
32
- }
33
-
34
- function resolvePageProvides (context) {
35
- const pageId = resolvePageId(context)
36
- return providesMap.__pages[pageId] || (providesMap.__pages[pageId] = Object.create(null))
27
+ return provides
37
28
  }
38
29
 
39
30
  export function provide (key, value) {
@@ -42,8 +33,7 @@ export function provide (key, value) {
42
33
  warn('provide() can only be used inside setup().')
43
34
  return
44
35
  }
45
- // 小程序无法实现组件父级引用,所以 provide scope 设置为组件所在页面
46
- const provides = resolvePageProvides(instance.target)
36
+ const provides = resolveProvides(instance)
47
37
  provides[key] = value
48
38
  }
49
39
 
@@ -53,11 +43,11 @@ export function inject (key, defaultValue, treatDefaultAsFactory = false) {
53
43
  warn('inject() can only be used inside setup()')
54
44
  return
55
45
  }
56
- const provides = resolvePageProvides(instance.target)
57
- if (key in provides) {
46
+ const provides = instance.parent && instance.parent.provides
47
+ if (provides && key in provides) {
58
48
  return provides[key]
59
- } else if (key in providesMap.__app) {
60
- return providesMap.__app[key]
49
+ } else if (key in appProvides) {
50
+ return appProvides[key]
61
51
  } else if (arguments.length > 1) {
62
52
  return treatDefaultAsFactory && isFunction(defaultValue)
63
53
  ? defaultValue.call(instance && instance.target)
@@ -1,4 +1,4 @@
1
- import { useEffect, useLayoutEffect, useSyncExternalStore, useRef, useMemo, useState, useCallback, createElement, memo, forwardRef, useImperativeHandle, useContext, Fragment, cloneElement } from 'react'
1
+ import { useEffect, useLayoutEffect, useSyncExternalStore, useRef, useMemo, useState, useCallback, createElement, memo, forwardRef, useImperativeHandle, useContext, Fragment, cloneElement, createContext } from 'react'
2
2
  import * as ReactNative from 'react-native'
3
3
  import { ReactiveEffect } from '../../observer/effect'
4
4
  import { watch } from '../../observer/watch'
@@ -11,6 +11,8 @@ import { queueJob, hasPendingJob } from '../../observer/scheduler'
11
11
  import { createSelectorQuery, createIntersectionObserver } from '@mpxjs/api-proxy'
12
12
  import { IntersectionObserverContext, RouteContext, KeyboardAvoidContext } from '@mpxjs/webpack-plugin/lib/runtime/components/react/dist/context'
13
13
 
14
+ const ProviderContext = createContext(null)
15
+
14
16
  function getSystemInfo () {
15
17
  const window = ReactNative.Dimensions.get('window')
16
18
  const screen = ReactNative.Dimensions.get('screen')
@@ -193,7 +195,7 @@ const instanceProto = {
193
195
  }
194
196
  }
195
197
 
196
- function createInstance ({ propsRef, type, rawOptions, currentInject, validProps, components, pageId, intersectionCtx }) {
198
+ function createInstance ({ propsRef, type, rawOptions, currentInject, validProps, components, pageId, intersectionCtx, relation, parentProvides }) {
197
199
  const instance = Object.create(instanceProto, {
198
200
  dataset: {
199
201
  get () {
@@ -244,9 +246,33 @@ function createInstance ({ propsRef, type, rawOptions, currentInject, validProps
244
246
  return currentInject.getRefsData || noop
245
247
  },
246
248
  enumerable: false
249
+ },
250
+ __parentProvides: {
251
+ get () {
252
+ return parentProvides || null
253
+ },
254
+ enumerable: false
247
255
  }
248
256
  })
249
257
 
258
+ if (type === 'component') {
259
+ Object.defineProperty(instance, '__componentPath', {
260
+ get () {
261
+ return currentInject.componentPath || ''
262
+ },
263
+ enumerable: false
264
+ })
265
+ }
266
+
267
+ if (relation) {
268
+ Object.defineProperty(instance, '__relation', {
269
+ get () {
270
+ return relation
271
+ },
272
+ enumerable: false
273
+ })
274
+ }
275
+
250
276
  // bind this & assign methods
251
277
  if (rawOptions.methods) {
252
278
  Object.entries(rawOptions.methods).forEach(([key, method]) => {
@@ -365,34 +391,68 @@ function usePageStatus (navigation, pageId) {
365
391
  const blurSubscription = navigation.addListener('blur', () => {
366
392
  pageStatusMap[pageId] = 'hide'
367
393
  })
368
- const unWatchAppFocusedState = watch(global.__mpxAppFocusedState, (value) => {
369
- pageStatusMap[pageId] = value
370
- })
371
394
 
372
395
  return () => {
373
396
  focusSubscription()
374
397
  blurSubscription()
375
- unWatchAppFocusedState()
376
398
  del(pageStatusMap, pageId)
377
399
  }
378
400
  }, [navigation])
379
401
  }
380
402
 
403
+ const RelationsContext = createContext(null)
404
+
405
+ const checkRelation = (options) => {
406
+ const relations = options.relations || {}
407
+ let hasDescendantRelation = false
408
+ let hasAncestorRelation = false
409
+ Object.keys(relations).forEach((path) => {
410
+ const relation = relations[path]
411
+ const type = relation.type
412
+ if (['child', 'descendant'].includes(type)) {
413
+ hasDescendantRelation = true
414
+ } else if (['parent', 'ancestor'].includes(type)) {
415
+ hasAncestorRelation = true
416
+ }
417
+ })
418
+ return {
419
+ hasDescendantRelation,
420
+ hasAncestorRelation
421
+ }
422
+ }
423
+
424
+ const provideRelation = (instance, relation) => {
425
+ const componentPath = instance.__componentPath
426
+ if (relation) {
427
+ return Object.assign({}, relation, { [componentPath]: instance })
428
+ } else {
429
+ return {
430
+ [componentPath]: instance
431
+ }
432
+ }
433
+ }
434
+
381
435
  export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) {
382
436
  rawOptions = mergeOptions(rawOptions, type, false)
383
437
  const components = Object.assign({}, rawOptions.components, currentInject.getComponents())
384
438
  const validProps = Object.assign({}, rawOptions.props, rawOptions.properties)
439
+ const { hasDescendantRelation, hasAncestorRelation } = checkRelation(rawOptions)
385
440
  if (rawOptions.methods) rawOptions.methods = wrapMethodsWithErrorHandling(rawOptions.methods)
386
441
  const defaultOptions = memo(forwardRef((props, ref) => {
387
442
  const instanceRef = useRef(null)
388
443
  const propsRef = useRef(null)
389
444
  const intersectionCtx = useContext(IntersectionObserverContext)
390
445
  const pageId = useContext(RouteContext)
446
+ const parentProvides = useContext(ProviderContext)
447
+ let relation = null
448
+ if (hasDescendantRelation || hasAncestorRelation) {
449
+ relation = useContext(RelationsContext)
450
+ }
391
451
  propsRef.current = props
392
452
  let isFirst = false
393
453
  if (!instanceRef.current) {
394
454
  isFirst = true
395
- instanceRef.current = createInstance({ propsRef, type, rawOptions, currentInject, validProps, components, pageId, intersectionCtx })
455
+ instanceRef.current = createInstance({ propsRef, type, rawOptions, currentInject, validProps, components, pageId, intersectionCtx, relation, parentProvides })
396
456
  }
397
457
  const instance = instanceRef.current
398
458
  useImperativeHandle(ref, () => {
@@ -442,10 +502,17 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) {
442
502
 
443
503
  useEffect(() => {
444
504
  if (type === 'page') {
445
- if (!global.__mpxAppLaunched && global.__mpxAppOnLaunch) {
505
+ if (!global.__mpxAppHotLaunched && global.__mpxAppOnLaunch) {
446
506
  global.__mpxAppOnLaunch(props.navigation)
447
507
  }
448
- proxy.callHook(ONLOAD, [props.route.params || {}])
508
+ const loadParams = {}
509
+ // 此处拿到的props.route.params内属性的value被进行过了一次decode, 不符合预期,此处额外进行一次encode来与微信对齐
510
+ if (isObject(props.route.params)) {
511
+ for (const key in props.route.params) {
512
+ loadParams[key] = encodeURIComponent(props.route.params[key])
513
+ }
514
+ }
515
+ proxy.callHook(ONLOAD, [loadParams])
449
516
  }
450
517
  proxy.mounted()
451
518
  return () => {
@@ -470,15 +537,28 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) {
470
537
  return proxy.finalMemoVersion
471
538
  }, [proxy.stateVersion, proxy.memoVersion])
472
539
 
473
- const root = useMemo(() => proxy.effect.run(), [finalMemoVersion])
540
+ let root = useMemo(() => proxy.effect.run(), [finalMemoVersion])
474
541
  if (root && root.props.ishost) {
475
542
  // 对于组件未注册的属性继承到host节点上,如事件、样式和其他属性等
476
543
  const rootProps = getRootProps(props, validProps)
477
544
  rootProps.style = Object.assign({}, root.props.style, rootProps.style)
478
545
  // update root props
479
- return cloneElement(root, rootProps)
546
+ root = cloneElement(root, rootProps)
480
547
  }
481
- return root
548
+
549
+ const provides = proxy.provides
550
+ if (provides) {
551
+ root = createElement(ProviderContext.Provider, { value: provides }, root)
552
+ }
553
+
554
+ return hasDescendantRelation
555
+ ? createElement(RelationsContext.Provider,
556
+ {
557
+ value: provideRelation(instance, relation)
558
+ },
559
+ root
560
+ )
561
+ : root
482
562
  }))
483
563
 
484
564
  if (rawOptions.options?.isCustomText) {
@@ -23,13 +23,11 @@ function transformProperties (properties) {
23
23
  } else {
24
24
  newFiled = Object.assign({}, rawFiled)
25
25
  }
26
- const rawObserver = rawFiled?.observer
27
26
  newFiled.observer = function (value, oldValue) {
28
27
  if (this.__mpxProxy) {
29
28
  this[key] = value
30
29
  this.__mpxProxy.propsUpdated()
31
30
  }
32
- rawObserver && rawObserver.call(this, value, oldValue)
33
31
  }
34
32
  newProps[key] = newFiled
35
33
  })