This commit is contained in:
2023-10-08 21:21:21 +08:00
commit 60bf302119
377 changed files with 111630 additions and 0 deletions

View File

@@ -0,0 +1,264 @@
/* @flow */
// The SSR codegen is essentially extending the default codegen to handle
// SSR-optimizable nodes and turn them into string render fns. In cases where
// a node is not optimizable it simply falls back to the default codegen.
import {
genIf,
genFor,
genData,
genText,
genElement,
genChildren,
CodegenState
} from 'compiler/codegen/index'
import {
genAttrSegments,
genDOMPropSegments,
genClassSegments,
genStyleSegments,
applyModelTransform
} from './modules'
import { escape } from 'web/server/util'
import { optimizability } from './optimizer'
import type { CodegenResult } from 'compiler/codegen/index'
export type StringSegment = {
type: number;
value: string;
};
// segment types
export const RAW = 0
export const INTERPOLATION = 1
export const EXPRESSION = 2
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genSSRElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
function genSSRElement (el: ASTElement, state: CodegenState): string {
if (el.for && !el.forProcessed) {
return genFor(el, state, genSSRElement)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state, genSSRElement)
} else if (el.tag === 'template' && !el.slotTarget) {
return el.ssrOptimizability === optimizability.FULL
? genChildrenAsStringNode(el, state)
: genSSRChildren(el, state) || 'void 0'
}
switch (el.ssrOptimizability) {
case optimizability.FULL:
// stringify whole tree
return genStringElement(el, state)
case optimizability.SELF:
// stringify self and check children
return genStringElementWithChildren(el, state)
case optimizability.CHILDREN:
// generate self as VNode and stringify children
return genNormalElement(el, state, true)
case optimizability.PARTIAL:
// generate self as VNode and check children
return genNormalElement(el, state, false)
default:
// bail whole tree
return genElement(el, state)
}
}
function genNormalElement (el, state, stringifyChildren) {
const data = el.plain ? undefined : genData(el, state)
const children = stringifyChildren
? `[${genChildrenAsStringNode(el, state)}]`
: genSSRChildren(el, state, true)
return `_c('${el.tag}'${
data ? `,${data}` : ''
}${
children ? `,${children}` : ''
})`
}
function genSSRChildren (el, state, checkSkip) {
return genChildren(el, state, checkSkip, genSSRElement, genSSRNode)
}
function genSSRNode (el, state) {
return el.type === 1
? genSSRElement(el, state)
: genText(el)
}
function genChildrenAsStringNode (el, state) {
return el.children.length
? `_ssrNode(${flattenSegments(childrenToSegments(el, state))})`
: ''
}
function genStringElement (el, state) {
return `_ssrNode(${elementToString(el, state)})`
}
function genStringElementWithChildren (el, state) {
const children = genSSRChildren(el, state, true)
return `_ssrNode(${
flattenSegments(elementToOpenTagSegments(el, state))
},"</${el.tag}>"${
children ? `,${children}` : ''
})`
}
function elementToString (el, state) {
return `(${flattenSegments(elementToSegments(el, state))})`
}
function elementToSegments (el, state): Array<StringSegment> {
// v-for / v-if
if (el.for && !el.forProcessed) {
el.forProcessed = true
return [{
type: EXPRESSION,
value: genFor(el, state, elementToString, '_ssrList')
}]
} else if (el.if && !el.ifProcessed) {
el.ifProcessed = true
return [{
type: EXPRESSION,
value: genIf(el, state, elementToString, '"<!---->"')
}]
} else if (el.tag === 'template') {
return childrenToSegments(el, state)
}
const openSegments = elementToOpenTagSegments(el, state)
const childrenSegments = childrenToSegments(el, state)
const { isUnaryTag } = state.options
const close = (isUnaryTag && isUnaryTag(el.tag))
? []
: [{ type: RAW, value: `</${el.tag}>` }]
return openSegments.concat(childrenSegments, close)
}
function elementToOpenTagSegments (el, state): Array<StringSegment> {
applyModelTransform(el, state)
let binding
const segments = [{ type: RAW, value: `<${el.tag}` }]
// attrs
if (el.attrs) {
segments.push.apply(segments, genAttrSegments(el.attrs))
}
// domProps
if (el.props) {
segments.push.apply(segments, genDOMPropSegments(el.props, el.attrs))
}
// v-bind="object"
if ((binding = el.attrsMap['v-bind'])) {
segments.push({ type: EXPRESSION, value: `_ssrAttrs(${binding})` })
}
// v-bind.prop="object"
if ((binding = el.attrsMap['v-bind.prop'])) {
segments.push({ type: EXPRESSION, value: `_ssrDOMProps(${binding})` })
}
// class
if (el.staticClass || el.classBinding) {
segments.push.apply(
segments,
genClassSegments(el.staticClass, el.classBinding)
)
}
// style & v-show
if (el.staticStyle || el.styleBinding || el.attrsMap['v-show']) {
segments.push.apply(
segments,
genStyleSegments(
el.attrsMap.style,
el.staticStyle,
el.styleBinding,
el.attrsMap['v-show']
)
)
}
// _scopedId
if (state.options.scopeId) {
segments.push({ type: RAW, value: ` ${state.options.scopeId}` })
}
segments.push({ type: RAW, value: `>` })
return segments
}
function childrenToSegments (el, state): Array<StringSegment> {
let binding
if ((binding = el.attrsMap['v-html'])) {
return [{ type: EXPRESSION, value: `_s(${binding})` }]
}
if ((binding = el.attrsMap['v-text'])) {
return [{ type: INTERPOLATION, value: `_s(${binding})` }]
}
if (el.tag === 'textarea' && (binding = el.attrsMap['v-model'])) {
return [{ type: INTERPOLATION, value: `_s(${binding})` }]
}
return el.children
? nodesToSegments(el.children, state)
: []
}
function nodesToSegments (
children: Array<ASTNode>,
state: CodegenState
): Array<StringSegment> {
const segments = []
for (let i = 0; i < children.length; i++) {
const c = children[i]
if (c.type === 1) {
segments.push.apply(segments, elementToSegments(c, state))
} else if (c.type === 2) {
segments.push({ type: INTERPOLATION, value: c.expression })
} else if (c.type === 3) {
let text = escape(c.text)
if (c.isComment) {
text = '<!--' + text + '-->'
}
segments.push({ type: RAW, value: text })
}
}
return segments
}
function flattenSegments (segments: Array<StringSegment>): string {
const mergedSegments = []
let textBuffer = ''
const pushBuffer = () => {
if (textBuffer) {
mergedSegments.push(JSON.stringify(textBuffer))
textBuffer = ''
}
}
for (let i = 0; i < segments.length; i++) {
const s = segments[i]
if (s.type === RAW) {
textBuffer += s.value
} else if (s.type === INTERPOLATION) {
pushBuffer()
mergedSegments.push(`_ssrEscape(${s.value})`)
} else if (s.type === EXPRESSION) {
pushBuffer()
mergedSegments.push(`(${s.value})`)
}
}
pushBuffer()
return mergedSegments.join('+')
}

