mirror of https://github.com/vuejs/core.git
wip(ssr): initial work on server-renderer
This commit is contained in:
parent
c07751fd36
commit
da25517377
|
@ -19,8 +19,11 @@ export interface App<HostElement = any> {
|
||||||
mount(rootContainer: HostElement | string): ComponentPublicInstance
|
mount(rootContainer: HostElement | string): ComponentPublicInstance
|
||||||
unmount(rootContainer: HostElement | string): void
|
unmount(rootContainer: HostElement | string): void
|
||||||
provide<T>(key: InjectionKey<T> | string, value: T): this
|
provide<T>(key: InjectionKey<T> | string, value: T): this
|
||||||
rootComponent: Component
|
|
||||||
rootContainer: HostElement | null
|
// internal. We need to expose these for the server-renderer
|
||||||
|
_component: Component
|
||||||
|
_props: Data | null
|
||||||
|
_container: HostElement | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
|
@ -85,18 +88,21 @@ export type CreateAppFunction<HostElement> = (
|
||||||
export function createAppAPI<HostNode, HostElement>(
|
export function createAppAPI<HostNode, HostElement>(
|
||||||
render: RootRenderFunction<HostNode, HostElement>
|
render: RootRenderFunction<HostNode, HostElement>
|
||||||
): CreateAppFunction<HostElement> {
|
): CreateAppFunction<HostElement> {
|
||||||
return function createApp(
|
return function createApp(rootComponent: Component, rootProps = null) {
|
||||||
rootComponent: Component,
|
if (rootProps != null && !isObject(rootProps)) {
|
||||||
rootProps?: Data | null
|
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
|
||||||
): App {
|
rootProps = null
|
||||||
|
}
|
||||||
|
|
||||||
const context = createAppContext()
|
const context = createAppContext()
|
||||||
const installedPlugins = new Set()
|
const installedPlugins = new Set()
|
||||||
|
|
||||||
let isMounted = false
|
let isMounted = false
|
||||||
|
|
||||||
const app: App = {
|
const app: App = {
|
||||||
rootComponent,
|
_component: rootComponent,
|
||||||
rootContainer: null,
|
_props: rootProps,
|
||||||
|
_container: null,
|
||||||
|
|
||||||
get config() {
|
get config() {
|
||||||
return context.config
|
return context.config
|
||||||
|
@ -176,11 +182,6 @@ export function createAppAPI<HostNode, HostElement>(
|
||||||
|
|
||||||
mount(rootContainer: HostElement): any {
|
mount(rootContainer: HostElement): any {
|
||||||
if (!isMounted) {
|
if (!isMounted) {
|
||||||
if (rootProps != null && !isObject(rootProps)) {
|
|
||||||
__DEV__ &&
|
|
||||||
warn(`root props passed to app.mount() must be an object.`)
|
|
||||||
rootProps = null
|
|
||||||
}
|
|
||||||
const vnode = createVNode(rootComponent, rootProps)
|
const vnode = createVNode(rootComponent, rootProps)
|
||||||
// store app context on the root VNode.
|
// store app context on the root VNode.
|
||||||
// this will be set on the root instance on initial mount.
|
// this will be set on the root instance on initial mount.
|
||||||
|
@ -195,7 +196,7 @@ export function createAppAPI<HostNode, HostElement>(
|
||||||
|
|
||||||
render(vnode, rootContainer)
|
render(vnode, rootContainer)
|
||||||
isMounted = true
|
isMounted = true
|
||||||
app.rootContainer = rootContainer
|
app._container = rootContainer
|
||||||
return vnode.component!.proxy
|
return vnode.component!.proxy
|
||||||
} else if (__DEV__) {
|
} else if (__DEV__) {
|
||||||
warn(
|
warn(
|
||||||
|
@ -206,7 +207,7 @@ export function createAppAPI<HostNode, HostElement>(
|
||||||
|
|
||||||
unmount() {
|
unmount() {
|
||||||
if (isMounted) {
|
if (isMounted) {
|
||||||
render(null, app.rootContainer!)
|
render(null, app._container!)
|
||||||
} else if (__DEV__) {
|
} else if (__DEV__) {
|
||||||
warn(`Cannot unmount an app that is not mounted.`)
|
warn(`Cannot unmount an app that is not mounted.`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,8 @@ export interface ComponentOptionsBase<
|
||||||
// Luckily `render()` doesn't need any arguments nor does it care about return
|
// Luckily `render()` doesn't need any arguments nor does it care about return
|
||||||
// type.
|
// type.
|
||||||
render?: Function
|
render?: Function
|
||||||
|
// SSR only. This is produced by compiler-ssr and attached in compiler-sfc
|
||||||
|
ssrRender?: Function
|
||||||
components?: Record<
|
components?: Record<
|
||||||
string,
|
string,
|
||||||
Component | { new (): ComponentPublicInstance<any, any, any, any, any> }
|
Component | { new (): ComponentPublicInstance<any, any, any, any, any> }
|
||||||
|
|
|
@ -154,7 +154,7 @@ export interface ComponentInternalInstance {
|
||||||
|
|
||||||
const emptyAppContext = createAppContext()
|
const emptyAppContext = createAppContext()
|
||||||
|
|
||||||
export function defineComponentInstance(
|
export function createComponentInstance(
|
||||||
vnode: VNode,
|
vnode: VNode,
|
||||||
parent: ComponentInternalInstance | null
|
parent: ComponentInternalInstance | null
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -85,7 +85,7 @@ type NormalizedProp =
|
||||||
type NormalizedPropsOptions = [Record<string, NormalizedProp>, string[]]
|
type NormalizedPropsOptions = [Record<string, NormalizedProp>, string[]]
|
||||||
|
|
||||||
// resolve raw VNode data.
|
// resolve raw VNode data.
|
||||||
// - filter out reserved keys (key, ref, slots)
|
// - filter out reserved keys (key, ref)
|
||||||
// - extract class and style into $attrs (to be merged onto child
|
// - extract class and style into $attrs (to be merged onto child
|
||||||
// component root)
|
// component root)
|
||||||
// - for the rest:
|
// - for the rest:
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
} from './vnode'
|
} from './vnode'
|
||||||
import {
|
import {
|
||||||
ComponentInternalInstance,
|
ComponentInternalInstance,
|
||||||
defineComponentInstance,
|
createComponentInstance,
|
||||||
setupStatefulComponent,
|
setupStatefulComponent,
|
||||||
Component,
|
Component,
|
||||||
Data
|
Data
|
||||||
|
@ -927,7 +927,7 @@ export function createRenderer<
|
||||||
parentSuspense: HostSuspenseBoundary | null,
|
parentSuspense: HostSuspenseBoundary | null,
|
||||||
isSVG: boolean
|
isSVG: boolean
|
||||||
) {
|
) {
|
||||||
const instance: ComponentInternalInstance = (initialVNode.component = defineComponentInstance(
|
const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
|
||||||
initialVNode,
|
initialVNode,
|
||||||
parentComponent
|
parentComponent
|
||||||
))
|
))
|
||||||
|
|
|
@ -39,7 +39,7 @@ export const createApp: CreateAppFunction<Element> = (...args) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const component = app.rootComponent
|
const component = app._component
|
||||||
if (
|
if (
|
||||||
__RUNTIME_COMPILE__ &&
|
__RUNTIME_COMPILE__ &&
|
||||||
!isFunction(component) &&
|
!isFunction(component) &&
|
||||||
|
|
|
@ -25,5 +25,8 @@
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/vuejs/vue/issues"
|
"url": "https://github.com/vuejs/vue/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/server-renderer#readme"
|
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/server-renderer#readme",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/runtime-dom": "3.0.0-alpha.3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,84 @@
|
||||||
export function renderToString() {
|
import {
|
||||||
// TODO
|
App,
|
||||||
|
Component,
|
||||||
|
ComponentInternalInstance,
|
||||||
|
SuspenseBoundary
|
||||||
|
} from '@vue/runtime-dom'
|
||||||
|
import { isString } from '@vue/shared'
|
||||||
|
|
||||||
|
type SSRBuffer = SSRBufferItem[]
|
||||||
|
type SSRBufferItem = string | Promise<SSRBuffer>
|
||||||
|
type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
|
||||||
|
|
||||||
|
function createSSRBuffer() {
|
||||||
|
let appendable = false
|
||||||
|
const buffer: SSRBuffer = []
|
||||||
|
return {
|
||||||
|
buffer,
|
||||||
|
push(item: SSRBufferItem) {
|
||||||
|
const isStringItem = isString(item)
|
||||||
|
if (appendable && isStringItem) {
|
||||||
|
buffer[buffer.length - 1] += item as string
|
||||||
|
} else {
|
||||||
|
buffer.push(item)
|
||||||
|
}
|
||||||
|
appendable = isStringItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renderToString(app: App): Promise<string> {
|
||||||
|
const resolvedBuffer = (await renderComponent(
|
||||||
|
app._component,
|
||||||
|
app._props,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)) as ResolvedSSRBuffer
|
||||||
|
return unrollBuffer(resolvedBuffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
function unrollBuffer(buffer: ResolvedSSRBuffer): string {
|
||||||
|
let ret = ''
|
||||||
|
for (let i = 0; i < buffer.length; i++) {
|
||||||
|
const item = buffer[i]
|
||||||
|
if (isString(item)) {
|
||||||
|
ret += item
|
||||||
|
} else {
|
||||||
|
ret += unrollBuffer(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renderComponent(
|
||||||
|
comp: Component,
|
||||||
|
props: Record<string, any> | null,
|
||||||
|
parentComponent: ComponentInternalInstance | null,
|
||||||
|
parentSuspense: SuspenseBoundary | null
|
||||||
|
): Promise<SSRBuffer> {
|
||||||
|
// 1. create component buffer
|
||||||
|
const { buffer, push } = createSSRBuffer()
|
||||||
|
|
||||||
|
// 2. TODO create actual instance
|
||||||
|
const instance = {
|
||||||
|
proxy: {
|
||||||
|
msg: 'hello'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof comp === 'function') {
|
||||||
|
// TODO FunctionalComponent
|
||||||
|
} else {
|
||||||
|
if (comp.ssrRender) {
|
||||||
|
// optimized
|
||||||
|
comp.ssrRender(push, instance.proxy)
|
||||||
|
} else if (comp.render) {
|
||||||
|
// TODO fallback to vdom serialization
|
||||||
|
} else {
|
||||||
|
// TODO warn component missing render function
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TS can't figure this out due to recursive occurance of Promise in type
|
||||||
|
// @ts-ignore
|
||||||
|
return Promise.all(buffer)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue