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

/img/bdx/yiyeshu-001.jpg

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

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

更新日志&Todos

  1. [2020-12-19 13:58:31] 创建

  2. [2021-01-04 19:47:10] 完成

  3. TODO defineProps 和 defineEmit 原理和用途

  4. TODO inlineTemplate with ssr: true options

  5. TODO more mind-maps

/img/vue3/compiler-sfc/vue-compiler-sfc-compile-script.svg

重点、特性、问题

/img/vue3/compiler-sfc/vue-compiler-sfc-keypoints.svg

  1. 🔗 <style> 标签中可通过 v-bind() 引用CSS 模块化后的变量

  2. <script><script setup> 中的 export default 会合并,且是 setup 优先级 更高,因此尽量不要存在重复属性。

f21c84c init 初始化工作

feat(init): compiler-sfc · gcclll/stb-vue-next@f21c84c

cp compiler-sfc form vue-next:/packages/compiler-dom

删除 compiler-sfc/src/* 下所有文件

新建 compiler-sfc/src/index.ts 入口文件

初始化 index.ts:

1
2
3
4
5
6
7
8
9
// API
export { generateCodeFrame } from '@vue/compiler-core'

// Types
export {
  CompilerOptions,
  CompilerError,
  BindingMetadata
} from '@vue/compiler-core'

feat(init): parse function · gcclll/stb-vue-next@e7e1cc1

声明一些基本类型,比如: <template>, <script>, <style> 这也是 *.vue 文件的三 大要素,这里需要多关注一点就是会发现 <script> 标签里面多有一个 setup 属性, 这个是 vue 自身定义的一种标签类型,比如在这里面可以直接使用 ref 声明变量,这里 面的变量都会自动变成响应式的等等。

SFC 块类型定义:

1
2
3
4
5
6
7
8
9
export interface SFCBlock {
  type: string
  content: string
  attrs: Record<string, string | true>
  loc: SourceLocation
  map?: RawSourceMap
  lang?: string
  src?: string
}

SFC <template> 标签类型定义:

1
2
3
4
export interface SFCTemplateBlock extends SFCBlock {
  type: 'template'
  ast: ElementNode
}

SFC <script> 脚本标签类型定义

1
2
3
4
5
6
7
export interface SFCScriptBlock extends SFCBlock {
  type: 'script'
  setup?: string | boolean
  bindings?: BindingMetadata
  scriptAst?: Statement[]
  scriptSetupAst?: Statement[]
}

SFC <style> 样式标签类型定义

1
2
3
4
5
export interface SFCStyleBlock extends SFCBlock {
  type: 'style'
  scoped?: boolean // 指定是不是只能用于当前文件
  module?: string | boolean // 是不是模块化样式
}

SFC 文件类型定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
export interface SFCDescriptor {
  filename: string
  source: string
  template: SFCTemplateBlock | null
  script: SFCScriptBlock | null
  scriptSetup: SFCScriptBlock | null
  styles: SFCStyleBlock[]
  customBlocks: SFCBlock[]
  cssVars: string[]
}

parse 函数定义:

1
2
3
4
5
6
export function parse(
  source: string,
  { sourceMap = true }: SFCParseOptions
): SFCParseResult {
  return {} as SFCParseResult
}

49ee210 parse function 实现部分

feat: sfc-> code parse function · gcclll/stb-vue-next@49ee210

实现 parse 函数的基本架构:

  1. sourceToSFC<key, source> 用来缓存 vue文件解析结果,首先取缓存结果

  2. 通过调用 compiler-dom 中的 compiler.parse 将文件内容 source解析成 AST

  3. 遍历所有 ast.children 根据 node.tag 类型决定走什么分支处理

    <template> 模板分支,这里面的所有内容会被 parse 继续解析出 ast

    <script [setup]> 脚本分支, 当做 RAWDATA 文本类型处理,如果有 setup 属性, 则所有 script 都不能带 src 属性,即不能引用外部文件,因为所有 script 内容会合 并到一起去处理。

    <style [lang=""]> 样式分支,当做 RAWDATA 文本类型处理

  4. 错误用法检测,主要是 <script setup> 脚本标签不能有 src 的检测

  5. souremap 的处理

  6. descriptor.cssVars = parseCssVars(descriptor) CSS 变量的解析,会全部解析到 数组 cssVars 里面去

  7. 缓存解析后的结果到 sourceToSFC.set(sourceKey, result)

  8. 对了,在 switch case 分支里面默认走的是自定义块的处理(vue 文件中还可以自定 义?)

CSS vars 变量处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
export const CSS_VARS_HELPER = `useCssVars`;
export const cssVarRE = /\bv-bind\(\s*(?:'([^']+)'|"([^"]+)"|([^'"][^)]*))\s*\)/g;

export function parseCssVars(sfc: SFCDescriptor): string[] {
  const vars: string[] = [];
  sfc.styles.forEach((style) => {
    let match;
    // v-bind('xxx'), v-bind("xxx"), v-bind()
    while ((match = cssVarRE.exec(style.content))) {
      vars.push(match[1] || match[2] || match[3]);
    }
  });
  return vars;
}

这里有个 cssVarRE 正则,来看下:

/img/vue3/re/sfc-css-vars-re.svg

这个正则可以匹配结果: v-bind('...'), v-bind("..."), v-bind(...)

compiler-src/__tests__/cssVars.spec.ts 用例中可窥见这种用法:

1
2
3
4
5
`<script>const a = 1</script>\n` +
   `<style>div{
     color: v-bind(color);
     font-size: v-bind('font.size');
   }</style>`

💟 现在可以直接在 <style> 变迁里面通过 v-bind() 来直接使用引入的 CSS 变量。

feat(add): sfc->parse add sourcemap · gcclll/stb-vue-next@afd8044

e32d508 parse <template> case

feat: sfc-> add <template> parse · gcclll/stb-vue-next@e32d508

主要增加代码: switch case -> 'template': http://qiniu.ii6g.com/img/20201219160507.png

增加函数: createBlock() 用来处理 SFC 标签的属性(如: lang, setup, src, scoped, module)

回顾下 compiler-dom, compiler-core 其实对于 <template> 标签的处理工作依然集中 在这两个包里面,所以这里就不再赘述模板 ast 的解析了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17

const {
  parse
} = require(process.env.PWD + '/../../static/js/vue/compiler-sfc.global.js')

const source = `
<template>
  <div>{{ test }}</div>
</template>
<script></script>
<style>
  div {
    color:v-bind('fontColor');
  }
</style>`
const res = parse(source)
console.log(res)
{
  descriptor: {
    filename: 'anonymous.vue',
    source: '\n' +
      '<template>\n' +
      '  <div>{{ test }}</div>\n' +
      '</template>\n' +
      '<script></script>\n' +
      '<style>\n' +
      '  div {\n' +
      "    color:v-bind('fontColor');\n" +
      '  }\n' +
      '</style>',
    template: {
      type: 'template',
      content: '\n  <div>{{ test }}</div>\n',
      loc: [Object],
      attrs: {},
      ast: [Object]
    },
    script: null,
    scriptSetup: null,
    styles: [],
    customBlocks: [],
    cssVars: []
  },
  errors: []
}
undefined

如上:一个最简单的 SFC 解析后的结构。

3160fed parse <script> case

feat(add): sfc-> script parse · gcclll/stb-vue-next@3160fed

增加 switch case script 逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
case 'script': // 脚本标签处理
    const scriptBlock = createBlock(node, source, pad) as SFCScriptBlock
    const isSetup = !!scriptBlock.attrs.setup
    if (isSetup && !descriptor.scriptSetup) {
        descriptor.scriptSetup = scriptBlock
        break
    }

    if (!isSetup && !descriptor.script) {
        descriptor.script = scriptBlock
        break
    }
    errors.push(createDuplicateBlockError(node, isSetup))
    break
break

createBlock() 中增加各属性的解析和设置:

lang -> block.lang

src -> block.src

style > scoped -> block.scoped

style > module -> block.module

script > setup -> block.setup

另外增加了 padContent() 检测回车换行符替换?

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const {
  parse
} = require(process.env.PWD + '/../../static/js/vue/compiler-sfc.global.js')

const source = `
<script setup>
import { x } from './x'
let a = 1
const b = 2
function c() {}
class d {}
</script>`
const res = parse(source)
console.log(res.descriptor)
{
  filename: 'anonymous.vue',
  source: '\n' +
    '<script setup>\n' +
    "import { x } from './x'\n" +
    'let a = 1\n' +
    'const b = 2\n' +
    'function c() {}\n' +
    'class d {}\n' +
    '</script>',
  template: null,
  script: null,
  scriptSetup: {
    type: 'script',
    content: '\n' +
      "import { x } from './x'\n" +
      'let a = 1\n' +
      'const b = 2\n' +
      'function c() {}\n' +
      'class d {}\n',
    loc: {
      source: '\n' +
        "import { x } from './x'\n" +
        'let a = 1\n' +
        'const b = 2\n' +
        'function c() {}\n' +
        'class d {}\n',
      start: [Object],
      end: [Object]
    },
    attrs: { setup: true },
    setup: true
  },
  styles: [],
  customBlocks: [],
  cssVars: []
}
undefined

aa037fe parse <style> case

feat(add): sfc-> parse <style> · gcclll/stb-vue-next@aa037fe

解析后的结果保存到 descriptor.styles.push(styleBlock) 所以可以有多个 <style> 存在。

Tip: 这里还有一个 styleBlock.attrs.vars 检测,难不成将来会支持直接 SFC 里面 声明 CSS 变量?

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const {
  parse
} = require(process.env.PWD + '/../../static/js/vue/compiler-sfc.global.js')

const source = `
<style scoped>
h1 {
  color: red;
  font-size: v-bind(fontSize);
  border: v-bind('border');
}
</style>`
const res = parse(source)
console.log(res.descriptor)
{
  filename: 'anonymous.vue',
  source: '\n' +
    '<style scoped>\n' +
    'h1 {\n' +
    '  color: red;\n' +
    '  font-size: v-bind(fontSize);\n' +
    "  border: v-bind('border');\n" +
    '}\n' +
    '</style>',
  template: null,
  script: null,
  scriptSetup: null,
  styles: [
    {
      type: 'style',
      content: '\n' +
        'h1 {\n' +
        '  color: red;\n' +
        '  font-size: v-bind(fontSize);\n' +
        "  border: v-bind('border');\n" +
        '}\n',
      loc: [Object],
      attrs: [Object],
      scoped: true
    }
  ],
  customBlocks: [],
  cssVars: [ 'fontSize', 'border' ]
}
undefined

对于 v-bind() 变量的引用,不管有没引号,都会当做变量处理。

compile <template>

c26e76c init compileTemplate

feat(init): sfc->compile <template> · gcclll/stb-vue-next@c26e76c

增加两个类型和 compileTemplate 函数定义:

SFCTemplateCompileResults 模板便后的结果类型

1
2
3
4
5
6
7
8
9
export interface SFCTemplateCompileResults {
  code: string
  ast?: RootNode
  preamble?: string
  source: string
  tips: string[]
  errors: (string | CompilerError)[]
  map?: RawSourceMap
}

SFCTemplateCompileOptions 模板编译器选项

 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

export interface SFCTemplateCompileOptions {
  source: string
  filename: string
  id: string
  scoped?: boolean
  isProd?: boolean
  ssr?: boolean
  ssrCssVars?: string[]
  inMap?: RawSourceMap
  compiler?: TemplateCompiler
  compilerOptions?: CompilerOptions
  preprocessLang?: string
  preprocessOptions?: any
  /**
   * In some cases, compiler-sfc may not be inside the project root (e.g. when
   * linked or globally installed). In such cases a custom `require` can be
   * passed to correctly resolve the preprocessors.
   */
  preprocessCustomRequire?: (id: string) => any
  /**
   * Configure what tags/attributes to transform into asset url imports,
   * or disable the transform altogether with `false`.
   */
  transformAssetUrls?: AssetURLOptions | AssetURLTagConfig | boolean
}

