诗号:六道同坠,魔劫万千,引渡如来。

/img/bdx/yiyeshu-001.jpg

stb-vue-next 完全拷贝于 vue-next ,主要目的用于学习。

声明 :vue-next compiler-ssr 服务端渲染模块,相关的所有测试代码均在 /js/vue/ 目录下面。

更新日志&Todos

  1. [2021-01-04 20:11:43] 创建

  2. [2021-01-08 10:11:47] 完成

模块初始化:feat(init): compiler-ssr · gcclll/stb-vue-next@dff9d31 · GitHub

脑图:

/img/vue3/compiler-ssr/vue-compiler-ssr.svg

Tip

  1. ssr 不处理 v-on/v-once/v-cloak 三个指令

  2. v-show 转成 exp ? 'null' : {display: 'none'} 合并到 style

  3. <Suspense> 组件,会将所有 children 放到一个 await 函数里面执行

  4. <Teleport> 作用?

05db578 compiler-ssr init

feat(init): compiler-ssr -> compile function · gcclll/stb-vue-next@05db578 · GitHub

源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

export function compile(
  template: string,
  options: CompilerOptions = {}
): CodegenResult {
  options = {
    ...options,
    // 引用 DOM parser 选项
    ...parserOptions,
    ssr: true,
    scopeId: options.mode === 'function' ? null : options.scopeId,
    // 总是加上前缀,ssr 不需要关系大小
    prefixIdentifiers: true,
    // ssr 下不需要缓存和静态提升优化
    cacheHandlers: false,
    hoistStatic: false
  }

  const ast = baseParse(template, options)
  // TODO Save raw options for AST. This is needed when performing sub-transforms
  // on slot vnode branches.

  transform(ast, {
    ...options,
    nodeTransforms: [
      // TODO ... ssr transforms

      ...(options.nodeTransforms || []) // user transforms
    ],
    directiveTransforms: {
      // 复用 compiler-core 的 v-bind
      bind: transformBind,
      // TODO ... more ssr directive transforms
      ...(options.directiveTransforms || {}) // user transforms
    }
  })

  // TODO traverse the template AST and convert into SSR codegen AST
  // by replacing ast.codegenNode.
  // 将 compiler-core 阶段生成的 codegenNode 转换成 SSR codegen AST

  return generate(ast, options)
}

沿用 compiler-core 的三个阶段(parse -> transform -> generate),另加上 SSR 渲染相 关的选项和其对应的 transforms 函数。

ssr text testing

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 源文件:/js/vue/lib.js
const {
  compileSFCScript,
  compileStyle,
  getCompiledSSRString: ssr,
  compileSSR,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const { code, matched, ast } = ssr("foo");
log(["codegenNode: ", ast.codegenNode || "null"]);
log('>>> render function')
log(code)
codegenNode:  null
>>> render function

return function ssrRender(_ctx, _push, _parent, _attrs) {
  null
}
undefined

结果显示没有 codegenNode, ssrRender 函数体内也就啥都没有。

FIX: 应该需要需要实现 ssrCodegenTransform

后面的所有测试都会依赖下面的正则(官方用例中的代码):

/_push\(\`<div\${\s*_ssrRenderAttrs\(_attrs\)\s*}>([^]*)<\/div>\`\)/

http://qiniu.ii6g.com/img/20210104214735.png

54ad7e2 coding ssrCodegenTransform function

feat(add): compiler-ssr-> ssrCodegenTransform function · gcclll/stb-vue-next@54ad7e2 · GitHub

生成 ssr codegenNode 的 transform 函数。

大致流程和 compiler-core 差不多。

  1. 创建上下文 context = createSSRTransformContext(ast, options)

  2. options.ssrCssVars 样式变量处理

  3. 如果多个且至少有一个为非文本节点,需要用到 fragment

  4. processChildren 递归处理所有孩子节点,生成 codegenNode , 所以这里是 核心

  5. helpers 合并

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 源文件:/js/vue/lib.js
const {
  compileSFCScript,
  compileStyle,
  getCompiledSSRString,
  compileSSR: ssr,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const { code, ast, matched } = ssr("foo");
log([">>> ast.children\n", ast.children]);
log([">>> ast.codegenNode.body\n", ast.codegenNode.body]);
log([">>> code\n", code]);
>>> ast.children
 [
  {
    type: 2,
    content: 'foo',
    loc: { start: [Object], end: [Object], source: 'foo' }
  }
]
>>> ast.codegenNode.body
 [
  {
    type: 14,
    loc: { source: '', start: [Object], end: [Object] },
    callee: '_push',
    arguments: [ [Object] ]
  }
]
>>> code

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`foo`)
}
undefined

Bug1: body 里面没东西, fix: body null · gcclll/stb-vue-next@f6d22c1 · GitHub

Bug2: div 没有被解析到,因为没有实现 ssrTransformElement 所有这里要先实现它, 测试用例中默认是 <div>${src}</div> 包起来的。

因为测试函数 getCompiledSSRString 中会将 src 用 <div> 包裹起来,所以需要先实 现 div 的解析,即 NodeTypes.ELEMENT 类型解析。

561d41b ELEMENT: ssrTransformElement

feat(add): ssr->ssrTransformElement · gcclll/stb-vue-next@561d41b · GitHub

新增两个函数实现:

  1. ssrProcessElement 处理标签

  2. ssrPostTransformElement ELEMENT 的转换函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const {
  compileSFCScript,
  compileStyle,
  getCompiledSSRString: ssrs,
  compileSSR: ssr,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const { code, ast, matched } = ssrs("foo");
log([">>> code\n", code]);
>>> code

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div>foo</div>`)
}
undefined

还是没有 _ssrRenderAttrs 匹配失败,与期待结果还差一步:属性解析。

feat(add): directives and node transforms from compiler-core · gcclll/stb-vue-next@dc15719 · GitHub

ea6bb01 add ssrInjectFallthroughAttrs 注入属性

feat(add): ssr-> add ssrInjectFallthroughAttrs · gcclll/stb-vue-next@ea6bb01 · GitHub

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
export const ssrInjectFallthroughAttrs: NodeTransform = (node, context) => {
  // _attrs is provided as a function argument.
  // mark it as a known identifier so that it doesn't get prefixed by
  // transformExpression.
  if (node.type === NodeTypes.ROOT) {
    context.identifiers._attrs = 1;
  }

  const parent = context.parent;
  if (!parent || parent.type !== NodeTypes.ROOT) {
    return;
  }

  if (node.type === NodeTypes.IF_BRANCH && hasSingleChild(node)) {
    injectFallThroughAttrs(node.children[0]);
  } else if (hasSingleChild(parent)) {
    injectFallThroughAttrs(node);
  }
};

这个函数是用来将 render 函数的 attrs 参数处理成 v-bind 指令。

前提条件:

  1. 必须要有 parent 父元素,即 ROOT 节点不会处理

  2. 且 parent 必须是 ROOT 节点,即 attrs 会注入到第一个最外层的元素上

    比如:实例中的 <div>${src}</div> , render 函数中的 attrs 会被注入到这个 div 上,这也是 _ssrRenderAttrs 的由来。

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const {
  compileSFCScript,
  compileStyle,
  getCompiledSSRString: ssrs,
  compileSSR: ssr,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const { code, ast, matched } = ssrs("foo");
log([">>> code\n", code]);
log(['>>> ast.children[0].props\n', ast.children[0].props])
>>> code

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div>foo</div>`)
}
>>> ast.children[0].props
 [
  {
    type: 7,
    name: 'bind',
    arg: undefined,
    exp: {
      type: 4,
      loc: [Object],
      content: '_attrs',
      isStatic: false,
      constType: 0
    },
    modifiers: [],
    loc: { source: '', start: [Object], end: [Object] }
  }
]
undefined

虽然结果还没达预期,但是上面结果显示已经有属性了,那么接下来就是要处理这个属性了, 这个在 ssrTransformElement 中处理。

7d20acd ELEMENT: ssrTransformElement>v-bind

feat(add): ssr->element:v-bind · gcclll/stb-vue-next@7d20acd · GitHub

新增处理代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 需要运行时做特殊处理
const needTagForRuntime = node.tag === "textarea" || node.tag.indexOf("-") > 0;
// 1. TODO v-bind
// v-bind="obj" or v-bind:[key] can potentially overwrite other static
// attrs and can affect final rendering result, so when they are present
// we need to bail out to full `renderAttrs`
const hasDynamicVBind = hasDynamicKeyVBind(node);
if (hasDynamicVBind) {
  const { props } = buildProps(node, context, node.props, true /* ssr */);
  if (props) {
    const propsExp = createCallExpression(context.helper(SSR_RENDER_ATTRS), [
      props,
    ]);

    if (node.tag === "textarea") {
      // TODO
    } else if (node.tag === "input") {
      // TODO
    }

    if (needTagForRuntime) {
      propsExp.arguments.push(`"${node.tag}"`);
    }

    openTag.push(propsExp);
  }
}

因为在上一节中将 attrs 注册为了 v-bind 属性,因此在 transform element 中就有 Props 需要处理了, ssrRenderAttrs 就是在这里增加了 SSR_RENDER_ATTRS

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const {
  compileSFCScript,
  compileStyle,
  getCompiledSSRString: ssrs,
  compileSSR: ssr,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const { code, ast, matched } = ssrs("foo");
log([">>> code\n", code]);
>>> code
 const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${_ssrRenderAttrs(_attrs)}>foo</div>`)
}
undefined

