]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: createApp / appContext
authorEvan You <yyx990803@gmail.com>
Mon, 2 Sep 2019 20:09:34 +0000 (16:09 -0400)
committerEvan You <yyx990803@gmail.com>
Mon, 2 Sep 2019 20:09:34 +0000 (16:09 -0400)
packages/runtime-core/src/apiCreateApp.ts [new file with mode: 0644]
packages/runtime-core/src/component.ts
packages/runtime-core/src/createRenderer.ts
packages/runtime-core/src/directives.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/vnode.ts
packages/runtime-dom/src/index.ts
packages/runtime-test/src/index.ts

diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts
new file mode 100644 (file)
index 0000000..7d6a7d4
--- /dev/null
@@ -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<T>(key: InjectionKey<T> | 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<string | RegExp>
+  keyCodes: Record<string, number | number[]>
+  optionMergeStrategies: {
+    [key: string]: (
+      parent: any,
+      child: any,
+      instance: ComponentRenderProxy
+    ) => any
+  }
+}
+
+export interface AppContext {
+  config: AppConfig
+  mixins: ComponentOptions[]
+  components: Record<string, Component>
+  directives: Record<string, Directive>
+  provides: Record<string | symbol, any>
+}
+
+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
+  }
+}
index a76095aabcadee886bc5b2a73130ecf830ca5e58..e42183f824979565b5fad88db5025b2a09b81c72 100644 (file)
@@ -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<P = {}> {
   displayName?: string
 }
 
+export type Component = ComponentOptions | FunctionalComponent
+
 type LifecycleHook = Function[] | null
 
 export const enum LifecycleHooks {
@@ -107,6 +110,7 @@ interface SetupContext {
 export type ComponentInstance<P = Data, S = Data> = {
   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,
index 7ad882b8103131541e3e1522fa2b3bc83a97bc44..15980b6bf348e3a1a8865fe36404e79635ffc7b8 100644 (file)
@@ -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
   }
 }
 
index 8e620b9dec86474749a606e0358461d968a4a052..e8079a2bdbbfa2e93ed6fc2e4e3e8f76d7472bcf 100644 (file)
@@ -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
index dba7dbae066fa950a129e7eb60b828f23f31dfee..f312a2fe98f1b33aacfe6d3dfc1e6937ba464b8a 100644 (file)
@@ -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,
index 84f4032eec34a8850a17dea2fde620d7b832dc80..1fe556819977613b4f630d0aa0add77740ee5b3f 100644 (file)
@@ -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
index 019999ff1ae0642c0db5fd2b37930b5364ce50f6..d3f4fc8753e54d897cda53d043f498a21a97a989 100644 (file)
@@ -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
index 9e1816f5ac2e0c1428c6c45915546844c3ecb9f9..fc1e440dd971ff1af9f4213bf1ce076f9b7a3f23 100644 (file)
@@ -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) {