From 32713f8fceb226a81d9669da090c54b2051ab876 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 2 Sep 2019 16:09:34 -0400 Subject: [PATCH] feat: createApp / appContext --- packages/runtime-core/src/apiCreateApp.ts | 168 ++++++++++++++++++++ packages/runtime-core/src/component.ts | 9 ++ packages/runtime-core/src/createRenderer.ts | 34 +++- packages/runtime-core/src/directives.ts | 3 +- packages/runtime-core/src/index.ts | 1 + packages/runtime-core/src/vnode.ts | 8 +- packages/runtime-dom/src/index.ts | 6 +- packages/runtime-test/src/index.ts | 6 +- 8 files changed, 226 insertions(+), 9 deletions(-) create mode 100644 packages/runtime-core/src/apiCreateApp.ts diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts new file mode 100644 index 00000000..7d6a7d47 --- /dev/null +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -0,0 +1,168 @@ +import { + ComponentOptions, + Component, + ComponentRenderProxy, + Data, + ComponentInstance +} from './component' +import { Directive } from './directives' +import { HostNode, RootRenderFunction } from './createRenderer' +import { InjectionKey } from './apiInject' +import { isFunction } from '@vue/shared' +import { warn } from './warning' +import { createVNode } from './vnode' + +export interface App { + config: AppConfig + use(plugin: Plugin, options?: any): this + mixin(mixin: ComponentOptions): this + component(name: string): Component | undefined + component(name: string, component: Component): this + directive(name: string): Directive | undefined + directive(name: string, directive: Directive): this + mount( + rootComponent: Component, + rootContainer: string | HostNode, + rootProps?: Data + ): ComponentRenderProxy + provide(key: InjectionKey | string, value: T): void +} + +export interface AppConfig { + silent: boolean + devtools: boolean + performance: boolean + errorHandler?: ( + err: Error, + instance: ComponentRenderProxy, + info: string + ) => void + warnHandler?: ( + msg: string, + instance: ComponentRenderProxy, + trace: string + ) => void + ignoredElements: Array + keyCodes: Record + optionMergeStrategies: { + [key: string]: ( + parent: any, + child: any, + instance: ComponentRenderProxy + ) => any + } +} + +export interface AppContext { + config: AppConfig + mixins: ComponentOptions[] + components: Record + directives: Record + provides: Record +} + +type PluginInstallFunction = (app: App) => any + +type Plugin = + | PluginInstallFunction + | { + install: PluginInstallFunction + } + +export function createAppContext(): AppContext { + return { + config: { + silent: false, + devtools: true, + performance: false, + errorHandler: undefined, + warnHandler: undefined, + ignoredElements: [], + keyCodes: {}, + optionMergeStrategies: {} + }, + mixins: [], + components: {}, + directives: {}, + provides: {} + } +} + +export function createAppAPI(render: RootRenderFunction): () => App { + return function createApp(): App { + const context = createAppContext() + + const app: App = { + get config() { + return context.config + }, + + set config(v) { + warn( + `app.config cannot be replaced. Modify individual options instead.` + ) + }, + + use(plugin: Plugin) { + if (isFunction(plugin)) { + plugin(app) + } else if (isFunction(plugin.install)) { + plugin.install(app) + } else if (__DEV__) { + warn( + `A plugin must either be a function or an object with an "install" ` + + `function.` + ) + } + return app + }, + + mixin(mixin: ComponentOptions) { + context.mixins.push(mixin) + return app + }, + + component(name: string, component?: Component) { + // TODO component name validation + if (!component) { + return context.components[name] as any + } else { + context.components[name] = component + return app + } + }, + + directive(name: string, directive?: Directive) { + // TODO directive name validation + if (!directive) { + return context.directives[name] as any + } else { + context.directives[name] = directive + return app + } + }, + + mount(rootComponent, rootContainer, rootProps?: Data) { + const vnode = createVNode(rootComponent, rootProps) + // store app context on the root VNode. + // this will be set on the root instance on initial mount. + vnode.appContext = context + render(vnode, rootContainer) + return (vnode.component as ComponentInstance) + .renderProxy as ComponentRenderProxy + }, + + provide(key, value) { + if (__DEV__ && key in context.provides) { + warn( + `App already provides property with key "${key}". ` + + `It will be overwritten with the new value.` + ) + } + context.provides[key as any] = value + } + } + + return app + } +} diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index a76095aa..e42183f8 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -13,6 +13,7 @@ import { callWithErrorHandling, callWithAsyncErrorHandling } from './errorHandling' +import { AppContext, createAppContext } from './apiCreateApp' export type Data = { [key: string]: unknown } @@ -79,6 +80,8 @@ export interface FunctionalComponent

{ displayName?: string } +export type Component = ComponentOptions | FunctionalComponent + type LifecycleHook = Function[] | null export const enum LifecycleHooks { @@ -107,6 +110,7 @@ interface SetupContext { export type ComponentInstance

= { type: FunctionalComponent | ComponentOptions parent: ComponentInstance | null + appContext: AppContext root: ComponentInstance vnode: VNode next: VNode | null @@ -184,6 +188,8 @@ export function createComponent(options: any) { return isFunction(options) ? { setup: options } : (options as any) } +const emptyAppContext = createAppContext() + export function createComponentInstance( vnode: VNode, parent: ComponentInstance | null @@ -191,6 +197,9 @@ export function createComponentInstance( const instance = { vnode, parent, + // inherit parent app context - or - if root, adopt from root vnode + appContext: + (parent ? parent.appContext : vnode.appContext) || emptyAppContext, type: vnode.type as any, root: null as any, // set later so it can point to itself next: null, diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index 7ad882b8..15980b6b 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -93,7 +93,12 @@ export interface RendererOptions { querySelector(selector: string): HostNode | null } -export function createRenderer(options: RendererOptions) { +export type RootRenderFunction = ( + vnode: VNode | null, + dom: HostNode | string +) => void + +export function createRenderer(options: RendererOptions): RootRenderFunction { const { insert: hostInsert, remove: hostRemove, @@ -1152,8 +1157,31 @@ export function createRenderer(options: RendererOptions) { } } - return function render(vnode: VNode | null, dom: HostNode): VNode | null { + return function render(vnode: VNode | null, dom: HostNode | string) { + if (isString(dom)) { + if (isFunction(hostQuerySelector)) { + dom = hostQuerySelector(dom) + if (!dom) { + if (__DEV__) { + warn( + `Failed to locate root container: ` + + `querySelector returned null.` + ) + } + return + } + } else { + if (__DEV__) { + warn( + `Failed to locate root container: ` + + `target platform does not support querySelector.` + ) + } + return + } + } if (vnode == null) { + debugger if (dom._vnode) { unmount(dom._vnode, null, true) } @@ -1161,7 +1189,7 @@ export function createRenderer(options: RendererOptions) { patch(dom._vnode, vnode, dom) } flushPostFlushCbs() - return (dom._vnode = vnode) + dom._vnode = vnode } } diff --git a/packages/runtime-core/src/directives.ts b/packages/runtime-core/src/directives.ts index 8e620b9d..e8079a2b 100644 --- a/packages/runtime-core/src/directives.ts +++ b/packages/runtime-core/src/directives.ts @@ -21,6 +21,7 @@ import { ComponentRenderProxy } from './component' import { callWithAsyncErrorHandling, ErrorTypes } from './errorHandling' +import { HostNode } from './createRenderer' export interface DirectiveBinding { instance: ComponentRenderProxy | null @@ -31,7 +32,7 @@ export interface DirectiveBinding { } export type DirectiveHook = ( - el: any, + el: HostNode, binding: DirectiveBinding, vnode: VNode, prevVNode: VNode | null diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index dba7dbae..f312a2fe 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -28,6 +28,7 @@ export { PublicShapeFlags as ShapeFlags } from './shapeFlags' export { getCurrentInstance } from './component' // For custom renderers +export { createAppAPI } from './apiCreateApp' export { createRenderer } from './createRenderer' export { handleError, diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 84f4032e..1fe55681 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -12,6 +12,7 @@ import { RawSlots } from './componentSlots' import { PatchFlags } from './patchFlags' import { ShapeFlags } from './shapeFlags' import { isReactive } from '@vue/reactivity' +import { AppContext } from './apiCreateApp' export const Fragment = Symbol('Fragment') export const Text = Symbol('Text') @@ -50,6 +51,9 @@ export interface VNode { patchFlag: number dynamicProps: string[] | null dynamicChildren: VNode[] | null + + // application root node only + appContext: AppContext | null } // Since v-if and v-for are the two possible ways node structure can dynamically @@ -152,7 +156,8 @@ export function createVNode( shapeFlag, patchFlag, dynamicProps, - dynamicChildren: null + dynamicChildren: null, + appContext: null } normalizeChildren(vnode, children) @@ -192,6 +197,7 @@ export function cloneVNode(vnode: VNode): VNode { patchFlag: vnode.patchFlag, dynamicProps: vnode.dynamicProps, dynamicChildren: vnode.dynamicChildren, + appContext: vnode.appContext, // these should be set to null since they should only be present on // mounted VNodes. If they are somehow not null, this means we have diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index 019999ff..d3f4fc87 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -1,11 +1,13 @@ -import { createRenderer, VNode } from '@vue/runtime-core' +import { createRenderer, VNode, createAppAPI } from '@vue/runtime-core' import { nodeOps } from './nodeOps' import { patchProp } from './patchProp' export const render = createRenderer({ patchProp, ...nodeOps -}) as (vnode: VNode | null, container: HTMLElement) => VNode +}) as (vnode: VNode | null, container: HTMLElement) => void + +export const createApp = createAppAPI(render) // re-export everything from core // h, Component, reactivity API, nextTick, flags & types diff --git a/packages/runtime-test/src/index.ts b/packages/runtime-test/src/index.ts index 9e1816f5..fc1e440d 100644 --- a/packages/runtime-test/src/index.ts +++ b/packages/runtime-test/src/index.ts @@ -1,4 +1,4 @@ -import { createRenderer, VNode } from '@vue/runtime-core' +import { createRenderer, VNode, createAppAPI } from '@vue/runtime-core' import { nodeOps, TestElement } from './nodeOps' import { patchProp } from './patchProp' import { serializeInner } from './serialize' @@ -6,7 +6,9 @@ import { serializeInner } from './serialize' export const render = createRenderer({ patchProp, ...nodeOps -}) as (node: VNode | null, container: TestElement) => VNode +}) as (node: VNode | null, container: TestElement) => void + +export const createApp = createAppAPI(render) // convenience for one-off render validations export function renderToString(vnode: VNode) {