到这里算是能满足测试用例中的正则要求了。

_attrs 注入逻辑脑图: http://qiniu.ii6g.com/img/20210106143502.png

f6d22c1 TEXT 节点类型解析

fix: body null · gcclll/stb-vue-next@f6d22c1 · GitHub

新增 pushStringPart 函数的实现,用来处理 NodeTypes.TEXT 节点类型。

1
2
3
4
5
switch (child.type) {
  case NodeTypes.TEXT:
    context.pushStringPart(escapeHtml(child.content));
    break;
}

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 源文件:/js/vue/lib.js
const {
  compileSFCScript,
  compileStyle,
  getCompiledSSRString: ssrs,
  compileSSR,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

log([">>> 静态文本\n", ssrs("foo").code]);
log([">>> 静态文本,含反斜杠\n", ssrs(`\\$foo`).code]);
log([">>> 静态文本,&lt; 等符号的\n", ssrs(`&lt;foo&gt;`).code]);
log([
  ">>> 静态文本,元素嵌套\n",
  ssrs(`<div><span>hello</span><span>bye</span></div>`).code,
]);
>>> 静态文本
 const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${_ssrRenderAttrs(_attrs)}>foo</div>`)
}
>>> 静态文本,含反斜杠
 const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${_ssrRenderAttrs(_attrs)}>\\\$foo</div>`)
}
>>> 静态文本,&lt; 等符号的
 const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${_ssrRenderAttrs(_attrs)}>&lt;foo&gt;</div>`)
}
>>> 静态文本,元素嵌套
 const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${_ssrRenderAttrs(_attrs)}><div><span>hello</span><span>bye</span></div></div>`)
}
undefined

8f09472 INTERPOLATION 插值处理

feat(add): ssr->interpolation · gcclll/stb-vue-next@8f09472 · GitHub

增加代码:

1
2
3
4
5
case NodeTypes.INTERPOLATION:
  context.pushStringPart(
    createCallExpression(context.helper(SSR_INTERPOLATE), [child.content])
  )
  break

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 源文件:/js/vue/lib.js
const {
  compileSFCScript,
  compileStyle,
  getCompiledSSRString: ssr,
  compileSSR,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

log([">>> 插值处理\n", ssr(`\`\${foo}\``).code])
log([">>> 插值处理,元素嵌套\n", ssr(`<div><span>{{ foo }} bar</span><span>baz {{ qux }}</span></div>`).code])
>>> 插值处理
 const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${_ssrRenderAttrs(_attrs)}>\`\${foo}\`</div>`)
}
>>> 插值处理,元素嵌套
 const { ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${
    _ssrRenderAttrs(_attrs)
  }><div><span>${
    _ssrInterpolate(_ctx.foo)
  } bar</span><span>baz ${
    _ssrInterpolate(_ctx.qux)
  }</span></div></div>`)
}
undefined

第一个并非直接的差值,而是字符串形式,所以并没有当做插值处理。

后面的差值调用 _ssrInterpolate(_ctx.foo) 处理

ssrTransformElement 续

954a9ee static class 属性处理

feat(add): ssr->static class attr · gcclll/stb-vue-next@954a9ee · GitHub

静态 class 属性处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
for (let i = 0; i < node.props.length; i++) {
  const prop = node.props[i];
  // 忽略 input 上的 true 值或 false 值
  if (node.tag === "input" && isTrueFalseValue(prop)) {
    continue;
  }

  // special cases with children override
  if (prop.type === NodeTypes.DIRECTIVE) {
    // TODO 指令处理
  } else {
    if (node.tag === "textarea" && prop.name === "value" && prop.value) {
      // TODO 特殊情况:value on <textarea>
    } else if (!hasDynamicVBind) {
      if (prop.name === "key" || prop.name === "ref") {
        continue;
      }

      // static prop
      if (prop.name === "class" && prop.value) {
        staticClassBinding = JSON.stringify(prop.value.content);
      }
      openTag.push(
        ` ${prop.name}` +
          (prop.value ? `="${escapeHtml(prop.value.content)}"` : ``)
      );
    }
  }
}

class 处理部分:

1
2
3
4
5
6
7
// static prop
if (prop.name === "class" && prop.value) {
  staticClassBinding = JSON.stringify(prop.value.content);
}
openTag.push(
  ` ${prop.name}` + (prop.value ? `="${escapeHtml(prop.value.content)}"` : ``)
);

等于是将 class="bar" 原样添加到 openTag 中了,只不过这里对值用 escapeHtml 处 理了一下。

匹配: const escapeRE = /["'&<>]/ 替换成对应的

charvalue
"&quot;
&&amp;
'&#39;
<&lt;
>&gt;

如下测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 源文件:/js/vue/lib.js
const {
  compileSFCScript,
  compileStyle,
  getCompiledSSRString: ssr,
  compileSSR,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

log([">>> static class\n", ssr('<div class="bar"></div><p class="foo>"></p>').code]);
log(['>>> ref/key 属性会被忽略,不论静态还是动态\n', ssr('<div key="1" ref="el"></div>').code])
log(['>>> ref/key 属性会被忽略,不论静态还是动态\n', ssr('<div :key="1" :ref="el"></div>').code])
>>> static class
 const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${_ssrRenderAttrs(_attrs)}><div class="bar"></div><p class="foo&gt;"></p></div>`)
}
>>> ref/key 属性会被忽略,不论静态还是动态
 const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${_ssrRenderAttrs(_attrs)}><div></div></div>`)
}
>>> ref/key 属性会被忽略,不论静态还是动态
 const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${_ssrRenderAttrs(_attrs)}><div></div></div>`)
}
undefined

c28d528 dynamic class 属性处理

feat(add): ssr->dynamic class · gcclll/stb-vue-next@c28d528 · GitHub

当既有 static 也有 dynamic class 时需要进行合并,且是将 static 往 dynamic 上进行 合并,最后成为动态的 class。

新增处理逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
if (attrName === "class") {
  openTag.push(
    ` class="`,
    (dynamicClassBinding = createCallExpression(
      context.helper(SSR_RENDER_CLASS),
      [value]
    )),
    `"`
  );
}

如果也有静态属性的时候,将两者合并,需要用到两个函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function mergeCall(call: CallExpression, arg: string | JSChildNode) {
  const existing = call.arguments[0] as ExpressionNode | ArrayExpression;
  if (existing.type === NodeTypes.JS_ARRAY_EXPRESSION) {
    existing.elements.push(arg);
  } else {
    call.arguments[0] = createArrayExpression([existing, arg]);
  }
}

function removeStaticBinding(
  tag: TemplateLiteral["elements"],
  binding: string
) {
  const regExp = new RegExp(`^ ${binding}=".+"$`);
  const i = tag.findIndex((e) => typeof e === "string" && regExp.test(e));

  if (i > -1) {
    tag.splice(i, 1);
  }
}

mergeCall: 将静态 class 合并到动态 class 上 removeStaticBinding: 删除原来的静态 class 属性

测试:

1
2
3
4
5
6
7

// 源文件:/js/vue/lib.js
const { compileSFCScript, compileStyle, getCompiledSSRString: ssr, compileSSR, log } = require(process.env.BLOG_JS + '/vue/lib.js')

