Ver código fonte

feat(runtime-core): hot module replacement

Evan You 6 anos atrás
pai
commit
efe39db023

+ 1 - 0
packages/global.d.ts

@@ -2,6 +2,7 @@
 declare var __DEV__: boolean
 declare var __DEV__: boolean
 declare var __TEST__: boolean
 declare var __TEST__: boolean
 declare var __BROWSER__: boolean
 declare var __BROWSER__: boolean
+declare var __BUNDLER__: boolean
 declare var __RUNTIME_COMPILE__: boolean
 declare var __RUNTIME_COMPILE__: boolean
 declare var __COMMIT__: string
 declare var __COMMIT__: string
 declare var __VERSION__: string
 declare var __VERSION__: string

+ 4 - 0
packages/runtime-core/src/apiOptions.ts

@@ -66,6 +66,10 @@ export interface ComponentOptionsBase<
   directives?: Record<string, Directive>
   directives?: Record<string, Directive>
   inheritAttrs?: boolean
   inheritAttrs?: boolean
 
 
+  // SFC & dev only
+  __scopeId?: string
+  __hmrId?: string
+
   // type-only differentiator to separate OptionWithoutProps from a constructor
   // type-only differentiator to separate OptionWithoutProps from a constructor
   // type returned by createComponent() or FunctionalComponent
   // type returned by createComponent() or FunctionalComponent
   call?: never
   call?: never

+ 1 - 0
packages/runtime-core/src/component.ts

@@ -37,6 +37,7 @@ export interface FunctionalComponent<P = {}> {
   props?: ComponentPropsOptions<P>
   props?: ComponentPropsOptions<P>
   inheritAttrs?: boolean
   inheritAttrs?: boolean
   displayName?: string
   displayName?: string
+  __hmrId?: string
 }
 }
 
 
 export type Component = ComponentOptions | FunctionalComponent
 export type Component = ComponentOptions | FunctionalComponent

+ 85 - 0
packages/runtime-core/src/hmr.ts

@@ -0,0 +1,85 @@
+import {
+  ComponentInternalInstance,
+  ComponentOptions,
+  RenderFunction
+} from './component'
+
+// Expose the HMR runtime on the global object
+// This makes it entirely tree-shakable without polluting the exports and makes
+// it easier to be used in toolings like vue-loader
+// Note: for a component to be eligible for HMR it also needs the __hmrId option
+// to be set so that its instances can be registered / removed.
+if (__BUNDLER__ && __DEV__) {
+  const globalObject: any =
+    typeof global !== 'undefined'
+      ? global
+      : typeof self !== 'undefined'
+        ? self
+        : typeof window !== 'undefined'
+          ? window
+          : {}
+
+  globalObject.__VUE_HMR_RUNTIME__ = {
+    isRecorded: tryWrap(isRecorded),
+    createRecord: tryWrap(createRecord),
+    rerender: tryWrap(rerender),
+    reload: tryWrap(reload)
+  }
+}
+
+interface HMRRecord {
+  comp: ComponentOptions
+  instances: Set<ComponentInternalInstance>
+}
+
+const map: Map<string, HMRRecord> = new Map()
+
+export function registerHMR(instance: ComponentInternalInstance) {
+  map.get(instance.type.__hmrId!)!.instances.add(instance)
+}
+
+export function unregisterHMR(instance: ComponentInternalInstance) {
+  map.get(instance.type.__hmrId!)!.instances.delete(instance)
+}
+
+function isRecorded(id: string): boolean {
+  return map.has(id)
+}
+
+function createRecord(id: string, comp: ComponentOptions) {
+  if (map.has(id)) {
+    return
+  }
+  map.set(id, {
+    comp,
+    instances: new Set()
+  })
+}
+
+function rerender(id: string, newRender: RenderFunction) {
+  map.get(id)!.instances.forEach(instance => {
+    instance.render = newRender
+    instance.renderCache = []
+    instance.update()
+    // TODO force scoped slots passed to children to have DYNAMIC_SLOTS flag
+  })
+}
+
+function reload(id: string, newComp: ComponentOptions) {
+  // TODO
+  console.log('reload', id)
+}
+
+function tryWrap(fn: (id: string, arg: any) => void): Function {
+  return (id: string, arg: any) => {
+    try {
+      fn(id, arg)
+    } catch (e) {
+      console.error(e)
+      console.warn(
+        `Something went wrong during Vue component hot-reload. ` +
+          `Full reload required.`
+      )
+    }
+  }
+}

