Przeglądaj źródła

feat: createApp / appContext

Evan You 6 lat temu
rodzic
commit
32713f8fce

+ 168 - 0
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<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
+  }
+}

+ 9 - 0
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<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,

+ 31 - 3
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
   }
 }
 

+ 2 - 1
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

+ 1 - 0
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,

+ 7 - 1
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

+ 4 - 2
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

+ 4 - 2
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) {