diff --git a/packages/webpack-plugin/lib/index.js b/packages/webpack-plugin/lib/index.js index 61e85371c0..60b7607cee 100644 --- a/packages/webpack-plugin/lib/index.js +++ b/packages/webpack-plugin/lib/index.js @@ -794,6 +794,7 @@ class MpxWebpackPlugin { // 若配置disableRequireAsync=true, 则全平台构建不支持异步分包 supportRequireAsync: !this.options.disableRequireAsync && (this.options.mode === 'wx' || this.options.mode === 'ali' || this.options.mode === 'tt' || isWeb(this.options.mode) || (isReact(this.options.mode) && this.options.rnConfig.supportSubpackage)), partialCompileRules: this.options.partialCompileRules, + useExtendComponents: this.options.useExtendComponents, collectDynamicEntryInfo: ({ resource, packageName, filename, entryType, hasAsync }) => { const curInfo = mpx.dynamicEntryInfo[packageName] = mpx.dynamicEntryInfo[packageName] || { hasPage: false, diff --git a/packages/webpack-plugin/lib/json-compiler/index.js b/packages/webpack-plugin/lib/json-compiler/index.js index 0b82ffdafb..e6bbf41bb0 100644 --- a/packages/webpack-plugin/lib/json-compiler/index.js +++ b/packages/webpack-plugin/lib/json-compiler/index.js @@ -13,6 +13,7 @@ const createJSONHelper = require('./helper') const RecordIndependentDependency = require('../dependencies/RecordIndependentDependency') const RecordRuntimeInfoDependency = require('../dependencies/RecordRuntimeInfoDependency') const { MPX_DISABLE_EXTRACTOR_CACHE, RESOLVE_IGNORED_ERR, JSON_JS_EXT } = require('../utils/const') +const { processExtendComponents } = require('../utils/process-extend-components') const resolve = require('../utils/resolve') const resolveTabBarPath = require('../utils/resolve-tab-bar-path') const resolveMpxCustomElementPath = require('../utils/resolve-mpx-custom-element-path') @@ -75,13 +76,17 @@ module.exports = function (content) { const { getRequestString } = createHelpers(this) let currentName - + let hasApp = true if (isApp) { currentName = appInfo.name } else { currentName = componentsMap[resourcePath] || pagesMap[resourcePath] } + if (!appInfo.name) { + hasApp = false + } + const relativePath = useRelativePath ? publicPath + path.dirname(currentName) : '' const copydir = (dir, context, callback) => { @@ -160,6 +165,18 @@ module.exports = function (content) { json.usingComponents = json.usingComponents || {} } + if (mode === 'wx' || mode === 'ali') { + const { useExtendComponents = {} } = mpx + if ((isApp || !hasApp) && useExtendComponents[mode]) { + const extendComponents = processExtendComponents({ + useExtendComponents, + mode, + emitWarning + }) + json.usingComponents = Object.assign({}, extendComponents, json.usingComponents) + } + } + // 快应用补全json配置,必填项 if (mode === 'qa' && isApp) { const defaultConf = { diff --git a/packages/webpack-plugin/lib/react/processJSON.js b/packages/webpack-plugin/lib/react/processJSON.js index de44504a3a..087e8abaac 100644 --- a/packages/webpack-plugin/lib/react/processJSON.js +++ b/packages/webpack-plugin/lib/react/processJSON.js @@ -12,6 +12,7 @@ const { transSubpackage } = require('../utils/trans-async-sub-rules') const createJSONHelper = require('../json-compiler/helper') const getRulesRunner = require('../platform/index') const { RESOLVE_IGNORED_ERR } = require('../utils/const') +const { processExtendComponents } = require('../utils/process-extend-components') const RecordResourceMapDependency = require('../dependencies/RecordResourceMapDependency') const RecordPageConfigsMapDependency = require('../dependencies/RecordPageConfigsMapDependency') @@ -32,11 +33,19 @@ module.exports = function (jsonContent, { mode, srcMode, env, - projectRoot + projectRoot, + useExtendComponents = {}, + appInfo } = mpx const context = loaderContext.context + let hasApp = true + + if (!appInfo.name) { + hasApp = false + } + const emitWarning = (msg) => { loaderContext.emitWarning( new Error('[Mpx json warning][' + loaderContext.resource + ']: ' + msg) @@ -118,6 +127,16 @@ module.exports = function (jsonContent, { if (ctorType !== 'app') { rulesRunnerOptions.mainKey = ctorType } + if (!hasApp || ctorType === 'app') { + if (useExtendComponents[mode]) { + const extendComponents = processExtendComponents({ + useExtendComponents, + mode, + emitWarning + }) + jsonObj.usingComponents = Object.assign({}, extendComponents, jsonObj.usingComponents) + } + } const rulesRunner = getRulesRunner(rulesRunnerOptions) diff --git a/packages/webpack-plugin/lib/react/processScript.js b/packages/webpack-plugin/lib/react/processScript.js index 9b88cb9b3a..ea05b1352f 100644 --- a/packages/webpack-plugin/lib/react/processScript.js +++ b/packages/webpack-plugin/lib/react/processScript.js @@ -68,6 +68,7 @@ import { getComponent, getAsyncSuspense } from ${stringifyRequest(loaderContext, output += buildI18n({ loaderContext }) } output += getRequireScript({ ctorType, script, loaderContext }) + output += `export default global.__mpxOptionsMap[${JSON.stringify(moduleId)}]\n` } diff --git a/packages/webpack-plugin/lib/react/script-helper.js b/packages/webpack-plugin/lib/react/script-helper.js index 31cf1a8a72..715bde2c24 100644 --- a/packages/webpack-plugin/lib/react/script-helper.js +++ b/packages/webpack-plugin/lib/react/script-helper.js @@ -4,7 +4,6 @@ const parseRequest = require('../utils/parse-request') const shallowStringify = require('../utils/shallow-stringify') const normalize = require('../utils/normalize') const addQuery = require('../utils/add-query') - function stringifyRequest (loaderContext, request) { return loaderUtils.stringifyRequest(loaderContext, request) } diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-recycle-view.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-recycle-view.tsx new file mode 100644 index 0000000000..145c51c09e --- /dev/null +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-recycle-view.tsx @@ -0,0 +1,398 @@ +import React, { forwardRef, useRef, useState, useEffect, useMemo, createElement, useImperativeHandle } from 'react' +import { SectionList, FlatList, RefreshControl, NativeSyntheticEvent, NativeScrollEvent } from 'react-native' +import useInnerProps, { getCustomEvent } from './getInnerListeners' +import { extendObject, useLayout, useTransformStyle } from './utils' +interface ListItem { + isSectionHeader?: boolean; + _originalItemIndex?: number; + [key: string]: any; +} + +interface Section { + headerData: ListItem | null; + data: ListItem[]; + hasSectionHeader?: boolean; + _originalItemIndex?: number; +} + +interface ItemHeightType { + value?: number; + getter?: (item: any, index: number) => number; +} + +interface RecycleViewProps { + enhanced?: boolean; + bounces?: boolean; + scrollEventThrottle?: number; + height?: number | string; + width?: number | string; + listData?: ListItem[]; + generichash?: string; + style?: Record; + itemHeight?: ItemHeightType; + sectionHeaderHeight?: ItemHeightType; + listHeaderData?: any; + listHeaderHeight?: ItemHeightType; + useListHeader?: boolean; + 'genericrecycle-item'?: string; + 'genericsection-header'?: string; + 'genericlist-header'?: string; + 'enable-var'?: boolean; + 'external-var-context'?: any; + 'parent-font-size'?: number; + 'parent-width'?: number; + 'parent-height'?: number; + 'enable-sticky'?: boolean; + 'enable-back-to-top'?: boolean; + 'end-reached-threshold'?: number; + 'refresher-enabled'?: boolean; + 'show-scrollbar'?: boolean; + 'refresher-triggered'?: boolean; + bindrefresherrefresh?: (event: any) => void; + bindscrolltolower?: (event: any) => void; + bindscroll?: (event: any) => void; + [key: string]: any; +} + +interface ScrollPositionParams { + index: number; + animated?: boolean; + viewOffset?: number; + viewPosition?: number; +} + +const getGeneric = (generichash: string, generickey: string) => { + if (!generichash || !generickey) return null + const GenericComponent = global.__mpxGenericsMap?.[generichash]?.[generickey]?.() + if (!GenericComponent) return null + + return forwardRef((props: any, ref: any) => { + return createElement(GenericComponent, extendObject({}, { + ref: ref + }, props)) + }) +} + +const getListHeaderComponent = (generichash: string, generickey: string, data: any) => { + if (!generichash || !generickey) return undefined + const ListHeaderComponent = getGeneric(generichash, generickey) + return ListHeaderComponent ? createElement(ListHeaderComponent, { listHeaderData: data }) : null +} + +const getSectionHeaderRenderer = (generichash: string, generickey: string) => { + if (!generichash || !generickey) return undefined + return (sectionData: { section: Section }) => { + if (!sectionData.section.hasSectionHeader) return null + const SectionHeaderComponent = getGeneric(generichash, generickey) + return SectionHeaderComponent ? createElement(SectionHeaderComponent, { itemData: sectionData.section.headerData }) : null + } +} + +const getItemRenderer = (generichash: string, generickey: string) => { + if (!generichash || !generickey) return undefined + return ({ item }: { item: any }) => { + const ItemComponent = getGeneric(generichash, generickey) + return ItemComponent ? createElement(ItemComponent, { itemData: item }) : null + } +} + +const RecycleView = forwardRef((props = {}, ref) => { + const { + enhanced = false, + bounces = true, + scrollEventThrottle = 0, + height, + width, + listData, + generichash, + style = {}, + itemHeight = {}, + sectionHeaderHeight = {}, + listHeaderHeight = {}, + listHeaderData = null, + useListHeader = false, + 'genericrecycle-item': genericrecycleItem, + 'genericsection-header': genericsectionHeader, + 'genericlist-header': genericListHeader, + 'enable-var': enableVar, + 'external-var-context': externalVarContext, + 'parent-font-size': parentFontSize, + 'parent-width': parentWidth, + 'parent-height': parentHeight, + 'enable-sticky': enableSticky = false, + 'enable-back-to-top': enableBackToTop = false, + 'end-reached-threshold': onEndReachedThreshold = 0.1, + 'refresher-enabled': refresherEnabled, + 'show-scrollbar': showScrollbar = true, + 'refresher-triggered': refresherTriggered + } = props + + const [refreshing, setRefreshing] = useState(!!refresherTriggered) + + const scrollViewRef = useRef(null) + + const indexMap = useRef<{ [key: string]: string | number }>({}) + + const reverseIndexMap = useRef<{ [key: string]: number }>({}) + + const { + hasSelfPercent, + setWidth, + setHeight + } = useTransformStyle(style, { enableVar, externalVarContext, parentFontSize, parentWidth, parentHeight }) + + const { layoutRef, layoutStyle, layoutProps } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: scrollViewRef }) + + useEffect(() => { + if (refreshing !== refresherTriggered) { + setRefreshing(!!refresherTriggered) + } + }, [refresherTriggered]) + + const onRefresh = () => { + const { bindrefresherrefresh } = props + bindrefresherrefresh && + bindrefresherrefresh( + getCustomEvent('refresherrefresh', {}, { layoutRef }, props) + ) + } + + const onEndReached = () => { + const { bindscrolltolower } = props + bindscrolltolower && + bindscrolltolower( + getCustomEvent('scrolltolower', {}, { layoutRef }, props) + ) + } + + const onScroll = (event: NativeSyntheticEvent) => { + const { bindscroll } = props + bindscroll && + bindscroll( + getCustomEvent('scroll', event.nativeEvent, { layoutRef }, props) + ) + } + + // 通过sectionIndex和rowIndex获取原始索引 + const getOriginalIndex = (sectionIndex: number, rowIndex: number | 'header'): number => { + const key = `${sectionIndex}_${rowIndex}` + return reverseIndexMap.current[key] ?? -1 // 如果找不到,返回-1 + } + + const scrollToIndex = ({ index, animated, viewOffset = 0, viewPosition = 0 }: ScrollPositionParams) => { + if (scrollViewRef.current) { + // 通过索引映射表快速定位位置 + const position = indexMap.current[index] + const [sectionIndex, itemIndex] = (position as string).split('_') + scrollViewRef.current.scrollToLocation?.({ + itemIndex: itemIndex === 'header' ? 0 : Number(itemIndex) + 1, + sectionIndex: Number(sectionIndex) || 0, + animated, + viewOffset, + viewPosition + }) + } + } + + const getItemHeight = ({ sectionIndex, rowIndex }: { sectionIndex: number, rowIndex: number }) => { + if (!itemHeight) { + return 0 + } + if ((itemHeight as ItemHeightType).getter) { + const item = convertedListData[sectionIndex].data[rowIndex] + // 使用getOriginalIndex获取原始索引 + const originalIndex = getOriginalIndex(sectionIndex, rowIndex) + return (itemHeight as ItemHeightType).getter?.(item, originalIndex) || 0 + } else { + return (itemHeight as ItemHeightType).value || 0 + } + } + + const getSectionHeaderHeight = ({ sectionIndex }: { sectionIndex: number }) => { + const item = convertedListData[sectionIndex] + const { hasSectionHeader } = item + // 使用getOriginalIndex获取原始索引 + const originalIndex = getOriginalIndex(sectionIndex, 'header') + if (!hasSectionHeader) return 0 + if ((sectionHeaderHeight as ItemHeightType).getter) { + return (sectionHeaderHeight as ItemHeightType).getter?.(item, originalIndex) || 0 + } else { + return (sectionHeaderHeight as ItemHeightType).value || 0 + } + } + + const convertedListData = useMemo(() => { + const sections: Section[] = [] + let currentSection: Section | null = null + // 清空之前的索引映射 + indexMap.current = {} + // 清空反向索引映射 + reverseIndexMap.current = {} + listData.forEach((item: ListItem, index: number) => { + if (item.isSectionHeader) { + // 如果已经存在一个 section,先把它添加到 sections 中 + if (currentSection) { + sections.push(currentSection) + } + // 创建新的 section + currentSection = { + headerData: item, + data: [], + hasSectionHeader: true, + _originalItemIndex: index + } + // 为 section header 添加索引映射 + const sectionIndex = sections.length + indexMap.current[index] = `${sectionIndex}_header` + // 添加反向索引映射 + reverseIndexMap.current[`${sectionIndex}_header`] = index + } else { + // 如果没有当前 section,创建一个默认的 + if (!currentSection) { + // 创建默认section (无header的section) + currentSection = { + headerData: null, + data: [], + hasSectionHeader: false, + _originalItemIndex: -1 + } + } + // 将 item 添加到当前 section 的 data 中 + const itemIndex = currentSection.data.length + currentSection.data.push(extendObject({}, item, { + _originalItemIndex: index + })) + let sectionIndex + // 为 item 添加索引映射 - 存储格式为: "sectionIndex_itemIndex" + if (!currentSection.hasSectionHeader && sections.length === 0) { + // 在默认section中(第一个且无header) + sectionIndex = 0 + indexMap.current[index] = `${sectionIndex}_${itemIndex}` + } else { + // 在普通section中 + sectionIndex = sections.length + indexMap.current[index] = `${sectionIndex}_${itemIndex}` + } + // 添加反向索引映射 + reverseIndexMap.current[`${sectionIndex}_${itemIndex}`] = index + } + }) + // 添加最后一个 section + if (currentSection) { + sections.push(currentSection) + } + return sections + }, [listData]) + + const { getItemLayout } = useMemo(() => { + const layouts: Array<{ length: number, offset: number, index: number }> = [] + let offset = 0 + + if (useListHeader) { + // 计算列表头部的高度 + offset += listHeaderHeight.getter?.() || listHeaderHeight.value || 0 + } + + // 遍历所有 sections + convertedListData.forEach((section: Section, sectionIndex: number) => { + // 添加 section header 的位置信息 + const headerHeight = getSectionHeaderHeight({ sectionIndex }) + layouts.push({ + length: headerHeight, + offset, + index: layouts.length + }) + offset += headerHeight + + // 添加该 section 中所有 items 的位置信息 + section.data.forEach((item: ListItem, itemIndex: number) => { + const contenteight = getItemHeight({ sectionIndex, rowIndex: itemIndex }) + layouts.push({ + length: contenteight, + offset, + index: layouts.length + }) + offset += contenteight + }) + + // 添加该 section 尾部位置信息 + // 因为即使 sectionList 没传 renderSectionFooter,getItemLayout 中的 index 的计算也会包含尾部节点 + layouts.push({ + length: 0, + offset, + index: layouts.length + }) + }) + return { + itemLayouts: layouts, + getItemLayout: (data: any, index: number) => layouts[index] + } + }, [convertedListData, useListHeader]) + + const scrollAdditionalProps = extendObject( + { + alwaysBounceVertical: false, + alwaysBounceHorizontal: false, + scrollEventThrottle: scrollEventThrottle, + scrollsToTop: enableBackToTop, + showsHorizontalScrollIndicator: showScrollbar, + onEndReachedThreshold, + ref: scrollViewRef, + bounces: false, + stickySectionHeadersEnabled: enableSticky, + onScroll: onScroll, + onEndReached: onEndReached + }, + layoutProps + ) + + if (enhanced) { + Object.assign(scrollAdditionalProps, { + bounces + }) + } + if (refresherEnabled) { + Object.assign(scrollAdditionalProps, { + refreshing: refreshing + }) + } + + useImperativeHandle(ref, () => { + return { + ...props, + scrollToIndex + } + }) + + const innerProps = useInnerProps(extendObject({}, props, scrollAdditionalProps), [ + 'id', + 'show-scrollbar', + 'lower-threshold', + 'refresher-triggered', + 'refresher-enabled', + 'bindrefresherrefresh' + ], { layoutRef }) + + return createElement( + SectionList, + extendObject( + { + style: [{ height, width }, style, layoutStyle], + sections: convertedListData, + renderItem: getItemRenderer(generichash, genericrecycleItem), + getItemLayout: getItemLayout, + ListHeaderComponent: useListHeader ? getListHeaderComponent(generichash, genericListHeader, listHeaderData) : null, + renderSectionHeader: getSectionHeaderRenderer(generichash, genericsectionHeader), + refreshControl: refresherEnabled + ? React.createElement(RefreshControl, { + onRefresh: onRefresh, + refreshing: refreshing + }) + : undefined + }, + innerProps + ) + ) +}) + +export default RecycleView diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-scroll-view.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-scroll-view.tsx index 9efe3c44d3..032f71d0d2 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-scroll-view.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-scroll-view.tsx @@ -259,6 +259,7 @@ const _ScrollView = forwardRef, S // layout 完成前先隐藏,避免安卓闪烁问题 const refresherLayoutStyle = useMemo(() => { return !hasRefresherLayoutRef.current ? HIDDEN_STYLE : {} }, [hasRefresherLayoutRef.current]) + const lastOffset = useRef(0) if (scrollX && scrollY) { diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-sticky-section.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-sticky-section.tsx index 63175e0099..b95aefb177 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-sticky-section.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-sticky-section.tsx @@ -1,9 +1,9 @@ - import { useRef, forwardRef, createElement, ReactNode, useCallback, useMemo } from 'react' import { View, ViewStyle } from 'react-native' import useNodesRef, { HandlerRef } from './useNodesRef' import { splitProps, splitStyle, useTransformStyle, wrapChildren, useLayout, extendObject } from './utils' import { StickyContext } from './context' + import useInnerProps from './getInnerListeners' interface StickySectionProps { diff --git a/packages/webpack-plugin/lib/runtime/components/web/mpx-recycle-view.vue b/packages/webpack-plugin/lib/runtime/components/web/mpx-recycle-view.vue new file mode 100644 index 0000000000..227036cd6b --- /dev/null +++ b/packages/webpack-plugin/lib/runtime/components/web/mpx-recycle-view.vue @@ -0,0 +1,508 @@ + + + + + diff --git a/packages/webpack-plugin/lib/runtime/components/wx/mpx-list-header-default.mpx b/packages/webpack-plugin/lib/runtime/components/wx/mpx-list-header-default.mpx new file mode 100644 index 0000000000..8cbd94aa69 --- /dev/null +++ b/packages/webpack-plugin/lib/runtime/components/wx/mpx-list-header-default.mpx @@ -0,0 +1,21 @@ + + + diff --git a/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-item-default.mpx b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-item-default.mpx new file mode 100644 index 0000000000..39ed23dc2c --- /dev/null +++ b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-item-default.mpx @@ -0,0 +1,21 @@ + + + diff --git a/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx new file mode 100644 index 0000000000..7607e1e8eb --- /dev/null +++ b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx @@ -0,0 +1,193 @@ + + + + + diff --git a/packages/webpack-plugin/lib/runtime/components/wx/mpx-section-header-default.mpx b/packages/webpack-plugin/lib/runtime/components/wx/mpx-section-header-default.mpx new file mode 100644 index 0000000000..27172694ea --- /dev/null +++ b/packages/webpack-plugin/lib/runtime/components/wx/mpx-section-header-default.mpx @@ -0,0 +1,21 @@ + + + diff --git a/packages/webpack-plugin/lib/template-compiler/compiler.js b/packages/webpack-plugin/lib/template-compiler/compiler.js index 2315e5d352..791044479a 100644 --- a/packages/webpack-plugin/lib/template-compiler/compiler.js +++ b/packages/webpack-plugin/lib/template-compiler/compiler.js @@ -1,7 +1,7 @@ const JSON5 = require('json5') const he = require('he') const config = require('../config') -const { MPX_ROOT_VIEW, MPX_APP_MODULE_ID, PARENT_MODULE_ID, MPX_TAG_PAGE_SELECTOR } = require('../utils/const') +const { MPX_ROOT_VIEW, MPX_APP_MODULE_ID, PARENT_MODULE_ID, MPX_TAG_PAGE_SELECTOR, EXTEND_COMPONENT_CONFIG } = require('../utils/const') const normalize = require('../utils/normalize') const { normalizeCondition } = require('../utils/match-condition') const isValidIdentifierStr = require('../utils/is-valid-identifier-str') @@ -728,6 +728,7 @@ function parse (template, options) { processElement(element, root, options, meta) tagNames.add(element.tag) + // 统计通过抽象节点方式使用的组件 element.attrsList.forEach((attr) => { if (genericRE.test(attr.name)) { @@ -2432,7 +2433,11 @@ function isRealNode (el) { } function isComponentNode (el) { - return usingComponents.indexOf(el.tag) !== -1 || el.tag === 'component' || componentGenerics[el.tag] + return usingComponents.indexOf(el.tag) !== -1 || el.tag === 'component' || componentGenerics[el.tag] || isExtendComponentNode(el) +} + +function isExtendComponentNode (el) { + return EXTEND_COMPONENT_CONFIG[el.tag]?.[mode] } function getComponentInfo (el) { @@ -2440,7 +2445,7 @@ function getComponentInfo (el) { } function isReactComponent (el) { - return !isComponentNode(el) && isRealNode(el) && !el.isBuiltIn + return !isComponentNode(el) && isRealNode(el) && !el.isBuiltIn && !isExtendComponentNode(el) } function processExternalClasses (el, options) { diff --git a/packages/webpack-plugin/lib/utils/const.js b/packages/webpack-plugin/lib/utils/const.js index 8fa1e2fef0..ebcf81d449 100644 --- a/packages/webpack-plugin/lib/utils/const.js +++ b/packages/webpack-plugin/lib/utils/const.js @@ -7,5 +7,16 @@ module.exports = { MPX_ROOT_VIEW: 'mpx-root-view', // 根节点类名 MPX_APP_MODULE_ID: 'mpx-app-scope', // app文件moduleId PARENT_MODULE_ID: '__pid', + // 扩展组件的平台配置:声明哪些组件在哪些平台有专用实现,哪些使用公共组件 + EXTEND_COMPONENT_CONFIG: { + 'recycle-view': { + wx: 'runtime/components/wx/mpx-recycle-view.mpx', + ali: 'runtime/components/ali/mpx-recycle-view.mpx', + web: 'runtime/components/web/mpx-recycle-view.vue', + ios: 'runtime/components/react/dist/mpx-recycle-view.jsx', + android: 'runtime/components/react/dist/mpx-recycle-view.jsx', + harmony: 'runtime/components/react/dist/mpx-recycle-view.jsx' + } + }, MPX_TAG_PAGE_SELECTOR: 'mpx-page' } diff --git a/packages/webpack-plugin/lib/utils/process-extend-components.js b/packages/webpack-plugin/lib/utils/process-extend-components.js new file mode 100644 index 0000000000..fa236402b0 --- /dev/null +++ b/packages/webpack-plugin/lib/utils/process-extend-components.js @@ -0,0 +1,43 @@ +const { EXTEND_COMPONENT_CONFIG } = require('./const') +const normalize = require('./normalize') + +/** + * 处理扩展组件的公共方法 + * @param {Object} options 配置选项 + * @param {Object} options.useExtendComponents 使用的扩展组件配置 + * @param {string} options.mode 当前模式 (wx, ali, web, rn 等) + * @param {Function} options.emitWarning 警告函数 + * @returns {Object} 扩展组件映射对象 + */ +function processExtendComponents (options) { + const { + useExtendComponents = {}, + mode, + emitWarning + } = options + + if (!useExtendComponents[mode]) { + return {} + } + + const extendComponents = {} + + useExtendComponents[mode].forEach((name) => { + // 从配置中获取该组件在当前平台的具体路径 + const componentConfig = EXTEND_COMPONENT_CONFIG[name] + + if (componentConfig && componentConfig[mode]) { + extendComponents[name] = normalize.lib(componentConfig[mode]) + } else if (componentConfig) { + emitWarning('extend component ' + name + ' is not configured for ' + mode + ' environment!') + } else { + emitWarning('extend component ' + name + ' is not supported in any environment!') + } + }) + + return extendComponents +} + +module.exports = { + processExtendComponents +} diff --git a/packages/webpack-plugin/lib/web/processJSON.js b/packages/webpack-plugin/lib/web/processJSON.js index eaad49dc9d..07e4087969 100644 --- a/packages/webpack-plugin/lib/web/processJSON.js +++ b/packages/webpack-plugin/lib/web/processJSON.js @@ -11,6 +11,7 @@ const resolve = require('../utils/resolve') const createJSONHelper = require('../json-compiler/helper') const getRulesRunner = require('../platform/index') const { RESOLVE_IGNORED_ERR } = require('../utils/const') +const { processExtendComponents } = require('../utils/process-extend-components') const RecordResourceMapDependency = require('../dependencies/RecordResourceMapDependency') module.exports = function (jsonContent, { @@ -30,11 +31,19 @@ module.exports = function (jsonContent, { mode, srcMode, env, - projectRoot + projectRoot, + useExtendComponents = {}, + appInfo } = mpx const context = loaderContext.context + let hasApp = true + + if (!appInfo.name) { + hasApp = false + } + const emitWarning = (msg) => { loaderContext.emitWarning( new Error('[Mpx json warning][' + loaderContext.resource + ']: ' + msg) @@ -116,7 +125,16 @@ module.exports = function (jsonContent, { if (ctorType !== 'app') { rulesRunnerOptions.mainKey = ctorType } - + if (!hasApp || ctorType === 'app') { + if (useExtendComponents[mode]) { + const extendComponents = processExtendComponents({ + useExtendComponents, + mode, + emitWarning + }) + jsonObj.usingComponents = Object.assign({}, extendComponents, jsonObj.usingComponents) + } + } const rulesRunner = getRulesRunner(rulesRunnerOptions) if (rulesRunner) {