feat: runtime component name validation (#217)

This commit is contained in:
月迷津渡 2019-10-15 03:36:30 +08:00 committed by Evan You
parent fd209f5a66
commit 66023a8886
4 changed files with 131 additions and 5 deletions

View File

@ -286,4 +286,85 @@ describe('api: createApp', () => {
app.mount(Root, nodeOps.createElement('div'))
expect(handler).toHaveBeenCalledTimes(1)
})
describe('config.isNativeTag', () => {
const isNativeTag = jest.fn(tag => tag === 'div')
test('Component.name', () => {
const app = createApp()
Object.defineProperty(app.config, 'isNativeTag', {
value: isNativeTag,
writable: false
})
const Root = {
name: 'div',
setup() {
return {
count: ref(0)
}
},
render() {
return null
}
}
app.mount(Root, nodeOps.createElement('div'))
expect(
`Do not use built-in or reserved HTML elements as component id: div`
).toHaveBeenWarned()
})
test('Component.components', () => {
const app = createApp()
Object.defineProperty(app.config, 'isNativeTag', {
value: isNativeTag,
writable: false
})
const Root = {
components: {
div: () => 'div'
},
setup() {
return {
count: ref(0)
}
},
render() {
return null
}
}
app.mount(Root, nodeOps.createElement('div'))
expect(
`Do not use built-in or reserved HTML elements as component id: div`
).toHaveBeenWarned()
})
test('register using app.component', () => {
const app = createApp()
Object.defineProperty(app.config, 'isNativeTag', {
value: isNativeTag,
writable: false
})
const Root = {
setup() {
return {
count: ref(0)
}
},
render() {
return null
}
}
app.component('div', () => 'div')
app.mount(Root, nodeOps.createElement('div'))
expect(
`Do not use built-in or reserved HTML elements as component id: div`
).toHaveBeenWarned()
})
})
})

View File

@ -1,10 +1,10 @@
import { Component, Data } from './component'
import { Component, Data, validateComponentName } from './component'
import { ComponentOptions } from './apiOptions'
import { ComponentPublicInstance } from './componentProxy'
import { Directive } from './directives'
import { RootRenderFunction } from './createRenderer'
import { InjectionKey } from './apiInject'
import { isFunction } from '@vue/shared'
import { isFunction, NO } from '@vue/shared'
import { warn } from './warning'
import { createVNode } from './vnode'
@ -27,6 +27,7 @@ export interface App<HostElement = any> {
export interface AppConfig {
devtools: boolean
performance: boolean
readonly isNativeTag?: (tag: string) => boolean
errorHandler?: (
err: Error,
instance: ComponentPublicInstance | null,
@ -60,6 +61,7 @@ export function createAppContext(): AppContext {
config: {
devtools: true,
performance: false,
isNativeTag: NO,
errorHandler: undefined,
warnHandler: undefined
},
@ -111,6 +113,9 @@ export function createAppAPI<HostNode, HostElement>(
},
component(name: string, component?: Component): any {
if (__DEV__) {
validateComponentName(name, context.config)
}
if (!component) {
return context.components[name]
} else {

View File

@ -12,7 +12,7 @@ import {
callWithErrorHandling,
callWithAsyncErrorHandling
} from './errorHandling'
import { AppContext, createAppContext } from './apiApp'
import { AppContext, createAppContext, AppConfig } from './apiApp'
import { Directive } from './directives'
import { applyOptions, ComponentOptions } from './apiOptions'
import {
@ -21,7 +21,8 @@ import {
capitalize,
NOOP,
isArray,
isObject
isObject,
NO
} from '@vue/shared'
import { SuspenseBoundary } from './suspense'
import {
@ -223,11 +224,37 @@ export const setCurrentInstance = (
currentInstance = instance
}
const BuiltInTagSet = new Set(['slot', 'component'])
const isBuiltInTag = (tag: string) => BuiltInTagSet.has(tag)
export function validateComponentName(name: string, config: AppConfig) {
const appIsNativeTag = config.isNativeTag || NO
if (isBuiltInTag(name) || appIsNativeTag(name)) {
warn(
'Do not use built-in or reserved HTML elements as component id: ' + name
)
}
}
export function setupStatefulComponent(
instance: ComponentInternalInstance,
parentSuspense: SuspenseBoundary | null
) {
const Component = instance.type as ComponentOptions
if (__DEV__) {
if (Component.name) {
validateComponentName(Component.name, instance.appContext.config)
}
if (Component.components) {
const names = Object.keys(Component.components)
for (let i = 0; i < names.length; i++) {
const name = names[i]
validateComponentName(name, instance.appContext.config)
}
}
}
// 1. create render proxy
instance.renderProxy = new Proxy(instance, PublicInstanceProxyHandlers)
// 2. create props proxy

View File

@ -1,4 +1,5 @@
import { createRenderer } from '@vue/runtime-core'
import { isHTMLTag, isSVGTag } from '@vue/shared'
import { nodeOps } from './nodeOps'
import { patchProp } from './patchProp'
@ -7,7 +8,19 @@ const { render, createApp } = createRenderer<Node, Element>({
...nodeOps
})
export { render, createApp }
const wrappedCreateApp = () => {
const app = createApp()
// inject `isNativeTag` dev only
Object.defineProperty(app.config, 'isNativeTag', {
value: (tag: string) => isHTMLTag(tag) || isSVGTag(tag),
writable: false
})
return app
}
const exportedCreateApp = __DEV__ ? wrappedCreateApp : createApp
export { render, exportedCreateApp as createApp }
// DOM-only runtime helpers
export {