及 compileTemplate 函数

1
2
3
4
5
export function compileTemplate(
  options: SFCTemplateCompileOptions
): SFCTemplateCompileResults {
  return {} as SFCTemplateCompileResults
}

TODO 1b2965f coding compileTemplate

feat: sfc->compile compileTemplate code · gcclll/stb-vue-next@1b2965f

这个函数相关的内容:

  1. preprocessLang

  2. preprocessCustomRequire

TODO 模板预处理器,没搞明白这里是要做什么?

代码逻辑:

if preprocessor -> doCompileTemplate()

elseif preprocessLang -> …

else -> doCompileTemplate()

⏹ 等待探索……

7b49db4 coding doCompileTemplate 函数实现

feat(add): sfc->compile doCompileTemplate · gcclll/stb-vue-next@7b49db4

函数功能:收集两个 transform 给 compiler.compile 在模板编译期间使用。

  1. asset url 资源地址转换用的 transform

    要处理的标签和对应的包含 url 的属性:

    tagprop with url
    <video>'src', 'poster'
    <source>'src'
    <img>'src'
    <image>'xlink:href', 'href'
    <use>'xlink:href', 'href'
  2. img/source 标签 src 地址转换

重点代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
 const shortId = id.replace(/^data-v-/, '')
  const longId = `data-v-${shortId}`

  let { code, ast, preamble, map } = compiler.compile(source, {
    mode: 'module',
    prefixIdentifiers: true,
    hoistStatic: true,
    cacheHandlers: true,
    ssrCssVars:
      ssr && ssrCssVars && ssrCssVars.length
        ? '' /* TODO genCssVarsFromList(ssrCssVars, shortId, isProd) */
        : '',
    // css 局部使用,加上对应的唯一 id
    scopeId: scoped ? longId : undefined,
    ...compilerOptions,
    nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
    filename,
    sourceMap: true,
    onError: e => errors.push(e)
  })

nodeTransforms: [transformAssetUrl, transformSrcset] 传递给编译器处理。

注意这里设置了几个属性: mode = 'module', prefixIdentifiers = true 所以这个应 该只能运行在非浏览器环境。

下面来实现一个相对简单的 transformAssetUrl() 函数 ……

2d82400 coding transformAssetUrl 转换资源 url

feat(add): sfc->compile templateTransformAssetUrl · gcclll/stb-vue-next@2d82400

fix: sfc preprocess function · gcclll/stb-vue-next@e08f805

几种URL使用情况和转换结果如下实例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const { compileTemplate } = require(process.env.VNEXT_PKG_SFC + '/dist/compiler-sfc.cjs.js')
const { code, ast } = compileTemplate({
  source: `<template><div id="test">
<img src="./test/test.png" />
<img src="./test/test.png" />
<img :src="imgUrl" />
<img src="" />
<img src="http://1.1.1.1:100/imgs/test/test.png" />
<img src="data:...." />
<img src="#test/test.png" />
<img src="~test/test.png" />
<img src="~/test/test.png" />
<img src="@test/test.png" />
<video src="./test/video.mp4" poster="./test/poster.png" />
<div src="./test/test.png" />

</div></template>`,
  id: '', filename: 'test.vue'
})
console.log(code)
import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"
import _imports_0 from './test/test.png'
import _imports_1 from 'test/test.png'
import _imports_2 from '@test/test.png'
import _imports_3 from './test/video.mp4'
import _imports_4 from './test/poster.png'


const _hoisted_1 = { id: "test" }
const _hoisted_2 = /*#__PURE__*/_createVNode("img", { src: _imports_0 }, null, -1 /* HOISTED */)
const _hoisted_3 = /*#__PURE__*/_createVNode("img", { src: _imports_0 }, null, -1 /* HOISTED */)
const _hoisted_4 = /*#__PURE__*/_createVNode("img", { src: "" }, null, -1 /* HOISTED */)
const _hoisted_5 = /*#__PURE__*/_createVNode("img", { src: "http://1.1.1.1:100/imgs/test/test.png" }, null, -1 /* HOISTED */)
const _hoisted_6 = /*#__PURE__*/_createVNode("img", { src: "data:...." }, null, -1 /* HOISTED */)
const _hoisted_7 = /*#__PURE__*/_createVNode("img", { src: "#test/test.png" }, null, -1 /* HOISTED */)
const _hoisted_8 = /*#__PURE__*/_createVNode("img", { src: _imports_1 }, null, -1 /* HOISTED */)
const _hoisted_9 = /*#__PURE__*/_createVNode("img", { src: _imports_1 }, null, -1 /* HOISTED */)
const _hoisted_10 = /*#__PURE__*/_createVNode("img", { src: _imports_2 }, null, -1 /* HOISTED */)
const _hoisted_11 = /*#__PURE__*/_createVNode("video", {
  src: _imports_3,
  poster: _imports_4
}, null, -1 /* HOISTED */)
const _hoisted_12 = /*#__PURE__*/_createVNode("div", { src: "./test/test.png" }, null, -1 /* HOISTED */)

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("template", null, [
    _createVNode("div", _hoisted_1, [
      _hoisted_2,
      _hoisted_3,
      _createVNode("img", { src: _ctx.imgUrl }, null, 8 /* PROPS */, ["src"]),
      _hoisted_4,
      _hoisted_5,
      _hoisted_6,
      _hoisted_7,
      _hoisted_8,
      _hoisted_9,
      _hoisted_10,
      _hoisted_11,
      _hoisted_12
    ])
  ]))
}
undefined

模板中资源URL不转换几种情况:

  1. 属性不是静态属性(NodeTypes.ATTRIBUTE)

  2. 非特定标签的不转换(或者通过 options.tags 里指定的标签)

    1
    2
    3
    4
    5
    6
    7
    
    tags: {
      video: ['src', 'poster'],
      source: ['src'],
      img: ['src'],
      image: ['xlink:href', 'href'],
      use: ['xlink:href', 'href']
    }
  3. 没有属性值的属性

  4. 外部链接的URL(https 开头的)

  5. data: 开头的资源地址

  6. 属性值以 # 开头的地址

  7. 非绝对路径且费相对路径的(以, .|~|@ 开头的地址)