log(['>>> dynamic class\n', ssr('<div :class="bar"></div>').code])
log(['>>> static class\n', ssr('<div class="foo"></div>').code])
log(['>>> static + dynamic class\n', ssr('<div class="foo" :class="bar"></div>').code])
>>> dynamic class
 const { ssrRenderClass: _ssrRenderClass, ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${
    _ssrRenderAttrs(_attrs)
  }><div class="${
    _ssrRenderClass(_ctx.bar)
  }"></div></div>`)
}
>>> static class
 const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${_ssrRenderAttrs(_attrs)}><div class="foo"></div></div>`)
}
>>> static + dynamic class
 const { ssrRenderClass: _ssrRenderClass, ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${
    _ssrRenderAttrs(_attrs)
  }><div class="${
    _ssrRenderClass([_ctx.bar, "foo"])
  }"></div></div>`)
}
undefined

逻辑脑图: http://qiniu.ii6g.com/img/20210106143239.png

ca39229 style 属性处理

feat(add): ssr->style prop · gcclll/stb-vue-next@ca39229 · GitHub

新增处理代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
if (attrName === "style") {
  // :style
  if (dynamicStyleBinding) {
    // 已经有 style 合并
    mergeCall(dynamicStyleBinding, value);
  } else {
    openTag.push(
      ` style="`,
      (dynamicStyleBinding = createCallExpression(
        context.helper(SSR_RENDER_STYLE),
        [value]
      )),
      `"`
    );
  }
}
1
2
3
4
5
6
7

// 源文件:/js/vue/lib.js
const { compileSFCScript, compileStyle, getCompiledSSRString: ssr, compileSSR, log } = require(process.env.BLOG_JS + '/vue/lib.js')

log(['>>> static style\n', ssr('<div style="color:red"></div>').code])
log(['>>> dynamic style\n', ssr('<div :style="bar"></div>').code])
log(['>>> dynamic + static style\n', ssr('<div :style="bar" style="color:red"></div>').code])
>>> static style
 const { ssrRenderStyle: _ssrRenderStyle, ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${
    _ssrRenderAttrs(_attrs)
  }><div style="${
    _ssrRenderStyle({"color":"red"})
  }"></div></div>`)
}
>>> dynamic style
 const { ssrRenderStyle: _ssrRenderStyle, ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${
    _ssrRenderAttrs(_attrs)
  }><div style="${
    _ssrRenderStyle(_ctx.bar)
  }"></div></div>`)
}
>>> dynamic + static style
 const { ssrRenderStyle: _ssrRenderStyle, ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${
    _ssrRenderAttrs(_attrs)
  }><div style="${
    _ssrRenderStyle([_ctx.bar, {"color":"red"}])
  }"></div></div>`)
}
undefined

dfd4fb9 v-html 指令处理

feat(add): ssr->v-html directive · gcclll/stb-vue-next@dfd4fb9 · GitHub

这个处理在 ssrTransformElement 中只需要增加一行代码就OK,但是需要结合 ssrProcessElement 来进行处理。

1
2
3
if (prop.name === "html" && prop.exp /* v-html */) {
  rawChildrenMap.set(node, prop.exp);
}

ssrProcessElement 中会对 rawChildrenMap 进行处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
export function ssrProcessElement(
  node: PlainElementNode,
  context: SSRTransformContext
) {
  // ...
  // 已缓存的处理结果
  const rawChildren = rawChildrenMap.get(node);
  if (rawChildren) {
    context.pushStringPart(rawChildren);
  } else if (node.children.length) {
    processChildren(node.children, context);
  }

  // ...
}

测试:

1
2
3
4
5

// 源文件:/js/vue/lib.js
const { compileSFCScript, compileStyle, getCompiledSSRString: ssr, compileSSR, log } = require(process.env.BLOG_JS + '/vue/lib.js')

log(['>>> v-html\n', ssr('<div v-html="foo"/>').code])

直接进行值替换。

http://qiniu.ii6g.com/img/20210106170904.png

678e98a v-text 指令处理

feat(add): ssr->v-text directive · gcclll/stb-vue-next@678e98a · GitHub

这里是用插值方式来处理了 v-text :

1
2
3
if (prop.name === "text" && prop.exp /* v-text */) {
  node.children = [createInterpolation(prop.exp, prop.loc)];
}

测试:

1
2
3
4
5

// 源文件:/js/vue/lib.js
const { compileSFCScript, compileStyle, getCompiledSSRString: ssr, compileSSR, log } = require(process.env.BLOG_JS + '/vue/lib.js')

log(ssr('<div v-text="foo"/>').code)
const { ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${
    _ssrRenderAttrs(_attrs)
  }><div>${
    _ssrInterpolate(_ctx.foo)
  }</div></div>`)
}
undefined

http://qiniu.ii6g.com/img/20210106170929.png

0472dfd v-slot 指令错误

feat(add): ssr->v-slot directive · gcclll/stb-vue-next@0472dfd · GitHub

由于指令不能应用于非 component 或 template 组件上,所以这里无法适用。

45e78e1 v-bind 指令

feat(add): ssr->v-bind · gcclll/stb-vue-next@45e78e1 · GitHub

下面的测试为综合情况测试,包含大部分使用情况。

  1. v-bind:arg(non-boolean)

  2. v-bind:[arg] 动态参数处理

  3. v-bind:[arg] + v-bind 混合方式

  4. style + :style

  5. class + :class

  6. v-on 会被忽略

  7. key/ref 无论静态动态都会被忽略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 源文件:/js/vue/lib.js
const {
  compileSFCScript,
  compileStyle,
  getCompiledSSRString: ssr,
  compileSSR,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

log(
  ssr(`<div
style="color:red"
:style="baz"
class="foo"
:class="bar"
:[key]="value"
:id="id"
v-bind="obj"
v-on="fxx"
@click="fxc"
:key="1"
:ref="el"
></div>`).code
);
const { mergeProps: _mergeProps } = require("vue")
const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${
    _ssrRenderAttrs(_attrs)
  }><div${
    _ssrRenderAttrs(_mergeProps({
      style: [{"color":"red"}, _ctx.baz],
      class: ["foo", _ctx.bar],
      [_ctx.key || ""]: _ctx.value,
      id: _ctx.id
    }, _ctx.obj, {
      key: 1,
      ref: _ctx.el
    }))
  }></div></div>`)
}
undefined

key, ref 为什么没有忽略???

http://qiniu.ii6g.com/img/20210106171132.png

value on textarea

http://qiniu.ii6g.com/img/20210106170832.png

e79e343 static value

feat(add): ssr->static value on textarea · gcclll/stb-vue-next@e79e343 · GitHub

静态 value 处理很简单,直接当做子节点替换。

1
2
3
4
if (node.tag === "textarea" && prop.name === "value" && prop.value) {
  // 特殊情况:value on <textarea>
  rawChildrenMap.set(node, escapeHtml(prop.value.content));
}

测试

1
2
3
4
5

// 源文件:/js/vue/lib.js
const { compileSFCScript, compileStyle, getCompiledSSRString:ssr, compileSSR, log } = require(process.env.BLOG_JS + '/vue/lib.js')

log(['>>> static value on textarea\n', ssr('<textarea value="fo&gt;o"/>').code])
>>> static value on textarea
 const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${_ssrRenderAttrs(_attrs)}><textarea>fo&gt;o</textarea></div>`)
}
undefined

dynamic value

处理代码:

1
2
3
4
5
if (isTextareaWithValue(node, prop) && prop.exp /* textarea with value */) {
  if (!hasDynamicVBind) {
    node.children = [createInterpolation(prop.exp, prop.loc)];
  }
}

当做插值类型处理,作为孩子节点。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 源文件:/js/vue/lib.js
const {
  compileSFCScript,
  compileStyle,
  getCompiledSSRString: ssr,
  compileSSR,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

log(ssr('<textarea :value="foo"/>').code);
const { ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${
    _ssrRenderAttrs(_attrs)
  }><textarea>${
    _ssrInterpolate(_ctx.foo)
  }</textarea></div>`)
}
undefined

cdd8fd0 dynamic arg 动态参数