+ 4 - 2
packages/runtime-core/src/index.ts

@@ -66,7 +66,9 @@ export {
   TransitionHooks
   TransitionHooks
 } from './components/BaseTransition'
 } from './components/BaseTransition'
 
 
-// Internal, for compiler generated code
+// Internal API ----------------------------------------------------------------
+
+// For compiler generated code
 // should sync with '@vue/compiler-core/src/runtimeConstants.ts'
 // should sync with '@vue/compiler-core/src/runtimeConstants.ts'
 export { withDirectives } from './directives'
 export { withDirectives } from './directives'
 export {
 export {
@@ -87,7 +89,7 @@ import { capitalize as _capitalize, camelize as _camelize } from '@vue/shared'
 export const capitalize = _capitalize as (s: string) => string
 export const capitalize = _capitalize as (s: string) => string
 export const camelize = _camelize as (s: string) => string
 export const camelize = _camelize as (s: string) => string
 
 
-// Internal, for integration with runtime compiler
+// For integration with runtime compiler
 export { registerRuntimeCompiler } from './component'
 export { registerRuntimeCompiler } from './component'
 
 
 // Types -----------------------------------------------------------------------
 // Types -----------------------------------------------------------------------

+ 11 - 0
packages/runtime-core/src/renderer.ts

@@ -53,6 +53,7 @@ import {
 } from './components/Suspense'
 } from './components/Suspense'
 import { ErrorCodes, callWithErrorHandling } from './errorHandling'
 import { ErrorCodes, callWithErrorHandling } from './errorHandling'
 import { KeepAliveSink, isKeepAlive } from './components/KeepAlive'
 import { KeepAliveSink, isKeepAlive } from './components/KeepAlive'
+import { registerHMR, unregisterHMR } from './hmr'
 
 
 export interface RendererOptions<HostNode = any, HostElement = any> {
 export interface RendererOptions<HostNode = any, HostElement = any> {
   patchProp(
   patchProp(
@@ -857,6 +858,11 @@ export function createRenderer<
       parentComponent
       parentComponent
     ))
     ))
 
 
+    // HMR
+    if (__BUNDLER__ && __DEV__ && instance.type.__hmrId != null) {
+      registerHMR(instance)
+    }
+
     if (__DEV__) {
     if (__DEV__) {
       pushWarningContext(initialVNode)
       pushWarningContext(initialVNode)
     }
     }
@@ -1549,6 +1555,11 @@ export function createRenderer<
     parentSuspense: HostSuspenseBoundary | null,
     parentSuspense: HostSuspenseBoundary | null,
     doRemove?: boolean
     doRemove?: boolean
   ) {
   ) {
+    // HMR
+    if (__BUNDLER__ && __DEV__ && instance.type.__hmrId != null) {
+      unregisterHMR(instance)
+    }
+
     const { bum, effects, update, subTree, um, da, isDeactivated } = instance
     const { bum, effects, update, subTree, um, da, isDeactivated } = instance
     // beforeUnmount hook
     // beforeUnmount hook
     if (bum !== null) {
     if (bum !== null) {

+ 2 - 0
rollup.config.js

@@ -147,6 +147,8 @@ function createReplacePlugin(
     __TEST__: isBundlerESMBuild ? `(process.env.NODE_ENV === 'test')` : false,
     __TEST__: isBundlerESMBuild ? `(process.env.NODE_ENV === 'test')` : false,
     // If the build is expected to run directly in the browser (global / esm builds)
     // If the build is expected to run directly in the browser (global / esm builds)
     __BROWSER__: isBrowserBuild,
     __BROWSER__: isBrowserBuild,
+    // is targeting bundlers?
+    __BUNDLER__: isBundlerESMBuild,
     // support compile in browser?
     // support compile in browser?
     __RUNTIME_COMPILE__: isRuntimeCompileBuild,
     __RUNTIME_COMPILE__: isRuntimeCompileBuild,
     // support options?
     // support options?