View File

@@ -0,0 +1,20 @@
/* @flow */
import { parse } from 'compiler/parser/index'
import { generate } from './codegen'
import { optimize } from './optimizer'
import { createCompilerCreator } from 'compiler/create-compiler'
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
optimize(ast, options)
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})

View File

@@ -0,0 +1,124 @@
/* @flow */
import {
RAW,
// INTERPOLATION,
EXPRESSION
} from './codegen'
import {
propsToAttrMap,
isRenderableAttr
} from 'web/server/util'
import {
isBooleanAttr,
isEnumeratedAttr
} from 'web/util/attrs'
import type { StringSegment } from './codegen'
import type { CodegenState } from 'compiler/codegen/index'
const plainStringRE = /^"(?:[^"\\]|\\.)*"$|^'(?:[^'\\]|\\.)*'$/
// let the model AST transform translate v-model into appropriate
// props bindings
export function applyModelTransform (el: ASTElement, state: CodegenState) {
if (el.directives) {
for (let i = 0; i < el.directives.length; i++) {
const dir = el.directives[i]
if (dir.name === 'model') {
state.directives.model(el, dir, state.warn)
// remove value for textarea as its converted to text
if (el.tag === 'textarea' && el.props) {
el.props = el.props.filter(p => p.name !== 'value')
}
break
}
}
}
}
export function genAttrSegments (
attrs: Array<ASTAttr>
): Array<StringSegment> {
return attrs.map(({ name, value }) => genAttrSegment(name, value))
}
export function genDOMPropSegments (
props: Array<ASTAttr>,
attrs: ?Array<ASTAttr>
): Array<StringSegment> {
const segments = []
props.forEach(({ name, value }) => {
name = propsToAttrMap[name] || name.toLowerCase()
if (isRenderableAttr(name) &&
!(attrs && attrs.some(a => a.name === name))
) {
segments.push(genAttrSegment(name, value))
}
})
return segments
}
function genAttrSegment (name: string, value: string): StringSegment {
if (plainStringRE.test(value)) {
// force double quote
value = value.replace(/^'|'$/g, '"')
// force enumerated attr to "true"
if (isEnumeratedAttr(name) && value !== `"false"`) {
value = `"true"`
}
return {
type: RAW,
value: isBooleanAttr(name)
? ` ${name}="${name}"`
: value === '""'
? ` ${name}`
: ` ${name}="${JSON.parse(value)}"`
}
} else {
return {
type: EXPRESSION,
value: `_ssrAttr(${JSON.stringify(name)},${value})`
}
}
}
export function genClassSegments (
staticClass: ?string,
classBinding: ?string
): Array<StringSegment> {
if (staticClass && !classBinding) {
return [{ type: RAW, value: ` class="${JSON.parse(staticClass)}"` }]
} else {
return [{
type: EXPRESSION,
value: `_ssrClass(${staticClass || 'null'},${classBinding || 'null'})`
}]
}
}
export function genStyleSegments (
staticStyle: ?string,
parsedStaticStyle: ?string,
styleBinding: ?string,
vShowExpression: ?string
): Array<StringSegment> {
if (staticStyle && !styleBinding && !vShowExpression) {
return [{ type: RAW, value: ` style=${JSON.stringify(staticStyle)}` }]
} else {
return [{
type: EXPRESSION,
value: `_ssrStyle(${
parsedStaticStyle || 'null'
},${
styleBinding || 'null'
}, ${
vShowExpression
? `{ display: (${vShowExpression}) ? '' : 'none' }`
: 'null'
})`
}]
}
}

