提交 f59898be 编写于 作者: fxy060608's avatar fxy060608

wip(mp): vOn

上级 b68ad612
因为 它太大了无法显示 source diff 。你可以改为 查看blob
...@@ -8,6 +8,9 @@ export function formatMiniProgramEvent( ...@@ -8,6 +8,9 @@ export function formatMiniProgramEvent(
isCapture?: boolean isCapture?: boolean
} }
) { ) {
if (eventName === 'click') {
eventName = 'tap'
}
let eventType = 'bind' let eventType = 'bind'
if (isCatch) { if (isCatch) {
eventType = 'catch' eventType = 'catch'
...@@ -15,6 +18,6 @@ export function formatMiniProgramEvent( ...@@ -15,6 +18,6 @@ export function formatMiniProgramEvent(
if (isCapture) { if (isCapture) {
return `capture-${eventType}:${eventName}` return `capture-${eventType}:${eventName}`
} }
// 原生组件不支持 bind:input 等写法,统一使用 bindinput // bind:foo-bar
return `${eventType}${eventName}` return eventType + (eventName.indexOf('-') > -1 ? ':' : '') + eventName
} }
// import { inspect } from './testUtils' // import { inspect } from './testUtils'
import { compile } from '../src' import { compile } from '../src'
import { CompilerOptions } from '../src/options'
function assert(template: string, templateCode: string, renderCode: string) { function assert(
template: string,
templateCode: string,
renderCode: string,
options: CompilerOptions
) {
const res = compile(template, { const res = compile(template, {
filename: 'foo.vue', filename: 'foo.vue',
prefixIdentifiers: true, prefixIdentifiers: true,
...@@ -14,6 +20,7 @@ function assert(template: string, templateCode: string, renderCode: string) { ...@@ -14,6 +20,7 @@ function assert(template: string, templateCode: string, renderCode: string) {
return '' return ''
}, },
}, },
...options,
}) })
// expect(res.template).toBe(templateCode) // expect(res.template).toBe(templateCode)
// expect(res.code).toBe(renderCode) // expect(res.code).toBe(renderCode)
...@@ -24,17 +31,16 @@ function assert(template: string, templateCode: string, renderCode: string) { ...@@ -24,17 +31,16 @@ function assert(template: string, templateCode: string, renderCode: string) {
} }
describe('compiler', () => { describe('compiler', () => {
test(`keyed v-for`, () => { test('should wrap as function if expression is inline statement', () => {
assert( assert(
`<view v-for="(item) in items" :key="item" />`, `<div v-on:click="foo" />`,
`<view wx:for="{{a}}" wx:for-item="item" wx:key="*this"/>`, `<view bindtap="{{a}}"/>`,
`(_ctx, _cache) => { `(_ctx, _cache) => {
return { return {
a: vFor(_ctx.items, item => { a: _vOn(_ctx.foo)
return {};
})
} }
}` }`,
{}
) )
}) })
}) })
...@@ -30,7 +30,7 @@ describe(`compiler: v-for`, () => { ...@@ -30,7 +30,7 @@ describe(`compiler: v-for`, () => {
`<view wx:for="{{a}}" wx:for-item="index"/>`, `<view wx:for="{{a}}" wx:for-item="index"/>`,
`(_ctx, _cache) => { `(_ctx, _cache) => {
return { return {
a: vFor([1, 2, 3, 4, 5], index => { a: _vFor([1, 2, 3, 4, 5], index => {
return {}; return {};
}) })
} }
...@@ -43,7 +43,7 @@ return { ...@@ -43,7 +43,7 @@ return {
`<view wx:for="{{a}}" wx:for-item="item"/>`, `<view wx:for="{{a}}" wx:for-item="item"/>`,
`(_ctx, _cache) => { `(_ctx, _cache) => {
return { return {
a: vFor(_ctx.items, item => { a: _vFor(_ctx.items, item => {
return {}; return {};
}) })
} }
...@@ -56,7 +56,7 @@ return { ...@@ -56,7 +56,7 @@ return {
`<view wx:for="{{a}}" wx:for-item="v0"/>`, `<view wx:for="{{a}}" wx:for-item="v0"/>`,
`(_ctx, _cache) => { `(_ctx, _cache) => {
return { return {
a: vFor(_ctx.items, ({ a: _vFor(_ctx.items, ({
id, id,
value value
}) => { }) => {
...@@ -72,7 +72,7 @@ return { ...@@ -72,7 +72,7 @@ return {
`<view wx:for="{{a}}" wx:for-item="v0"/>`, `<view wx:for="{{a}}" wx:for-item="v0"/>`,
`(_ctx, _cache) => { `(_ctx, _cache) => {
return { return {
a: vFor(_ctx.items, ([id, value]) => { a: _vFor(_ctx.items, ([id, value]) => {
return {}; return {};
}) })
} }
...@@ -85,7 +85,7 @@ return { ...@@ -85,7 +85,7 @@ return {
`<view wx:for="{{a}}" wx:for-item="item" wx:for-index="key"/>`, `<view wx:for="{{a}}" wx:for-item="item" wx:for-index="key"/>`,
`(_ctx, _cache) => { `(_ctx, _cache) => {
return { return {
a: vFor(_ctx.items, (item, key) => { a: _vFor(_ctx.items, (item, key) => {
return {}; return {};
}) })
} }
...@@ -98,7 +98,7 @@ return { ...@@ -98,7 +98,7 @@ return {
`<view wx:for="{{a}}" wx:for-item="item" wx:for-index="key"/>`, `<view wx:for="{{a}}" wx:for-item="item" wx:for-index="key"/>`,
`(_ctx, _cache) => { `(_ctx, _cache) => {
return { return {
a: vFor(_ctx.items, (item, key, index) => { a: _vFor(_ctx.items, (item, key, index) => {
return {}; return {};
}) })
} }
...@@ -111,7 +111,7 @@ return { ...@@ -111,7 +111,7 @@ return {
`<view wx:for="{{a}}" wx:for-item="value"/>`, `<view wx:for="{{a}}" wx:for-item="value"/>`,
`(_ctx, _cache) => { `(_ctx, _cache) => {
return { return {
a: vFor(_ctx.items, (value, __, index) => { a: _vFor(_ctx.items, (value, __, index) => {
return {}; return {};
}) })
} }
...@@ -124,7 +124,7 @@ return { ...@@ -124,7 +124,7 @@ return {
`<view wx:for="{{a}}" wx:for-item="v0"/>`, `<view wx:for="{{a}}" wx:for-item="v0"/>`,
`(_ctx, _cache) => { `(_ctx, _cache) => {
return { return {
a: vFor(_ctx.items, (_, __, index) => { a: _vFor(_ctx.items, (_, __, index) => {
return {}; return {};
}) })
} }
...@@ -137,7 +137,7 @@ return { ...@@ -137,7 +137,7 @@ return {
`<view wx:for="{{a}}" wx:for-item="item"/>`, `<view wx:for="{{a}}" wx:for-item="item"/>`,
`(_ctx, _cache) => { `(_ctx, _cache) => {
return { return {
a: vFor(_ctx.items, item => { a: _vFor(_ctx.items, item => {
return {}; return {};
}) })
} }
...@@ -150,7 +150,7 @@ return { ...@@ -150,7 +150,7 @@ return {
`<view wx:for="{{a}}" wx:for-item="item" wx:for-index="key"/>`, `<view wx:for="{{a}}" wx:for-item="item" wx:for-index="key"/>`,
`(_ctx, _cache) => { `(_ctx, _cache) => {
return { return {
a: vFor(_ctx.items, (item, key) => { a: _vFor(_ctx.items, (item, key) => {
return {}; return {};
}) })
} }
...@@ -163,7 +163,7 @@ return { ...@@ -163,7 +163,7 @@ return {
`<view wx:for="{{a}}" wx:for-item="value" wx:for-index="key"/>`, `<view wx:for="{{a}}" wx:for-item="value" wx:for-index="key"/>`,
`(_ctx, _cache) => { `(_ctx, _cache) => {
return { return {
a: vFor(_ctx.items, (value, key, index) => { a: _vFor(_ctx.items, (value, key, index) => {
return {}; return {};
}) })
} }
...@@ -176,7 +176,7 @@ return { ...@@ -176,7 +176,7 @@ return {
`<view wx:for="{{a}}" wx:for-item="value"/>`, `<view wx:for="{{a}}" wx:for-item="value"/>`,
`(_ctx, _cache) => { `(_ctx, _cache) => {
return { return {
a: vFor(_ctx.items, (value, __, index) => { a: _vFor(_ctx.items, (value, __, index) => {
return {}; return {};
}) })
} }
...@@ -189,7 +189,7 @@ return { ...@@ -189,7 +189,7 @@ return {
`<view wx:for="{{a}}" wx:for-item="v0"/>`, `<view wx:for="{{a}}" wx:for-item="v0"/>`,
`(_ctx, _cache) => { `(_ctx, _cache) => {
return { return {
a: vFor(_ctx.items, (_, __, index) => { a: _vFor(_ctx.items, (_, __, index) => {
return {}; return {};
}) })
} }
...@@ -202,7 +202,7 @@ return { ...@@ -202,7 +202,7 @@ return {
`<block wx:for="{{a}}" wx:for-item="item">hello<view/></block>`, `<block wx:for="{{a}}" wx:for-item="item">hello<view/></block>`,
`(_ctx, _cache) => { `(_ctx, _cache) => {
return { return {
a: vFor(_ctx.items, item => { a: _vFor(_ctx.items, item => {
return {}; return {};
}) })
} }
...@@ -215,7 +215,7 @@ return { ...@@ -215,7 +215,7 @@ return {
`<block wx:for="{{a}}" wx:for-item="item"><slot/></block>`, `<block wx:for="{{a}}" wx:for-item="item"><slot/></block>`,
`(_ctx, _cache) => { `(_ctx, _cache) => {
return { return {
a: vFor(_ctx.items, item => { a: _vFor(_ctx.items, item => {
return {}; return {};
}) })
} }
...@@ -229,7 +229,7 @@ return { ...@@ -229,7 +229,7 @@ return {
`<block wx:for="{{a}}" wx:for-item="item" wx:key="b"><view id="{{item.a}}"/></block>`, `<block wx:for="{{a}}" wx:for-item="item" wx:key="b"><view id="{{item.a}}"/></block>`,
`(_ctx, _cache) => { `(_ctx, _cache) => {
return { return {
a: vFor(_ctx.items, item => { a: _vFor(_ctx.items, item => {
return { return {
a: item.id, a: item.id,
b: item.id b: item.id
...@@ -245,7 +245,7 @@ return { ...@@ -245,7 +245,7 @@ return {
`<slot wx:for="{{a}}" wx:for-item="item"></slot>`, `<slot wx:for="{{a}}" wx:for-item="item"></slot>`,
`(_ctx, _cache) => { `(_ctx, _cache) => {
return { return {
a: vFor(_ctx.items, item => { a: _vFor(_ctx.items, item => {
return {}; return {};
}) })
} }
...@@ -258,7 +258,7 @@ return { ...@@ -258,7 +258,7 @@ return {
`<view wx:for="{{a}}" wx:for-item="item" wx:key="*this"/>`, `<view wx:for="{{a}}" wx:for-item="item" wx:key="*this"/>`,
`(_ctx, _cache) => { `(_ctx, _cache) => {
return { return {
a: vFor(_ctx.items, item => { a: _vFor(_ctx.items, item => {
return {}; return {};
}) })
} }
...@@ -271,7 +271,7 @@ return { ...@@ -271,7 +271,7 @@ return {
`<block wx:for="{{a}}" wx:for-item="item" wx:key="*this">hello<view/></block>`, `<block wx:for="{{a}}" wx:for-item="item" wx:key="*this">hello<view/></block>`,
`(_ctx, _cache) => { `(_ctx, _cache) => {
return { return {
a: vFor(_ctx.items, item => { a: _vFor(_ctx.items, item => {
return {}; return {};
}) })
} }
...@@ -286,7 +286,7 @@ return { ...@@ -286,7 +286,7 @@ return {
return { return {
b: _ctx.ok, b: _ctx.ok,
...(_ctx.ok ? { ...(_ctx.ok ? {
a: vFor(_ctx.list, i => { a: _vFor(_ctx.list, i => {
return {}; return {};
}) })
} : {}) } : {})
...@@ -303,7 +303,7 @@ return { ...@@ -303,7 +303,7 @@ return {
return { return {
b: _ctx.ok, b: _ctx.ok,
...(_ctx.ok ? { ...(_ctx.ok ? {
a: vFor(_ctx.list, i => { a: _vFor(_ctx.list, i => {
return {}; return {};
}) })
} : {}) } : {})
...@@ -709,7 +709,7 @@ return { ...@@ -709,7 +709,7 @@ return {
`<view wx:for="{{a}}" wx:for-item="item" wx:key="a">test</view>`, `<view wx:for="{{a}}" wx:for-item="item" wx:key="a">test</view>`,
`(_ctx, _cache) => { `(_ctx, _cache) => {
return { return {
a: vFor(_ctx.items, item => { a: _vFor(_ctx.items, item => {
return { return {
a: _ctx.itemKey(item) a: _ctx.itemKey(item)
}; };
...@@ -726,7 +726,7 @@ return { ...@@ -726,7 +726,7 @@ return {
`<block wx:for="{{a}}" wx:for-item="item" wx:key="a">test</block>`, `<block wx:for="{{a}}" wx:for-item="item" wx:key="a">test</block>`,
`(_ctx, _cache) => { `(_ctx, _cache) => {
return { return {
a: vFor(_ctx.items, item => { a: _vFor(_ctx.items, item => {
return { return {
a: _ctx.itemKey(item) a: _ctx.itemKey(item)
}; };
...@@ -742,7 +742,7 @@ return { ...@@ -742,7 +742,7 @@ return {
`<block wx:for="{{a}}" wx:for-item="item" key="key">test</block>`, `<block wx:for="{{a}}" wx:for-item="item" key="key">test</block>`,
`(_ctx, _cache) => { `(_ctx, _cache) => {
return { return {
a: vFor(_ctx.items, item => { a: _vFor(_ctx.items, item => {
return {}; return {};
}) })
} }
......
import { ElementNode } from '@vue/compiler-core'
import { compile } from '../src'
import { X_V_ON_DYNAMIC_EVENT } from '../src/errors'
import { CompilerOptions } from '../src/options'
function parseWithVOn(template: string, options: CompilerOptions = {}) {
const { ast } = compile(template, options)
return {
root: ast,
node: ast.children[0] as ElementNode,
}
}
describe('compiler(mp): transform v-on', () => {
test('should error if dynamic event', () => {
const onError = jest.fn()
parseWithVOn(`<div v-on:[event]="onClick" />`, { onError })
expect(onError.mock.calls[0][0]).toMatchObject({
code: X_V_ON_DYNAMIC_EVENT,
loc: {
start: {
line: 1,
column: 6,
},
end: {
line: 1,
column: 28,
},
},
})
})
})
import { ElementNode, ErrorCodes } from '@vue/compiler-core'
import { compile } from '../src'
import { CompilerOptions } from '../src/options'
import { assert } from './testUtils'
function parseWithVOn(template: string, options: CompilerOptions = {}) {
const { ast } = compile(template, options)
return {
root: ast,
node: ast.children[0] as ElementNode,
}
}
describe('compiler: transform v-on', () => {
test('basic', () => {
assert(
`<view v-on:click="onClick"/>`,
`<view bindtap="{{a}}"/>`,
`(_ctx, _cache) => {
return {
a: _vOn(_ctx.onClick)
}
}`
)
})
test('dynamic arg', () => {
// <view v-on:[event]="handler"/>
})
test('dynamic arg with prefixing', () => {
// <view v-on:[event]="handler"/>
})
test('dynamic arg with complex exp prefixing', () => {
// <view v-on:[event(foo)]="handler"/>
})
test('should wrap as function if expression is inline statement', () => {
assert(
`<view @click="i++"/>`,
`<view bindtap="{{a}}"/>`,
`(_ctx, _cache) => {
return {
a: _vOn($event => _ctx.i++)
}
}`
)
})
test('should handle multiple inline statement', () => {
assert(
`<view @click="foo();bar()"/>`,
`<view bindtap="{{a}}"/>`,
`(_ctx, _cache) => {
return {
a: _vOn($event => {
_ctx.foo();
_ctx.bar();
})
}
}`
)
})
test('should handle multi-line statement', () => {
assert(
`<view @click="\nfoo();\nbar()\n"/>`,
`<view bindtap="{{a}}"/>`,
`(_ctx, _cache) => {
return {
a: _vOn($event => {
foo();
bar();
})
}
}`,
{ prefixIdentifiers: false }
)
})
test('inline statement w/ prefixIdentifiers: true', () => {
assert(
`<view @click="foo($event)"/>`,
`<view bindtap="{{a}}"/>`,
`(_ctx, _cache) => {
return {
a: _vOn($event => _ctx.foo($event))
}
}`
)
})
test('multiple inline statements w/ prefixIdentifiers: true', () => {
assert(
`<view @click="foo($event);bar()"/>`,
`<view bindtap="{{a}}"/>`,
`(_ctx, _cache) => {
return {
a: _vOn($event => {
_ctx.foo($event);
_ctx.bar();
})
}
}`
)
})
test('should NOT wrap as function if expression is already function expression', () => {
assert(
`<view @click="$event => foo($event)"/>`,
`<view bindtap="{{a}}"/>`,
`(_ctx, _cache) => {
return {
a: _vOn($event => _ctx.foo($event))
}
}`
)
})
test('should NOT wrap as function if expression is already function expression (with newlines)', () => {
assert(
`<view @click="
$event => {
foo($event)
}
"/>`,
`<view bindtap="{{a}}"/>`,
`(_ctx, _cache) => {
return {
a: _vOn($event => {
_ctx.foo($event);
})
}
}`
)
})
test('should NOT wrap as function if expression is already function expression (with newlines + function keyword)', () => {
assert(
`<view @click="
function($event) {
foo($event)
}
"/>`,
`<view bindtap="{{a}}"/>`,
`(_ctx, _cache) => {
return {
a: _vOn(function ($event) {
_ctx.foo($event);
})
}
}`
)
})
test('should NOT wrap as function if expression is complex member expression', () => {
assert(
`<view @click="a['b' + c]"/>`,
`<view bindtap="{{a}}"/>`,
`(_ctx, _cache) => {
return {
a: _vOn(a['b' + c])
}
}`,
{
prefixIdentifiers: false,
}
)
})
test('complex member expression w/ prefixIdentifiers: true', () => {
assert(
`<view @click="a['b' + c]"/>`,
`<view bindtap="{{a}}"/>`,
`(_ctx, _cache) => {
return {
a: _vOn(_ctx.a['b' + _ctx.c])
}
}`
)
})
test('function expression w/ prefixIdentifiers: true', () => {
assert(
`<view @click="e => foo(e)"/>`,
`<view bindtap="{{a}}"/>`,
`(_ctx, _cache) => {
return {
a: _vOn(e => _ctx.foo(e))
}
}`
)
})
test('should error if no expression AND no modifier', () => {
const onError = jest.fn()
parseWithVOn(`<div v-on:click />`, { onError })
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_V_ON_NO_EXPRESSION,
loc: {
start: {
line: 1,
column: 6,
},
end: {
line: 1,
column: 16,
},
},
})
})
test('should NOT error if no expression but has modifier', () => {
const onError = jest.fn()
parseWithVOn(`<div v-on:click.prevent />`, { onError })
expect(onError).not.toHaveBeenCalled()
})
test('case conversion for kebab-case events', () => {
assert(
`<view v-on:foo-bar="onMount"/>`,
`<view bind:foo-bar="{{a}}"/>`,
`(_ctx, _cache) => {
return {
a: _vOn(_ctx.onMount)
}
}`
)
})
test('case conversion for vnode hooks', () => {
assert(
`<view v-on:vnode-mounted="onMount"/>`,
`<view bind:vnode-mounted="{{a}}"/>`,
`(_ctx, _cache) => {
return {
a: _vOn(_ctx.onMount)
}
}`
)
})
describe('cacheHandler', () => {
test('empty handler', () => {
assert(
`<view v-on:click.prevent />`,
`<view catchtap="{{a}}"/>`,
`(_ctx, _cache) => {
return {
a: _vOn(() => {})
}
}`
)
})
test('member expression handler', () => {
// <div v-on:click="foo" />
})
test('compound member expression handler', () => {
// <div v-on:click="foo.bar" />
})
test('bail on component member expression handler', () => {
// <comp v-on:click="foo" />
})
test('should not be cached inside v-once', () => {
// <div v-once><div v-on:click="foo"/></div>
})
test('inline function expression handler', () => {
// <div v-on:click="() => foo()" />
})
test('inline async arrow function expression handler', () => {
// <div v-on:click="async () => await foo()" />
})
test('inline async function expression handler', () => {
// <div v-on:click="async function () { await foo() } " />
})
test('inline statement handler', () => {
// <div v-on:click="foo++" />
})
})
})
...@@ -32,6 +32,7 @@ import { ...@@ -32,6 +32,7 @@ import {
import { CodegenScope, CodegenVForScope, CodegenVIfScope } from './options' import { CodegenScope, CodegenVForScope, CodegenVIfScope } from './options'
import { TransformContext } from './transform' import { TransformContext } from './transform'
import { genExpr } from './codegen' import { genExpr } from './codegen'
import { V_FOR } from './runtimeHelpers'
export function createIdentifier(name: string) { export function createIdentifier(name: string) {
return identifier(name) return identifier(name)
...@@ -78,12 +79,15 @@ function numericLiteralToArrayExpr(num: number) { ...@@ -78,12 +79,15 @@ function numericLiteralToArrayExpr(num: number) {
return arrayExpression(elements) return arrayExpression(elements)
} }
export function createVForCallExpression(vForScope: CodegenVForScope) { export function createVForCallExpression(
vForScope: CodegenVForScope,
context: TransformContext
) {
let sourceExpr: Expression = vForScope.sourceExpr! let sourceExpr: Expression = vForScope.sourceExpr!
if (isNumericLiteral(sourceExpr)) { if (isNumericLiteral(sourceExpr)) {
sourceExpr = numericLiteralToArrayExpr((sourceExpr as NumericLiteral).value) sourceExpr = numericLiteralToArrayExpr((sourceExpr as NumericLiteral).value)
} }
return callExpression(identifier('vFor'), [ return callExpression(identifier(context.helperString(V_FOR)), [
sourceExpr, sourceExpr,
createVForArrowFunctionExpression(vForScope), createVForArrowFunctionExpression(vForScope),
]) ])
......
...@@ -9,37 +9,45 @@ import { transformIdentifier } from './transforms/transformIdentifier' ...@@ -9,37 +9,45 @@ import { transformIdentifier } from './transforms/transformIdentifier'
import { transformIf } from './transforms/vIf' import { transformIf } from './transforms/vIf'
import { transformFor } from './transforms/vFor' import { transformFor } from './transforms/vFor'
import { generate as genTemplate } from './template/codegen' import { generate as genTemplate } from './template/codegen'
import { transformOn } from './transforms/vOn'
import { transformElement } from './transforms/transformElement'
export type TransformPreset = [ export type TransformPreset = [
NodeTransform[], NodeTransform[],
Record<string, DirectiveTransform> Record<string, DirectiveTransform>
] ]
export function getBaseTransformPreset( export function getBaseTransformPreset({
prefixIdentifiers?: boolean prefixIdentifiers,
): TransformPreset { skipTransformIdentifier,
}: {
prefixIdentifiers: boolean
skipTransformIdentifier: boolean
}): TransformPreset {
const nodeTransforms = [transformIf, transformFor] const nodeTransforms = [transformIf, transformFor]
if (!skipTransformIdentifier) {
nodeTransforms.push(transformIdentifier)
}
nodeTransforms.push(transformElement)
if (prefixIdentifiers) { if (prefixIdentifiers) {
nodeTransforms.push(transformExpression) nodeTransforms.push(transformExpression)
} }
return [nodeTransforms, {}] return [nodeTransforms, { on: transformOn }]
} }
export function baseCompile(template: string, options: CompilerOptions = {}) { export function baseCompile(template: string, options: CompilerOptions = {}) {
const prefixIdentifiers = const prefixIdentifiers =
options.prefixIdentifiers === true || options.mode === 'module' options.prefixIdentifiers === true || options.mode === 'module'
const ast = isString(template) ? baseParse(template, options) : template const ast = isString(template) ? baseParse(template, options) : template
const [nodeTransforms, directiveTransforms] = const [nodeTransforms, directiveTransforms] = getBaseTransformPreset({
getBaseTransformPreset(prefixIdentifiers) prefixIdentifiers,
skipTransformIdentifier: options.skipTransformIdentifier === true,
})
const context = transform( const context = transform(
ast, ast,
extend({}, options, { extend({}, options, {
prefixIdentifiers, prefixIdentifiers,
nodeTransforms: [ nodeTransforms: [...nodeTransforms, ...(options.nodeTransforms || [])],
...nodeTransforms,
...(options.nodeTransforms || []),
...(options.skipTransformIdentifier ? [] : [transformIdentifier]),
],
directiveTransforms: extend( directiveTransforms: extend(
{}, {},
directiveTransforms, directiveTransforms,
......
export const X_V_ON_DYNAMIC_EVENT = 0
export const errorMessages: Record<number, string> = {
[X_V_ON_DYNAMIC_EVENT]: 'v-on:[event] is not supported.',
}
...@@ -10,6 +10,8 @@ import { baseCompile } from './compile' ...@@ -10,6 +10,8 @@ import { baseCompile } from './compile'
import { parserOptions } from './parserOptions' import { parserOptions } from './parserOptions'
import { CompilerOptions } from './options' import { CompilerOptions } from './options'
export * from './runtimeHelpers'
export function parse(template: string, options: ParserOptions = {}): RootNode { export function parse(template: string, options: ParserOptions = {}): RootNode {
return baseParse(template, extend({}, parserOptions, options)) return baseParse(template, extend({}, parserOptions, options))
} }
......
...@@ -21,6 +21,7 @@ interface SharedTransformCodegenOptions { ...@@ -21,6 +21,7 @@ interface SharedTransformCodegenOptions {
export interface TransformOptions export interface TransformOptions
extends SharedTransformCodegenOptions, extends SharedTransformCodegenOptions,
ErrorHandlingOptions { ErrorHandlingOptions {
cacheHandlers?: boolean
nodeTransforms?: NodeTransform[] nodeTransforms?: NodeTransform[]
directiveTransforms?: Record<string, DirectiveTransform | undefined> directiveTransforms?: Record<string, DirectiveTransform | undefined>
isBuiltInComponent?: (tag: string) => symbol | void isBuiltInComponent?: (tag: string) => symbol | void
......
import { registerRuntimeHelpers } from '@vue/compiler-core'
export const V_ON = Symbol(`vOn`)
export const V_FOR = Symbol(`vFor`)
registerRuntimeHelpers({
[V_ON]: 'vOn',
[V_FOR]: 'vFor',
})
import { formatMiniProgramEvent } from '@dcloudio/uni-cli-shared'
import { import {
AttributeNode, AttributeNode,
DirectiveNode, DirectiveNode,
...@@ -141,13 +142,26 @@ export function genElementProps( ...@@ -141,13 +142,26 @@ export function genElementProps(
} }
} else { } else {
const { name } = prop const { name } = prop
if (name === 'bind') { push(` `)
push(` `) if (name === 'on') {
genOn(prop, context)
} else {
genDirectiveNode(prop, context) genDirectiveNode(prop, context)
} }
} }
}) })
} }
function genOn(prop: DirectiveNode, { push }: TemplateCodegenContext) {
const arg = (prop.arg as SimpleExpressionNode).content
const exp = (prop.exp as SimpleExpressionNode).content
const modifiers = prop.modifiers
push(
`${formatMiniProgramEvent(arg, {
isCatch: modifiers.includes('stop') || modifiers.includes('prevent'),
isCapture: modifiers.includes('capture'),
})}="{{${exp}}}"`
)
}
function genDirectiveNode( function genDirectiveNode(
prop: DirectiveNode, prop: DirectiveNode,
......
import { NOOP, EMPTY_OBJ, extend, isString, isArray } from '@vue/shared' import {
NOOP,
EMPTY_OBJ,
extend,
isString,
isArray,
capitalize,
camelize,
} from '@vue/shared'
import { import {
DirectiveNode, DirectiveNode,
...@@ -15,6 +23,9 @@ import { ...@@ -15,6 +23,9 @@ import {
ExpressionNode, ExpressionNode,
ElementTypes, ElementTypes,
isVSlot, isVSlot,
JSChildNode,
CacheExpression,
locStub,
} from '@vue/compiler-core' } from '@vue/compiler-core'
import IdentifierGenerator from './identifier' import IdentifierGenerator from './identifier'
import { import {
...@@ -51,16 +62,20 @@ export interface ErrorHandlingOptions { ...@@ -51,16 +62,20 @@ export interface ErrorHandlingOptions {
export interface TransformContext export interface TransformContext
extends Required<Omit<TransformOptions, 'filename'>> { extends Required<Omit<TransformOptions, 'filename'>> {
selfName: string | null
currentNode: RootNode | TemplateChildNode | null currentNode: RootNode | TemplateChildNode | null
parent: ParentNode | null parent: ParentNode | null
childIndex: number childIndex: number
helpers: Map<symbol, number> helpers: Map<symbol, number>
components: Set<string>
identifiers: { [name: string]: number | undefined } identifiers: { [name: string]: number | undefined }
cached: number
scopes: { scopes: {
vFor: number vFor: number
} }
scope: CodegenRootScope scope: CodegenRootScope
currentScope: CodegenScope currentScope: CodegenScope
inVOnce: boolean
helper<T extends symbol>(name: T): T helper<T extends symbol>(name: T): T
removeHelper<T extends symbol>(name: T): void removeHelper<T extends symbol>(name: T): void
helperString(name: symbol): string helperString(name: symbol): string
...@@ -72,6 +87,7 @@ export interface TransformContext ...@@ -72,6 +87,7 @@ export interface TransformContext
popScope(): CodegenScope | undefined popScope(): CodegenScope | undefined
addVIfScope(initScope: CodegenVIfScopeInit): CodegenVIfScope addVIfScope(initScope: CodegenVIfScopeInit): CodegenVIfScope
addVForScope(initScope: CodegenVForScopeInit): CodegenVForScope addVForScope(initScope: CodegenVForScopeInit): CodegenVForScope
cache<T extends JSChildNode>(exp: T, isVNode?: boolean): CacheExpression | T
} }
export function isRootScope(scope: CodegenScope): scope is CodegenRootScope { export function isRootScope(scope: CodegenScope): scope is CodegenRootScope {
...@@ -175,9 +191,11 @@ function defaultOnWarn(msg: CompilerError) { ...@@ -175,9 +191,11 @@ function defaultOnWarn(msg: CompilerError) {
export function createTransformContext( export function createTransformContext(
root: RootNode, root: RootNode,
{ {
filename = '',
isTS = false, isTS = false,
inline = false, inline = false,
bindingMetadata = EMPTY_OBJ, bindingMetadata = EMPTY_OBJ,
cacheHandlers = false,
prefixIdentifiers = false, prefixIdentifiers = false,
skipTransformIdentifier = false, skipTransformIdentifier = false,
nodeTransforms = [], nodeTransforms = [],
...@@ -212,11 +230,14 @@ export function createTransformContext( ...@@ -212,11 +230,14 @@ export function createTransformContext(
} }
const identifiers = Object.create(null) const identifiers = Object.create(null)
const scopes: CodegenScope[] = [scope] const scopes: CodegenScope[] = [scope]
const nameMatch = filename.replace(/\?.*$/, '').match(/([^/\\]+)\.\w+$/)
const context: TransformContext = { const context: TransformContext = {
// options // options
selfName: nameMatch && capitalize(camelize(nameMatch[1])),
isTS, isTS,
inline, inline,
bindingMetadata, bindingMetadata,
cacheHandlers,
prefixIdentifiers, prefixIdentifiers,
nodeTransforms, nodeTransforms,
directiveTransforms, directiveTransforms,
...@@ -230,6 +251,8 @@ export function createTransformContext( ...@@ -230,6 +251,8 @@ export function createTransformContext(
parent: null, parent: null,
childIndex: 0, childIndex: 0,
helpers: new Map(), helpers: new Map(),
components: new Set(),
cached: 0,
identifiers, identifiers,
scope, scope,
scopes: { scopes: {
...@@ -239,6 +262,7 @@ export function createTransformContext( ...@@ -239,6 +262,7 @@ export function createTransformContext(
return scopes[scopes.length - 1] return scopes[scopes.length - 1]
}, },
currentNode: root, currentNode: root,
inVOnce: false,
// methods // methods
popScope() { popScope() {
return scopes.pop() return scopes.pop()
...@@ -327,6 +351,9 @@ export function createTransformContext( ...@@ -327,6 +351,9 @@ export function createTransformContext(
removeId(exp.content) removeId(exp.content)
} }
}, },
cache(exp, isVNode = false) {
return createCacheExpression(context.cached++, exp, isVNode)
},
} }
function addId(id: string) { function addId(id: string) {
...@@ -344,6 +371,20 @@ export function createTransformContext( ...@@ -344,6 +371,20 @@ export function createTransformContext(
return context return context
} }
function createCacheExpression(
index: number,
value: JSChildNode,
isVNode: boolean = false
): CacheExpression {
return {
type: NodeTypes.JS_CACHE_EXPRESSION,
index,
value,
isVNode,
loc: locStub,
}
}
export declare type StructuralDirectiveTransform = ( export declare type StructuralDirectiveTransform = (
node: ElementNode, node: ElementNode,
dir: DirectiveNode, dir: DirectiveNode,
......
import {
NodeTypes,
ElementTypes,
createCompilerError,
ErrorCodes,
ElementNode,
isBindKey,
TemplateLiteral,
Property,
ExpressionNode,
} from '@vue/compiler-core'
import { NodeTransform, TransformContext } from '../transform'
export interface DirectiveTransformResult {
props: Property[]
needRuntime?: boolean | symbol
ssrTagParts?: TemplateLiteral['elements']
}
export const transformElement: NodeTransform = (node, context) => {
return function postTransformElement() {
node = context.currentNode!
if (
!(
node.type === NodeTypes.ELEMENT &&
(node.tagType === ElementTypes.ELEMENT ||
node.tagType === ElementTypes.COMPONENT)
)
) {
return
}
const { props } = node
if (props.length > 0) {
processProps(node, context)
}
}
}
function processProps(node: ElementNode, context: TransformContext) {
const { tag, props } = node
const isComponent = node.tagType === ElementTypes.COMPONENT
for (let i = 0; i < props.length; i++) {
const prop = props[i]
if (prop.type === NodeTypes.DIRECTIVE) {
// directives
const { name, arg, exp, loc } = prop
const isVBind = name === 'bind'
const isVOn = name === 'on'
// skip v-slot - it is handled by its dedicated transform.
if (name === 'slot') {
if (!isComponent) {
context.onError(
createCompilerError(ErrorCodes.X_V_SLOT_MISPLACED, loc)
)
}
continue
}
// skip v-once/v-memo - they are handled by dedicated transforms.
if (name === 'once' || name === 'memo') {
continue
}
// skip v-is and :is on <component>
if (
name === 'is' ||
(isVBind && isBindKey(arg, 'is') && isComponentTag(tag))
) {
continue
}
// special case for v-bind and v-on with no argument
if (!arg && (isVBind || isVOn)) {
if (exp) {
if (isVOn) {
context.onError(
createCompilerError(ErrorCodes.X_V_ON_NO_EXPRESSION, loc)
)
}
} else {
context.onError(
createCompilerError(
isVBind
? ErrorCodes.X_V_BIND_NO_EXPRESSION
: ErrorCodes.X_V_ON_NO_EXPRESSION,
loc
)
)
}
continue
}
const directiveTransform = context.directiveTransforms[name]
if (directiveTransform) {
const { props } = directiveTransform(prop, node, context)
prop.exp = props[0].value as ExpressionNode
console.log('prop', prop)
}
}
}
}
function isComponentTag(tag: string) {
return tag[0].toLowerCase() + tag.slice(1) === 'component'
}
...@@ -45,7 +45,6 @@ export const transformIdentifier: NodeTransform = (node, context) => { ...@@ -45,7 +45,6 @@ export const transformIdentifier: NodeTransform = (node, context) => {
exp.content = '*this' exp.content = '*this'
continue continue
} }
dir.exp = rewriteExpression(exp, context) dir.exp = rewriteExpression(exp, context)
} }
} }
......
...@@ -149,7 +149,7 @@ export const transformFor = createStructuralDirectiveTransform( ...@@ -149,7 +149,7 @@ export const transformFor = createStructuralDirectiveTransform(
const id = parentScope.id.next() const id = parentScope.id.next()
vFor.sourceAlias = id vFor.sourceAlias = id
parentScope.properties.push( parentScope.properties.push(
createObjectProperty(id, createVForCallExpression(vForScope)) createObjectProperty(id, createVForCallExpression(vForScope, context))
) )
popScope() popScope()
} }
......
import {
createCompilerError,
createCompoundExpression,
createObjectProperty,
createSimpleExpression,
DirectiveNode,
ElementTypes,
ErrorCodes,
ExpressionNode,
hasScopeRef,
isMemberExpression,
NodeTypes,
SimpleExpressionNode,
TO_HANDLER_KEY,
} from '@vue/compiler-core'
import { camelize, toHandlerKey } from '@vue/shared'
import { V_ON } from '..'
import { errorMessages, X_V_ON_DYNAMIC_EVENT } from '../errors'
import { DirectiveTransform, TransformContext } from '../transform'
import { DirectiveTransformResult } from './transformElement'
import { processExpression } from './transformExpression'
const fnExpRE =
/^\s*([\w$_]+|(async\s*)?\([^)]*?\))\s*=>|^\s*(async\s+)?function(?:\s+[\w$]+)?\s*\(/
export interface VOnDirectiveNode extends DirectiveNode {
// v-on without arg is handled directly in ./transformElements.ts due to it affecting
// codegen for the entire props object. This transform here is only for v-on
// *with* args.
arg: ExpressionNode
// exp is guaranteed to be a simple expression here because v-on w/ arg is
// skipped by transformExpression as a special case.
exp: SimpleExpressionNode | undefined
}
export const transformOn: DirectiveTransform = (
dir,
node,
context,
augmentor
) => {
const { loc, modifiers, arg } = dir as VOnDirectiveNode
if (!dir.exp && !modifiers.length) {
context.onError(createCompilerError(ErrorCodes.X_V_ON_NO_EXPRESSION, loc))
}
let eventName: ExpressionNode
if (arg.type === NodeTypes.SIMPLE_EXPRESSION) {
if (arg.isStatic) {
const rawName = arg.content
// for all event listeners, auto convert it to camelCase. See issue #2249
eventName = createSimpleExpression(
toHandlerKey(camelize(rawName)),
true,
arg.loc
)
} else {
// TODO 不支持动态事件
context.onError(
createCompilerError(X_V_ON_DYNAMIC_EVENT, loc, errorMessages)
)
// #2388
eventName = createCompoundExpression([
// `${context.helperString(TO_HANDLER_KEY)}(`,
arg,
// `)`,
])
}
} else {
// already a compound expression.
eventName = arg
eventName.children.unshift(`${context.helperString(TO_HANDLER_KEY)}(`)
eventName.children.push(`)`)
}
// handler processing
let exp: ExpressionNode | undefined = dir.exp as
| SimpleExpressionNode
| undefined
if (exp && !exp.content.trim()) {
exp = undefined
}
let shouldCache: boolean = context.cacheHandlers && !exp && !context.inVOnce
if (exp) {
const isMemberExp = isMemberExpression(exp.content, context as any)
const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content))
const hasMultipleStatements = exp.content.includes(`;`)
// process the expression since it's been skipped
if (context.prefixIdentifiers) {
isInlineStatement && context.addIdentifiers(`$event`)
exp = dir.exp = processExpression(
exp,
context,
false,
hasMultipleStatements
)
isInlineStatement && context.removeIdentifiers(`$event`)
// with scope analysis, the function is hoistable if it has no reference
// to scope variables.
shouldCache =
context.cacheHandlers &&
// unnecessary to cache inside v-once
!context.inVOnce &&
// runtime constants don't need to be cached
// (this is analyzed by compileScript in SFC <script setup>)
!(exp.type === NodeTypes.SIMPLE_EXPRESSION && exp.constType > 0) &&
// #1541 bail if this is a member exp handler passed to a component -
// we need to use the original function to preserve arity,
// e.g. <transition> relies on checking cb.length to determine
// transition end handling. Inline function is ok since its arity
// is preserved even when cached.
!(isMemberExp && node.tagType === ElementTypes.COMPONENT) &&
// bail if the function references closure variables (v-for, v-slot)
// it must be passed fresh to avoid stale values.
!hasScopeRef(exp, context.identifiers)
// If the expression is optimizable and is a member expression pointing
// to a function, turn it into invocation (and wrap in an arrow function
// below) so that it always accesses the latest value when called - thus
// avoiding the need to be patched.
if (shouldCache && isMemberExp) {
if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
exp.content = `${exp.content} && ${exp.content}(...args)`
} else {
exp.children = [...exp.children, ` && `, ...exp.children, `(...args)`]
}
}
}
if (isInlineStatement || (shouldCache && isMemberExp)) {
// wrap inline statement in a function expression
exp = createCompoundExpression([
`${
isInlineStatement
? context.isTS
? `($event: any)`
: `$event`
: `${context.isTS ? `\n//@ts-ignore\n` : ``}(...args)`
} => ${hasMultipleStatements ? `{` : `(`}`,
exp,
hasMultipleStatements ? `}` : `)`,
])
}
}
let ret: DirectiveTransformResult = {
props: [
createObjectProperty(
eventName,
exp || createSimpleExpression(`() => {}`, false, loc)
),
],
}
// apply extended compiler augmentor
if (augmentor) {
ret = augmentor(ret)
}
// TODO
if (shouldCache) {
// cache handlers so that it's always the same handler being passed down.
// this avoids unnecessary re-renders when users use inline handlers on
// components.
// ret.props[0].value = wrapper(
// context.cache(ret.props[0].value) as ExpressionNode,
// context
// )
ret.props[0].value = wrapper(ret.props[0].value as ExpressionNode, context)
} else {
ret.props[0].value = wrapper(ret.props[0].value as ExpressionNode, context)
}
// mark the key as handler for props normalization check
ret.props.forEach((p) => (p.key.isHandlerKey = true))
return ret
}
function wrapper(value: ExpressionNode, context: TransformContext) {
return createCompoundExpression([
`${context.helperString(V_ON)}(`,
value,
`)`,
])
}
...@@ -4951,10 +4951,51 @@ var plugin = { ...@@ -4951,10 +4951,51 @@ var plugin = {
}, },
}; };
function vOn(value) {
const instance = getCurrentInstance();
const name = 'e' + instance.$ei++;
const mpInstance = instance.ctx.$scope;
if (!value) {
// remove
delete mpInstance[name];
return name;
}
const existingInvoker = mpInstance[name];
if (existingInvoker) {
// patch
existingInvoker.value = value;
}
else {
// add
mpInstance[name] = createInvoker(value, instance);
}
return name;
}
function createInvoker(initialValue, instance) {
const invoker = (e) => {
callWithAsyncErrorHandling(patchStopImmediatePropagation(e, invoker.value), instance, 5 /* NATIVE_EVENT_HANDLER */, [e]);
};
invoker.value = initialValue;
return invoker;
}
function patchStopImmediatePropagation(e, value) {
if (isArray(value)) {
const originalStop = e.stopImmediatePropagation;
e.stopImmediatePropagation = () => {
originalStop && originalStop.call(e);
e._stopped = true;
};
return value.map((fn) => (e) => !e._stopped && fn(e));
}
else {
return value;
}
}
function createApp(rootComponent, rootProps = null) { function createApp(rootComponent, rootProps = null) {
rootComponent && (rootComponent.mpType = 'app'); rootComponent && (rootComponent.mpType = 'app');
return createVueApp(rootComponent, rootProps).use(plugin); return createVueApp(rootComponent, rootProps).use(plugin);
} }
const createSSRApp = createApp; const createSSRApp = createApp;
export { EffectScope, ReactiveEffect, callWithAsyncErrorHandling, callWithErrorHandling, computed, createApp, createSSRApp, createVNode$1 as createVNode, createVueApp, customRef, defineComponent, defineEmits, defineExpose, defineProps, effect, effectScope, getCurrentInstance, getCurrentScope, inject, injectHook, isInSSRComponentSetup, isProxy, isReactive, isReadonly, isRef, logError, markRaw, mergeDefaults, mergeProps, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onDeactivated, onErrorCaptured, onMounted, onRenderTracked, onRenderTriggered, onScopeDispose, onUnmounted, onUpdated, provide, proxyRefs, queuePostFlushCb, reactive, readonly, ref, resolveComponent, resolveDirective, resolveFilter, shallowReactive, shallowReadonly, shallowRef, stop, toHandlers, toRaw, toRef, toRefs, triggerRef, unref, useAttrs, useSSRContext, useSlots, version, warn$1 as warn, watch, watchEffect, watchPostEffect, watchSyncEffect, withAsyncContext, withCtx, withDefaults, withDirectives, withModifiers, withScopeId }; export { EffectScope, ReactiveEffect, callWithAsyncErrorHandling, callWithErrorHandling, computed, createApp, createSSRApp, createVNode$1 as createVNode, createVueApp, customRef, defineComponent, defineEmits, defineExpose, defineProps, effect, effectScope, getCurrentInstance, getCurrentScope, inject, injectHook, isInSSRComponentSetup, isProxy, isReactive, isReadonly, isRef, logError, markRaw, mergeDefaults, mergeProps, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onDeactivated, onErrorCaptured, onMounted, onRenderTracked, onRenderTriggered, onScopeDispose, onUnmounted, onUpdated, provide, proxyRefs, queuePostFlushCb, reactive, readonly, ref, resolveComponent, resolveDirective, resolveFilter, shallowReactive, shallowReadonly, shallowRef, stop, toHandlers, toRaw, toRef, toRefs, triggerRef, unref, useAttrs, useSSRContext, useSlots, vOn, version, warn$1 as warn, watch, watchEffect, watchPostEffect, watchSyncEffect, withAsyncContext, withCtx, withDefaults, withDirectives, withModifiers, withScopeId };
import { isArray } from '@vue/shared'
import {
callWithAsyncErrorHandling,
ComponentInternalInstance,
ErrorCodes,
getCurrentInstance,
} from 'vue'
type EventValue = Function | Function[]
interface Invoker extends EventListener {
value: EventValue
}
export function vOn(value: EventValue | undefined) {
const instance = getCurrentInstance()! as unknown as {
$ei: number
ctx: { $scope: Record<string, any> }
}
const name = 'e' + instance.$ei++
const mpInstance = instance.ctx.$scope
if (!value) {
// remove
delete mpInstance[name]
return name
}
const existingInvoker = mpInstance[name] as Invoker
if (existingInvoker) {
// patch
existingInvoker.value = value
} else {
// add
mpInstance[name] = createInvoker(
value,
instance as unknown as ComponentInternalInstance
)
}
return name
}
function createInvoker(
initialValue: EventValue,
instance: ComponentInternalInstance | null
) {
const invoker: Invoker = (e: Event) => {
callWithAsyncErrorHandling(
patchStopImmediatePropagation(e, invoker.value),
instance,
ErrorCodes.NATIVE_EVENT_HANDLER,
[e]
)
}
invoker.value = initialValue
return invoker
}
function patchStopImmediatePropagation(
e: Event,
value: EventValue
): EventValue {
if (isArray(value)) {
const originalStop = e.stopImmediatePropagation
e.stopImmediatePropagation = () => {
originalStop && originalStop.call(e)
;(e as any)._stopped = true
}
return value.map((fn) => (e: Event) => !(e as any)._stopped && fn(e))
} else {
return value
}
}
...@@ -5,6 +5,7 @@ export function createApp(rootComponent: unknown, rootProps = null) { ...@@ -5,6 +5,7 @@ export function createApp(rootComponent: unknown, rootProps = null) {
rootComponent && ((rootComponent as any).mpType = 'app') rootComponent && ((rootComponent as any).mpType = 'app')
return createVueApp(rootComponent, rootProps).use(plugin) return createVueApp(rootComponent, rootProps).use(plugin)
} }
export { vOn } from './helpers/vOn'
export const createSSRApp = createApp export const createSSRApp = createApp
// @ts-ignore // @ts-ignore
export * from '../lib/vue.runtime.esm.js' export * from '../lib/vue.runtime.esm.js'
'use strict'; 'use strict';
var version = "3.0.0-alpha-3021020211012004"; var version = "3.0.0-alpha-3021020211012005";
const STAT_VERSION = version; const STAT_VERSION = version;
const STAT_URL = 'https://tongji.dcloud.io/uni/stat'; const STAT_URL = 'https://tongji.dcloud.io/uni/stat';
......
var version = "3.0.0-alpha-3021020211012004"; var version = "3.0.0-alpha-3021020211012005";
const STAT_VERSION = version; const STAT_VERSION = version;
const STAT_URL = 'https://tongji.dcloud.io/uni/stat'; const STAT_URL = 'https://tongji.dcloud.io/uni/stat';
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册