需要处理的又分两种情况:

  1. 给定了 options.base 基地址的(.|~|@ 为第一个字符的)

    直接用 options.base + assert url 处理

  2. 非1中清空的使用 import imgName from '...img url' 引入

PS. 对于 CSS 中的URL引用放到后续 compileStyle 中去展示。

56358a8 compile <style>

feat(add): sfc-> compile style · gcclll/stb-vue-next@56358a8

这部分代码都是直接 Ctrl-c, Ctrl-v 来的,也没深入研究,所以这节也没什么好讲述的。

待到以后有时间再来研究。

 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
const { compileStyle } = require(process.env.VNEXT_PKG_SFC + '/dist/compiler-sfc.cjs.js')

const c = (source, option = {}) => compileStyle({
  source,
  filename: 'test.css',
  id: 'data-v-test',
  scoped: true,
  ...option
})

const log = console.log
const res = c(`
h1 { color: red; }
.foo { color: red; }
h1 .foo { color: red; }
h1 .foo, .bar, .baz { color: red; }
.foo:after { color: red; }
::selection { display: none; }
.abc, ::selection { color: red; }

:deep(.foo) { color: red; }
::v-deep(.foo) { color: red; }
::v-deep(.foo .bar) { color: red; }
.baz .qux ::v-deep(.foo .bar) { color: red; }

:slotted(.foo) { color: red; }
::v-slotted(.foo) { color: red; }
::v-slotted(.foo .bar) { color: red; }
.baz .qux ::v-slotted(.foo .bar) { color: red; }

:global(.foo) { color: red; }
::v-global(.foo) { color: red; }
::v-global(.foo .bar) { color: red; }
.baz .qux ::v-global(.foo .bar) { color: red; }

@media print { .foo { color: red }}
@supports(display: grid) { .foo { display: grid }}

.anim {
  animation: color 5s infinite, other 5s;
}
.anim-2 {
  animation-name: color;
  animation-duration: 5s;
}
.anim-3 {
  animation: 5s color infinite, 5s other;
}
.anim-multiple {
  animation: color 5s infinite, opacity 2s;
}
.anim-multiple-2 {
  animation-name: color, opacity;
  animation-duration: 5s, 2s;
}

@keyframes color {
  from { color: red; }
  to { color: green; }
}
@-webkit-keyframes color {
  from { color: red; }
  to { color: green; }
}
@keyframes opacity {
  from { opacity: 0; }
  to { opacity: 1; }
}
@-webkit-keyframes opacity {
  from { opacity: 0; }
  to { opacity: 1; }
}
`)
log(res.code)

h1[data-v-test] { color: red;
}
.foo[data-v-test] { color: red;
}
h1 .foo[data-v-test] { color: red;
}
h1 .foo[data-v-test], .bar[data-v-test], .baz[data-v-test] { color: red;
}
.foo[data-v-test]:after { color: red;
}
[data-v-test]::selection { display: none;
}
.abc[data-v-test],[data-v-test]::selection { color: red;
}
[data-v-test] .foo { color: red;
}
[data-v-test] .foo { color: red;
}
[data-v-test] .foo .bar { color: red;
}
.baz .qux[data-v-test] .foo .bar { color: red;
}
.foo[data-v-test-s] { color: red;
}
.foo[data-v-test-s] { color: red;
}
.foo .bar[data-v-test-s] { color: red;
}
.baz .qux .foo .bar[data-v-test-s] { color: red;
}
.foo { color: red;
}
.foo { color: red;
}
.foo .bar { color: red;
}
.foo .bar { color: red;
}
@media print {
.foo[data-v-test] { color: red
}}
@supports(display: grid) {
.foo[data-v-test] { display: grid
}}
.anim[data-v-test] {
  animation: color-test 5s infinite, other 5s;
}
.anim-2[data-v-test] {
  animation-name: color-test;
  animation-duration: 5s;
}
.anim-3[data-v-test] {
  animation: 5s color-test infinite, 5s other;
}
.anim-multiple[data-v-test] {
  animation: color-test 5s infinite,opacity-test 2s;
}
.anim-multiple-2[data-v-test] {
  animation-name: color-test,opacity-test;
  animation-duration: 5s, 2s;
}
@keyframes color-test {
from { color: red;
}
to { color: green;
}
}
@-webkit-keyframes color-test {
from { color: red;
}
to { color: green;
}
}
@keyframes opacity-test {
from { opacity: 0;
}
to { opacity: 1;
}
}
@-webkit-keyframes opacity-test {
from { opacity: 0;
}
to { opacity: 1;
}
}

undefined

PS. 对于 CSS 的解析需要 postcss 以及各种预处理来处理,这里暂时不展开。

4d66531 compile <script>重点

这节会是重点部分。

/img/vue3/compiler-sfc/vue-compiler-sfc-compile-script.svg

init compileScript function

初始化 compileScript() 函数以及参数选项类型 SFCScriptCompileOptions

SFCScriptCompileOptions:

  • id: string, 传递给 compileStyle 用于作为 injected CSS 变量前缀用

  • isProd?: boolean 决定生成的 CSS 变量是否要加上 hash 值

  • babelParserPlugins?: ParserPlugin[]

  • refSugar?: boolean 使能 ref 语法糖

  • inlineTemplate?: boolean 内联模板???

compileScript:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/**
 * Compile `<script setup>`
 * It requires the whole SFC descriptor because we need to handle and merge
 * normal `<script>` + `<script setup>` if both are present.
 */
export function compileScript(
  sfc: SFCDescriptor,
  options: SFCScriptCompileOptions
): SFCScriptBlock {
  return {} as SFCScriptBlock
}

feat(add): sfc->script, compileScript steps comment · gcclll/stb-vue-next@54ea72a

列出 compileScript() 将要完成的任务:

No.DescLink
0前置处理-
1处理存在的 <script> 代码体-
2解析 <script setup>,遍历置顶的语句-
3将 ref访问转换成对 ref.value 的引用-
4释放 setup 上下文类型的运行时 props/emits 代码-
5检查用户选项(useOptions)参数,确保它没有引用 setup 下的变量-
6删除 non-script 的内容-
7分析 binding metadata-
8注入 `useCssVars` 调用-
9完成 setup() 参数签名-
10生成返回语句(return)-
11完成 default export-
12完成 Vue helpers imports-

接下来就是按照上表的步骤来一步步完成 compileScript()

PS. 下面每个对应章节都有对应的原版英文注释,英语不好~~~~~。

增加一些逻辑无关的变量声明: feat(add): sfc->script compileScript declarations · gcclll/stb-vue-next@06f1d95

在进入正式步骤之前,来简单看看使用到的 @babel/parser 这个插件是如何使用的,输 出结果又是啥?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const { parse } = require(process.env.BABEL_DIR + '/parser/lib/index.js')
const log = console.log
let code = `
import { a } from './a.js';

const value = 1 * 10 + 100 - 20 / 30 + 1
export const name = a.getName();

export default { name }
`
const res = parse(code, { sourceType: 'module' })
console.log(res.program.body.map(body => body.type).join('\n'))
ImportDeclaration
VariableDeclaration
ExportNamedDeclaration
ExportDefaultDeclaration
undefined

以上输出是每个语句在 parser 中对应的 AST 类型。

0⃣ d7369ae 无 <script setup> 时

feat(add): script without setup-script parse · gcclll/stb-vue-next@d7369ae

一开始会检测有没有 script setup 如果没有,继续检测 <script> 普通标签,如果两 者都不存在,抛出异常。

如果 <script> 存在,则直接调用 @babel/parserparse 函数进行解析,因此后面 一坨代码在这种情况下(只有普通的 script 时)是不需要的。

新增代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const scriptAst = _parse(script.content, {
    plugins,
    sourceType: 'module'
}).program.body
const bindings = analyzeScriptBindings(scriptAst)
const needRewrite = cssVars.length || hasInheritAttrsFlag
let content = script.content
if (needRewrite) {
// TODO need rewrite
}
return {
    ...script,
    content,
    bindings,
    scriptAst
}

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const { compileScript, parse } = require(process.env.VNEXT_PKG_SFC + '/dist/compiler-sfc.cjs.js')

const compile = (src, options) => {
  const { descriptor } = parse(src)
  return compileScript(descriptor, { ...options, id: 'xxxx' })
}

const code = `
<script>
import { a } from './a.js';
</script>
`
const res = compile(code)
console.log(res.type, '\n', res.scriptAst)
script
 [
  Node {
    type: 'ImportDeclaration',
    start: 1,
    end: 28,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    range: undefined,
    leadingComments: undefined,
    trailingComments: undefined,
    innerComments: undefined,
    extra: undefined,
    specifiers: [ [Node] ],
    source: Node {
      type: 'StringLiteral',
      start: 19,
      end: 27,
      loc: [SourceLocation],
      range: undefined,
      leadingComments: undefined,
      trailingComments: undefined,
      innerComments: undefined,
      extra: [Object],
      value: './a.js'
    }
  }
]
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
const { compileScript, parse } = require(process.env.VNEXT_PKG_SFC + '/dist/compiler-sfc.cjs.js')
const { log } = require(process.env.BLOG_JS + '/utils.js')
const compile = (src, options) => {
  const { descriptor } = parse(src)
  return compileScript(descriptor, { ...options, id: 'xxxx' })
}