View File

@@ -0,0 +1,141 @@
/* @flow */
/**
* In SSR, the vdom tree is generated only once and never patched, so
* we can optimize most element / trees into plain string render functions.
* The SSR optimizer walks the AST tree to detect optimizable elements and trees.
*
* The criteria for SSR optimizability is quite a bit looser than static tree
* detection (which is designed for client re-render). In SSR we bail only for
* components/slots/custom directives.
*/
import { no, makeMap, isBuiltInTag } from 'shared/util'
// optimizability constants
export const optimizability = {
FALSE: 0, // whole sub tree un-optimizable
FULL: 1, // whole sub tree optimizable
SELF: 2, // self optimizable but has some un-optimizable children
CHILDREN: 3, // self un-optimizable but have fully optimizable children
PARTIAL: 4 // self un-optimizable with some un-optimizable children
}
let isPlatformReservedTag
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
isPlatformReservedTag = options.isReservedTag || no
walk(root, true)
}
function walk (node: ASTNode, isRoot?: boolean) {
if (isUnOptimizableTree(node)) {
node.ssrOptimizability = optimizability.FALSE
return
}
// root node or nodes with custom directives should always be a VNode
const selfUnoptimizable = isRoot || hasCustomDirective(node)
const check = child => {
if (child.ssrOptimizability !== optimizability.FULL) {
node.ssrOptimizability = selfUnoptimizable
? optimizability.PARTIAL
: optimizability.SELF
}
}
if (selfUnoptimizable) {
node.ssrOptimizability = optimizability.CHILDREN
}
if (node.type === 1) {
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
walk(child)
check(child)
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
const block = node.ifConditions[i].block
walk(block, isRoot)
check(block)
}
}
if (node.ssrOptimizability == null ||
(!isRoot && (node.attrsMap['v-html'] || node.attrsMap['v-text']))
) {
node.ssrOptimizability = optimizability.FULL
} else {
node.children = optimizeSiblings(node)
}
} else {
node.ssrOptimizability = optimizability.FULL
}
}
function optimizeSiblings (el) {
const children = el.children
const optimizedChildren = []
let currentOptimizableGroup = []
const pushGroup = () => {
if (currentOptimizableGroup.length) {
optimizedChildren.push({
type: 1,
parent: el,
tag: 'template',
attrsList: [],
attrsMap: {},
rawAttrsMap: {},
children: currentOptimizableGroup,
ssrOptimizability: optimizability.FULL
})
}
currentOptimizableGroup = []
}
for (let i = 0; i < children.length; i++) {
const c = children[i]
if (c.ssrOptimizability === optimizability.FULL) {
currentOptimizableGroup.push(c)
} else {
// wrap fully-optimizable adjacent siblings inside a template tag
// so that they can be optimized into a single ssrNode by codegen
pushGroup()
optimizedChildren.push(c)
}
}
pushGroup()
return optimizedChildren
}
function isUnOptimizableTree (node: ASTNode): boolean {
if (node.type === 2 || node.type === 3) { // text or expression
return false
}
return (
isBuiltInTag(node.tag) || // built-in (slot, component)
!isPlatformReservedTag(node.tag) || // custom component
!!node.component || // "is" component
isSelectWithModel(node) // <select v-model> requires runtime inspection
)
}
const isBuiltInDir = makeMap('text,html,show,on,bind,model,pre,cloak,once')
function hasCustomDirective (node: ASTNode): ?boolean {
return (
node.type === 1 &&
node.directives &&
node.directives.some(d => !isBuiltInDir(d.name))
)
}
// <select v-model> cannot be optimized because it requires a runtime check
// to determine proper selected option
function isSelectWithModel (node: ASTNode): boolean {
return (
node.type === 1 &&
node.tag === 'select' &&
node.directives != null &&
node.directives.some(d => d.name === 'model')
)
}

