From 9c0077d7d5f2bfb21588f5b87406bc67b411f31c Mon Sep 17 00:00:00 2001 From: fxy060608 Date: Wed, 20 Oct 2021 17:32:10 +0800 Subject: [PATCH] wip(mp): v-model (Component) --- packages/uni-cli-shared/src/mp/event.ts | 12 +- .../uni-mp-compiler/__tests__/vModel.spec.ts | 34 ++++++ packages/uni-mp-compiler/package.json | 1 + packages/uni-mp-compiler/src/ast.ts | 19 ++- packages/uni-mp-compiler/src/errors.ts | 25 +++- .../uni-mp-compiler/src/template/codegen.ts | 2 - .../src/transforms/transformElement.ts | 60 +++++----- .../uni-mp-compiler/src/transforms/vBind.ts | 14 +-- .../uni-mp-compiler/src/transforms/vModel.ts | 111 ++++++++++++++++++ .../uni-mp-compiler/src/transforms/vOn.ts | 12 +- yarn.lock | 2 +- 11 files changed, 239 insertions(+), 53 deletions(-) create mode 100644 packages/uni-mp-compiler/__tests__/vModel.spec.ts create mode 100644 packages/uni-mp-compiler/src/transforms/vModel.ts diff --git a/packages/uni-cli-shared/src/mp/event.ts b/packages/uni-cli-shared/src/mp/event.ts index 7eaf95a8d..b66bdccb3 100644 --- a/packages/uni-cli-shared/src/mp/event.ts +++ b/packages/uni-cli-shared/src/mp/event.ts @@ -19,5 +19,15 @@ export function formatMiniProgramEvent( return `capture-${eventType}:${eventName}` } // bind:foo-bar - return eventType + (eventName.indexOf('-') > -1 ? ':' : '') + eventName + return eventType + (isSimpleExpr(eventName) ? '' : ':') + eventName +} + +function isSimpleExpr(name: string) { + if (name.indexOf('-') > -1) { + return false + } + if (name.indexOf(':') > -1) { + return false + } + return true } diff --git a/packages/uni-mp-compiler/__tests__/vModel.spec.ts b/packages/uni-mp-compiler/__tests__/vModel.spec.ts new file mode 100644 index 000000000..976fa217b --- /dev/null +++ b/packages/uni-mp-compiler/__tests__/vModel.spec.ts @@ -0,0 +1,34 @@ +import { assert } from './testUtils' + +describe('compiler: transform v-model', () => { + test(`component v-model`, () => { + assert( + ``, + ``, + `(_ctx, _cache) => { + return { a: _ctx.model, b: _vOn($event => _ctx.model = $event.detail.__args__[0]) } +}` + ) + }) + test(`component v-model with cache`, () => { + assert( + ``, + ``, + `(_ctx, _cache) => { + return { a: _ctx.model, b: _vOn($event => _ctx.model = $event.detail.__args__[0]) } +}`, + { + cacheHandlers: true, + } + ) + }) + // test(`input,textarea v-model`, () => { + // assert( + // ``, + // ``, + // `(_ctx, _cache) => { + // return { a: _ctx.model, b: _vOn(($event)=>_ctx.model = $event.detail.value) } + // }` + // ) + // }) +}) diff --git a/packages/uni-mp-compiler/package.json b/packages/uni-mp-compiler/package.json index b91850562..9b3180338 100644 --- a/packages/uni-mp-compiler/package.json +++ b/packages/uni-mp-compiler/package.json @@ -23,6 +23,7 @@ "@babel/parser": "^7.15.0", "@babel/types": "^7.15.0", "@vue/compiler-core": "^3.2.20", + "@vue/compiler-dom": "^3.2.20", "estree-walker": "^2.0.2" } } diff --git a/packages/uni-mp-compiler/src/ast.ts b/packages/uni-mp-compiler/src/ast.ts index fa8cc9595..d2ace2408 100644 --- a/packages/uni-mp-compiler/src/ast.ts +++ b/packages/uni-mp-compiler/src/ast.ts @@ -199,20 +199,29 @@ export function parseStringLiteral( return stringLiteral('') } -export function createBindDirectiveNode( +function createDirectiveNode( name: string, - value: string + arg: string, + exp: string ): DirectiveNode { return { type: NodeTypes.DIRECTIVE, - name: 'bind', + name, modifiers: [], loc: locStub, - arg: createSimpleExpression(name, true), - exp: createSimpleExpression(value, false), + arg: createSimpleExpression(arg, true), + exp: createSimpleExpression(exp, false), } } +export function createOnDirectiveNode(name: string, value: string) { + return createDirectiveNode('on', name, value) +} + +export function createBindDirectiveNode(name: string, value: string) { + return createDirectiveNode('bind', name, value) +} + export function createAttributeNode( name: string, content: string diff --git a/packages/uni-mp-compiler/src/errors.ts b/packages/uni-mp-compiler/src/errors.ts index c237dcc00..cbe088431 100644 --- a/packages/uni-mp-compiler/src/errors.ts +++ b/packages/uni-mp-compiler/src/errors.ts @@ -1,3 +1,9 @@ +import { + CompilerError, + createCompilerError, + SourceLocation, +} from '@vue/compiler-core' + export const enum MPErrorCodes { X_V_ON_NO_ARGUMENT, X_V_ON_DYNAMIC_EVENT, @@ -10,7 +16,7 @@ export const enum MPErrorCodes { X_DYNAMIC_COMPONENT_NOT_SUPPORTED, } -export const errorMessages: Record = { +const MPErrorMessages: Record = { [MPErrorCodes.X_V_ON_NO_ARGUMENT]: 'v-on="" is not supported', [MPErrorCodes.X_V_ON_DYNAMIC_EVENT]: 'v-on:[event]="" is not supported.', [MPErrorCodes.X_V_BIND_NO_ARGUMENT]: 'v-bind="" is not supported.', @@ -23,3 +29,20 @@ export const errorMessages: Record = { [MPErrorCodes.X_NOT_SUPPORTED]: 'not supported: ', [MPErrorCodes.X_V_IS_NOT_SUPPORTED]: 'v-is not supported', } + +export interface MPCompilerError extends CompilerError { + code: MPErrorCodes +} + +export function createMPCompilerError( + code: MPErrorCodes, + loc?: SourceLocation, + additionalMessage?: string +) { + return createCompilerError( + code, + loc, + MPErrorMessages, + additionalMessage + ) as MPCompilerError +} diff --git a/packages/uni-mp-compiler/src/template/codegen.ts b/packages/uni-mp-compiler/src/template/codegen.ts index c942e65b0..2b7800c78 100644 --- a/packages/uni-mp-compiler/src/template/codegen.ts +++ b/packages/uni-mp-compiler/src/template/codegen.ts @@ -303,8 +303,6 @@ function genDirectiveNode( if (prop.arg) { push(`slot="${(prop.arg as SimpleExpressionNode).content}"`) } - } else if (prop.name === 'model') { - // TODO } else if (prop.name === 'show') { push(`hidden="{{!${(prop.exp as SimpleExpressionNode).content}}}"`) } else if (prop.arg && prop.exp) { diff --git a/packages/uni-mp-compiler/src/transforms/transformElement.ts b/packages/uni-mp-compiler/src/transforms/transformElement.ts index 1fed011b6..be88d0c98 100644 --- a/packages/uni-mp-compiler/src/transforms/transformElement.ts +++ b/packages/uni-mp-compiler/src/transforms/transformElement.ts @@ -16,8 +16,9 @@ import { findDir, locStub, AttributeNode, + DirectiveNode, } from '@vue/compiler-core' -import { errorMessages, MPErrorCodes } from '../errors' +import { createMPCompilerError, MPErrorCodes } from '../errors' import { BindingComponentTypes, @@ -25,6 +26,7 @@ import { TransformContext, } from '../transform' import { createAttributeNode } from '../ast' +import { transformModel } from './vModel' export interface DirectiveTransformResult { props: Property[] @@ -101,20 +103,15 @@ function processComponent(node: ElementNode, context: TransformContext) { // 1. dynamic component if (isComponentTag(tag)) { return context.onError( - createCompilerError( + createMPCompilerError( MPErrorCodes.X_DYNAMIC_COMPONENT_NOT_SUPPORTED, - node.loc, - errorMessages + node.loc ) ) } if (findDir(node, 'is')) { return context.onError( - createCompilerError( - MPErrorCodes.X_V_IS_NOT_SUPPORTED, - node.loc, - errorMessages - ) + createMPCompilerError(MPErrorCodes.X_V_IS_NOT_SUPPORTED, node.loc) ) } // TODO not supported @@ -125,12 +122,7 @@ function processComponent(node: ElementNode, context: TransformContext) { const builtIn = isCoreComponent(tag) || context.isBuiltInComponent(tag) if (builtIn) { return context.onError( - createCompilerError( - MPErrorCodes.X_NOT_SUPPORTED, - node.loc, - errorMessages, - tag - ) + createMPCompilerError(MPErrorCodes.X_NOT_SUPPORTED, node.loc, tag) ) } @@ -147,12 +139,7 @@ function processComponent(node: ElementNode, context: TransformContext) { const dotIndex = tag.indexOf('.') if (dotIndex > 0) { return context.onError( - createCompilerError( - MPErrorCodes.X_NOT_SUPPORTED, - node.loc, - errorMessages, - tag - ) + createMPCompilerError(MPErrorCodes.X_NOT_SUPPORTED, node.loc, tag) ) } @@ -253,12 +240,11 @@ function processProps(node: ElementNode, context: TransformContext) { // v-bind="" if (!arg) { context.onError( - createCompilerError( + createMPCompilerError( isVBind ? MPErrorCodes.X_V_BIND_NO_ARGUMENT : MPErrorCodes.X_V_ON_NO_ARGUMENT, - loc, - errorMessages + loc ) ) continue @@ -269,12 +255,11 @@ function processProps(node: ElementNode, context: TransformContext) { // v-bind:[a.b]="" if (!(arg.type === NodeTypes.SIMPLE_EXPRESSION && arg.isStatic)) { context.onError( - createCompilerError( + createMPCompilerError( isVBind ? MPErrorCodes.X_V_BIND_DYNAMIC_ARGUMENT : MPErrorCodes.X_V_ON_DYNAMIC_EVENT, - loc, - errorMessages + loc ) ) continue @@ -293,11 +278,28 @@ function processProps(node: ElementNode, context: TransformContext) { const directiveTransform = context.directiveTransforms[name] if (directiveTransform) { - prop.exp = directiveTransform(prop, node, context).props[0] - .value as ExpressionNode + const { props } = directiveTransform(prop, node, context) + if (props.length) { + prop.exp = props[0].value as ExpressionNode + } } } } + processVModel(node, context) +} + +function processVModel(node: ElementNode, context: TransformContext) { + const { props } = node + const dirs: DirectiveNode[] = [] + for (let i = 0; i < props.length; i++) { + const prop = props[i] + if (prop.type === NodeTypes.DIRECTIVE && prop.name === 'model') { + dirs.push(...transformModel(prop, node, context)) + props.splice(i, 1) + i-- + } + } + props.push(...dirs) } function isComponentTag(tag: string) { diff --git a/packages/uni-mp-compiler/src/transforms/vBind.ts b/packages/uni-mp-compiler/src/transforms/vBind.ts index 05967400e..923c5254e 100644 --- a/packages/uni-mp-compiler/src/transforms/vBind.ts +++ b/packages/uni-mp-compiler/src/transforms/vBind.ts @@ -8,7 +8,7 @@ import { createSimpleExpression, } from '@vue/compiler-core' import { DirectiveTransform } from '../transform' -import { errorMessages, MPErrorCodes } from '../errors' +import { createMPCompilerError, MPErrorCodes } from '../errors' export const transformBind: DirectiveTransform = (dir, _node, context) => { const { exp, modifiers, loc } = dir @@ -37,20 +37,12 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => { if (modifiers.includes('prop')) { context.onWarn( - createCompilerError( - MPErrorCodes.X_V_BIND_MODIFIER_PROP, - loc, - errorMessages - ) + createMPCompilerError(MPErrorCodes.X_V_BIND_MODIFIER_PROP, loc) ) } if (modifiers.includes('attr')) { context.onWarn( - createCompilerError( - MPErrorCodes.X_V_BIND_MODIFIER_ATTR, - loc, - errorMessages - ) + createMPCompilerError(MPErrorCodes.X_V_BIND_MODIFIER_ATTR, loc) ) } diff --git a/packages/uni-mp-compiler/src/transforms/vModel.ts b/packages/uni-mp-compiler/src/transforms/vModel.ts new file mode 100644 index 000000000..4043c8568 --- /dev/null +++ b/packages/uni-mp-compiler/src/transforms/vModel.ts @@ -0,0 +1,111 @@ +import { + Property, + transformModel as baseTransform, + ElementTypes, + findProp, + NodeTypes, + DirectiveNode, + ElementNode, + ExpressionNode, +} from '@vue/compiler-core' +import { DOMErrorCodes, createDOMCompilerError } from '@vue/compiler-dom' +import { camelize } from '@vue/shared' +import { createBindDirectiveNode, createOnDirectiveNode } from '../ast' +import { genExpr } from '../codegen' +import { TransformContext } from '../transform' +import { wrapperVOn } from './vOn' + +export const transformModel = ( + dir: DirectiveNode, + node: ElementNode, + context: TransformContext +) => { + const baseResult = baseTransform(dir, node, context as any) + // base transform has errors OR component v-model (only need props) + if (!baseResult.props.length || node.tagType === ElementTypes.COMPONENT) { + return transformComponentVModel(baseResult.props, context) + } + + if (dir.arg) { + context.onError( + createDOMCompilerError( + DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT, + dir.arg.loc + ) + ) + } + + function checkDuplicatedValue() { + const value = findProp(node, 'value') + if (value) { + context.onError( + createDOMCompilerError( + DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE, + value.loc + ) + ) + } + } + + const { tag } = node + if (tag === 'input' || tag === 'textarea') { + checkDuplicatedValue() + } else { + context.onError( + createDOMCompilerError( + DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT, + dir.loc + ) + ) + } + + // native vmodel doesn't need the `modelValue` props since they are also + // passed to the runtime as `binding.value`. removing it reduces code size. + baseResult.props = baseResult.props.filter( + (p) => + !( + p.key.type === NodeTypes.SIMPLE_EXPRESSION && + p.key.content === 'modelValue' + ) + ) + + return [] +} + +function transformComponentVModel( + props: Property[], + context: TransformContext +): DirectiveNode[] { + if (props.length !== 2) { + return [] + } + const { key: modelValueArg, value: modelValeExpr } = props[0] + const { key: onUpdateArg, value: onUpdateExpr } = props[1] + + if (modelValueArg.type !== NodeTypes.SIMPLE_EXPRESSION) { + return [] + } + if ( + onUpdateArg.type !== NodeTypes.SIMPLE_EXPRESSION || + !onUpdateArg.content.startsWith('onUpdate:') + ) { + return [] + } + const vBindModelValue = createBindDirectiveNode( + modelValueArg.content, + genExpr(modelValeExpr as ExpressionNode) + ) + const vOnUpdate = createOnDirectiveNode( + camelize(onUpdateArg.content.replace('onUpdate:', 'update-')), + genExpr( + wrapperVOn( + // onUpdateExpr 通常是 ExpressionNode 或者被 cache 的 ExpressionNode + (onUpdateExpr.type === NodeTypes.JS_CACHE_EXPRESSION + ? onUpdateExpr.value + : onUpdateExpr) as ExpressionNode, + context + ) + ).replace(`= $event`, `= $event.detail.__args__[0]`) + ) + return [vBindModelValue, vOnUpdate] +} diff --git a/packages/uni-mp-compiler/src/transforms/vOn.ts b/packages/uni-mp-compiler/src/transforms/vOn.ts index 33a3fec0b..317dd7ec7 100644 --- a/packages/uni-mp-compiler/src/transforms/vOn.ts +++ b/packages/uni-mp-compiler/src/transforms/vOn.ts @@ -159,9 +159,15 @@ export const transformOn: DirectiveTransform = ( // context.cache(ret.props[0].value) as ExpressionNode, // context // ) - ret.props[0].value = wrapper(ret.props[0].value as ExpressionNode, context) + ret.props[0].value = wrapperVOn( + ret.props[0].value as ExpressionNode, + context + ) } else { - ret.props[0].value = wrapper(ret.props[0].value as ExpressionNode, context) + ret.props[0].value = wrapperVOn( + ret.props[0].value as ExpressionNode, + context + ) } // mark the key as handler for props normalization check @@ -169,7 +175,7 @@ export const transformOn: DirectiveTransform = ( return ret } -function wrapper(value: ExpressionNode, context: TransformContext) { +export function wrapperVOn(value: ExpressionNode, context: TransformContext) { return createCompoundExpression([ `${context.helperString(V_ON)}(`, value, diff --git a/yarn.lock b/yarn.lock index 41c0657eb..927144079 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2774,7 +2774,7 @@ estree-walker "^2.0.2" source-map "^0.6.1" -"@vue/compiler-dom@3.2.20": +"@vue/compiler-dom@3.2.20", "@vue/compiler-dom@^3.2.20": version "3.2.20" resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.20.tgz#8e0ef354449c0faf41519b00bfc2045eae01dcb5" integrity sha512-QnI77ec/JtV7R0YBbcVayYTDCRcI9OCbxiUQK6izVyqQO0658n0zQuoNwe+bYgtqnvGAIqTR3FShTd5y4oOjdg== -- GitLab