const code = `
<script>
  export default {
    props: ['foo', 'bar']
  }
</script>`
const { type, scriptAst: ast } = compile(code)
// 首先是个 ExportDefaultDeclaration 类型
// export 的值为一个 ObjectExpression 类型
log(`>>> <script> 解析后的类型`)
console.log(type)
const node = ast[0]
log(`>>> export default 解析后的类型`)
log(node.type)
log(`>>> { props : ... } 解析后的 ast 包含的 keys`)
log(Object.keys(node.declaration))
log(`> properties 为 ObjectExpression 对象的成员列表,如: props`)
log.props(node.declaration.properties[0], ['type', 'key', 'value'])
log(node.declaration.properties[0].value.elements)

+RESULTS: 精简之后的输出

>>> <script> 解析后的类型
script
>>> export default 解析后的类型
ExportDefaultDeclaration
>>> { props : ... } 解析后的 ast 包含的 keys
[
  'type',
  'start',
  'end',
  'loc',
  'range',
  'leadingComments',
  'trailingComments',
  'innerComments',
  'extra',
  'properties'
]
> properties 为 ObjectExpression 对象的成员列表,如: props
{
  type: 'ObjectProperty',
  key: Node {
    type: 'Identifier',
    name: 'props'
  },
  value: Node {
    type: 'ArrayExpression',
    elements: [ [Node], [Node] ]
  }
}
[
  Node {
    type: 'StringLiteral',
    extra: { rawValue: 'foo', raw: "'foo'" },
    value: 'foo'
  },
  Node {
    type: 'StringLiteral',
    extra: { rawValue: 'bar', raw: "'bar'" },
    value: 'bar'
  }
]

819a413 export default {} 解析

feat(add): sfc->script, parse export default members into bindings · gcclll/stb-vue-next@819a413

(property.type === 'ObjectMethod' &&property.key.type === 'Identifier' &&(property.key.name === 'setup' || property.key.name === 'data'))

成员最后在 bindings 里面存在类型值:

nametype(BindingTypes)value
props'PROPS''props'
inject'PROPS''props'
computed'OPTIONS''options'
methods'OPTIONS''options'
setupSETUP_MAYBE_REF'setup-maybe-ref'
dataSETUP_MAYBE_REF'setup-maybe-ref'

到这里还只是借助 @babel/parser 进行了解析,vue 自身的一些特性处理在 analyzeScriptBindings() 中,这个函数解析的类型是 ExportDefaultDeclaration 也 就是 export default {} 的代码部分。

然后调用 analyzeBindingsFromOptions(node.declaration) 解析对象成员,这里要处理 的主要有两种:

  1. ObjectProperty 属性类型成员

    (property.type === 'ObjectProperty' &&!property.computed &&property.key.type === 'Identifier')

     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
    
    const { compileScript, parse } =
    require(process.env.VNEXT_PKG_SFC + '/dist/compiler-sfc.cjs.js')
    const { log } = require(process.env.BLOG_JS + '/utils.js')
    const compile = (src, options) => {
    const { descriptor } = parse(src)
    return compileScript(descriptor, { ...options, id: 'xxxx' })
    }
    
    const res = compile(`
    <script>
    export default {
     props: ['firstName', 'secondName'],
     inject: { foo: {} },
     computed: {
       fullName() {
         return this.firstName + this.secondName + this.thirdName
       }
     },
     methods: {
       getName() {
         return this.fullName
       }
     }
    }
    </script>
    `)
    
    console.log(res.bindings)
    
    {
      firstName: 'props',
      secondName: 'props',
      foo: 'options',
      fullName: 'options',
      getName: 'options'
    }
    undefined
    
  2. ObjectMethod 方法类型成员,且只处理 setupdata 方法

    feat(add): sfc->script, parse export default data&setup into bingdings · gcclll/stb-vue-next@c7b617b

    需要增加代码:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    if (
       property.type === 'ObjectMethod' &&
       property.key.type === 'Identifier' &&
       (property.key.name === 'setup' || property.key.name === 'data')
     ) {
       for (const bodyItem of property.body.body) {
         // setup() {
         //   return {
         //     foo: null
         //   }
         // }
         if (
           bodyItem.type === 'ReturnStatement' &&
           bodyItem.argument &&
           bodyItem.argument.type === 'ObjectExpression'
         ) {
           for (const key of getObjectExpressionKeys(bodyItem.argument)) {
             bindings[key] = property.key.name = 'setup'
               ? BindingTypes.SETUP_MAYBE_REF
               : BindingTypes.DATA
           }
         }
       }
     }
    

    测试:

     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
    
    const { compileScript, parse } =
    require(process.env.VNEXT_PKG_SFC + '/dist/compiler-sfc.cjs.js')
    const { log } = require(process.env.BLOG_JS + '/utils.js')
    const compile = (src, options) => {
    const { descriptor } = parse(src)
    return compileScript(descriptor, { ...options, id: 'xxxx' })
    }
    
    const code = `
    <script>
    export default {
    setup() {
     return {
       foo: null
     }
    },
    data() {
     return {
       bar: null
     }
    },
    props: ['baz']
    }
    </script>`
    const res = compile(code)
    log(res.bindings)
    
    { foo: 'setup-maybe-ref', bar: 'setup-maybe-ref', baz: 'props' }
    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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
const { compileScript, parse } =
  require(process.env.VNEXT_PKG_SFC + '/dist/compiler-sfc.cjs.js')
const { log } = require(process.env.BLOG_JS + '/utils.js')
const compile = (src, options) => {
  const { descriptor } = parse(src)
  return compileScript(descriptor, { ...options, id: 'xxxx' })
}

log(`>>> setup return`)
log(compile(`
<script>
const bar = 2
  export default {
    setup() {
    return {
        foo: 1,
        bar
    }
  }
}
</script>`).bindings)
log(`>>> async setup return`)
log(compile(`
<script>
const bar = 2
  export default {
    async setup() {
      return {
        foo: 1,
        bar
      }
  }
}
</script>`).bindings)
log(`>>> computeds`)
log(compile(`
    <script>
    export default {
      computed: {
        foo() {},
        bar: {
            get() {},
            set() {},
        }
      }
    }
    </script>
`).bindings)
log(`>>> 混合 bindings`)
log(compile(`
    <script>
    export default {
      inject: ['foo'],
        props: {
        bar: String,
      },
      setup() {
        return {
            baz: null,
        }
      },
      data() {
        return {
            qux: null
        }
      },
      methods: {
        quux() {}
      },
      computed: {
        quuz() {}
      }
    }
    </script>
`).bindings)

1⃣ eb650ca 解析 <script>

feat(add): sfc->script, export default handle · gcclll/stb-vue-next@eb650ca

process normal <script> first if it exists

用到的插件:

Plugin
@babel/parser · Babel

这一节中的普通 <script> 前提是,至少有一个 <script setup> 存在,否则会直接在 上一节 就退出解析了。

@babel/parser 解析 import 结果对照表

类型
import { a } from './x'ImportDeclaration
aImportSpecifiernode.specifiers[i].imported.name
'./x'StringLiteralnode.source.value
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const { parse } = require(process.env.BABEL_DIR + '/parser/lib/index.js')

const code = `
import { a } from './x'`
const res = parse(code, { sourceType: 'module' }).program.body
const node = res[0]
const spec = node.specifiers[0]
console.log(`>>> node type > ${node.type}`)
console.log(`>>> node source type > ${node.source.type}`)
console.log(`>>> node source value > ${node.source.value}`)
console.log(`>>> spec type > ${spec.type}`)
console.log(`>>> spec imported type > ${spec.imported.type}`)
console.log(`>>> spec imported name > ${spec.imported.name}`)
>>> node type > ImportDeclaration
>>> node source type > StringLiteral
>>> node source value > ./x
>>> spec type > ImportSpecifier
>>> spec imported type > Identifier
>>> spec imported name > a

所以 vue-next 中新增的代码处理逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// import ... from './x' 语句类型
if (node.type === 'ImportDeclaration') {
      // record imports for dedupe
  // import 进来的变量列表
  for (const specifier of node.specifiers) {
    // 变量名
    const imported =
      specifier.type === 'ImportSpecifier' &&
      specifier.imported.type === 'Identifier' &&
      specifier.imported.name
    // 注册到 userImports[local] = { isType, imported, source } 中
    registerUserImport(
      node.source.value,
      specifier.local.name,
      imported,
      node.importKind === 'type'
    )
  }
}

然后 compileScript 中有一段处理不明白:

1
2
3
4
if (scriptSetup && scriptSetupLang !== 'ts') {
    // do not process non js/ts script blocks
    return scriptSetup
  }

这里是说如果有 <script setup> 但是类型不是 ts 就直接返回 scriptSetup ?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const { compileScript, parse } = require(process.env.VNEXT_PKG_SFC +
  "/dist/compiler-sfc.cjs.js");