feat(add): ssr->dynamic arg on textarea · gcclll/stb-vue-next@cdd8fd0 · GitHub

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
if (node.tag === "textarea") {
  // TODO
  const existingText = node.children[0] as
    | TextNode
    | InterpolationNode
    | undefined;
  // If interpolation, this is dynamic <textarea> content, potentially
  // injected by v-model and takes higher priority than v-bind value
  // v-model 的优先级高于 v-bind value
  if (!existingText || existingText.type !== NodeTypes.INTERPOLATION) {
    // <textarea> with dynamic v-bind. We don't know if the final props
    // will contain .value, so we will have to do something special:
    // assign the merged props to a temp variable, and check whether
    // it contains value (if yes, render is as children).
    // 当 textarea 包含动态参数时,我们并不能确定最后的结果是否包含 .value
    // 因此我们将不得不做些特殊处理来应对:
    // 将已合并的 props 保存成一个临时变量,然后检查它是否包含 value 属性(如果
    // 包含,则将它当做 children 来渲染)
    const tempId = `_temp${context.temps++}`;
    propsExp.arguments = [
      createAssignmentExpression(createSimpleExpression(tempId, false), props),
    ];

    rawChildrenMap.set(
      node,
      createCallExpression(context.helper(SSR_INTERPOLATE), [
        createConditionalExpression(
          createSimpleExpression(`"value" in ${tempId}`, false),
          createSimpleExpression(`${tempId}.value`, false),
          createSimpleExpression(
            existingText ? existingText.content : ``,
            true
          ),
          false
        ),
      ])
    );
  }
}

在包含动态参数的时候,并不能确定最终参数名就是 value 所以需要做些特殊处理。

1
2
3
4
5

// 源文件:/js/vue/lib.js
const { compileSFCScript, compileStyle, getCompiledSSRString, compileSSR:ssr, log } = require(process.env.BLOG_JS + '/vue/lib.js')

log(ssr(`<textarea v-bind="obj">fallback</textarea>`).code)
const { mergeProps: _mergeProps } = require("vue")
const { ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  let _temp0

  _push(`<textarea${
    _ssrRenderAttrs(_temp0 = _mergeProps(_ctx.obj, _attrs), "textarea")
  }>${
    _ssrInterpolate(("value" in _temp0) ? _temp0.value : "fallback")
  }</textarea>`)
}
undefined

等于先将所有属性合并起来,在运行时决定是否有 value 属性,如果存在就使用这个值 内容填充 <textarea> 孩子节点,否则直接使用原来的孩子节点内容(如: "fallback")

源码处理中有两个前提条件,才会这样处理

  1. 没有孩子节点

  2. 或者孩子节点不是插值类型

即如果有插值类型的孩子节点,是不会进行如上的处理的,看下面的实例:

1
2
3
4
5

// 源文件:/js/vue/lib.js
const { compileSFCScript, compileStyle, getCompiledSSRString, compileSSR: ssr, log } = require(process.env.BLOG_JS + '/vue/lib.js')

log(ssr('<textarea v-bind="obj">{{ foo }}</textarea>').code)
const { mergeProps: _mergeProps } = require("vue")
const { ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<textarea${
    _ssrRenderAttrs(_mergeProps(_ctx.obj, _attrs), "textarea")
  }>${
    _ssrInterpolate(_ctx.foo)
  }</textarea>`)
}
undefined

结果如上 ↑。

b97d467 input + boolean attr

feat(add): ssr->v-bind boolean on input · gcclll/stb-vue-next@b97d467 · GitHub

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 源文件:/js/vue/lib.js
const {
  compileSFCScript,
  compileStyle,
  getCompiledSSRString: ssr,
  compileSSR,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

log([">>> input\n", ssr("<input>").code]);
log([
  ">>> input with v-bind:arg(boolean)\n",
  ssr(`<input type="checkbox" :checked="checked">`).code,
]);
>>> input
 const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${_ssrRenderAttrs(_attrs)}><input></div>`)
}
>>> input with v-bind:arg(boolean)
 const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${
    _ssrRenderAttrs(_attrs)
  }><input type="checkbox"${
    (_ctx.checked) ? " checked" : ""
  }></div>`)
}
undefined

TODO 对于 v-bind + v-model 的结合使用,需要实现 ssrTransformModel 函数,这里暂时不做处理。

893681b v-model transform

feat(add): ssr->v-model, text type · gcclll/stb-vue-next@893681b · GitHub

1
2
3
4
5

// 源文件:/js/vue/lib.js
const { compileSFCScript, compileStyle, getCompiledSSRString:ssr, compileSSR, log } = require(process.env.BLOG_JS + '/vue/lib.js')

log(['>>> <input> (text types,默认)', ssr(`<input v-model="bar">`).code])

脑图:

http://qiniu.ii6g.com/img/20210106174207.png

7502230 input type: radio

feat(add): ssr->v-model, type radio · gcclll/stb-vue-next@7502230 · GitHub

v-model 根据 model表达式的值和 value 属性的值,判断最终转成 checked 属性 (<input type="radio" checked>)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
if (type.value) {
  // 静态类型
  switch (type.value.content) {
    case "radio":
      res.props = [
        createObjectProperty(
          `checked`,
          createCallExpression(context.helper(SSR_LOOSE_EQUAL), [model, value])
        ),
      ];
      break;
  }
}

测试:

1
2
3
4
5

// 源文件:/js/vue/lib.js
const { compileSFCScript, compileStyle, getCompiledSSRString:ssr, compileSSR, log } = require(process.env.BLOG_JS + '/vue/lib.js')

log(ssr(`<input type="radio" value="foo" v-model="bar">`).code)
const { ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${
    _ssrRenderAttrs(_attrs)
  }><input type="radio" value="foo"${
    (_ssrLooseEqual(_ctx.bar, "foo")) ? " checked" : ""
  }></div>`)
}
undefined

input type: checkbox

类型为 checkbox 的时候要检查 true-value/false-value 属性。

880eaf3 with true/false-value

feat(add): ssr->v-model, type checkbox with true/false-value · gcclll/stb-vue-next@880eaf3 · GitHub

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
switch (type.value.content) {
  // ...
  case "checkbox":
    const trueValueBinding = findProp(node, "true-value");
    if (trueValueBinding) {
      const trueValue =
        trueValueBinding.type === NodeTypes.ATTRIBUTE
          ? JSON.stringify(trueValueBinding.value!.content)
          : trueValueBinding.exp!;

      res.props = [
        createObjectProperty(
          `checked`,
          createCallExpression(context.helper(SSR_LOOSE_EQUAL), [
            model,
            trueValue,
          ])
        ),
      ];
    } else {
    }
    break;
  // ...
}

如果存在 true-value/false-value 的时候,检测条件就是这两个值的比较结果,只有这 两个值相等的情况下才会 checked

1
2
3
4
5
6
7

// 源文件:/js/vue/lib.js
const { compileSFCScript, compileStyle, getCompiledSSRString:ssr, compileSSR, log } = require(process.env.BLOG_JS + '/vue/lib.js')

log(['>>> v-bind true-value/false-value\n', ssr(`<input type="checkbox" :true-value="foo" :false-value="bar" v-model="baz">`).code])
log(['>>> static true-value/false-value\n', ssr(`<input type="checkbox" true-value="foo" false-value="bar" v-model="baz">`).code])
log(['>>> true-value/false-value 只有其中一个\n', ssr(`<input type="checkbox" false-value="bar" v-model="baz">`).code])
>>> v-bind true-value/false-value
 const { ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${
    _ssrRenderAttrs(_attrs)
  }><input type="checkbox"${
    (_ssrLooseEqual(_ctx.baz, _ctx.foo)) ? " checked" : ""
  }></div>`)
}
>>> static true-value/false-value
 const { ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${
    _ssrRenderAttrs(_attrs)
  }><input type="checkbox"${
    (_ssrLooseEqual(_ctx.baz, "foo")) ? " checked" : ""
  }></div>`)
}
>>> true-value/false-value 只有其中一个
 const { ssrLooseContain: _ssrLooseContain, ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${
    _ssrRenderAttrs(_attrs)
  }><input type="checkbox"${
    ((Array.isArray(_ctx.baz))
      ? _ssrLooseContain(_ctx.baz, null)
      : _ctx.baz) ? " checked" : ""
  }></div>`)
}
undefined

❓ 为什么 false-value 还在?

FIX: fix: false.value -> false-value · gcclll/stb-vue-next@e0fc173 · GitHub

从结果看,貌似 false-value 并没有被用到,用到的只有 true-value 去和 v-model 表达式值进行比较。

59b1577 without true/false-value

feat(add): ssr->v-model, type checkbox without true/false-value · gcclll/stb-vue-next@59b1577 · GitHub

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 源文件:/js/vue/lib.js
const {
  compileSFCScript,
  compileStyle,
  getCompiledSSRString: ssr,
  compileSSR,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

log(ssr(`<input type="checkbox" value="foo" v-model="bar">`).code);
const { ssrLooseContain: _ssrLooseContain, ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${
    _ssrRenderAttrs(_attrs)
  }><input type="checkbox" value="foo"${
    ((Array.isArray(_ctx.bar))
      ? _ssrLooseContain(_ctx.bar, "foo")
      : _ctx.bar) ? " checked" : ""
  }></div>`)
}
undefined