View File

@@ -0,0 +1,150 @@
/* @flow */
import { escape, isSSRUnsafeAttr } from 'web/server/util'
import { isObject, extend } from 'shared/util'
import { renderAttr } from 'web/server/modules/attrs'
import { renderClass } from 'web/util/class'
import { genStyle } from 'web/server/modules/style'
import { normalizeStyleBinding } from 'web/util/style'
import {
normalizeChildren,
simpleNormalizeChildren
} from 'core/vdom/helpers/normalize-children'
import {
propsToAttrMap,
isRenderableAttr
} from 'web/server/util'
const ssrHelpers = {
_ssrEscape: escape,
_ssrNode: renderStringNode,
_ssrList: renderStringList,
_ssrAttr: renderAttr,
_ssrAttrs: renderAttrs,
_ssrDOMProps: renderDOMProps,
_ssrClass: renderSSRClass,
_ssrStyle: renderSSRStyle
}
export function installSSRHelpers (vm: Component) {
if (vm._ssrNode) {
return
}
let Vue = vm.constructor
while (Vue.super) {
Vue = Vue.super
}
extend(Vue.prototype, ssrHelpers)
if (Vue.FunctionalRenderContext) {
extend(Vue.FunctionalRenderContext.prototype, ssrHelpers)
}
}
class StringNode {
isString: boolean;
open: string;
close: ?string;
children: ?Array<any>;
constructor (
open: string,
close?: string,
children?: Array<any>,
normalizationType?: number
) {
this.isString = true
this.open = open
this.close = close
if (children) {
this.children = normalizationType === 1
? simpleNormalizeChildren(children)
: normalizationType === 2
? normalizeChildren(children)
: children
} else {
this.children = void 0
}
}
}
function renderStringNode (
open: string,
close?: string,
children?: Array<any>,
normalizationType?: number
): StringNode {
return new StringNode(open, close, children, normalizationType)
}
function renderStringList (
val: any,
render: (
val: any,
keyOrIndex: string | number,
index?: number
) => string
): string {
let ret = ''
let i, l, keys, key
if (Array.isArray(val) || typeof val === 'string') {
for (i = 0, l = val.length; i < l; i++) {
ret += render(val[i], i)
}
} else if (typeof val === 'number') {
for (i = 0; i < val; i++) {
ret += render(i + 1, i)
}
} else if (isObject(val)) {
keys = Object.keys(val)
for (i = 0, l = keys.length; i < l; i++) {
key = keys[i]
ret += render(val[key], key, i)
}
}
return ret
}
function renderAttrs (obj: Object): string {
let res = ''
for (const key in obj) {
if (isSSRUnsafeAttr(key)) {
continue
}
res += renderAttr(key, obj[key])
}
return res
}
function renderDOMProps (obj: Object): string {
let res = ''
for (const key in obj) {
const attr = propsToAttrMap[key] || key.toLowerCase()
if (isRenderableAttr(attr)) {
res += renderAttr(attr, obj[key])
}
}
return res
}
function renderSSRClass (
staticClass: ?string,
dynamic: any
): string {
const res = renderClass(staticClass, dynamic)
return res === '' ? res : ` class="${escape(res)}"`
}
function renderSSRStyle (
staticStyle: ?Object,
dynamic: any,
extra: ?Object
): string {
const style = {}
if (staticStyle) extend(style, staticStyle)
if (dynamic) extend(style, normalizeStyleBinding(dynamic))
if (extra) extend(style, extra)
const res = genStyle(style)
return res === '' ? res : ` style=${JSON.stringify(escape(res))}`
}