const { log } = require(process.env.BLOG_JS + "/utils.js");
const compile = (src, options) => {
  const { descriptor } = parse(src);
  return compileScript(descriptor, { ...options, id: "xxxx" });
};

const res = compile(`
<script lang="ts">
import { a, a1, a2 } from './a'
</script>
<script lang="ts" setup>
import { b } from './b'
</script>
`);
handling script ... with setup
userImports >
 [Object: null prototype] {
  a: { isType: false, imported: 'a', source: './a' },
  a1: { isType: false, imported: 'a1', source: './a' },
  a2: { isType: false, imported: 'a2', source: './a' }
}
userImportAlias >
 [Object: null prototype] {}
undefined

到此,因为还没实现 <script setup> 解析,所以只能看到普通 script 标签的处理结果。

import … from 处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
for (const node of scriptAst) {
  // import ... from '...'
  if (node.type === "ImportDeclaration") {
    // record imports for dedupe
    for (const specifier of node.specifiers) {
      const imported =
        specifier.type === "ImportSpecifier" &&
        specifier.imported.type === "Identifier" &&
        specifier.imported.name;
      registerUserImport(
        node.source.value,
        specifier.local.name,
        imported,
        node.importKind === "type"
      );
    }
    console.log("userImports > \n", userImports);
    console.log("userImportAlias > \n", userImportAlias);
  }
}

上面处理:遍历 script 中所有 ast 节点,找出 import ... from ... 语句,取出

引入的文件源部分: node.source.value

引入之后的变量或解构后的变量: imported.name

组成新的结构 { isType: false, imported: 'a', source: './a'} 保存到 userImports

修改下,引入多个变量呢?结果如下:

