diff --git a/.eslintrc.js b/.eslintrc.js index 37bb6af70f..0cd2003456 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,10 +5,7 @@ module.exports = { sourceType: 'module' }, extends: 'standard', - plugins: [ - 'html', - 'jest' - ], + plugins: ['html', 'jest'], globals: { wx: 'readonly', my: 'readonly', @@ -45,7 +42,7 @@ module.exports = { extends: [ 'standard', 'plugin:@typescript-eslint/eslint-recommended', - 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended' ], plugins: ['@typescript-eslint'], rules: { @@ -56,7 +53,13 @@ module.exports = { '@typescript-eslint/no-empty-interface': 0, '@typescript-eslint/no-unused-vars': 0, '@typescript-eslint/no-non-null-assertion': 0, - camelcase: 0, + camelcase: 0 + } + }, { + files: ['packages/webpack-plugin/lib/runtime/components/react/**/*.{js,jsx,ts,tsx}'], + plugins: ['react-hooks'], + rules: { + 'react-hooks/rules-of-hooks': 'error' } } ] diff --git a/lerna.json b/lerna.json index fa9f091534..edc1456c92 100644 --- a/lerna.json +++ b/lerna.json @@ -1,3 +1,3 @@ { - "version": "2.9.69" + "version": "2.9.70" } \ No newline at end of file diff --git a/package.json b/package.json index 3239959915..d57ad55a53 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "eslint-plugin-jest": "^27.0.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^5.1.1", + "eslint-plugin-react-hooks": "^5.1.0", "identity-obj-proxy": "^3.0.0", "jest": "^27.2.0", "lerna": "^8.1.8", diff --git a/packages/api-proxy/package.json b/packages/api-proxy/package.json index b4dd0a5985..780291bace 100644 --- a/packages/api-proxy/package.json +++ b/packages/api-proxy/package.json @@ -1,6 +1,6 @@ { "name": "@mpxjs/api-proxy", - "version": "2.9.69", + "version": "2.9.70", "description": "convert miniprogram API at each end", "module": "src/index.js", "types": "@types/index.d.ts", @@ -37,7 +37,7 @@ }, "homepage": "https://github.com/didi/mpx#readme", "dependencies": { - "@mpxjs/utils": "^2.9.69", + "@mpxjs/utils": "^2.9.70", "axios": "^1.7.3" }, "peerDependencies": { diff --git a/packages/api-proxy/src/platform/api/system/index.web.js b/packages/api-proxy/src/platform/api/system/index.web.js index 2614aa2562..0e756b73d9 100644 --- a/packages/api-proxy/src/platform/api/system/index.web.js +++ b/packages/api-proxy/src/platform/api/system/index.web.js @@ -139,7 +139,7 @@ const getLaunchOptionsSync = function () { throwSSRWarning('getLaunchOptionsSync API is running in non browser environments') return } - return global.__mpxEnterOptions || {} + return global.__mpxLaunchOptions || {} } export { diff --git a/packages/api-proxy/src/platform/api/system/rnSystem.js b/packages/api-proxy/src/platform/api/system/rnSystem.js index acf3965ed9..abc0501466 100644 --- a/packages/api-proxy/src/platform/api/system/rnSystem.js +++ b/packages/api-proxy/src/platform/api/system/rnSystem.js @@ -41,18 +41,11 @@ const getWindowInfo = function () { } const getLaunchOptionsSync = function () { - const options = global.__mpxEnterOptions || {} - const { path, scene, query } = options - return { - path, - scene, - query - } + return global.__mpxLaunchOptions || {} } const getEnterOptionsSync = function () { - const result = getLaunchOptionsSync() - return result + return global.__mpxEnterOptions || {} } export { diff --git a/packages/core/package.json b/packages/core/package.json index 5e0a073d0d..4c7dea508a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@mpxjs/core", - "version": "2.9.69", + "version": "2.9.70", "description": "mpx runtime core", "keywords": [ "miniprogram", @@ -19,7 +19,7 @@ ], "main": "src/index.js", "dependencies": { - "@mpxjs/utils": "^2.9.69", + "@mpxjs/utils": "^2.9.70", "lodash": "^4.1.1", "miniprogram-api-typings": "^3.10.0" }, @@ -30,9 +30,9 @@ "@mpxjs/store": "^2.9.0", "@react-navigation/native": "^7.0.3", "@react-navigation/stack": "^7.0.4", + "promise": "^8.3.0", "react": "*", "react-native": "*", - "promise": "^8.3.0", "react-native-gesture-handler": "^2.19.0", "react-native-linear-gradient": "^2.8.3", "react-native-safe-area-context": "^4.14.0", @@ -110,4 +110,4 @@ }, "sideEffects": false, "gitHead": "2d37697869b9bdda3efab92dda8c910b68fd05c0" -} \ No newline at end of file +} diff --git a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js index 28fbe08a4a..3566007f75 100644 --- a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js +++ b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js @@ -181,7 +181,7 @@ export default function styleHelperMixin () { } else if (appClassMap[className]) { // todo 全局样式在每个页面和组件中生效,以支持全局原子类,后续支持样式模块复用后可考虑移除 Object.assign(result, appClassMap[className]) - } else if (this.__props[className] && isObject(this.__props[className])) { + } else if (isObject(this.__props[className])) { // externalClasses必定以对象形式传递下来 Object.assign(result, this.__props[className]) } diff --git a/packages/core/src/platform/createApp.ios.js b/packages/core/src/platform/createApp.ios.js index 226894d9fc..64123508ec 100644 --- a/packages/core/src/platform/createApp.ios.js +++ b/packages/core/src/platform/createApp.ios.js @@ -1,13 +1,12 @@ import transferOptions from '../core/transferOptions' import builtInKeysMap from './patch/builtInKeysMap' -import { makeMap, spreadProp, getFocusedNavigation, hasOwn, extend } from '@mpxjs/utils' +import { makeMap, spreadProp, getFocusedNavigation, hasOwn } from '@mpxjs/utils' import { mergeLifecycle } from '../convertor/mergeLifecycle' import { LIFECYCLE } from '../platform/patch/lifecycle/index' import Mpx from '../index' import { createElement, memo, useRef, useEffect } from 'react' import * as ReactNative from 'react-native' import { Image } from 'react-native' -import { ref } from '../observer/ref' const appHooksMap = makeMap(mergeLifecycle(LIFECYCLE).app) @@ -30,22 +29,24 @@ function filterOptions (options, appData) { return newOptions } -function createAppInstance (appData) { - return extend({}, Mpx.prototype, appData) -} - -export default function createApp (option, config = {}) { +export default function createApp (options) { const appData = {} const { NavigationContainer, createStackNavigator, SafeAreaProvider } = global.__navigationHelper // app选项目前不需要进行转换 - const { rawOptions, currentInject } = transferOptions(option, 'app', false) + const { rawOptions, currentInject } = transferOptions(options, 'app', false) const defaultOptions = filterOptions(spreadProp(rawOptions, 'methods'), appData) - defaultOptions.onAppInit && defaultOptions.onAppInit() // 在页面script执行前填充getApp() global.getApp = function () { return appData } + + defaultOptions.onShow && global.__mpxAppCbs.show.push(defaultOptions.onShow.bind(appData)) + defaultOptions.onHide && global.__mpxAppCbs.hide.push(defaultOptions.onHide.bind(appData)) + defaultOptions.onError && global.__mpxAppCbs.error.push(defaultOptions.onError.bind(appData)) + defaultOptions.onUnhandledRejection && global.__mpxAppCbs.rejection.push(defaultOptions.onUnhandledRejection.bind(appData)) + defaultOptions.onAppInit && defaultOptions.onAppInit() + const pages = currentInject.getPages() || {} const firstPage = currentInject.firstPage const Stack = createStackNavigator() @@ -82,55 +83,43 @@ export default function createApp (option, config = {}) { } global.__mpxAppLaunched = false - - global.__mpxAppFocusedState = ref('show') + global.__mpxAppHotLaunched = false global.__mpxOptionsMap[currentInject.moduleId] = memo((props) => { - const instanceRef = useRef(null) - if (!instanceRef.current) { - instanceRef.current = createAppInstance(appData) - } - const instance = instanceRef.current const initialRouteRef = useRef({ initialRouteName: firstPage, initialParams: {} }) - if (!global.__mpxAppLaunched) { + if (!global.__mpxAppHotLaunched) { const { initialRouteName, initialParams } = Mpx.config.rnConfig.parseAppProps?.(props) || {} initialRouteRef.current.initialRouteName = initialRouteName || initialRouteRef.current.initialRouteName initialRouteRef.current.initialParams = initialParams || initialRouteRef.current.initialParams global.__mpxAppOnLaunch = (navigation) => { - global.__mpxAppLaunched = true const state = navigation.getState() Mpx.config.rnConfig.onStateChange?.(state) const current = state.routes[state.index] - global.__mpxEnterOptions = { + const options = { path: current.name, query: current.params, scene: 0, shareTicket: '', referrerInfo: {} } - defaultOptions.onLaunch && defaultOptions.onLaunch.call(instance, global.__mpxEnterOptions) - defaultOptions.onShow && defaultOptions.onShow.call(instance, global.__mpxEnterOptions) + global.__mpxEnterOptions = options + if (!global.__mpxAppLaunched) { + global.__mpxLaunchOptions = options + defaultOptions.onLaunch && defaultOptions.onLaunch.call(appData, options) + } + global.__mpxAppCbs.show.forEach((cb) => { + cb(options) + }) + global.__mpxAppLaunched = true + global.__mpxAppHotLaunched = true } } useEffect(() => { - if (defaultOptions.onShow) { - global.__mpxAppCbs.show.push(defaultOptions.onShow.bind(instance)) - } - if (defaultOptions.onHide) { - global.__mpxAppCbs.hide.push(defaultOptions.onHide.bind(instance)) - } - if (defaultOptions.onError) { - global.__mpxAppCbs.error.push(defaultOptions.onError.bind(instance)) - } - if (defaultOptions.onUnhandledRejection) { - global.__mpxAppCbs.rejection.push(defaultOptions.onUnhandledRejection.bind(instance)) - } - const changeSubscription = ReactNative.AppState.addEventListener('change', (currentState) => { if (currentState === 'active') { let options = global.__mpxEnterOptions @@ -177,13 +166,14 @@ export default function createApp (option, config = {}) { return () => { changeSubscription && changeSubscription.remove() resizeSubScription && resizeSubScription.remove() + // 热启动情况下,app会被销毁重建,将__mpxAppHotLaunched重置保障路由等初始化逻辑正确执行 + global.__mpxAppHotLaunched = false } }, []) const { initialRouteName, initialParams } = initialRouteRef.current const headerBackImageProps = Mpx.config.rnConfig.headerBackImageProps || null const navScreenOpts = { - gestureEnabled: true, // 7.x替换headerBackTitleVisible // headerBackButtonDisplayMode: 'minimal', headerBackTitleVisible: false, diff --git a/packages/core/src/platform/createApp.js b/packages/core/src/platform/createApp.js index 0f35cf97e5..f3e847cb61 100644 --- a/packages/core/src/platform/createApp.js +++ b/packages/core/src/platform/createApp.js @@ -24,19 +24,23 @@ function filterOptions (options, appData) { return newOptions } -export default function createApp (option, config = {}) { - // 在App中挂载mpx对象供周边工具访问,如e2e测试 +export default function createApp (options, config = {}) { + const appData = {} + // app选项目前不需要进行转换 + const { rawOptions, currentInject } = transferOptions(options, 'app', false) const builtInMixins = [{ + // 在App中挂载mpx对象供周边工具访问,如e2e测试 getMpx () { return Mpx } }] - const appData = {} if (__mpx_mode__ === 'web') { builtInMixins.push({ - created () { - Object.assign(this, Mpx.prototype) + beforeCreate () { + // for vue provide vm access Object.assign(this, appData) + }, + created () { const current = this.$root.$options?.router?.currentRoute || {} const options = { path: current.path && current.path.replace(/^\//, ''), @@ -45,48 +49,41 @@ export default function createApp (option, config = {}) { shareTicket: '', referrerInfo: {} } + // web不分冷启动和热启动 global.__mpxEnterOptions = options - this.$options.onLaunch && this.$options.onLaunch.call(this, options) - if (isBrowser) { - if (this.$options.onShow) { - this.$options.onShow.call(this, options) - global.__mpxAppCbs.show.push(this.$options.onShow.bind(this)) - } - if (this.$options.onHide) { - global.__mpxAppCbs.hide.push(this.$options.onHide.bind(this)) - } - if (this.$options.onError) { - global.__mpxAppCbs.error.push(this.$options.onError.bind(this)) - } - if (this.$options.onUnhandledRejection) { - global.__mpxAppCbs.rejection.push(this.$options.onUnhandledRejection.bind(this)) - } - } + global.__mpxLaunchOptions = options + rawOptions.onLaunch && rawOptions.onLaunch.call(appData, options) + global.__mpxAppCbs.show.forEach((cb) => { + cb(options) + }) } }) } else { builtInMixins.push({ onLaunch () { - Object.assign(this, Mpx.prototype) + initAppProvides(rawOptions.provide, this) } }) } - // app选项目前不需要进行转换 - const { rawOptions, currentInject } = transferOptions(option, 'app', false) rawOptions.mixins = builtInMixins const defaultOptions = filterOptions(spreadProp(mergeOptions(rawOptions, 'app', false), 'methods'), appData) if (__mpx_mode__ === 'web') { - global.__mpxOptionsMap = global.__mpxOptionsMap || {} - global.__mpxOptionsMap[currentInject.moduleId] = defaultOptions global.getApp = function () { if (!isBrowser) { console.error('[Mpx runtime error]: Dangerous API! global.getApp method is running in non browser environments') } return appData } + if (isBrowser) { + defaultOptions.onShow && global.__mpxAppCbs.show.push(defaultOptions.onShow.bind(appData)) + defaultOptions.onHide && global.__mpxAppCbs.hide.push(defaultOptions.onHide.bind(appData)) + defaultOptions.onError && global.__mpxAppCbs.error.push(defaultOptions.onError.bind(appData)) + defaultOptions.onUnhandledRejection && global.__mpxAppCbs.rejection.push(defaultOptions.onUnhandledRejection.bind(appData)) + } + global.__mpxOptionsMap = global.__mpxOptionsMap || {} + global.__mpxOptionsMap[currentInject.moduleId] = defaultOptions } else { - initAppProvides(rawOptions) defaultOptions.onAppInit && defaultOptions.onAppInit() const ctor = config.customCtor || global.currentCtor || App ctor(defaultOptions) diff --git a/packages/core/src/platform/export/inject.js b/packages/core/src/platform/export/inject.js index 4e2e704361..1f260a8850 100644 --- a/packages/core/src/platform/export/inject.js +++ b/packages/core/src/platform/export/inject.js @@ -5,11 +5,10 @@ import { currentInstance } from '../../core/proxy' let appProvides = Object.create(null) /** @internal createApp() 初始化应用层 scope provide */ -export function initAppProvides (appOptions) { - const provideOpt = appOptions.provide +export function initAppProvides (provideOpt, instance) { if (provideOpt) { const provided = isFunction(provideOpt) - ? callWithErrorHandling(provideOpt.bind(appOptions), appOptions, 'createApp provide function') + ? callWithErrorHandling(provideOpt.bind(instance), instance, 'createApp provide function') : provideOpt if (isObject(provided)) { appProvides = provided diff --git a/packages/core/src/platform/patch/getDefaultOptions.ios.js b/packages/core/src/platform/patch/getDefaultOptions.ios.js index 1f44e346f6..0189823b53 100644 --- a/packages/core/src/platform/patch/getDefaultOptions.ios.js +++ b/packages/core/src/platform/patch/getDefaultOptions.ios.js @@ -3,7 +3,7 @@ import * as ReactNative from 'react-native' import { ReactiveEffect } from '../../observer/effect' import { watch } from '../../observer/watch' import { reactive, set, del } from '../../observer/reactive' -import { hasOwn, isFunction, noop, isObject, isArray, getByPath, collectDataset, hump2dash, callWithErrorHandling, wrapMethodsWithErrorHandling } from '@mpxjs/utils' +import { hasOwn, isFunction, noop, isObject, isArray, getByPath, collectDataset, hump2dash, dash2hump, callWithErrorHandling, wrapMethodsWithErrorHandling } from '@mpxjs/utils' import MpxProxy from '../../core/proxy' import { BEFOREUPDATE, ONLOAD, UPDATED, ONSHOW, ONHIDE, ONRESIZE, REACTHOOKSEXEC } from '../../core/innerLifecycle' import mergeOptions from '../../core/mergeOptions' @@ -58,14 +58,12 @@ function createEffect (proxy, components) { proxy.toggleRecurse(true) } -function getRootProps (props) { +function getRootProps (props, validProps) { const rootProps = {} for (const key in props) { - if (hasOwn(props, key)) { - const match = /^(bind|catch|capture-bind|capture-catch|style|enable-var):?(.*?)(?:\.(.*))?$/.exec(key) - if (match) { - rootProps[key] = props[key] - } + const altKey = dash2hump(key) + if (!hasOwn(validProps, key) && !hasOwn(validProps, altKey) && key !== 'children') { + rootProps[key] = props[key] } } return rootProps @@ -367,14 +365,10 @@ function usePageStatus (navigation, pageId) { const blurSubscription = navigation.addListener('blur', () => { pageStatusMap[pageId] = 'hide' }) - const unWatchAppFocusedState = watch(global.__mpxAppFocusedState, (value) => { - pageStatusMap[pageId] = value - }) return () => { focusSubscription() blurSubscription() - unWatchAppFocusedState() del(pageStatusMap, pageId) } }, [navigation]) @@ -444,7 +438,7 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) { useEffect(() => { if (type === 'page') { - if (!global.__mpxAppLaunched && global.__mpxAppOnLaunch) { + if (!global.__mpxAppHotLaunched && global.__mpxAppOnLaunch) { global.__mpxAppOnLaunch(props.navigation) } proxy.callHook(ONLOAD, [props.route.params || {}]) @@ -474,7 +468,8 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) { const root = useMemo(() => proxy.effect.run(), [finalMemoVersion]) if (root && root.props.ishost) { - const rootProps = getRootProps(props) + // 对于组件未注册的属性继承到host节点上,如事件、样式和其他属性等 + const rootProps = getRootProps(props, validProps) rootProps.style = Object.assign({}, root.props.style, rootProps.style) // update root props return cloneElement(root, rootProps) diff --git a/packages/fetch/package.json b/packages/fetch/package.json index 9aeaeaf9c7..9cccf50069 100644 --- a/packages/fetch/package.json +++ b/packages/fetch/package.json @@ -1,6 +1,6 @@ { "name": "@mpxjs/fetch", - "version": "2.9.69", + "version": "2.9.70", "description": "mpx fetch lib", "author": "donghongping", "license": "Apache-2.0", @@ -29,8 +29,8 @@ "test": "echo \"Error: run tests from root\" && exit 1" }, "dependencies": { - "@mpxjs/api-proxy": "^2.9.69", - "@mpxjs/utils": "^2.9.69", + "@mpxjs/api-proxy": "^2.9.70", + "@mpxjs/utils": "^2.9.70", "path-to-regexp": "^6.2.0" }, "gitHead": "2d37697869b9bdda3efab92dda8c910b68fd05c0" diff --git a/packages/pinia/package.json b/packages/pinia/package.json index af0f442b15..08918eb462 100644 --- a/packages/pinia/package.json +++ b/packages/pinia/package.json @@ -1,6 +1,6 @@ { "name": "@mpxjs/pinia", - "version": "2.9.69", + "version": "2.9.70", "description": "A pinia style store in miniprogram", "types": "@types/index.d.ts", "main": "src/index.js", @@ -31,7 +31,7 @@ "access": "public" }, "dependencies": { - "@mpxjs/utils": "^2.9.69" + "@mpxjs/utils": "^2.9.70" }, "peerDependencies": { "@mpxjs/core": "^2.9.0", diff --git a/packages/store/package.json b/packages/store/package.json index 47570130a4..d25bad9199 100644 --- a/packages/store/package.json +++ b/packages/store/package.json @@ -1,6 +1,6 @@ { "name": "@mpxjs/store", - "version": "2.9.69", + "version": "2.9.70", "description": "A store for mpx framework", "main": "src/index.js", "types": "@types/index.d.ts", @@ -26,7 +26,7 @@ }, "homepage": "https://github.com/didi/mpx#readme", "dependencies": { - "@mpxjs/utils": "^2.9.69" + "@mpxjs/utils": "^2.9.70" }, "peerDependencies": { "@mpxjs/core": "^2.9.0" diff --git a/packages/utils/package.json b/packages/utils/package.json index 60fc5904bd..2a526fa8b8 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@mpxjs/utils", - "version": "2.9.69", + "version": "2.9.70", "description": "A toolkit for mpx framework", "main": "src/index.js", "scripts": { diff --git a/packages/webpack-plugin/lib/parser.js b/packages/webpack-plugin/lib/parser.js index 6c7598cf9b..97db82f730 100644 --- a/packages/webpack-plugin/lib/parser.js +++ b/packages/webpack-plugin/lib/parser.js @@ -14,7 +14,7 @@ module.exports = (content, { filePath, needMap, mode, env }) => { output = compiler.parseComponent(content, { mode, filePath, - pad: 'line', + // pad: 'line', // stylus编译遇到大量空行时会出现栈溢出,故注释掉 env }) if (needMap) { diff --git a/packages/webpack-plugin/lib/platform/style/wx/index.js b/packages/webpack-plugin/lib/platform/style/wx/index.js index 8a8cc97e3d..4a8c5768ac 100644 --- a/packages/webpack-plugin/lib/platform/style/wx/index.js +++ b/packages/webpack-plugin/lib/platform/style/wx/index.js @@ -374,11 +374,11 @@ module.exports = function getSpec ({ warn, error }) { // transform 转换 const formatTransform = ({ prop, value, selector }, { mode }) => { // css var & 数组直接返回 - if (Array.isArray(value) || calcExp.test(value)) return { prop, value } + if (Array.isArray(value) || cssVariableExp.test(value)) return { prop, value } const values = parseValues(value) const transform = [] values.forEach(item => { - const match = item.match(/([/\w]+)\(([^)]+)\)/) + const match = item.match(/([/\w]+)\((.+)\)/) if (match && match.length >= 3) { let key = match[1] const val = match[2] @@ -407,23 +407,23 @@ module.exports = function getSpec ({ warn, error }) { case 'rotate3d': // x y z angle case 'translate3d': // x y 支持 z不支持 case 'scale3d': // x y 支持 z不支持 - { - // 2 个以上的值处理 - key = key.replace('3d', '') - const vals = parseValues(val, ',').splice(0, key === 'rotate' ? 4 : 3) - // scale(.5) === scaleX(.5) scaleY(.5) - if (vals.length === 1 && key === 'scale') { - vals.push(vals[0]) - } - const xyz = ['X', 'Y', 'Z'] - transform.push(...vals.map((v, index) => { - if (key !== 'rotate' && index > 1) { - unsupportedPropError({ prop: `${key}Z`, value, selector }, { mode }) - } - return { [`${key}${xyz[index] || ''}`]: v.trim() } - })) - break + { + // 2 个以上的值处理 + key = key.replace('3d', '') + const vals = parseValues(val, ',').splice(0, key === 'rotate' ? 4 : 3) + // scale(.5) === scaleX(.5) scaleY(.5) + if (vals.length === 1 && key === 'scale') { + vals.push(vals[0]) } + const xyz = ['X', 'Y', 'Z'] + transform.push(...vals.map((v, index) => { + if (key !== 'rotate' && index > 1) { + unsupportedPropError({ prop: `${key}Z`, value, selector }, { mode }) + } + return { [`${key}${xyz[index] || ''}`]: v.trim() } + })) + break + } case 'translateZ': case 'scaleZ': default: diff --git a/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts b/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts index 10f0ae1301..6d00a5ceba 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts +++ b/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts @@ -1,5 +1,6 @@ import { useRef, useMemo, RefObject } from 'react' import { hasOwn, collectDataset } from '@mpxjs/utils' +import { useNavigation } from '@react-navigation/native' import { omit, extendObject } from './utils' import eventConfigMap from './event.config' import { @@ -10,15 +11,22 @@ import { InnerRef, SetTimeoutReturnType, LayoutRef, - NativeTouchEvent + NativeTouchEvent, + Navigation } from './types/getInnerListeners' +const globalEventState = { + needPress: true +} + const getTouchEvent = ( type: string, event: NativeTouchEvent, props: Props, - config: UseInnerPropsConfig + config: UseInnerPropsConfig, + navigation: Navigation ) => { + const { y: navigationY = 0 } = navigation?.layout || {} const nativeEvent = event.nativeEvent const { timestamp, pageX, pageY, touches, changedTouches } = nativeEvent const { id } = props @@ -49,24 +57,24 @@ const getTouchEvent = ( target, detail: { x: pageX, - y: pageY + y: pageY - navigationY }, touches: touches.map((item) => { return { identifier: item.identifier, pageX: item.pageX, - pageY: item.pageY, - clientX: item.locationX, - clientY: item.locationY + pageY: item.pageY - navigationY, + clientX: item.pageX, + clientY: item.pageY - navigationY } }), changedTouches: changedTouches.map((item) => { return { identifier: item.identifier, pageX: item.pageX, - pageY: item.pageY, - clientX: item.locationX, - clientY: item.locationY + pageY: item.pageY - navigationY, + clientX: item.pageX, + clientY: item.pageY - navigationY } }), persist: event.persist, @@ -105,7 +113,8 @@ function handleEmitEvent ( type: string, oe: NativeTouchEvent, propsRef: Record, - config: UseInnerPropsConfig + config: UseInnerPropsConfig, + navigation: Navigation ) { events.forEach((event) => { if (propsRef.current[event]) { @@ -114,7 +123,7 @@ function handleEmitEvent ( oe.stopPropagation() } propsRef.current[event]( - getTouchEvent(type, oe, propsRef.current, config) + getTouchEvent(type, oe, propsRef.current, config, navigation) ) } }) @@ -126,17 +135,17 @@ function checkIsNeedPress (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: const currentPageX = nativeEvent.changedTouches[0].pageX const currentPageY = nativeEvent.changedTouches[0].pageY if ( - Math.abs(currentPageX - tapDetailInfo.x) > 1 || - Math.abs(currentPageY - tapDetailInfo.y) > 1 + Math.abs(currentPageX - tapDetailInfo.x) > 3 || + Math.abs(currentPageY - tapDetailInfo.y) > 3 ) { - ref.current!.needPress[type] = false + globalEventState.needPress = false ref.current!.startTimer[type] && clearTimeout(ref.current!.startTimer[type] as SetTimeoutReturnType) ref.current!.startTimer[type] = null } } -function handleTouchstart (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: RefObject, propsRef: Record, config: UseInnerPropsConfig) { +function handleTouchstart (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: RefObject, propsRef: Record, config: UseInnerPropsConfig, navigation: Navigation) { e.persist() const bubbleTouchEvent = ['catchtouchstart', 'bindtouchstart'] const bubblePressEvent = ['catchlongpress', 'bindlongpress'] @@ -149,7 +158,7 @@ function handleTouchstart (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: 'capture-bindlongpress' ] ref.current!.startTimer[type] = null - ref.current!.needPress[type] = true + globalEventState.needPress = true const nativeEvent = e.nativeEvent ref.current!.mpxPressInfo.detail = { x: nativeEvent.changedTouches[0].pageX, @@ -159,7 +168,7 @@ function handleTouchstart (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: type === 'bubble' ? bubbleTouchEvent : captureTouchEvent const currentPressEvent = type === 'bubble' ? bubblePressEvent : capturePressEvent - handleEmitEvent(currentTouchEvent, 'touchstart', e, propsRef, config) + handleEmitEvent(currentTouchEvent, 'touchstart', e, propsRef, config, navigation) const { catchlongpress, bindlongpress, @@ -173,13 +182,14 @@ function handleTouchstart (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: captureBindlongpress ) { ref.current!.startTimer[type] = setTimeout(() => { - ref.current!.needPress[type] = false - handleEmitEvent(currentPressEvent, 'longpress', e, propsRef, config) + // 只要触发过longpress, 全局就不再触发tap + globalEventState.needPress = false + handleEmitEvent(currentPressEvent, 'longpress', e, propsRef, config, navigation) }, 350) } } -function handleTouchmove (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: RefObject, propsRef: Record, config: UseInnerPropsConfig) { +function handleTouchmove (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: RefObject, propsRef: Record, config: UseInnerPropsConfig, navigation: Navigation) { const bubbleTouchEvent = ['catchtouchmove', 'bindtouchmove'] const captureTouchEvent = [ 'capture-catchtouchmove', @@ -187,11 +197,11 @@ function handleTouchmove (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: ] const currentTouchEvent = type === 'bubble' ? bubbleTouchEvent : captureTouchEvent - handleEmitEvent(currentTouchEvent, 'touchmove', e, propsRef, config) + handleEmitEvent(currentTouchEvent, 'touchmove', e, propsRef, config, navigation) checkIsNeedPress(e, type, ref) } -function handleTouchend (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: RefObject, propsRef: Record, config: UseInnerPropsConfig) { +function handleTouchend (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: RefObject, propsRef: Record, config: UseInnerPropsConfig, navigation: Navigation) { // move event may not be triggered checkIsNeedPress(e, type, ref) const bubbleTouchEvent = ['catchtouchend', 'bindtouchend'] @@ -208,19 +218,22 @@ function handleTouchend (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: R ref.current!.startTimer[type] && clearTimeout(ref.current!.startTimer[type] as SetTimeoutReturnType) ref.current!.startTimer[type] = null - handleEmitEvent(currentTouchEvent, 'touchend', e, propsRef, config) - if (ref.current!.needPress[type]) { + handleEmitEvent(currentTouchEvent, 'touchend', e, propsRef, config, navigation) + if (globalEventState.needPress) { if (type === 'bubble' && config.disableTap) { return } - handleEmitEvent(currentTapEvent, 'tap', e, propsRef, config) + handleEmitEvent(currentTapEvent, 'tap', e, propsRef, config, navigation) } } function handleTouchcancel ( e: NativeTouchEvent, type: 'bubble' | 'capture', - ref: RefObject, propsRef: Record, config: UseInnerPropsConfig + ref: RefObject, + propsRef: Record, + config: UseInnerPropsConfig, + navigation: Navigation ) { const bubbleTouchEvent = ['catchtouchcancel', 'bindtouchcancel'] const captureTouchEvent = [ @@ -232,11 +245,11 @@ function handleTouchcancel ( ref.current!.startTimer[type] && clearTimeout(ref.current!.startTimer[type] as SetTimeoutReturnType) ref.current!.startTimer[type] = null - handleEmitEvent(currentTouchEvent, 'touchcancel', e, propsRef, config) + handleEmitEvent(currentTouchEvent, 'touchcancel', e, propsRef, config, navigation) } function createTouchEventHandler (eventName: 'onTouchStart'|'onTouchMove'|'onTouchEnd'|'onTouchCancel', type: 'bubble' | 'capture') { - return (e: NativeTouchEvent, ref: RefObject, propsRef: Record, config: UseInnerPropsConfig) => { + return (e: NativeTouchEvent, ref: RefObject, propsRef: Record, config: UseInnerPropsConfig, navigation: Navigation) => { const handlerMap = { onTouchStart: handleTouchstart, onTouchMove: handleTouchmove, @@ -246,7 +259,7 @@ function createTouchEventHandler (eventName: 'onTouchStart'|'onTouchMove'|'onTou const handler = handlerMap[eventName] if (handler) { - handler(e, type, ref, propsRef, config) + handler(e, type, ref, propsRef, config, navigation) } } } @@ -273,10 +286,6 @@ const useInnerProps = ( bubble: null, capture: null }, - needPress: { - bubble: false, - capture: false - }, mpxPressInfo: { detail: { x: 0, @@ -291,6 +300,8 @@ const useInnerProps = ( layoutRef: { current: {} }, disableTap: false } + const navigation = useNavigation() + const removeProps = [ 'children', 'enable-background', @@ -332,7 +343,7 @@ const useInnerProps = ( touchEventList.forEach((item) => { if (finalEventKeys.includes(item.eventName)) { events[item.eventName] = (e: NativeTouchEvent) => - item.handler(e, ref, propsRef, config) + item.handler(e, ref, propsRef, config, navigation) } }) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-canvas/html.ts b/packages/webpack-plugin/lib/runtime/components/react/mpx-canvas/html.ts index c8a3a8140b..e8d6bdbdfa 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-canvas/html.ts +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-canvas/html.ts @@ -316,8 +316,7 @@ var handleError = function (err, message) { document.removeEventListener('message', handleIncomingMessage); }; -function handleIncomingMessage(e) { - var data = JSON.parse(e.data); +function handleIncomingMessage(data) { if (Array.isArray(data)) { for (var i = 0; i < data.length; i++) { try { @@ -335,8 +334,7 @@ function handleIncomingMessage(e) { } } -window.addEventListener('message', handleIncomingMessage); -document.addEventListener('message', handleIncomingMessage); +window.mpxWebviewMessageCallback = handleIncomingMessage diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-canvas/index.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-canvas/index.tsx index b5b31cc3d1..3c56abde39 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-canvas/index.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-canvas/index.tsx @@ -106,7 +106,11 @@ const _Canvas = forwardRef, CanvasPr useEffect(() => { const webviewPostMessage = (message: WebviewMessage) => { if (canvasRef.current.webview) { - canvasRef.current.webview.postMessage(JSON.stringify(message)) + const jsCode = ` + window.mpxWebviewMessageCallback(${JSON.stringify(message)}); + true; + ` + canvasRef.current.webview.injectJavaScript(jsCode) } } diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-movable-view.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-movable-view.tsx index 3a0a330251..b6410e9034 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-movable-view.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-movable-view.tsx @@ -19,10 +19,10 @@ */ import { useEffect, forwardRef, ReactNode, useContext, useCallback, useRef, useMemo, createElement } from 'react' import { StyleSheet, NativeSyntheticEvent, View, LayoutChangeEvent } from 'react-native' -import { getCustomEvent } from './getInnerListeners' +import useInnerProps, { getCustomEvent } from './getInnerListeners' import useNodesRef, { HandlerRef } from './useNodesRef' import { MovableAreaContext } from './context' -import { useTransformStyle, splitProps, splitStyle, HIDDEN_STYLE, wrapChildren, GestureHandler, flatGesture, extendObject } from './utils' +import { useTransformStyle, splitProps, splitStyle, HIDDEN_STYLE, wrapChildren, GestureHandler, flatGesture, extendObject, omit } from './utils' import { GestureDetector, Gesture, GestureTouchEvent, GestureStateChangeEvent, PanGestureHandlerEventPayload, PanGesture } from 'react-native-gesture-handler' import Animated, { useSharedValue, @@ -33,6 +33,7 @@ import Animated, { useAnimatedReaction, withSpring } from 'react-native-reanimated' +import { collectDataset, noop } from '@mpxjs/utils' interface MovableViewProps { children: ReactNode; @@ -42,17 +43,22 @@ interface MovableViewProps { y?: number; disabled?: boolean; animation?: boolean; + id?: string; bindchange?: (event: unknown) => void; - bindtouchstart?: (event: NativeSyntheticEvent) => void; - catchtouchstart?: (event: NativeSyntheticEvent) => void; - bindtouchmove?: (event: NativeSyntheticEvent) => void; - catchtouchmove?: (event: NativeSyntheticEvent) => void; - catchtouchend?: (event: NativeSyntheticEvent) => void; - bindtouchend?: (event: NativeSyntheticEvent) => void; - bindhtouchmove?: (event: NativeSyntheticEvent) => void; - bindvtouchmove?: (event: NativeSyntheticEvent) => void; - catchhtouchmove?: (event: NativeSyntheticEvent) => void; - catchvtouchmove?: (event: NativeSyntheticEvent) => void; + bindtouchstart?: (event: GestureTouchEvent) => void; + catchtouchstart?: (event: GestureTouchEvent) => void; + bindtouchmove?: (event: GestureTouchEvent) => void; + catchtouchmove?: (event: GestureTouchEvent) => void; + catchtouchend?: (event: GestureTouchEvent) => void; + bindtouchend?: (event: GestureTouchEvent) => void; + bindhtouchmove?: (event: GestureTouchEvent) => void; + bindvtouchmove?: (event: GestureTouchEvent) => void; + catchhtouchmove?: (event: GestureTouchEvent) => void; + catchvtouchmove?: (event: GestureTouchEvent) => void; + bindlongpress?: (event: GestureTouchEvent) => void; + catchlongpress?: (event: GestureTouchEvent) => void; + bindtap?: (event: GestureTouchEvent) => void; + catchtap?: (event: GestureTouchEvent) => void; onLayout?: (event: LayoutChangeEvent) => void; 'out-of-bounds'?: boolean; 'wait-for'?: Array; @@ -153,10 +159,10 @@ const _MovableView = forwardRef, MovableViewP }) const hasSimultaneousHandlersChanged = prevSimultaneousHandlersRef.current.length !== (originSimultaneousHandlers?.length || 0) || - (originSimultaneousHandlers || []).some((handler, index) => handler !== prevSimultaneousHandlersRef.current[index]) + (originSimultaneousHandlers || []).some((handler, index) => handler !== prevSimultaneousHandlersRef.current[index]) const hasWaitForHandlersChanged = prevWaitForHandlersRef.current.length !== (waitFor?.length || 0) || - (waitFor || []).some((handler, index) => handler !== prevWaitForHandlersRef.current[index]) + (waitFor || []).some((handler, index) => handler !== prevWaitForHandlersRef.current[index]) if (hasSimultaneousHandlersChanged || hasWaitForHandlersChanged) { gestureSwitch.current = !gestureSwitch.current @@ -333,8 +339,7 @@ const _MovableView = forwardRef, MovableViewP props.onLayout && props.onLayout(e) } - const extendEvent = useCallback((e: any) => { - 'worklet' + const extendEvent = useCallback((e: any, obj?: Record) => { const touchArr = [e.changedTouches, e.allTouches] touchArr.forEach(touches => { touches && touches.forEach((item: { absoluteX: number; absoluteY: number; pageX: number; pageY: number }) => { @@ -342,48 +347,69 @@ const _MovableView = forwardRef, MovableViewP item.pageY = item.absoluteY }) }) - e.touches = e.allTouches + Object.assign(e, { + touches: e.allTouches, + detail: { + x: e.changedTouches[0].absoluteX, + y: e.changedTouches[0].absoluteY + }, + currentTarget: { + id: props.id || '', + dataset: collectDataset(props), + offsetLeft: 0, + offsetTop: 0 + } + }, obj) }, []) - const gesture = useMemo(() => { - const handleTriggerStart = (e: any) => { - 'worklet' - extendEvent(e) - bindtouchstart && runOnJS(bindtouchstart)(e) - catchtouchstart && runOnJS(catchtouchstart)(e) - } - - const handleTriggerMove = (e: any) => { - 'worklet' - extendEvent(e) - const hasTouchmove = !!bindhtouchmove || !!bindvtouchmove || !!bindtouchmove - const hasCatchTouchmove = !!catchhtouchmove || !!catchvtouchmove || !!catchtouchmove + const triggerStartOnJS = ({ e }: { e: GestureTouchEvent }) => { + extendEvent(e) + bindtouchstart && bindtouchstart(e) + catchtouchstart && catchtouchstart(e) + } - if (hasTouchmove) { - if (touchEvent.value === 'htouchmove') { - bindhtouchmove && runOnJS(bindhtouchmove)(e) - } else if (touchEvent.value === 'vtouchmove') { - bindvtouchmove && runOnJS(bindvtouchmove)(e) - } - bindtouchmove && runOnJS(bindtouchmove)(e) + const triggerMoveOnJS = ({ e, hasTouchmove, hasCatchTouchmove, touchEvent }: { e: GestureTouchEvent; hasTouchmove: boolean; hasCatchTouchmove: boolean; touchEvent: string }) => { + extendEvent(e) + if (hasTouchmove) { + if (touchEvent === 'htouchmove') { + bindhtouchmove && bindhtouchmove(e) + } else if (touchEvent === 'vtouchmove') { + bindvtouchmove && bindvtouchmove(e) } + bindtouchmove && bindtouchmove(e) + } - if (hasCatchTouchmove) { - if (touchEvent.value === 'htouchmove') { - catchhtouchmove && runOnJS(catchhtouchmove)(e) - } else if (touchEvent.value === 'vtouchmove') { - catchvtouchmove && runOnJS(catchvtouchmove)(e) - } - catchtouchmove && runOnJS(catchtouchmove)(e) + if (hasCatchTouchmove) { + if (touchEvent === 'htouchmove') { + catchhtouchmove && catchhtouchmove(e) + } else if (touchEvent === 'vtouchmove') { + catchvtouchmove && catchvtouchmove(e) } + catchtouchmove && catchtouchmove(e) } + } + + const triggerEndOnJS = ({ e }: { e: GestureTouchEvent }) => { + extendEvent(e) + bindtouchend && bindtouchend(e) + catchtouchend && catchtouchend(e) + } - const handleTriggerEnd = (e: any) => { + const gesture = useMemo(() => { + const handleTriggerMove = (e: GestureTouchEvent) => { 'worklet' - extendEvent(e) - bindtouchend && runOnJS(bindtouchend)(e) - catchtouchend && runOnJS(catchtouchend)(e) + const hasTouchmove = !!bindhtouchmove || !!bindvtouchmove || !!bindtouchmove + const hasCatchTouchmove = !!catchhtouchmove || !!catchvtouchmove || !!catchtouchmove + if (hasTouchmove || hasCatchTouchmove) { + runOnJS(triggerMoveOnJS)({ + e, + touchEvent: touchEvent.value, + hasTouchmove, + hasCatchTouchmove + }) + } } + const gesturePan = Gesture.Pan() .onTouchesDown((e: GestureTouchEvent) => { 'worklet' @@ -393,12 +419,14 @@ const _MovableView = forwardRef, MovableViewP x: changedTouches.x, y: changedTouches.y } - handleTriggerStart(e) + if (bindtouchstart || catchtouchstart) { + runOnJS(triggerStartOnJS)({ e }) + } }) .onTouchesMove((e: GestureTouchEvent) => { 'worklet' - isMoving.value = true const changedTouches = e.changedTouches[0] || { x: 0, y: 0 } + isMoving.value = true if (isFirstTouch.value) { touchEvent.value = Math.abs(changedTouches.x - startPosition.value.x) > Math.abs(changedTouches.y - startPosition.value.y) ? 'htouchmove' : 'vtouchmove' isFirstTouch.value = false @@ -430,7 +458,9 @@ const _MovableView = forwardRef, MovableViewP 'worklet' isFirstTouch.value = true isMoving.value = false - handleTriggerEnd(e) + if (bindtouchend || catchtouchend) { + runOnJS(triggerEndOnJS)({ e }) + } if (disabled) return if (!inertia) { const { x, y } = checkBoundaryPosition({ positionX: offsetX.value, positionY: offsetY.value }) @@ -454,8 +484,8 @@ const _MovableView = forwardRef, MovableViewP }) .onFinalize((e: GestureStateChangeEvent) => { 'worklet' - if (!inertia || disabled || !animation) return isMoving.value = false + if (!inertia || disabled || !animation) return if (direction === 'horizontal' || direction === 'all') { xInertialMotion.value = true offsetX.value = withDecay({ @@ -497,35 +527,50 @@ const _MovableView = forwardRef, MovableViewP } }) - const injectCatchEvent = (props: Record) => { - const eventHandlers: Record = {} - const catchEventList = [ - { name: 'onTouchStart', value: ['catchtouchstart'] }, - { name: 'onTouchMove', value: ['catchtouchmove', 'catchvtouchmove', 'catchhtouchmove'] }, - { name: 'onTouchEnd', value: ['catchtouchend'] } + const rewriteCatchEvent = () => { + const handlers: Record = {} + + const events = [ + { type: 'touchstart' }, + { type: 'touchmove', alias: ['vtouchmove', 'htouchmove'] }, + { type: 'touchend' } ] - catchEventList.forEach(event => { - event.value.forEach(name => { - if (props[name] && !eventHandlers[event.name]) { - eventHandlers[event.name] = (e: NativeSyntheticEvent) => { - e.stopPropagation() - } - } - }) + events.forEach(({ type, alias = [] }) => { + const hasCatchEvent = + props[`catch${type}` as keyof typeof props] || + alias.some(name => props[`catch${name}` as keyof typeof props]) + if (hasCatchEvent) handlers[`catch${type}`] = noop }) - return eventHandlers + + return handlers } - const catchEventHandlers = injectCatchEvent(props) const layoutStyle = !hasLayoutRef.current && hasSelfPercent ? HIDDEN_STYLE : {} + // bind 相关 touch 事件直接由 gesture 触发,无须重复挂载 + // catch 相关 touch 事件需要重写并通过 useInnerProps 注入阻止冒泡逻辑 + const filterProps = omit(props, [ + 'bindtouchstart', + 'bindtouchmove', + 'bindvtouchmove', + 'bindhtouchmove', + 'bindtouchend', + 'catchtouchstart', + 'catchtouchmove', + 'catchvtouchmove', + 'catchhtouchmove', + 'catchtouchend' + ]) + + const innerProps = useInnerProps(filterProps, extendObject({ + ref: nodeRef, + onLayout: onLayout, + style: [innerStyle, animatedStyles, layoutStyle] + }, rewriteCatchEvent())) + return createElement(GestureDetector, { gesture: gesture }, createElement( Animated.View, - extendObject({ - ref: nodeRef, - onLayout: onLayout, - style: [innerStyle, animatedStyles, layoutStyle] - }, catchEventHandlers), + innerProps, wrapChildren( props, { diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx index cdc8c02aef..9939bff0a1 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx @@ -551,7 +551,7 @@ function inheritStyle (innerStyle: ExtendedViewStyle = {}) { : undefined) } -function wrapImage (imageStyle?: ExtendedViewStyle, innerStyle?: Record, enableFastImage?: boolean) { +function useWrapImage (imageStyle?: ExtendedViewStyle, innerStyle?: Record, enableFastImage?: boolean) { // 预处理数据 const preImageInfo: PreImageInfo = preParseImage(imageStyle) // 预解析 @@ -660,7 +660,8 @@ function wrapWithChildren (props: _ViewProps, { hasVarDec, enableBackground, tex }) return [ - enableBackground ? wrapImage(backgroundStyle, innerStyle, enableFastImage) : null, + // eslint-disable-next-line react-hooks/rules-of-hooks + enableBackground ? useWrapImage(backgroundStyle, innerStyle, enableFastImage) : null, children ] } @@ -777,11 +778,14 @@ const _View = forwardRef, _ViewProps>((viewProps, r if (enableAnimationRef.current !== enableAnimation) { error('[Mpx runtime error]: animation use should be stable in the component lifecycle, or you can set [enable-animation] with true.') } + const finalStyle = enableAnimationRef.current - ? [viewStyle, useAnimationHooks({ - animation, - style: viewStyle - })] + ? [viewStyle, + // eslint-disable-next-line react-hooks/rules-of-hooks + useAnimationHooks({ + animation, + style: viewStyle + })] : viewStyle const innerProps = useInnerProps( props, diff --git a/packages/webpack-plugin/lib/runtime/components/react/types/common.ts b/packages/webpack-plugin/lib/runtime/components/react/types/common.d.ts similarity index 100% rename from packages/webpack-plugin/lib/runtime/components/react/types/common.ts rename to packages/webpack-plugin/lib/runtime/components/react/types/common.d.ts diff --git a/packages/webpack-plugin/lib/runtime/components/react/types/getInnerListeners.ts b/packages/webpack-plugin/lib/runtime/components/react/types/getInnerListeners.d.ts similarity index 93% rename from packages/webpack-plugin/lib/runtime/components/react/types/getInnerListeners.ts rename to packages/webpack-plugin/lib/runtime/components/react/types/getInnerListeners.d.ts index 78df219009..317d483d62 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/types/getInnerListeners.ts +++ b/packages/webpack-plugin/lib/runtime/components/react/types/getInnerListeners.d.ts @@ -13,6 +13,8 @@ type RemoveProps = string[]; type NativeTouchEvent = NativeSyntheticEvent +type Navigation = Record + interface NativeEvent { timestamp: number; pageX: number; @@ -36,10 +38,6 @@ interface InnerRef { bubble: null | ReturnType; capture: null | ReturnType; }; - needPress: { - bubble: boolean; - capture: boolean; - }; mpxPressInfo: { detail: { x: number; @@ -65,5 +63,6 @@ export { InnerRef, LayoutRef, SetTimeoutReturnType, - DataSetType + DataSetType, + Navigation } diff --git a/packages/webpack-plugin/lib/runtime/components/react/types/global.d.ts b/packages/webpack-plugin/lib/runtime/components/react/types/global.d.ts index 052fc1ac56..82d5a57476 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/types/global.d.ts +++ b/packages/webpack-plugin/lib/runtime/components/react/types/global.d.ts @@ -20,6 +20,12 @@ declare module '@mpxjs/utils' { left: number right: number }, + layout: { + x: number + y: number + width: number + height: number + }, setOptions: (params: Record) => void } | undefined } @@ -27,3 +33,7 @@ declare module '@mpxjs/utils' { declare let global: { __formatValue (value: string): string | number } & Record + +declare module '@react-navigation/native' { + export function useNavigation (): Record +} diff --git a/packages/webpack-plugin/lib/runtime/components/react/useAnimationHooks.ts b/packages/webpack-plugin/lib/runtime/components/react/useAnimationHooks.ts index 77d1ac0503..71a41f5379 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/useAnimationHooks.ts +++ b/packages/webpack-plugin/lib/runtime/components/react/useAnimationHooks.ts @@ -110,7 +110,7 @@ const parseTransform = (transformStr: string) => { const values = parseValues(transformStr) const transform: {[propName: string]: string|number|number[]}[] = [] values.forEach(item => { - const match = item.match(/([/\w]+)\(([^)]+)\)/) + const match = item.match(/([/\w]+)\((.+)\)/) if (match && match.length >= 3) { let key = match[1] const val = match[2] @@ -240,14 +240,13 @@ export default function useAnimationHooks (props: _ViewProps) { } // 添加每个key的多次step动画 animatedKeys.forEach(key => { + const ruleV = isTransform(key) ? transform.get(key) : rules.get(key) // key不存在,第一轮取shareValMap[key]value,非第一轮取上一轮的 - const toVal = rules.get(key) !== undefined - ? rules.get(key) - : transform.get(key) !== undefined - ? transform.get(key) - : index > 0 - ? lastValueMap[key] - : shareValMap[key].value + const toVal = ruleV !== undefined + ? ruleV + : index > 0 + ? lastValueMap[key] + : shareValMap[key].value const animation = getAnimation({ key, value: toVal! }, { delay, duration, easing }, needSetCallback ? setTransformOrigin : undefined) needSetCallback = false if (!sequence[key]) { diff --git a/packages/webpack-plugin/lib/runtime/components/react/utils.tsx b/packages/webpack-plugin/lib/runtime/components/react/utils.tsx index a00bb771ea..08fac95572 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/utils.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/utils.tsx @@ -1,9 +1,10 @@ import { useEffect, useCallback, useMemo, useRef, ReactNode, ReactElement, isValidElement, useContext, useState, Dispatch, SetStateAction, Children, cloneElement } from 'react' import { LayoutChangeEvent, TextStyle, ImageProps, Image } from 'react-native' -import { isObject, isFunction, isNumber, hasOwn, diffAndCloneA, error, warn, getFocusedNavigation } from '@mpxjs/utils' +import { isObject, isFunction, isNumber, hasOwn, diffAndCloneA, error, warn } from '@mpxjs/utils' import { VarContext } from './context' import { ExpressionParser, parseFunc, ReplaceSource } from './parser' import { initialWindowMetrics } from 'react-native-safe-area-context' +import { useNavigation } from '@react-navigation/native' import FastImage, { FastImageProps } from '@d11/react-native-fast-image' import type { AnyFunc, ExtendedFunctionComponent } from './types/common' @@ -18,8 +19,10 @@ export const HIDDEN_STYLE = { opacity: 0 } -const varDecRegExp = /^--.*/ +const varDecRegExp = /^--/ const varUseRegExp = /var\(/ +const unoVarDecRegExp = /^--un-/ +const unoVarUseRegExp = /var\(--un-/ const calcUseRegExp = /calc\(/ const envUseRegExp = /env\(/ @@ -30,14 +33,13 @@ const safeAreaInsetMap: Record = { 'safe-area-inset-left': 'left' } -function getSafeAreaInset (name: string) { - const navigation = getFocusedNavigation() +function getSafeAreaInset (name: string, navigation: Record) { const insets = extendObject({}, initialWindowMetrics?.insets, navigation?.insets) return insets[safeAreaInsetMap[name]] } export function omit (obj: T, fields: K[]): Omit { - const shallowCopy: any = Object.assign({}, obj) + const shallowCopy: any = extendObject({}, obj) for (let i = 0; i < fields.length; i += 1) { const key = fields[i] delete shallowCopy[key] @@ -77,7 +79,7 @@ export const parseInlineStyle = (inlineStyle = ''): Record => { const [k, v, ...rest] = style.split(':') if (rest.length || !v || !k) return styleObj const key = k.trim().replace(/-./g, c => c.substring(1).toUpperCase()) - return Object.assign(styleObj, { [key]: global.__formatValue(v.trim()) }) + return extendObject(styleObj, { [key]: global.__formatValue(v.trim()) }) }, {}) } @@ -230,7 +232,7 @@ function transformVar (styleObj: Record, varKeyPaths: Array, envKeyPaths: Array>) { +function transformEnv (styleObj: Record, envKeyPaths: Array>, navigation: Record) { envKeyPaths.forEach((envKeyPath) => { setStyle(styleObj, envKeyPath, ({ target, key, value }) => { const parsed = parseFunc(value, 'env') @@ -238,7 +240,7 @@ function transformEnv (styleObj: Record, envKeyPaths: Array { const name = args[0] const fallback = args[1] || '' - const value = '' + (getSafeAreaInset(name) ?? global.__formatValue(fallback)) + const value = '' + (getSafeAreaInset(name, navigation) ?? global.__formatValue(fallback)) replaced.replace(start, end - 1, value) }) target[key] = global.__formatValue(replaced.source()) @@ -286,20 +288,27 @@ interface TransformStyleConfig { export function useTransformStyle (styleObj: Record = {}, { enableVar, externalVarContext, parentFontSize, parentWidth, parentHeight }: TransformStyleConfig) { const varStyle: Record = {} + const unoVarStyle: Record = {} const normalStyle: Record = {} + const normalStyleRef = useRef>({}) + const normalStyleChangedRef = useRef(false) let hasVarDec = false let hasVarUse = false let hasSelfPercent = false const varKeyPaths: Array> = [] + const unoVarKeyPaths: Array> = [] const percentKeyPaths: Array> = [] const calcKeyPaths: Array> = [] const envKeyPaths: Array> = [] const [width, setWidth] = useState(0) const [height, setHeight] = useState(0) + const navigation = useNavigation() function varVisitor ({ key, value, keyPath }: VisitorArg) { if (keyPath.length === 1) { - if (varDecRegExp.test(key)) { + if (unoVarDecRegExp.test(key)) { + unoVarStyle[key] = value + } else if (varDecRegExp.test(key)) { hasVarDec = true varStyle[key] = value } else { @@ -308,25 +317,32 @@ export function useTransformStyle (styleObj: Record = {}, { enableV } } // 对于var定义中使用的var无需替换值,可以通过resolveVar递归解析出值 - if (!varDecRegExp.test(key) && varUseRegExp.test(value)) { - hasVarUse = true - varKeyPaths.push(keyPath.slice()) + if (!varDecRegExp.test(key)) { + // 一般情况下一个样式属性中不会混用unocss var和普通css var,可分开进行互斥处理 + if (unoVarUseRegExp.test(value)) { + unoVarKeyPaths.push(keyPath.slice()) + } else if (varUseRegExp.test(value)) { + hasVarUse = true + varKeyPaths.push(keyPath.slice()) + } } } - // traverse var + // traverse var & generate normalStyle traverseStyle(styleObj, [varVisitor]) + hasVarDec = hasVarDec || !!externalVarContext enableVar = enableVar || hasVarDec || hasVarUse const enableVarRef = useRef(enableVar) if (enableVarRef.current !== enableVar) { error('css variable use/declare should be stable in the component lifecycle, or you can set [enable-var] with true.') } - // apply var + // apply css var const varContextRef = useRef({}) if (enableVarRef.current) { + // eslint-disable-next-line react-hooks/rules-of-hooks const varContext = useContext(VarContext) - const newVarContext = Object.assign({}, varContext, externalVarContext, varStyle) + const newVarContext = extendObject({}, varContext, externalVarContext, varStyle) // 缓存比较newVarContext是否发生变化 if (diffAndCloneA(varContextRef.current, newVarContext).diff) { varContextRef.current = newVarContext @@ -334,70 +350,86 @@ export function useTransformStyle (styleObj: Record = {}, { enableV transformVar(normalStyle, varKeyPaths, varContextRef.current) } - function envVisitor ({ value, keyPath }: VisitorArg) { - if (envUseRegExp.test(value)) { - envKeyPaths.push(keyPath.slice()) - } + // apply unocss var + if (unoVarKeyPaths.length) { + transformVar(normalStyle, unoVarKeyPaths, unoVarStyle) } - function calcVisitor ({ value, keyPath }: VisitorArg) { - if (calcUseRegExp.test(value)) { - calcKeyPaths.push(keyPath.slice()) - } + const { clone, diff } = diffAndCloneA(normalStyle, normalStyleRef.current) + if (diff) { + normalStyleRef.current = clone + normalStyleChangedRef.current = !normalStyleChangedRef.current } - function percentVisitor ({ key, value, keyPath }: VisitorArg) { - if (hasOwn(selfPercentRule, key) && PERCENT_REGEX.test(value)) { - hasSelfPercent = true - percentKeyPaths.push(keyPath.slice()) - } else if ((key === 'fontSize' || key === 'lineHeight') && PERCENT_REGEX.test(value)) { - percentKeyPaths.push(keyPath.slice()) + const memoResult = useMemo(() => { + // transform can be memoized + function envVisitor ({ value, keyPath }: VisitorArg) { + if (envUseRegExp.test(value)) { + envKeyPaths.push(keyPath.slice()) + } } - } - // traverse env & calc & percent - traverseStyle(normalStyle, [envVisitor, percentVisitor, calcVisitor]) + function calcVisitor ({ value, keyPath }: VisitorArg) { + if (calcUseRegExp.test(value)) { + calcKeyPaths.push(keyPath.slice()) + } + } - const percentConfig = { - width, - height, - fontSize: normalStyle.fontSize, - parentWidth, - parentHeight, - parentFontSize - } + function percentVisitor ({ key, value, keyPath }: VisitorArg) { + if (hasOwn(selfPercentRule, key) && PERCENT_REGEX.test(value)) { + hasSelfPercent = true + percentKeyPaths.push(keyPath.slice()) + } else if ((key === 'fontSize' || key === 'lineHeight') && PERCENT_REGEX.test(value)) { + percentKeyPaths.push(keyPath.slice()) + } + } - // apply env - transformEnv(normalStyle, envKeyPaths) - // apply percent - transformPercent(normalStyle, percentKeyPaths, percentConfig) - // apply calc - transformCalc(normalStyle, calcKeyPaths, (value: string, key: string) => { - if (PERCENT_REGEX.test(value)) { - const resolved = resolvePercent(value, key, percentConfig) - return typeof resolved === 'number' ? resolved : 0 - } else { - const formatted = global.__formatValue(value) - if (typeof formatted === 'number') { - return formatted + // traverse env & calc & percent + traverseStyle(normalStyle, [envVisitor, percentVisitor, calcVisitor]) + + const percentConfig = { + width, + height, + fontSize: normalStyle.fontSize, + parentWidth, + parentHeight, + parentFontSize + } + + // apply env + transformEnv(normalStyle, envKeyPaths, navigation) + // apply percent + transformPercent(normalStyle, percentKeyPaths, percentConfig) + // apply calc + transformCalc(normalStyle, calcKeyPaths, (value: string, key: string) => { + if (PERCENT_REGEX.test(value)) { + const resolved = resolvePercent(value, key, percentConfig) + return typeof resolved === 'number' ? resolved : 0 } else { - warn('calc() only support number, px, rpx, % temporarily.') - return 0 + const formatted = global.__formatValue(value) + if (typeof formatted === 'number') { + return formatted + } else { + warn('calc() only support number, px, rpx, % temporarily.') + return 0 + } } + }) + // transform number enum stringify + transformStringify(normalStyle) + + return { + normalStyle, + hasSelfPercent } - }) - // transform number enum stringify - transformStringify(normalStyle) + }, [normalStyleChangedRef.current, width, height, parentWidth, parentHeight, parentFontSize]) - return { - normalStyle, - hasSelfPercent, + return extendObject({ hasVarDec, - enableVarRef, varContextRef, setWidth, setHeight - } + }, memoResult) } export interface VisitorArg { @@ -564,8 +596,8 @@ export const useStableCallback = ( ) } -export const usePrevious = (value: T): T | undefined => { - const ref = useRef(undefined) +export function usePrevious (value: T): T | undefined { + const ref = useRef() const prev = ref.current ref.current = value return prev diff --git a/packages/webpack-plugin/lib/template-compiler/compiler.js b/packages/webpack-plugin/lib/template-compiler/compiler.js index dbe300bf09..2c565454b5 100644 --- a/packages/webpack-plugin/lib/template-compiler/compiler.js +++ b/packages/webpack-plugin/lib/template-compiler/compiler.js @@ -117,6 +117,7 @@ let hasOptionalChaining = false let processingTemplate = false const rulesResultMap = new Map() let usingComponents = [] +let usingComponentsInfo = {} function updateForScopesMap () { forScopesMap = {} @@ -636,6 +637,7 @@ function parse (template, options) { if (typeof options.usingComponentsInfo === 'string') options.usingComponentsInfo = JSON.parse(options.usingComponentsInfo) usingComponents = Object.keys(options.usingComponentsInfo) + usingComponentsInfo = options.usingComponentsInfo const _warn = content => { const currentElementRuleResult = rulesResultMap.get(currentEl) || rulesResultMap.set(currentEl, { @@ -2185,16 +2187,20 @@ function isRealNode (el) { return !virtualNodeTagMap[el.tag] } -function isComponentNode (el, options) { +function isComponentNode (el) { return usingComponents.indexOf(el.tag) !== -1 || el.tag === 'component' } -function isReactComponent (el, options) { - return !isComponentNode(el, options) && isRealNode(el) && !el.isBuiltIn +function getComponentInfo (el) { + return usingComponentsInfo[el.tag] || {} +} + +function isReactComponent (el) { + return !isComponentNode(el) && isRealNode(el) && !el.isBuiltIn } function processExternalClasses (el, options) { - const isComponent = isComponentNode(el, options) + const isComponent = isComponentNode(el) const classLikeAttrNames = isComponent ? ['class'].concat(options.externalClasses) : ['class'] classLikeAttrNames.forEach((classLikeAttrName) => { @@ -2308,8 +2314,7 @@ function postProcessAliComponentRootView (el, options, meta) { { condition: /^style$/, action: 'move' }, { condition: /^slot$/, action: 'move' } ] - const tagName = el.tag - const mid = options.usingComponentsInfo[tagName]?.mid || moduleId + const mid = getComponentInfo(el).mid const processAppendAttrsRules = [ { name: 'class', value: `${MPX_ROOT_VIEW} host-${mid}` } ] @@ -2416,7 +2421,7 @@ function processShow (el, options, root) { show = has ? `{{${parseMustacheWithContext(show).result}&&mpxShow}}` : '{{mpxShow}}' } if (show === undefined) return - if (isComponentNode(el, options)) { + if (isComponentNode(el) && getComponentInfo(el).hasVirtualHost) { if (show === '') { show = '{{false}}' } @@ -2708,14 +2713,14 @@ function closeElement (el, options, meta) { if (!isTemplate) { if (!isNative) { postProcessComponentIs(el, (child) => { - if (!hasVirtualHost && mode === 'ali') { + if (!getComponentInfo(el).hasVirtualHost && mode === 'ali') { postProcessAliComponentRootView(child, options) } else { postProcessIf(child) } }) } - if (isComponentNode(el, options) && !hasVirtualHost && mode === 'ali' && el.tag !== 'component') { + if (isComponentNode(el) && !getComponentInfo(el).hasVirtualHost && mode === 'ali' && el.tag !== 'component') { postProcessAliComponentRootView(el, options, meta) } } diff --git a/packages/webpack-plugin/lib/utils/pre-process-json.js b/packages/webpack-plugin/lib/utils/pre-process-json.js index 0510cccf15..6454a6553b 100644 --- a/packages/webpack-plugin/lib/utils/pre-process-json.js +++ b/packages/webpack-plugin/lib/utils/pre-process-json.js @@ -6,6 +6,7 @@ const addQuery = require('./add-query') const resolve = require('./resolve') const getJSONContent = require('./get-json-content') const getRulesRunner = require('../platform') +const { matchCondition } = require('./match-condition') const async = require('async') module.exports = function ({ @@ -19,8 +20,7 @@ module.exports = function ({ }, callback) { const mpx = loaderContext.getMpx() const context = loaderContext.context - const mode = mpx.mode - const pagesMap = mpx.pagesMap + const { mode, pagesMap, autoVirtualHostRules } = mpx async.waterfall([ (callback) => { getJSONContent(json, null, loaderContext, callback) @@ -78,8 +78,8 @@ module.exports = function ({ componentGenerics = Object.assign({}, ret.componentGenerics) } if (usingComponents) { - const setUsingComponentInfo = (name, moduleId) => { - usingComponentsInfo[name] = { mid: moduleId } + const setUsingComponentInfo = (name, info) => { + usingComponentsInfo[name] = info } async.eachOf(usingComponents, (component, name, callback) => { if (ctorType === 'app') { @@ -96,7 +96,11 @@ module.exports = function ({ if (err) return callback(err) const { rawResourcePath } = parseRequest(resource) const moduleId = mpx.getModuleId(rawResourcePath, ctorType === 'app') - setUsingComponentInfo(name, moduleId) + const hasVirtualHost = matchCondition(rawResourcePath, autoVirtualHostRules) + setUsingComponentInfo(name, { + mid: moduleId, + hasVirtualHost + }) callback() }) }, (err) => { diff --git a/packages/webpack-plugin/package.json b/packages/webpack-plugin/package.json index 84291a684d..f8aa7f321a 100644 --- a/packages/webpack-plugin/package.json +++ b/packages/webpack-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@mpxjs/webpack-plugin", - "version": "2.9.69", + "version": "2.9.70", "description": "mpx compile core", "keywords": [ "mpx" @@ -28,7 +28,7 @@ "@better-scroll/wheel": "^2.5.1", "@better-scroll/zoom": "^2.5.1", "@mpxjs/template-engine": "^2.8.7", - "@mpxjs/utils": "^2.9.69", + "@mpxjs/utils": "^2.9.70", "acorn": "^8.11.3", "acorn-walk": "^7.2.0", "async": "^2.6.0", @@ -83,7 +83,7 @@ "devDependencies": { "@ant-design/react-native": "^5.2.2", "@d11/react-native-fast-image": "^8.6.12", - "@mpxjs/api-proxy": "^2.9.69", + "@mpxjs/api-proxy": "^2.9.70", "@types/babel-traverse": "^6.25.4", "@types/babel-types": "^7.0.4", "@types/react": "^18.2.79", @@ -91,8 +91,8 @@ "react-native-gesture-handler": "^2.18.1", "react-native-linear-gradient": "^2.8.3", "react-native-reanimated": "^3.15.2", - "react-native-svg": "^15.8.0", "react-native-safe-area-context": "^4.12.0", + "react-native-svg": "^15.8.0", "react-native-webview": "^13.12.2", "rimraf": "^6.0.1" }, diff --git a/packages/webview-bridge/package.json b/packages/webview-bridge/package.json index 598255d24a..e966417c8f 100644 --- a/packages/webview-bridge/package.json +++ b/packages/webview-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@mpxjs/webview-bridge", - "version": "2.9.58", + "version": "2.9.70", "description": "a bridge for mpx webview", "author": "skyadmin", "license": "ISC",