Skip to content

Commit

Permalink
feat(plugin-compiler): 新增组件级别多端产物互通能力支持 (#97)
Browse files Browse the repository at this point in the history
  • Loading branch information
BboyZaki authored Dec 12, 2023
1 parent ec0cd19 commit cc97565
Show file tree
Hide file tree
Showing 5 changed files with 322 additions and 3 deletions.
12 changes: 9 additions & 3 deletions packages/plugin-compiler-alipay/src/templateProcessorToOther.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,18 @@ const EVENT_HANDLER_NAME = 'data-mor-event-handlers'
const PROXY_DISABLE_EVENT_NAME = '$morDisableScrollProxy'

/**
* 将时间代理存储到 node 节点上
* 将事件代理存储到 node 节点上
*/
function processEventProxy(
node: posthtml.Node,
options: FileParserOptions,
context: Record<string, any>
) {
if (context.morHandlersMap && Object.keys(context.morHandlersMap).length) {
if (
context.morHandlersMap &&
Object.keys(context.morHandlersMap).length &&
!options.userConfig?.processComponentsPropsFunction
) {
node.attrs[EVENT_HANDLER_NAME] = Buffer.from(
JSON.stringify(context.morHandlersMap)
).toString('base64')
Expand Down Expand Up @@ -148,7 +152,9 @@ function processEventsAttributes(
node.attrs[newAttr] = node.attrs[attrName]
} else {
const newAttr = `bind:${eventName}`
node.attrs[newAttr] = PROXY_EVENT_NAME
node.attrs[newAttr] = options.userConfig?.processComponentsPropsFunction
? node.attrs[attrName]
: PROXY_EVENT_NAME
morHandlersMap[eventName] = node.attrs[attrName] as string
}

Expand Down
5 changes: 5 additions & 0 deletions packages/plugin-compiler/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,11 @@ export const CompilerUserConfigSchema = z.object({
*/
processPlaceholderComponents: z.boolean().optional(),

/**
* 是否处理组件入参函数
*/
processComponentsPropsFunction: z.boolean().optional().default(false),

/**
* 配置可以共享的 node_modules 模块, 通常用于主子分包分仓库管理集成的场景
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/plugin-compiler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { ModuleSharingAndConsumingPlugin } from './plugins/moduleSharingAndConsu
import { OptimizeSplitChunksPlugin } from './plugins/optimizeSplitChunksPlugin'
import { PhantomDependencyPlugin } from './plugins/phantomDependencyPlugin'
import { PreRuntimeDetectionPlugin } from './plugins/preRuntimeDetectionPlugin'
import { ProcessComponentsPropsFunctionPlugin } from './plugins/processComponentsPropsFunctionPlugin'
import { ProgressPlugin } from './plugins/progressPlugin'
import { RuntimeInjectPlugin } from './plugins/runtimeInjectPlugin'
import { preprocess } from './preprocessors/codePreprocessor'
Expand Down Expand Up @@ -85,6 +86,7 @@ class MorCompile {
new DefineSupportPlugin().apply(runner)
new PhantomDependencyPlugin().apply(runner)
new PreRuntimeDetectionPlugin().apply(runner)
new ProcessComponentsPropsFunctionPlugin().apply(runner)

// 应用 parser 插件
new ConfigParserPlugin().apply(runner)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
import {
EntryFileType,
EntryType,
fsExtra as fs,
lodash as _,
Plugin,
Runner,
SourceTypes,
typescript as ts
} from '@morjs/utils'
import path from 'path'

/**
* 处理组件入参函数
*/
export class ProcessComponentsPropsFunctionPlugin implements Plugin {
name = 'ProcessComponentsPropsFunctionPlugin'
entryBuilder = null
filePathIdentifier = 'mor'
needProcessList = null
processPropsObj = null

constructor() {
this.needProcessList = []
this.processPropsObj = {}
}

apply(runner: Runner<any>) {
runner.hooks.entryBuilder.tap(this.name, (entryBuilder) => {
this.entryBuilder = entryBuilder
})

runner.hooks.beforeRun.tap(this.name, () => {
// 仅对开启了 processComponentsPropsFunction 为 true,支付宝转其他端的情况
if (
runner.userConfig?.processComponentsPropsFunction &&
runner.userConfig?.sourceType === SourceTypes.alipay &&
runner.userConfig?.target !== SourceTypes.alipay
) {
// 筛选出所有组件的 script 文件,通过 ast 判断该文件是否用到了 props 传递函数
runner.hooks.addEntry.tap(this.name, (entryInfo) => {
if (
entryInfo?.entry?.entryType === EntryType.component &&
entryInfo?.entry?.entryFileType === EntryFileType.script
) {
const sourceText =
fs.readFileSync(entryInfo?.entry?.fullPath, 'utf-8') || ''
if (sourceText.includes('props')) {
const sourceFile: ts.Node = ts.createSourceFile(
entryInfo?.entry?.relativePath || '__temp.ts',
sourceText,
ts.ScriptTarget.ESNext,
true
)

const visitNode = (node: ts.Node) => {
if (
ts.isPropertyAssignment(node) &&
ts.isIdentifier(node.name) &&
node.name.escapedText === 'props' &&
ts.isObjectLiteralExpression(node.initializer)
) {
const nodePropsList = node.initializer.properties
const [propsNormalList, propsFunctionList] = [[], []]
const [propsNormalHandlers, propsMorHandlers] = [{}, {}]

for (const item of nodePropsList) {
if (
ts.isPropertyAssignment(item) &&
ts.isIdentifier(item.name) &&
(ts.isArrowFunction(item.initializer) ||
ts.isFunctionExpression(item.initializer))
) {
const escapedText = String(item.name.escapedText)
propsFunctionList.push(escapedText)
propsMorHandlers[this.getEventName(escapedText)] =
this.filePathIdentifier
} else if (
ts.isPropertyAssignment(item) &&
ts.isIdentifier(item.name)
) {
const escapedText = String(item.name.escapedText)
propsNormalList.push(escapedText)
propsNormalHandlers[escapedText] = this.getPropsType(
item.initializer
)
}
}

// 只有 props 传了函数的才执行以下流程
if (propsFunctionList.length > 0) {
// 修改 js 文件产物目录,其他三种基础文件在编译阶段再通过 setEntrySource 新增
// 原因: setEntrySource 新增的 js 文件未经过 webpack 编译
entryInfo.name = `${entryInfo.name}-${this.filePathIdentifier}`

this.needProcessList.push(entryInfo?.entry?.entryName)
this.processPropsObj[entryInfo?.entry?.entryName] = {
normal: propsNormalList,
function: propsFunctionList,
normalHandlers: propsNormalHandlers,
morHandlers: propsMorHandlers
}
}
}
ts.forEachChild(node, visitNode)
}

visitNode(sourceFile)
}
}
return entryInfo
})

// 处理 xml、css 文件
runner.hooks.postprocessorParser.tap(
this.name,
(fileContent, options) => {
if (this.isNeedProcess(options)) {
if (options?.fileInfo?.entryFileType === EntryFileType.template) {
this.addEntrySource(options)
const entryPathParse = path.parse(
options?.fileInfo?.entryName || ''
)
const propsNormalList =
this.processPropsObj?.[
`${entryPathParse.dir}/${entryPathParse.name}`
]?.normal || []
const propsFunctionList =
this.processPropsObj?.[
`${entryPathParse.dir}/${entryPathParse.name}`
]?.function || []
const propsMorHandlers =
this.processPropsObj?.[
`${entryPathParse.dir}/${entryPathParse.name}`
]?.morHandlers || {}
// props 中的普通参数
const propsNormal = propsNormalList
.map((item) => {
return `${item}="{{${item}}}" `
})
.join('')
// props 中的函数传参
const propsFunction = propsFunctionList
.map((item) => {
return `bind:${this.getEventName(
item
)}="$morEventHandlerProxy" `
})
.join('')
const eventHandlerName = Buffer.from(
JSON.stringify(propsMorHandlers)
).toString('base64')
return `<mor-component ${propsNormal} ${propsFunction} data-mor-event-handlers="${eventHandlerName}"></mor-component>`
} else if (
options?.fileInfo?.entryFileType === EntryFileType.style
) {
this.addEntrySource(options)
return ``
}
}
return fileContent
}
)

// 处理 js 文件
runner.hooks.scriptParser.tap(this.name, (transformers, options) => {
if (this.isNeedProcess(options)) {
const entryPathParse = path.parse(
options?.fileInfo?.entryName || ''
)
const entryName = `${entryPathParse.dir}/${entryPathParse.name}${entryPathParse.ext}`
const propsNormalList =
this.processPropsObj?.[
`${entryPathParse.dir}/${entryPathParse.name}`
]?.normal || []
const propsNormalHandlers =
this.processPropsObj?.[
`${entryPathParse.dir}/${entryPathParse.name}`
]?.normalHandlers || {}
const properties = propsNormalList.map((item) => {
return `${item}: ${propsNormalHandlers[item]}`
})

this.entryBuilder.setEntrySource(
entryName,
`Component({
properties: {
${properties}
},
methods: {
$morEventHandlerProxy(event) {
const { detail } = event;
if (detail.name) {
this.triggerEvent(event.type, { ...detail.args[0] })
}
},
},
})
`,
'additional'
)
}
return transformers
})

// 处理 json 文件
runner.hooks.configParser.tap(this.name, (config, options) => {
if (this.isNeedProcess(options)) {
this.addEntrySource(options)
return {
component: true,
usingComponents: {
'mor-component': `./index-${this.filePathIdentifier}`
}
}
}
return config
})
}
})
}

isNeedProcess(options) {
const entryPathParse = path.parse(options?.fileInfo?.entryName || '')
return (
this.needProcessList.length > 0 &&
options?.fileInfo?.entryName &&
options?.fileInfo?.entryType === EntryType.component &&
(options?.fileInfo?.entryFileType === EntryFileType.template ||
options?.fileInfo?.entryFileType === EntryFileType.style ||
options?.fileInfo?.entryFileType === EntryFileType.script ||
options?.fileInfo?.entryFileType === EntryFileType.config) &&
this.needProcessList.includes(
`${entryPathParse.dir}/${entryPathParse.name}`
)
)
}

addEntrySource(options) {
const entryPathParse = path.parse(options?.fileInfo?.entryName || '')
const entryName = `${entryPathParse.dir}/${entryPathParse.name}-${this.filePathIdentifier}${entryPathParse.ext}`
this.entryBuilder.setEntrySource(
entryName,
options?.fileInfo?.content,
'additional'
)
}

getEventName(attr) {
// 判断小程序组件属性是否是事件(是否以on开头)
const eventAttrReg = /^on([A-Za-z]+)/
const onMatch = attr.match(eventAttrReg)
if (onMatch) {
const eventName = onMatch[1]
return _.lowerFirst(eventName)
}
// 判断小程序组件属性是否是catch事件(是否以catch开头)
const catchEventAttrReg = /^catch([A-Za-z]+)/
const catchMatch = attr.match(catchEventAttrReg)
if (catchMatch) {
const eventName = catchMatch[1]
return _.lowerFirst(eventName)
}
return ''
}

getPropsType(node) {
if (ts.isStringLiteral(node)) return `String`
if (ts.isNumericLiteral(node)) return `Number`
if (ts.isObjectLiteralExpression(node)) return `Object`
if (ts.isArrayLiteralExpression(node)) return `Array`
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node))
return `Function`
if (!node.text && !node.properties && !node.elements && !node.parameters)
return `Boolean`
return null
}
}
28 changes: 28 additions & 0 deletions website/docs/guides/basic/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,34 @@ class YourCustomMorJSPlugin {
}
```

### processComponentsPropsFunction - 是否处理组件入参函数

- 类型: `boolean`
- 默认值: `false`

用于配置是否处理组件的入参函数,常与组件级别编译配合使用(即 `compileType: 'component'` 时)

默认情况下:由于微信本身不支持诸如 `this.props.onXxxClick`,MorJS 为了抹平差异,在引用组件的 `Page` 的 xml 中注入一段类似如下代码:

```xml
<!-- 伪代码 -->
<component-demo
data="{{data}}"
bind:comClick="$morEventHandlerProxy"
data-mor-event-handlers="hashxxxxxxx"
></component-demo>
```

其中 `props``onXxxClick` 事件被代理为 `$morEventHandlerProxy` 方法,`data-mor-event-handlers` 则为组件事件触发页面方法的对应对象通过 `base64` 加密得到的一串 hash 值,如触发 `this.props.onXxxClick` 事件时,实际是把事件交给 ` $morEventHandlerProxy` 代理方法来触发 `this.triggerEvent`,从而抹平转端间的差异。

但是以上方案无法完美兼容组件级别的转端,若以组件的方式编译(即 `compileType: 'component'` 时),编译出的组件提供给微信原生小程序,仅能显示正常组件视图,而无法触发组件的入参函数(原生小程序的 `Page` 缺少本该注入的事件代理)

<img src="https://img.alicdn.com/imgextra/i1/O1CN01uceSYd1JVcOoEZQR0_!!6000000001034-0-tps-1522-1312.jpg" width="500" />

设置为 `true` 开启后:将处理 `props` 中入参含有函数的组件,将事件代理从页面转为组件代理层,编译出的组件可按照普通组件提供给微信原生小程序混用

<img src="https://img.alicdn.com/imgextra/i1/O1CN01GUjMNH1D7Rnuyg3Gi_!!6000000000169-0-tps-1804-1330.jpg" width="500" />

### processNodeModules - 是否处理 node_modules

- 类型: `boolean | { include?: RegExp | RegExp[], exclude?: RegExp | RegExp[] }`
Expand Down

0 comments on commit cc97565

Please sign in to comment.