userImports >
 [Object: null prototype] {
  a: { isType: false, imported: 'a', source: './a' },
  a1: { isType: false, imported: 'a1', source: './a' },
  a2: { isType: false, imported: 'a2', source: './a' }

每个变量作为一项保存。

export default {} 处理

export default {} 语法的处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/* else */ if (node.type === "ExportDefaultDeclaration") {
  // export default
  defaultExport = node;
  const start = node.start! + scriptStartOffset!;
  s.overwrite(
    start,
    start + `export default`.length,
    `const ${defaultTempVar} =`
  );
}

变量 s :

const s = new MagicString(source)

等于是将 export default 内容赋值给 __default__ 变量上。

export default {...} 处理成 const __default__ =

GitHub - Rich-Harris/magic-string: Manipulate strings like a wizard

从仓库介绍:

Suppose you have some source code. You want to make some light modifications to it - replacing a few characters here and there…

这个库的作用是用来替换源码中的部分代码的(字符串的一些操作)。

先看测试吧:输出处理前后的 source -> s

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

const [result] = compileSFC(`
<script lang="ts">
export default {
  data() {},
  computed: {}
}
</script>
<script lang="ts" setup>
export default {}
</script>
`);
handling script ... with setup
----- before -----
<script lang="ts">
export default {
  data() {},
  computed: {}
}
</script>
<script lang="ts" setup>
export default {}
</script>

----- after -----
<script lang="ts">
const __default__ = {
  data() {},
  computed: {}
}
</script>
<script lang="ts" setup>
export default {}
</script>

undefined

结果如上。

Tip: 请忽略 setup 部分,因为必须要有一个 <script setup> 且必须是 ts 语言才能进 入到这部分处理。

export … [from] 处理

export { ...} from '...' 的处理。

如:

export { x as default } from './x'

export { x as default }

 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
/*else*/ if (node.type === "ExportNamedDeclaration" && node.specifiers) {
  const defaultSpecifier = node.specifiers.find(
    (s) => s.exported.type === "Identifier" && s.exported.name === "default"
  ) as ExportSpecifier;
  if (defaultSpecifier) {
    defaultExport = node;
    // 1. remove specifier
    if (node.specifiers.length > 1) {
      s.remove(
        defaultSpecifier.start! + scriptStartOffset!,
        defaultSpecifier.end! + scriptStartOffset!
      );
    } else {
      s.remove(
        node.start! + scriptStartOffset!,
        node.end! + scriptStartOffset!
      );
    }

    if (node.source) {
      // export { x as default } from './x'
      // 重写成 rewrite to `import { x as __default } from './x'
      // 然后添加到顶部
      s.prepend(
        `import { ${defaultSpecifier.local.name} as ${defaultTempVar} } from '${node.source.value}'\n`
      );
    } else {
      // export { x as default }
      // 重写成 `const __default__ = x` 且移到最后
      s.append(`\nconst ${defaultTempVar} = ${defaultSpecifier.local.name}\n`);
    }
  }
}

测试

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

compileSFC(`
<script lang="ts">export { a as default } from './x'</script>
<script lang="ts" setup>export default {}</script>
`);

compileSFC(`
<script lang="ts">
const a = {}
export { a as default }
</script>
<script lang="ts" setup>export default {}</script>
`);
handling script ... with setup
----- s, source, before -----

<script lang="ts">export { a as default } from './x'</script>
<script lang="ts" setup>export default {}</script>

----- s, source, after -----
import { a as __default__ } from './x'

<script lang="ts"></script>
<script lang="ts" setup>export default {}</script>

handling script ... with setup
----- s, source, before -----

<script lang="ts">
const a = {}
export { a as default }
</script>
<script lang="ts" setup>export default {}</script>

----- s, source, after -----

<script lang="ts">
const a = {}

</script>
<script lang="ts" setup>export default {}</script>

const __default__ = a

undefined

从文件导入的,放到 source 最前面去了

用变量导出的,放到 source 最后面去了

2⃣ 8edf0d7 解析 <script setup>

feat(add): sfc->script, setup ref process · gcclll/stb-vue-next@8edf0d7

如果只保留关键代码,这里的处理主要在 processRefExpression()

 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
// @babel/parser 解析出<script setup> 的 ast
const scriptSetupAst = parse(
  scriptSetup.content,
  {
    plugins: [
      ...plugins,
      // allow top level await but only inside <script setup>
      "topLevelAwait",
    ],
    sourceType: "module",
  },
  startOffset
);

for (const node of scriptSetupAst) {
  // ... 省略
  // 处理 `ref: x` 绑定,转成 refs
  if (
    node.type === "LabeledStatement" &&
    node.label.name === "ref" &&
    node.body.type === "ExpressionStatement"
  ) {
    // 必须要开启 ref 功能
    if (enableRefSugar) {
      warnExperimental(`ref: sugar`, 228);
      s.overwrite(
        node.label.start! + startOffset,
        node.boy.start! + startOffset,
        "const "
      );
      processRefExpression(node.body.expression, node);
    }
  }
}

下面是 processRefExpression 对 ref: 语法糖的各种使用情况分析。

d4f6497 ref: n = 100

feat(add): sfc->script, ref: in setup · gcclll/stb-vue-next@d4f6497

ref: n = 100 翻译成 const n = _ref(100)

新增核心处理代码: http://qiniu.ii6g.com/img/20201226211608.png

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

compileSFC(
  `
<script lang="ts">
const a = {}
export { a as default }
</script>
<script lang="ts" setup>
ref: n = 100
</script>
`,
  { enableRefSugar: true }
);

db7cb02 ref: { n = 1 } = useFoo()

feat(add): sfc->script, ref: ({ b: 1} = {}) · gcclll/stb-vue-next@db7cb02

对象解构语法支持。

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

compileSFC(
  `
<script>
export default { b: 2 }
</script>
<script setup>
ref: ({ b = 1, foo: bar, nested: { baz: bax } } = { count: 0, b: 2 })

</script>
`,
  { enableRefSugar: true }
);
---- before ----

<script>
const __default__ = { b: 2 }
</script>
<script setup>
ref: ({ b = 1, foo: bar, nested: { baz: bax } } = { count: 0, b: 2 })

</script>

---- after ----

<script>
const __default__ = { b: 2 }
</script>
<script setup>
const { b: __b = 1, foo: __bar, nested: { baz: __bax } } = { count: 0, b: 2 }

</script>

undefined

支持解构后重命名:feat(add): sfc->script, ref: ({ b: bb} = {}) rename · gcclll/stb-vue-next@e83d25a

对象嵌套解构:feat(add): sfc->script, ref deconstruct nested object · gcclll/stb-vue-next@5c615d5

解构重命名:feat(add): sfc->script, ref deconstruct object rename · gcclll/stb-vue-next@e3ffe6f

af26553 ref: [a] = useFoo() 数据解构

feat(add): sfc->script, ref deconstruct array · gcclll/stb-vue-next@af26553

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const { compileSFC, log } = require(process.env.BLOG_JS + "/vue/lib.js");

compileSFC(
  `
<script>
export default { b: 2 }
</script>
<script setup>
ref: ({ foo: [bar], baz: [,,bax]} = useFoo())
</script>
`,
  { enableRefSugar: true }
);
---- before ----

<script>
const __default__ = { b: 2 }
</script>
<script setup>
ref: ({ foo: [bar], baz: [,,bax]} = useFoo())
</script>

---- after ----

<script>
const __default__ = { b: 2 }
</script>
<script setup>
const { foo: [__bar], baz: [,,__bax]} = useFoo()
const bar = _ref(__bar);
const bax = _ref(__bax);
</script>

undefined

a9f4469 ref: ({…foo} = useFoo()) 展开符

feat(add): sfc->script, ref deconstruct with es6 rest element · gcclll/stb-vue-next@a9f4469

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const { compileSFC, log } = require(process.env.BLOG_JS + "/vue/lib.js");

compileSFC(
  `
<script>
export default { b: 2 }
</script>
<script setup>
ref: ({...foo} = useFoo())
</script>
`,
  { enableRefSugar: true }
);
---- before ----

<script>
const __default__ = { b: 2 }
</script>
<script setup>
ref: ({...fo} = useFoo())
</script>

---- after ----

<script>
const __default__ = { b: 2 }
</script>
<script setup>
const {...__fo} = useFoo()
</script>

undefined

d52a6d0 _ref(…) 增加 ref 声明

feat(add): sfc->script, ref all variables · gcclll/stb-vue-next@d52a6d0

在解析完所有 ref: xxx 语法之后,需要将解构出来的编码,进行 ref 化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

const { compileSFC, log } = require(process.env.BLOG_JS + '/vue/lib.js')

compileSFC(`
<script>export default {}</script>
<script setup>
ref: ({
  a, b: { foo, bar: bax }, c = 1, d: [doo1,, doo3], e: e1 = 2
} = useFoo());
</script>
`, { enableRefSugar: true })
---- before ----

<script>const __default__ = {}</script>
<script setup>
ref: ({
  a, b: { foo, bar: bax }, c = 1, d: [doo1,, doo3], e: e1 = 2
} = useFoo());
</script>

---- after ----

<script>const __default__ = {}</script>
<script setup>
const {
  a: __a, b: { foo: __foo, bar: __bax }, c: __c = 1, d: [__doo1,, __doo3], e: __e1 = 2
} = useFoo();
const a = _ref(__a);
const foo = _ref(__foo);
const bax = _ref(__bax);
const c = _ref(__c);
const doo1 = _ref(__doo1);
const doo3 = _ref(__doo3);
const e1 = _ref(__e1);
</script>

undefined

到这里 ref 语法才算解析完成了。

  1. 借助 @babel/parser 得到 script[setup] ast 处理 ref 及解构语法

  2. 将解构之后的变量进行 ref 语法化。

168041c ref: a = 1, b = 2 多条语句

feat(add): sfc->script, multiple statements after ref: · gcclll/stb-vue-next@168041c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const { compileSFC, log } = require(process.env.BLOG_JS + "/vue/lib.js");

compileSFC(
  `
<script>export default {}</script>
<script setup>
ref: a = 1, b = 2, c = 3
</script>
`,
  { enableRefSugar: true }
);
---- before ----

<script>const __default__ = {}</script>
<script setup>
ref: a = 1, b = 2, c = 3
</script>

---- after ----

<script>const __default__ = {}</script>
<script setup>
const a = _ref(1), b = _ref(2), c = _ref(3)
</script>

undefined

6e337c5 imports 置顶🔝

feat(add): sfc->script, hoist imports to top · gcclll/stb-vue-next@6e337c5

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const { compileSFC, log } = require(process.env.BLOG_JS + "/vue/lib.js");

compileSFC(
  `
<script>export default {}</script>
<script setup>
import { a } from './a'
import { b } from './b'
import { foo, bar } from './baz'
</script>
`,
  { enableRefSugar: true }
);
---- before ----

<script>const __default__ = {}</script>
<script setup>
import { a } from './a'
import { b } from './b'
import { foo, bar } from './baz'
</script>

---- after ----
import { a } from './a'

<script>const __default__ = {}</script>
<script setup>
import { b } from './b'
import { foo, bar } from './baz'
</script>

---- before ----
import { a } from './a'

<script>const __default__ = {}</script>
<script setup>
import { b } from './b'
import { foo, bar } from './baz'
</script>

---- after ----
import { a } from './a'
import { b } from './b'

<script>const __default__ = {}</script>
<script setup>
import { foo, bar } from './baz'
</script>

---- before ----
import { a } from './a'
import { b } from './b'

<script>const __default__ = {}</script>
<script setup>
import { foo, bar } from './baz'
</script>

---- after ----
import { a } from './a'
import { b } from './b'
import { foo, bar } from './baz'

<script>const __default__ = {}</script>
<script setup>
</script>

undefined

如上,经过几轮循环,将三个 import 提升到了最开始位置。

TODO 68d4940 defineProps/Emit() 处理

feat(add): sfc->script, defineProps/Emit · gcclll/stb-vue-next@68d4940

 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
const { compile, log } = require(process.env.BLOG_JS + "/vue/lib.js");

const {content, bindings} = compile(`
<script setup lang="ts">
import { defineProps } from 'vue'
interface Test {}

type Alias = number[]

defineProps<{
string: string
number: number
boolean: boolean
object: object
objectLiteral: { a: number }
fn: (n: number) => void
functionRef: Function
objectRef: Object
array: string[]
arrayRef: Array<any>
tuple: [number, number]
set: Set<string>
literal: 'foo'
optional?: any
recordRef: Record<string, null>
interface: Test
alias: Alias

union: string | number
literalUnion: 'foo' | 'bar'
literalUnionMixed: 'foo' | 1 | boolean
intersection: Test & {}
}>()
</script>`)

3⃣ 5160a6d ref -> ref.value

feat(add): sfc->script, ref -> ref.value · gcclll/stb-vue-next@5160a6d

将对 ref 变量的访问转成对 ref.value 的访问。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const { compileSFC, log, compile } = require(process.env.BLOG_JS +
  "/vue/lib.js");

const { content } = compile(
  `
<script setup>
ref: a = 1
console.log(a)
function get() {
  return a + 1
}
</script>
`
);
console.log(content);
{
  enableRefSugar: true,
  refBindings: [Object: null prototype] { a: 'setup-ref' }
}
<script setup>
const a = _ref(1)
console.log(a.value)
function get() {
  return a.value + 1
}
</script>
undefined

TODO 4⃣ a6f4dae extract define props/emits

feat(add): sfc->script, extract props/emits · gcclll/stb-vue-next@a6f4dae

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const { compile, log } = require(process.env.BLOG_JS + '/vue/lib.js')

const { content } = compile(`
<script setup>
defineProps({
  foo: String
})
</script>
`)
console.log(content)
ExpressionStatement --
undefined {} xx

undefined

TODO 5⃣ 480acf0 checkInvalidScopeReference

feat(add): sfc->script, check invalid scope references · gcclll/stb-vue-next@480acf0

检查 useOptions ,是否包含 <setup> 中已经存在的变量,即 useOptions 中不能 有 setup 中声明的变量。

7⃣ deed8c1 analyze binding metadata(bindingMetadata)

feat(add): sfc->script, analyze binding metadata · gcclll/stb-vue-next@deed8c1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const { compile, log } = require(process.env.BLOG_JS + "/vue/lib.js");
const res = compile(`
<script setup>
const props = defineProps({
  foo: String
})
ref: a = 1
const b = 2
</script>
`);
console.log(res.bindings);
{
  foo: 'props',
  props: 'setup-const',
  a: 'setup-ref',
  b: 'setup-const'
}
undefined

8⃣ 9cd2fd5 inject useCssVars, css 变量处理

feat(add): sfc->script, inject useCssVars · gcclll/stb-vue-next@9cd2fd5

<style> v-bind css 变量

处理 style 中使用的 css 变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// compileScript.ts
// TODO 8. 注入 `useCssVars` 调用
  if (cssVars.length) {
    helperImports.add(CSS_VARS_HELPER)
    helperImports.add('unref')
    s.prependRight(
      startOffset,
      `\n${genCssVarsCode(
        cssVars,
        bindingMetadata,
        scopeId,
        !!options.isProd
      )}\n`
    )
  }

测试:

 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
const { compile, log } = require(process.env.BLOG_JS + "/vue/lib.js");

const { content } = compile(`
<script>const a = 1</script>
<script setup>
import { defineProps, ref } from 'vue'
const color = 'red'
const height = ref('10px')
defineProps({
  foo: Striing
})
</script>
<style>
div {
  color: v-bind(color);
  font-size: v-bind('font.size');
  height: v-bind(height);
  border: v-bind(foo)
}
</style>
`);
// 1. 本地变量绑定
// 2. 本地 ref 绑定
// 3. props 绑定
console.log(content);
import { ref } from 'vue'
const a = 1
_useCssVars(_ctx => ({
  "xxxxxxxx-color": (color),
  "xxxxxxxx-font_size": (_ctx.font.size),
  "xxxxxxxx-height": (height.value),
  "xxxxxxxx-foo": (__props.foo)
}))

const color = 'red'
const height = ref('10px')
undefined

css 变量重写:

源码处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const needRewrite = cssVars.length || hasInheritAttrsFlag;
let content = script.content;
if (needRewrite) {
  content = rewriteDefault(content, `__default__`, plugins);
  if (cssVars.length) {
    content += genNormalScriptCssVarsCode(
      cssVars,
      bindings,
      scopeId,
      !!options.isProd
    );
  }

  if (hasInheritAttrsFlag) {
    content += `__default__.inheritAttrs = false`;
  }
  content += `\nexport default __default__`;
}

测试:

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

const { code } = compileStyle({
  source: `.foo {
    color: v-bind(color);
    font-size: v-bind('font.size');
  }`,
  filename: "test.css",
  id: "data-v-test",
});

console.log(code);
.foo {
    color: var(--test-color);
    font-size: var(--test-font_size);
}
undefined

isProd option 使用 hash 变量名

生产模式,使用随机 hash 值作为名字:

1
2
3
4
5
6
7
8
// cssVars.ts
function genVarName(id: string, raw: string, isProd: boolean): string {
  if (isProd) {
    return hash(id + raw)
  } else {
    return `${id}-${raw.replace(/([^\w-])/g, '_')}`
  }
}

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const { compile, log } = require(process.env.BLOG_JS + "/vue/lib.js");

const { content } = compile(
  `<script>const a = 1</script>\n` +
    `<style>div{
          color: v-bind(color);
          font-size: v-bind('font.size');
        }</style>`,
  { isProd: true }
);
console.log(content);
const a = 1
const __default__ = {}
import { useCssVars as _useCssVars } from 'vue'
const __injectCSSVars__ = () => {
_useCssVars(_ctx => ({
  "4003f1a6": (_ctx.color),
  "41b6490a": (_ctx.font.size)
}))}
const __setup__ = __default__.setup
__default__.setup = __setup__
  ? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }
  : __injectCSSVars__