_ssrLooseContain(_ctx.bar, "foo") 简单的数组找值操作:

1
2
3
export function ssrLooseContain(arr: unknown[], value: unknown): boolean {
  return looseIndexOf(arr, value) > -1;
}

相当于,如果 v-model="bar" 的值 bar 是个数组,只需要其中有一个满足条件就会 checked ,这也是经常使用到的方式,将一组数据保存到一个数组里面,然后对应一组 checkboxs 用来控制这些组件的选中未选中状态。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<template>
<input type="checkbox" value="1" v-model="checkedBoxes">
<input type="checkbox" value="2" v-model="checkedBoxes">
<input type="checkbox" value="3" v-model="checkedBoxes">
</template>
<script>
export default {
  data() {
    return { checkedBoxes: [1, 2, 3] }
  }
}
<script>

就如上面的例子,只要 checkedBoxes 里面的值发生改变,就会触发 checkbox 状态更 新,且只有在数组内的值与当前 checkbox 的 value 属性值相等就会被选中,反之不会 被选中。

a0d4a40 input type: file 时不能用 v-model

feat(add): ssr->v-model, type file with v-model error · gcclll/stb-vue-next@a0d4a40 · GitHub

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 源文件:/js/vue/lib.js
const {
  compileSFCScript,
  compileStyle,
  getCompiledSSRString: ssr,
  compileSSR,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

try {
  ssr('<input type="file" v-model="foo">');
} catch (e) {
  console.log(e.message);
}
v-model cannot be used on file inputs since they are read-only. Use a v-on:change listener instead.
undefined

d7309be v-model on textarea

feat(add): ssr->v-model on textarea · gcclll/stb-vue-next@d7309be · GitHub

当做插值处理,替换成孩子节点。

1
2
3
4
5

// 源文件:/js/vue/lib.js
const { compileSFCScript, compileStyle, getCompiledSSRString:ssr, compileSSR, log } = require(process.env.BLOG_JS + '/vue/lib.js')

log(ssr(`<textarea v-model="foo">bar</textarea>`).code)
const { ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${
    _ssrRenderAttrs(_attrs)
  }><textarea>${
    _ssrInterpolate(_ctx.foo)
  }</textarea></div>`)
}
undefined

169027e v-show transform

feat(add): ssr->v-show · gcclll/stb-vue-next@169027e · GitHub

v-show 指令的处理相对简单,根据指令表达式值,创建一个三元条件表达式,利用 display:none 属性隐藏元素(非删除操作)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
export const ssrTransformShow: DirectiveTransform = (dir, node, context) => {
  if (!dir.exp) {
    context.onError(
      createDOMCompilerError(DOMErrorCodes.X_V_SHOW_NO_EXPRESSION)
    );
  }

  return {
    props: [
      createObjectProperty(
        `style`,
        // -> dir.exp ? `null` : `display:none`
        createConditionalExpression(
          dir.exp!,
          createSimpleExpression(`null`, false),
          createObjectExpression([
            createObjectProperty(
              `display`,
              createSimpleExpression(`none`, true)
            ),
          ]),
          false /* no newline */
        )
      ),
    ],
  };
};

测试:

1
2
3
4
5
6
7

// 源文件:/js/vue/lib.js
const { compileSFCScript, compileStyle, getCompiledSSRString:ssrs, compileSSR:ssr, log } = require(process.env.BLOG_JS + '/vue/lib.js')

log(['>>> basic 作为根节点\n', ssr(`<div v-show="foo"/>`).code])
log(['\n>>> basic 非根节点\n', ssrs(`<div v-show="foo"/>`).code])
log(['\n>>> basic 非根节点,包含静态和动态 style\n', ssrs(`<div v-show="foo" style="color:red" :style="bar"/>`).code])
>>> basic 作为根节点
 const { mergeProps: _mergeProps } = require("vue")
const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${_ssrRenderAttrs(_mergeProps({
    style: (_ctx.foo) ? null : { display: "none" }
  }, _attrs))}></div>`)
}

>>> basic 非根节点
 const { ssrRenderStyle: _ssrRenderStyle, ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${
    _ssrRenderAttrs(_attrs)
  }><div style="${
    _ssrRenderStyle((_ctx.foo) ? null : { display: "none" })
  }"></div></div>`)
}

>>> basic 非根节点,包含静态和动态 style
 const { ssrRenderStyle: _ssrRenderStyle, ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${
    _ssrRenderAttrs(_attrs)
  }><div style="${
    _ssrRenderStyle([
      (_ctx.foo) ? null : { display: "none" },
      {"color":"red"},
      _ctx.bar
    ])
  }"></div></div>`)
}
undefined

5bf3644 v-if transform

feat(add): ssr->v-if · gcclll/stb-vue-next@5bf3644 · GitHub

ssrTransformIf 也是直接使用了 compiler-core: transformIf 进行处理。

fix: ssr->template v-if no ] · gcclll/stb-vue-next@094d5c0 · GitHub

1
2
3
4
5
6
// Plugin for the first transform pass, which simply constructs the AST node
// 先经过 core: transformIf 处理一道
export const ssrTransformIf = createStructuralDirectiveTransform(
  /^(if|else|else-if)$/,
  processIf
)

剩下的 ssr 的部分,需要用到 ssrProcessIf() 进行单独处理。

1
2
3
4
5
// 源文件:/js/vue/lib.js
const { compileSFCScript, compileStyle, getCompiledSSRString, compileSSR:ssr, log } = require(process.env.BLOG_JS + '/vue/lib.js')

const { code, ast } = ssr('<div v-if="foo"></div>')
log([ast.children[0].branches[0], '\n', code])
{
  type: 10,
  loc: {
    start: { column: 1, line: 1, offset: 0 },
    end: { column: 23, line: 1, offset: 22 },
    source: '<div v-if="foo"></div>'
  },
  condition: {
    type: 4,
    content: '_ctx.foo',
    isStatic: false,
    constType: 0,
    loc: { start: [Object], end: [Object], source: 'foo' }
  },
  children: [
    {
      type: 1,
      ns: 0,
      tag: 'div',
      tagType: 0,
      props: [Array],
      isSelfClosing: false,
      children: [],
      loc: [Object],
      codegenNode: undefined,
      ssrCodegenNode: [Object]
    }
  ],
  userKey: undefined
}
 const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {

}
undefined

啥也没有?

但是从 ast.children[0].branches[0] 结果看确实被 core 正确处理了

所以还是需要实现 ssrProcessIf() 并且在 ssrCodegenTransform->processChildren 增加 NodeTypes.IF 分支处理。

加上 ssrProcessIf 再测试一遍(测试均来自官方测试用例 ssrVIf.spec.ts 其他同):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 源文件:/js/vue/lib.js
const { compileSFCScript, compileStyle, getCompiledSSRString, compileSSR:ssr, log } = require(process.env.BLOG_JS + '/vue/lib.js')

log(['>>> basic\n', ssr('<div v-if="foo"></div>').code])
log(['\n>>> with nested content\n', ssr(`<div v-if="foo">hello<span>ok</span></div>`).code])
log(['>>> v-if + v-else\n', ssr(`<div v-if="foo"/><span v-else/>`).code])
log(['>>> v-if + v-else-if\n', ssr(`<div v-if="foo"/><span v-else-if="bar"/>`).code])
log(['>>> v-if + v-else-if + v-else\n', ssr(`<div v-if="foo"/><span v-else-if="bar"/><span v-else/>`).code])
log(['>>> <template v-if>(text)', ssr(`<template v-if="foo">hello</template>`).code])
log(['>>> <template v-if>(single element)', ssr(`<template v-if="foo"><div>hi</div></template>`).code])
log(['>>> <template v-if>(multiple element)', ssr(`<template v-if="foo"><div>hi</div><div>hi</div><div>ho</div></template>`).code])
// v-for transform 到此还没实现,所以这个会报错
// log(['>>> <template v-if> (with v-for inside)', ssr(`<template v-if="foo"><div v-for="i in list"/></template>`).code])
log(['>>> <template v-if> + normal v-else', ssr(`<template v-if="foo"><div>hi</div></template><div v-else/>`).code])
>>> basic
 const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  if (_ctx.foo) {
    _push(`<div${_ssrRenderAttrs(_attrs)}></div>`)
  } else {
    _push(`<!---->`)
  }
}