export default __default__
undefined

TODO 9⃣ eef6fd5 setup() 参数签名

feat(add): sfc->script, setup() 参数签名 · gcclll/stb-vue-next@eef6fd5

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

const { compile, log } = require(process.env.BLOG_JS + '/vue/lib.js')

const { content } = compile(`
<script setup lang="ts">
import { defineProps, defineEmit } from 'vue'
const props = defineProps({ foo: String })
const emit = defineEmit(['a', 'b'])
</script>`)
console.log(content)
import { defineComponent as _defineComponent } from 'vue'


export default _defineComponent({
  expose: [],
  props: { foo: String },
  emits: ['a', 'b'],
  setup(__props, { emit }) {

const props = __props



return { props, emit }
}

})
undefined

🔟 9cedeab 生成 return 语句

feat(add): sfc->script, process render function return · gcclll/stb-vue-next@9cedeab

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const { compile, log } = require(process.env.BLOG_JS + "/vue/lib.js");
const { content } = compile(`
<script setup>
import { x } from './x'
let a = 1
const b = 2
function c() {}
class d {}
</script>`);
console.log(content);
import { x } from './x'

let a = 1
const b = 2
function c() {}
class d {}

return { a, b, c, d, x }
}
undefined

将所有变量都返回出去了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const { compile, log } = require(process.env.BLOG_JS + "/vue/lib.js");
const { content } = compile(
  `
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
    <div>{{ count }}</div>
    <div>static</div>
</template>
<style>
div { color: v-bind(count) }
</style>`,
  {
    inlineTemplate: true,
  }
);
console.log(content);
import { toDisplayString as _toDisplayString, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from "vue"

const _hoisted_1 = /*#__PURE__*/_createVNode("div", null, "static", -1 /* HOISTED */)

import { ref } from 'vue'

_useCssVars(_ctx => ({
  "xxxxxxxx-count": (count.value)
}))

const count = ref(0)

return (_ctx, _cache) => {
  return (_openBlock(), _createBlock(_Fragment, null, [
    _createVNode("div", null, _toDisplayString(count.value), 1 /* TEXT */),
    _hoisted_1
  ], 64 /* STABLE_FRAGMENT */))
}
}
undefined

如果要支持:

1
2
3
4
5
6
{
   inlineTemplate: true,
   templateOptions: {
     ssr: true
   }
}

还需要实现 compiler-ssr 模块:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// TODO 10. 生成返回语句(return)
let returned;
if (options.inlineTemplate) {
  if (sfc.template && !sfc.template.src) {
    // TODO 需要 compiler-ssr 支持
  } else {
    returned = `() => {}`;
  }
} else {
  // return bindings from setup
  const allBindings: Record<string, any> = { ...setupBindings };
  for (const key in userImports) {
    if (!userImports[key].isType) {
      allBindings[key] = true;
    }
  }
  returned = `{ ${Object.keys(allBindings).join(", ")} }`;
}
s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`);

TODO 1⃣1⃣ cfca9de finalize default export

feat(add): sfc->script, finalize default export · gcclll/stb-vue-next@cfca9de

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

const { compile, log } = require(process.env.BLOG_JS + '/vue/lib.js')
const { content, bindings } = compile(`
<script setup>
import { defineEmit } from 'vue'
const myEmit = defineEmit(['foo', 'bar'])
const props = defineProps({
  foo: String
})
</script>
  `)
console.log(content)
export default {
  expose: [],
  props: {
  foo: String
},
  emits: ['foo', 'bar'],
  setup(__props, { emit: myEmit }) {

const props = __props



return { myEmit, props }
}

}
undefined

emits 哪去了??? -> fix: d631810

FIX: fix: sfc->script, expose indent · gcclll/stb-vue-next@d631810

61c3b7a transform src set

feat(add): sfc->srcset transform · gcclll/stb-vue-next@61c3b7a

转换 <img><source>srcset 属性。

img srcset 属性值: <img srcset="url 1x, url2 2x, ..."> 浏览器会根据实际情况来 选用 srcset 中合适的图片地址来显示。

有关 Reponsive Images 说明: Responsive images - Learn web development | MDN

测试:

1
2
3
4
const { compileWithSrcset: compile, log, src } = require(process.env.BLOG_JS + '/vue/lib.js')

const { code } = compile(src)
console.log(code)
import { createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from "vue"
import _imports_0 from './logo.png'


const _hoisted_1 = _imports_0
const _hoisted_2 = _imports_0 + '2x'
const _hoisted_3 = _imports_0 + '2x'
const _hoisted_4 = _imports_0 + ', ' + _imports_0 + '2x'
const _hoisted_5 = _imports_0 + '2x, ' + _imports_0
const _hoisted_6 = _imports_0 + '2x, ' + _imports_0 + '3x'
const _hoisted_7 = _imports_0 + ', ' + _imports_0 + '2x, ' + _imports_0 + '3x'
const _hoisted_8 = "/logo.png" + ', ' + _imports_0 + '2x'

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock(_Fragment, null, [
    _createVNode("img", {
      src: "./logo.png",
      srcset: _hoisted_1
    }),
    _createVNode("img", {
      src: "./logo.png",
      srcset: _hoisted_2
    }),
    _createVNode("img", {
      src: "./logo.png",
      srcset: _hoisted_3
    }),
    _createVNode("img", {
      src: "./logo.png",
      srcset: _hoisted_4
    }),
    _createVNode("img", {
      src: "./logo.png",
      srcset: _hoisted_5
    }),
    _createVNode("img", {
      src: "./logo.png",
      srcset: _hoisted_6
    }),
    _createVNode("img", {
      src: "./logo.png",
      srcset: _hoisted_7
    }),
    _createVNode("img", {
      src: "/logo.png",
      srcset: "/logo.png, /logo.png 2x"
    }),
    _createVNode("img", {
      src: "https://example.com/logo.png",
      srcset: "https://example.com/logo.png, https://example.com/logo.png 2x"
    }),
    _createVNode("img", {
      src: "/logo.png",
      srcset: _hoisted_8
    }),
    _createVNode("img", {
      src: "data:image/png;base64,i",
      srcset: "data:image/png;base64,i 1x, data:image/png;base64,i 2x"
    })
  ], 64 /* STABLE_FRAGMENT */))
}
undefined

指定 options.base: '/foo' 测试结果:

1
2
3
4
const { compileWithSrcset: compile, log, src } = require(process.env.BLOG_JS + '/vue/lib.js')

const { code } = compile(src, { base: '/foo' })
console.log(code)
import { createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock(_Fragment, null, [
    _createVNode("img", {
      src: "./logo.png",
      srcset: "/foo/logo.png"
    }),
    _createVNode("img", {
      src: "./logo.png",
      srcset: "/foo/logo.png 2x"
    }),
    _createVNode("img", {
      src: "./logo.png",
      srcset: "/foo/logo.png 2x"
    }),
    _createVNode("img", {
      src: "./logo.png",
      srcset: "/foo/logo.png, /foo/logo.png 2x"
    }),
    _createVNode("img", {
      src: "./logo.png",
      srcset: "/foo/logo.png 2x, /foo/logo.png"
    }),
    _createVNode("img", {
      src: "./logo.png",
      srcset: "/foo/logo.png 2x, /foo/logo.png 3x"
    }),
    _createVNode("img", {
      src: "./logo.png",
      srcset: "/foo/logo.png, /foo/logo.png 2x, /foo/logo.png 3x"
    }),
    _createVNode("img", {
      src: "/logo.png",
      srcset: "/logo.png, /logo.png 2x"
    }),
    _createVNode("img", {
      src: "https://example.com/logo.png",
      srcset: "https://example.com/logo.png, https://example.com/logo.png 2x"
    }),
    _createVNode("img", {
      src: "/logo.png",
      srcset: "/logo.png, /foo/logo.png 2x"
    }),
    _createVNode("img", {
      src: "data:image/png;base64,i",
      srcset: "data:image/png;base64,i 1x, data:image/png;base64,i 2x"
    })
  ], 64 /* STABLE_FRAGMENT */))
}
undefined

options.includeAbsolute: true 选项:

1
2
3
4
const { compileWithSrcset: compile, log, src } = require(process.env.BLOG_JS + '/vue/lib.js')

const { code } = compile(src, { includeAbsolute: true })
console.log(code)
import { createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from "vue"
import _imports_0 from './logo.png'
import _imports_1 from '/logo.png'


const _hoisted_1 = _imports_0
const _hoisted_2 = _imports_0 + '2x'
const _hoisted_3 = _imports_0 + '2x'
const _hoisted_4 = _imports_0 + ', ' + _imports_0 + '2x'
const _hoisted_5 = _imports_0 + '2x, ' + _imports_0
const _hoisted_6 = _imports_0 + '2x, ' + _imports_0 + '3x'
const _hoisted_7 = _imports_0 + ', ' + _imports_0 + '2x, ' + _imports_0 + '3x'
const _hoisted_8 = _imports_1 + ', ' + _imports_1 + '2x'
const _hoisted_9 = "https://example.com/logo.png" + ', ' + "https://example.com/logo.png" + '2x'
const _hoisted_10 = _imports_1 + ', ' + _imports_0 + '2x'
const _hoisted_11 = "data:image/png;base64,i" + '1x, ' + "data:image/png;base64,i" + '2x'

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock(_Fragment, null, [
    _createVNode("img", {
      src: "./logo.png",
      srcset: _hoisted_1
    }),
    _createVNode("img", {
      src: "./logo.png",
      srcset: _hoisted_2
    }),
    _createVNode("img", {
      src: "./logo.png",
      srcset: _hoisted_3
    }),
    _createVNode("img", {
      src: "./logo.png",
      srcset: _hoisted_4
    }),
    _createVNode("img", {
      src: "./logo.png",
      srcset: _hoisted_5
    }),
    _createVNode("img", {
      src: "./logo.png",
      srcset: _hoisted_6
    }),
    _createVNode("img", {
      src: "./logo.png",
      srcset: _hoisted_7
    }),
    _createVNode("img", {
      src: "/logo.png",
      srcset: _hoisted_8
    }),
    _createVNode("img", {
      src: "https://example.com/logo.png",
      srcset: _hoisted_9
    }),
    _createVNode("img", {
      src: "/logo.png",
      srcset: _hoisted_10
    }),
    _createVNode("img", {
      src: "data:image/png;base64,i",
      srcset: _hoisted_11
    })
  ], 64 /* STABLE_FRAGMENT */))
}
undefined

总结

SFC 模块的作用:

  1. 解析 Render 函数,替换 ref 变量

  2. 解析 <script> 标签

  3. 解析 <script setup>

  4. ref: 解析,将 ref: 类型访问转成对 ref.value 的访问

  5. 将 ref: { … } 解构后的变量进行 ref(…) 化

  6. defineProps({ foo: String }) 解析,合并到 export default { props: {…} }

  7. defineEmit({ … }) 解析,合并到 export default { emits: {…} }

  8. cssVar v-bind 变量使用,转换,包含 ref 变量引用转换

  9. asset url 转换(相对路径,绝对路径, ~@path/..., @path/.. 转换)

  10. <img>, <source>srcset URL转换

综合测试:

script[setup] 测试

 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

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

const { content, bindings, attrs } = compileSFCScript(`
<template>
  <div>
    <img src="./logo.png" srcset="./logo.png, ./logo.png 2x, ./logo.png 3x"/>
  </div>
  <div>{{ count }}</div>
  <div>static</div>
</template>
<script>
  const a = 1
export default {
  props: ['foo', 'bar'],
  data() {
    return { foo: null, bar }
  },
  setup() {
    return { foo: 1, a }
  },
  computed: {
    fcc() {},
    bcc: {
      get() {},
      set() {}
    }
  },
  inject: ['fjj', 'bjj'], // { fjj: {}, bjj: {} },
  methods: {
    quux() {}
  },
}
</script>
<script setup>
import { defineProps, ref } from 'vue'
let a, b, c, d
ref: aa = 1 + (await foa)
ref: height = 100
// 对象解构要用括号包裹起来
ref: ({ foo, bar: bar, baz: { bax } } = useFoo());
ref: [ar, br] = useFoo()
ref: count = 0
const color = 'red'
const size = ref('10px')
defineProps({
  foo: String
})
defineEmit(['fox', 'foy'])

// ref 在函数中被访问
function test() {
  const { a } = aa
}
</script>
<style>
div {
  color: v-bind(color);
  font-size: v-bind('font.size');
  border: v-bind(foo);
  height: v-bind(height);
}
</style>
`)
log(`>>> content 输出结果:`)
log(content)
log(`>>> bindings 输出结果:`)
log(bindings)
>>> content 输出结果:
import { ref as _ref, useCssVars as _useCssVars, unref as _unref } from 'vue'
import { ref } from 'vue'

  const a = 1
const __default__ = {
  props: ['foo', 'bar'],
  data() {
    return { foo: null, bar }
  },
  setup() {
    return { foo: 1, a }
  },
  computed: {
    fcc() {},
    bcc: {
      get() {},
      set() {}
    }
  },
  inject: ['fjj', 'bjj'], // { fjj: {}, bjj: {} },
  methods: {
    quux() {}
  },
}

async function setup(__props) {

_useCssVars(_ctx => ({
  "xxxxxxxx-color": (color),
  "xxxxxxxx-font_size": (_ctx.font.size),
  "xxxxxxxx-foo": (foo.value),
  "xxxxxxxx-height": (height.value)
}))

let a, b, c, d
const aa = _ref(1 + (await foa))
const height = _ref(100)
// 对象解构要用括号包裹起来
const { foo: __foo, bar: __bar, baz: { bax: __bax } } = useFoo();
const foo = _ref(__foo);
const bar = _ref(__bar);
const bax = _ref(__bax);
const [__ar, __br] = useFoo()
const ar = _ref(__ar);
const br = _ref(__br);
const count = _ref(0)
const color = 'red'
const size = ref('10px')



// ref 在函数中被访问
function test() {
  const { a } = aa.value
}

return { a, b, c, d, aa, height, foo, bar, bax, ar, br, count, color, size, test, ref }
}


export default /*#__PURE__*/ Object.assign(__default__, {
  expose: [],
  props: {
  foo: String
},
  emits: ['fox', 'foy'],
  setup
})
>>> bindings 输出结果:
{
  foo: 'setup-ref',
  bar: 'setup-ref',
  a: 'setup-let',
  fcc: 'options',
  bcc: 'options',
  fjj: 'options',
  bjj: 'options',
  quux: 'options',
  ref: 'setup-const',
  b: 'setup-let',
  c: 'setup-let',
  d: 'setup-let',
  aa: 'setup-ref',
  height: 'setup-ref',
  bax: 'setup-ref',
  ar: 'setup-ref',
  br: 'setup-ref',
  count: 'setup-ref',
  color: 'setup-const',
  size: 'setup-ref',
  test: 'setup-const'
}
undefined

结果简析:

  1. <script setup> 标签内的代码都会被解析到 setup() {...} 函数中

    即它和 <script> 中代码是不冲突的,比如 <script> 里面的变量 a<script setup> 中的同名变量 a 互不影响。

  2. <style> 中使用的 v-bind 指令会使用 _useCssVars 进行注册替换成实际的样 式值

  3. defineProps 中定义的变量对应 export default -> props

  4. defineEmits 中定义的变量对应 export default -> emits

  5. ref: height = 100 中的 ref: 语法最终会转成对应的 _ref(100) reactive 变 量(语法糖,vue>compile-sfc解析)

    并且支持多种用法,解构/多个声明组合/解构默认值,对于解构后的变量进行 _ref(...) 然后 setup return 中返回。

    ref: 语句中如果有解构(对象解构)操作,那么后面的解构表达式必须用括号(({ a } = useFoo()))包起来。

    Imp. ref: 解构中的变量名不能以 $ 开头。

  6. 如果 scriptscript setup 中的 export default 中同时包含同名属性,会被 script setup 中的替换掉。

    因为生成的代码中是将 script setup 往 script 上合并。

    PS. <script> 和 <script setup> 中 export default 的内容尽量不要重复。

typescript 语言

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18

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

const { scriptAst, content } = compile(`
<script lang="ts">
import { Options, Vue } from 'vue-class-component';
@Options({
    components: {
    HelloWorld,
    },
    props: ['foo', 'bar']
})
export default class Home extends Vue {}
</script>
`)

console.log(content)

import { Options, Vue } from 'vue-class-component';
@Options({
    components: {
    HelloWorld,
    },
    props: ['foo', 'bar']
})
export default class Home extends Vue {}

undefined