>>> with nested content
 const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  if (_ctx.foo) {
    _push(`<div${_ssrRenderAttrs(_attrs)}>hello<span>ok</span></div>`)
  } else {
    _push(`<!---->`)
  }
}
>>> v-if + v-else
 const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  if (_ctx.foo) {
    _push(`<div${_ssrRenderAttrs(_attrs)}></div>`)
  } else {
    _push(`<span${_ssrRenderAttrs(_attrs)}></span>`)
  }
}
>>> v-if + v-else-if
 const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  if (_ctx.foo) {
    _push(`<div${_ssrRenderAttrs(_attrs)}></div>`)
  } else if (_ctx.bar) {
    _push(`<span${_ssrRenderAttrs(_attrs)}></span>`)
  } else {
    _push(`<!---->`)
  }
}
>>> v-if + v-else-if + v-else
 const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  if (_ctx.foo) {
    _push(`<div${_ssrRenderAttrs(_attrs)}></div>`)
  } else if (_ctx.bar) {
    _push(`<span${_ssrRenderAttrs(_attrs)}></span>`)
  } else {
    _push(`<span${_ssrRenderAttrs(_attrs)}></span>`)
  }
}
>>> <template v-if>(text)
return function ssrRender(_ctx, _push, _parent, _attrs) {
  if (_ctx.foo) {
    _push(`<!--[-->hello<!--]-->`)
  } else {
    _push(`<!---->`)
  }
}
>>> <template v-if>(single element) const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  if (_ctx.foo) {
    _push(`<div${_ssrRenderAttrs(_attrs)}>hi</div>`)
  } else {
    _push(`<!---->`)
  }
}
>>> <template v-if>(multiple element)
return function ssrRender(_ctx, _push, _parent, _attrs) {
  if (_ctx.foo) {
    _push(`<!--[--><div>hi</div><div>hi</div><div>ho</div><!--]-->`)
  } else {
    _push(`<!---->`)
  }
}
>>> <template v-if> + normal v-else const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  if (_ctx.foo) {
    _push(`<div${_ssrRenderAttrs(_attrs)}>hi</div>`)
  } else {
    _push(`<div${_ssrRenderAttrs(_attrs)}></div>`)
  }
}
undefined

为什么是 if(){}else{} ???

这个要追溯到 compiler-core: codegen.ts 里面对 ssr 环境下的 if 指令的处理代码:

1
2
3
4
5
switch (node.type) {
  case NodeTypes.JS_IF_STATEMENT:
    !__BROWSER__ && genIfStatement(node, context);
    break;
}

这个 genIfStatement 就是用来生成 if...else 代码的。

脑图: http://qiniu.ii6g.com/img/20210106224304.png

所以 ssr v-if 处理大致流程简单分两步:

  1. core: transformIf 得到 node.branches

  2. ssrProcessIf 处理,生成 if -> else if -> else

4839090 v-for transform

fix: ssr->v-for add transform · gcclll/stb-vue-next@4839090 · GitHub

主要还是借助了 compiler-core: transformFor 处理逻辑,加上 ssrProcessFor 加工处理 了下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 源文件:/js/vue/lib.js
const { compileSFCScript, compileStyle, getCompiledSSRString, compileSSR:ssr, log } = require(process.env.BLOG_JS + '/vue/lib.js')

log(['>>> basic\n', ssr(`<div v-for="i in list" />`).code])
log(['>>> nested content\n', ssr(`<div v-for="i in list">foo<span>bar</span></div>`).code])
log(['>>> nested v-for\n', ssr(`<div v-for="row, i in list">` +
          `<div v-for="j in row">{{ i }},{{ j }}</div>` +
          `</div>`).code])
log(['>>> template v-for(text)\n', ssr(`<template v-for="i in list">{{ i }}</template>`).code])
log(['>>> template v-for (single element)\n', ssr(`<template v-for="i in list"><span>{{ i }}</span></template>`).code])
log(['>>> template v-for (multi element)\n', ssr(`<template v-for="i in list"><span>{{ i }}</span><span>{{ i + 1 }}</span></template>`).code])
log(['>>> render loop args should not be prefixed\n', '> render loop 循环回调的参数不应该加前缀\n', ssr(`<div v-for="{ foo }, index in list">{{ foo + bar + index }}</div>`).code])
>>> basic
 const { ssrRenderList: _ssrRenderList } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<!--[-->`)
  _ssrRenderList(_ctx.list, (i) => {
    _push(`<div></div>`)
  })
  _push(`<!--]-->`)
}
>>> nested content
 const { ssrRenderList: _ssrRenderList } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<!--[-->`)
  _ssrRenderList(_ctx.list, (i) => {
    _push(`<div>foo<span>bar</span></div>`)
  })
  _push(`<!--]-->`)
}
>>> nested v-for
 const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<!--[-->`)
  _ssrRenderList(_ctx.list, (row, i) => {
    _push(`<div><!--[-->`)
    _ssrRenderList(row, (j) => {
      _push(`<div>${
        _ssrInterpolate(i)
      },${
        _ssrInterpolate(j)
      }</div>`)
    })
    _push(`<!--]--></div>`)
  })
  _push(`<!--]-->`)
}
>>> template v-for(text)
 const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<!--[-->`)
  _ssrRenderList(_ctx.list, (i) => {
    _push(`<!--[-->${_ssrInterpolate(i)}<!--]-->`)
  })
  _push(`<!--]-->`)
}
>>> template v-for (single element)
 const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<!--[-->`)
  _ssrRenderList(_ctx.list, (i) => {
    _push(`<span>${_ssrInterpolate(i)}</span>`)
  })
  _push(`<!--]-->`)
}
>>> template v-for (multi element)
 const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<!--[-->`)
  _ssrRenderList(_ctx.list, (i) => {
    _push(`<!--[--><span>${
      _ssrInterpolate(i)
    }</span><span>${
      _ssrInterpolate(i + 1)
    }</span><!--]-->`)
  })
  _push(`<!--]-->`)
}
>>> render loop args should not be prefixed
 > render loop 循环回调的参数不应该加前缀
 const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<!--[-->`)
  _ssrRenderList(_ctx.list, ({ foo }, index) => {
    _push(`<div>${_ssrInterpolate(foo + _ctx.bar + index)}</div>`)
  })
  _push(`<!--]-->`)
}
undefined

代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
export function ssrProcessFor(
  node: ForNode,
  context: SSRTransformContext,
  disableNestedFragments = false
) {
  // 需要 Fragment 的条件
  // 1. disableNestedFragments = false
  // 2. 有两个及以上的孩子节点或者第一个孩子节点的类型不是 ELEMENT(可能是用户组件)
  const needFragmentWrapper =
    !disableNestedFragments &&
    (node.children.length !== 1 || node.children[0].type !== NodeTypes.ELEMENT)

  // 创建 for (...) 表达式
  const renderLoop = createFunctionExpression(
    createForLoopParams(node.parseResult)
  )

  renderLoop.body = processChildrenAsStatement(
    node.children,
    context,
    needFragmentWrapper
  )

  // v-for always renders a fragment unless explicitly disabled
  if (!disableNestedFragments) {
    context.pushStringPart(`<!--[-->`)
  }

  // 创建表达式
  context.pushStatement(
    createCallExpression(context.helper(SSR_RENDER_LIST), [
      node.source,
      renderLoop
    ])
  )

  if (!disableNestedFragments) {
    context.pushStringPart(`<!--]-->`)
  }
}

v-for 处理除了 tranformFor 剩下的处理都在这个 ssrProcessFor 里面了。

8036837 其他不需要处理的情况

fix: ssr->add other useless cases in process children · gcclll/stb-vue-next@8036837 · GitHub

比如:

  1. IF_BRANCHssrProcessIf 中被处理了

  2. TEXT_CALLCOMPOUND_EXPRESSION 在 ssr 中不会被用到

  3. COMMENT 注释简单的还原处理即可

TODO 16833be component transform

fix: ssr->component transform · gcclll/stb-vue-next@16833be · GitHub

http://qiniu.ii6g.com/img/20210107171258.png

1
2
3
4
5
6
7

// 源文件:/js/vue/lib.js
const { compileSFCScript, compileStyle, getCompiledSSRString, compileSSR: ssr, log } = require(process.env.BLOG_JS + '/vue/lib.js')

log(['>>> basic\n', ssr(`<foo id="a" :prop="b" />`).code])
log(['>>> 动态组件 is\n', ssr(`<component is="foo" prop="b" />`).code])
log(['>>> 动态组件 :is\n', ssr(`<component :is="foo" prop="b" />`).code])
>>> basic
 const { resolveComponent: _resolveComponent, mergeProps: _mergeProps } = require("vue")
const { ssrRenderComponent: _ssrRenderComponent } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  const _component_foo = _resolveComponent("foo")

  _push(_ssrRenderComponent(_component_foo, _mergeProps({
    id: "a",
    prop: _ctx.b
  }, _attrs), null, _parent))
}
>>> 动态组件 is
 const { resolveDynamicComponent: _resolveDynamicComponent, mergeProps: _mergeProps, createVNode: _createVNode } = require("vue")
const { ssrRenderVNode: _ssrRenderVNode } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent("foo"), _mergeProps({ prop: "b" }, _attrs), null), _parent)
}
>>> 动态组件 :is
 const { resolveDynamicComponent: _resolveDynamicComponent, mergeProps: _mergeProps, createVNode: _createVNode } = require("vue")
const { ssrRenderVNode: _ssrRenderVNode } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent(_ctx.foo), _mergeProps({ prop: "b" }, _attrs), null), _parent)
}
undefined

cdba013 component slot outlet

feat(add): ssr->slot outlet transform · gcclll/stb-vue-next@cdba013 · GitHub

<slot></slot> 插槽处理。

处理逻辑:

  1. transform 阶段 -> ssrTransformSlotOutlet

    这里还只是创建了 ssrCodegenNode 并没有实际创建 render 函数

    1
    
    _ssrRenderSlot(_ctx.$slots, slotName, slotProps, fallback, _push, _parent)
    
  2. codegen 处理 -> ssrCodgenTransform

    这个阶段是扩展 1 中的第四个参数,也就是 fallback,检测 <slot></slot> 下是不 是有孩子节点,如果有当做 fallback 处理,替换 node.ssrCodegenNode.arguments[3] 的值。

http://qiniu.ii6g.com/img/20210107171820.png

1
2
3
4
5
6
7
8
9

// 源文件:/js/vue/lib.js
const { compileSFCScript, compileStyle, getCompiledSSRString, compileSSR:ssr, log } = require(process.env.BLOG_JS + '/vue/lib.js')

log(['>>> basic\n', ssr(`<slot/>`).code])
log(['>>> with named\n', ssr(`<slot name="foo"/>`).code])
log(['>>> with dynamic named\n', ssr(`<slot :name="bar.baz"/>`).code])
log(['>>> with props and fallback\n', ssr(`<slot name="foo" :p1="1" bar="2" >some {{ fallback }} content</slot>`).code])
log(['>>> with fallback\n', ssr(`<slot>some {{ fallback }} content</slot>`).code])
>>> basic
 const { ssrRenderSlot: _ssrRenderSlot } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _ssrRenderSlot(_ctx.$slots, "default", {}, null, _push, _parent)
}
>>> with named
 const { ssrRenderSlot: _ssrRenderSlot } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _ssrRenderSlot(_ctx.$slots, "foo", {}, null, _push, _parent)
}
>>> with dynamic named
 const { ssrRenderSlot: _ssrRenderSlot } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _ssrRenderSlot(_ctx.$slots, _ctx.bar.baz, {}, null, _push, _parent)
}
>>> with props and fallback
 const { ssrRenderSlot: _ssrRenderSlot, ssrInterpolate: _ssrInterpolate } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _ssrRenderSlot(_ctx.$slots, "foo", {
    p1: 1,
    bar: "2"
  }, () => {
    _push(`some ${_ssrInterpolate(_ctx.fallback)} content`)
  }, _push, _parent)
}
>>> with fallback
 const { ssrRenderSlot: _ssrRenderSlot, ssrInterpolate: _ssrInterpolate } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _ssrRenderSlot(_ctx.$slots, "default", {}, () => {
    _push(`some ${_ssrInterpolate(_ctx.fallback)} content`)
  }, _push, _parent)
}
undefined

ssrRenderSlot 函数实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

export function ssrRenderSlot(
  slots: Slots | SSRSlots,
  slotName: string,
  slotProps: Props,
  fallbackRenderFn: (() => void) | null,
  push: PushFn,
  parentComponent: ComponentInternalInstance
) {
  // template-compiled slots are always rendered as fragments
  push(`<!--[-->`)
  const slotFn = slots[slotName]
  if (slotFn) {
    const scopeId = parentComponent && parentComponent.type.__scopeId
    const ret = slotFn(
      slotProps,
      push,
      parentComponent,
      scopeId ? ` ${scopeId}-s` : ``
    )
    if (Array.isArray(ret)) {
      // normal slot
      renderVNodeChildren(push, ret, parentComponent)
    }
  } else if (fallbackRenderFn) {
    fallbackRenderFn()
  }
  push(`<!--]-->`)
}

fallback 用途:在没有任何 slot template 时候会默认用 fallback 里的内容来渲染这个 slot。

如:

1
2
3
4
5
6
7
8
<template>
<div>
<slot>some content</slot>
</div>
</template>

<!-- 当引用这个组件的组件没有提供任何 slot 模板的时候: 相当于直接使用 fallback 替换插槽-->
<div>some content</div>

359f856 suspense 内置组件

feat(add): ssr->slot suspense component · gcclll/stb-vue-next@359f856 · GitHub

http://qiniu.ii6g.com/img/20210107191831.png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13

// 源文件:/js/vue/lib.js
const { compileSFCScript, compileStyle, getCompiledSSRString, compileSSR:ssr, log } = require(process.env.BLOG_JS + '/vue/lib.js')

log(['>>> implicit default\n', ssr(`<suspense><foo/></suspense>`).code])
log(['>>> explicit slots\n', ssr(`<suspense>
      <template #default>
        <foo/>
      </template>
      <template #fallback>
        loading...
      </template>
    </suspense>`).code])
>>> implicit default
 const { resolveComponent: _resolveComponent, withCtx: _withCtx } = require("vue")
const { ssrRenderComponent: _ssrRenderComponent, ssrRenderSuspense: _ssrRenderSuspense } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  const _component_foo = _resolveComponent("foo")

  _ssrRenderSuspense(_push, {
    default: () => {
      _push(_ssrRenderComponent(_component_foo, null, null, _parent))
    },
    _: 1 /* STABLE */
  })
}
>>> explicit slots
 const { resolveComponent: _resolveComponent, withCtx: _withCtx } = require("vue")
const { ssrRenderComponent: _ssrRenderComponent, ssrRenderSuspense: _ssrRenderSuspense } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  const _component_foo = _resolveComponent("foo")

  _ssrRenderSuspense(_push, {
    default: () => {
      _push(_ssrRenderComponent(_component_foo, null, null, _parent))
    },
    fallback: () => {
      _push(` loading... `)
    },
    _: 1 /* STABLE */
  })
}
undefined

ssrRenderSuspense 实际上只是一个 await 异步函数封装:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

export async function ssrRenderSuspense(
  push: PushFn,
  { default: renderContent }: Record<string, (() => void) | undefined>
) {
  if (renderContent) {
    renderContent()
  } else {
    push(`<!---->`)
  }
}

最终将 SUSPENSE 中的组件异步渲染。

e27a5e4 teleport 内置组件

feat(add): ssr->teleport component · gcclll/stb-vue-next@e27a5e4 · GitHub

http://qiniu.ii6g.com/img/20210107195939.png

作用❓

1
2
3
4
5
6
7

// 源文件:/js/vue/lib.js
const { compileSFCScript, compileStyle, getCompiledSSRString, compileSSR:ssr, log } = require(process.env.BLOG_JS + '/vue/lib.js')

log(['>>> basic\n', ssr(`<teleport :to="target"><div/></teleport>`).code])
log(['>>> disabled prop\n', ssr(`<teleport :to="target" disabled><div/></teleport>`).code])
log(['>>> disabled prop with value\n', ssr(`<teleport :to="target" :disabled="foo"><div/></teleport>`).code])
>>> basic
 const { ssrRenderTeleport: _ssrRenderTeleport } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _ssrRenderTeleport(_push, (_push) => {
    _push(`<div></div>`)
  }, _ctx.target, false, _parent)
}
>>> disabled prop
 const { ssrRenderTeleport: _ssrRenderTeleport } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _ssrRenderTeleport(_push, (_push) => {
    _push(`<div></div>`)
  }, _ctx.target, true, _parent)
}
>>> disabled prop with value
 const { ssrRenderTeleport: _ssrRenderTeleport } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _ssrRenderTeleport(_push, (_push) => {
    _push(`<div></div>`)
  }, _ctx.target, _ctx.foo, _parent)
}
undefined

ssrRenderTeleport :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
export function ssrRenderTeleport(
  parentPush: PushFn,
  contentRenderFn: (push: PushFn) => void,
  target: string,
  disabled: boolean,
  parentComponent: ComponentInternalInstance
) {
  parentPush('<!--teleport start-->')

  let teleportContent: SSRBufferItem

  if (disabled) {
    contentRenderFn(parentPush)
    teleportContent = `<!---->`
  } else {
    const { getBuffer, push } = createBuffer()
    contentRenderFn(push)
    push(`<!---->`) // teleport end anchor
    teleportContent = getBuffer()
  }

  const context = parentComponent.appContext.provides[
    ssrContextKey as any
  ] as SSRContext
  const teleportBuffers =
    context.__teleportBuffers || (context.__teleportBuffers = {})
  if (teleportBuffers[target]) {
    teleportBuffers[target].push(teleportContent)
  } else {
    teleportBuffers[target] = [teleportContent]
  }

  parentPush('<!--teleport end-->')
}

e0fc173 transition group transform

fix: false.value -> false-value · gcclll/stb-vue-next@e0fc173 · GitHub

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

// 源文件:/js/vue/lib.js
const { compileSFCScript, compileStyle, getCompiledSSRString, compileSSR:ssr, log } = require(process.env.BLOG_JS + '/vue/lib.js')

log(['>>> basic\n', ssr(`<transition-group><div v-for="i in list"/></transition-group>`).code])
log(['>>> with static tag\n', ssr(`<transition-group tag="ul"><div v-for="i in list"/></transition-group>`).code])
log(['>>> with dynamic tag\n', ssr(`<transition-group :tag="someTag"><div v-for="i in list"/></transition-group>`).code])
log(['>>> with multi fragments children\n', ssr(`<transition-group>
              <div v-for="i in 10"/>
              <div v-for="i in 10"/>
              <template v-if="ok"><div>ok</div></template>
            </transition-group>`).code])
>>> basic
 const { ssrRenderList: _ssrRenderList } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<!--[-->`)
  _ssrRenderList(_ctx.list, (i) => {
    _push(`<div></div>`)
  })
  _push(`<!--]-->`)
}
>>> with static tag
 const { ssrRenderList: _ssrRenderList } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<ul>`)
  _ssrRenderList(_ctx.list, (i) => {
    _push(`<div></div>`)
  })
  _push(`</ul>`)
}
>>> with dynamic tag
 const { ssrRenderList: _ssrRenderList } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<${_ctx.someTag}>`)
  _ssrRenderList(_ctx.list, (i) => {
    _push(`<div></div>`)
  })
  _push(`</${_ctx.someTag}>`)
}
>>> with multi fragments children
 const { ssrRenderList: _ssrRenderList } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<!--[-->`)
  _ssrRenderList(10, (i) => {
    _push(`<div></div>`)
  })
  _ssrRenderList(10, (i) => {
    _push(`<div></div>`)
  })
  if (_ctx.ok) {
    _push(`<div>ok</div>`)
  } else {
    _push(`<!---->`)
  }
  _push(`<!--]-->`)
}
undefined

ssrRenderList:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

export function ssrRenderList(
  source: unknown,
  renderItem: (value: unknown, key: string | number, index?: number) => void
) {
  if (isArray(source) || isString(source)) {
    for (let i = 0, l = source.length; i < l; i++) {
      renderItem(source[i], i)
    }
  } else if (typeof source === 'number') {
    if (__DEV__ && !Number.isInteger(source)) {
      warn(`The v-for range expect an integer value but got ${source}.`)
      return
    }
    for (let i = 0; i < source; i++) {
      renderItem(i + 1, i)
    }
  } else if (isObject(source)) {
    if (source[Symbol.iterator as any]) {
      const arr = Array.from(source as Iterable<any>)
      for (let i = 0, l = arr.length; i < l; i++) {
        renderItem(arr[i], i)
      }
    } else {
      const keys = Object.keys(source)
      for (let i = 0, l = keys.length; i < l; i++) {
        const key = keys[i]
        renderItem(source[key], key, i)
      }
    }
  }
}
  1. 也就是将 children 直接调用 renderItem 渲染出来,那这个跟动画有什么关系呢❓

c8e1d56 ssrCssVars inject

feat(add): ssr->ssrCssVars inject · gcclll/stb-vue-next@c8e1d56 · GitHub

1
2
3
4
5
6

// 源文件:/js/vue/lib.js
const { compileSFCScript, compileStyle, getCompiledSSRString, compileSSR:ssr, log } = require(process.env.BLOG_JS + '/vue/lib.js')

log(['>>> basic\n', ssr(`<div/>`, { ssrCssVars: `{ color }` }).code])
log(['>>> fragment\n', ssr(`<div/><div/><div><p/></div>`, { ssrCssVars: `{ color }` }).code])
>>> basic
 const { mergeProps: _mergeProps } = require("vue")
const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  const _cssVars = { style: { color: _ctx.color }}
  _push(`<div${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}></div>`)
}
>>> fragment
 const { ssrRenderAttrs: _ssrRenderAttrs } = require("@vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
  const _cssVars = { style: { color: _ctx.color }}
  _push(`<!--[--><div${
    _ssrRenderAttrs(_cssVars)
  }></div><div${
    _ssrRenderAttrs(_cssVars)
  }></div><div${
    _ssrRenderAttrs(_cssVars)
  }><p></p></div><!--]-->`)
}
undefined

cssVars 属性会注入到每个外层同级元素上。

总结

  1. ELEMENT 解析

    • v-html: <div v-html="foo"/> => <div>${foo}</div>

    • v-text: <div v-text="foo"/> => <div>${_ssrInterpolate(_ctx.foo)}</div>

    • v-slot: 只能用在 <template><component> 或用户组件上

    • v-on: ssr 中不处理

    • v-bind: 忽略 ref和key 属性,class 合并成动态 class 属性(style 也一样)

      <div class="foo" :class="bar"/> > ~<div class"${_ssrRenderCalss([_ctx.bar, 'foo'])}"></div>~

  2. input: radio

    <input type="radio" value="foo" v-model="bar">

    =>

    1
    
    <input type="radio" value="${(_ssrLooseEqual(_ctx.bar, 'foo')) ? 'checked' : ''}">
  3. input: checkbox, 详情->

    有关属性: value, true-value, false-value, v-model

    如果使用 true/false-value 配套,则只支持 v-model 绑定单个属性值。

    如果单独使用 value ,则 v-model 支持绑定一个数组,只要当前 checkbox 的 value 值在该数组里面,则为 checked 状态,否则非选中状态。

  4. input: file 不支持,如果没有 type 属性,默认为 text

  5. ssrInjectFallthroughAttrs ,将 ssrRender 函数的 _attrs 参数作为属性注入到最外 层的元素上。

  6. INTEROLATION: 插值调用 _ssrInterpolate(_ctx.foo) 处理

  7. v-show 指令处理,就是在元素上增加一个 style = { display: 'none' } 来切换元 素显示隐藏

  8. v-if 先调用 compiler-core 的 transformIf 解析出 node.branches,然后使用 ssr 端的 processIf 处理成 if (condition) {} else if () {} else {} 语句,而不是 非 ssr 情况下的三元表达式(?:)

  9. v-for 指令使用 _ssrRenderList(_ctx.list, (row, i) => {...})

  10. <slot/> 标签的处理

    1
    2
    3
    4
    5
    6
    7
    
    // `<slot name="foo" :p1="1" bar="2" >some {{ fallback }} content</slot>
    _ssrRenderSlot(_ctx.$slots, "foo", {
    p1: 1,
    bar: "2"
    }, () => {
    _push(`some ${_ssrInterpolate(_ctx.fallback)} content`)
    }, _push, _parent)
    
  11. <Suspense/> 内置组件,内部处理其实等于将 children 用一个 await 函数包裹 起来了,成为异步操作。

  12. <Teleport/> 内置组件,需要制定 to="target"

    支持 disabled 属性

    1
    2
    3
    4
    
    // <teleport :to="target" :disabled="foo"><div/></teleport>
    _ssrRenderTeleport(_push, (_push) => {
        _push(`<div></div>`)
    }, _ctx.target, _ctx.foo, _parent)
    
  13. ssr css vars 简单在元素上注入 style = { color } 属性