Vue3 源码头脑风暴之 5 ☞ compiler-sfc
文章目录
stb-vue-next 完全拷贝于 vue-next ,主要目的用于学习。
声明 :vue-next compiler-sfc 模块,相关的所有测试代码均在
/js/vue/
目录下面。更新日志&Todos :
[2020-12-19 13:58:31] 创建
[2021-01-04 19:47:10] 完成
TODO defineProps 和 defineEmit 原理和用途
TODO inlineTemplate with ssr: true options
TODO more mind-maps
重点、特性、问题
🔗
<style>
标签中可通过v-bind()
引用CSS 模块化后的变量<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:
|
|
feat(init): parse function · gcclll/stb-vue-next@e7e1cc1
声明一些基本类型,比如: <template>, <script>, <style>
这也是 *.vue
文件的三
大要素,这里需要多关注一点就是会发现 <script>
标签里面多有一个 setup
属性,
这个是 vue 自身定义的一种标签类型,比如在这里面可以直接使用 ref
声明变量,这里
面的变量都会自动变成响应式的等等。
SFC 块类型定义:
|
|
SFC <template>
标签类型定义:
|
|
SFC <script>
脚本标签类型定义
|
|
SFC <style>
样式标签类型定义
|
|
SFC 文件类型定义
|
|
parse 函数定义:
|
|
49ee210 parse function 实现部分
feat: sfc-> code parse function · gcclll/stb-vue-next@49ee210
实现 parse 函数的基本架构:
sourceToSFC<key, source>
用来缓存 vue文件解析结果,首先取缓存结果通过调用 compiler-dom 中的 compiler.parse 将文件内容 source解析成 AST
遍历所有 ast.children 根据 node.tag 类型决定走什么分支处理
<template>
模板分支,这里面的所有内容会被 parse 继续解析出 ast<script [setup]>
脚本分支, 当做 RAWDATA 文本类型处理,如果有setup
属性, 则所有 script 都不能带 src 属性,即不能引用外部文件,因为所有 script 内容会合 并到一起去处理。<style [lang=""]>
样式分支,当做 RAWDATA 文本类型处理错误用法检测,主要是
<script setup>
脚本标签不能有 src 的检测souremap
的处理descriptor.cssVars = parseCssVars(descriptor)
CSS 变量的解析,会全部解析到 数组cssVars
里面去缓存解析后的结果到
sourceToSFC.set(sourceKey, result)
对了,在
switch case
分支里面默认走的是自定义块的处理(vue 文件中还可以自定 义?)
CSS vars 变量处理:
|
|
这里有个 cssVarRE 正则,来看下:
这个正则可以匹配结果: v-bind('...'), v-bind("..."), v-bind(...)
从 compiler-src/__tests__/cssVars.spec.ts
用例中可窥见这种用法:
|
|
💟 现在可以直接在
<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':
增加函数: createBlock()
用来处理 SFC 标签的属性(如: lang, setup, src,
scoped, module
)
回顾下 compiler-dom, compiler-core 其实对于 <template>
标签的处理工作依然集中
在这两个包里面,所以这里就不再赘述模板 ast 的解析了。
|
|
{ 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 逻辑:
|
|
createBlock() 中增加各属性的解析和设置:
lang
-> block.lang
src
-> block.src
style > scoped
-> block.scoped
style > module
-> block.module
script > setup
-> block.setup
另外增加了 padContent()
检测回车换行符替换?
测试:
|
|
{ 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 变量?
测试:
|
|
{ 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 模板便后的结果类型
|
|
SFCTemplateCompileOptions 模板编译器选项
|
|
及 compileTemplate 函数
|
|
TODO 1b2965f coding compileTemplate
feat: sfc->compile compileTemplate code · gcclll/stb-vue-next@1b2965f
这个函数相关的内容:
preprocessLang
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 在模板编译期间使用。
asset url 资源地址转换用的 transform
要处理的标签和对应的包含 url 的属性:
tag prop with url <video>
'src', 'poster' <source>
'src' <img>
'src' <image>
'xlink:href', 'href' <use>
'xlink:href', 'href' img/source 标签 src 地址转换
重点代码:
|
|
将 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使用情况和转换结果如下实例:
|
|
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不转换几种情况:
属性不是静态属性(
NodeTypes.ATTRIBUTE
)非特定标签的不转换(或者通过
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'] }
没有属性值的属性
外部链接的URL(
https
开头的)data:
开头的资源地址属性值以
#
开头的地址非绝对路径且费相对路径的(以,
.|~|@
开头的地址)
需要处理的又分两种情况:
给定了
options.base
基地址的(.|~|@
为第一个字符的)直接用
options.base + assert url
处理非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 来的,也没深入研究,所以这节也没什么好讲述的。
待到以后有时间再来研究。
|
|
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>重点
这节会是重点部分。
init compileScript function
初始化 compileScript()
函数以及参数选项类型 SFCScriptCompileOptions
SFCScriptCompileOptions:
id: string
, 传递给compileStyle
用于作为 injected CSS 变量前缀用isProd?: boolean
决定生成的 CSS 变量是否要加上 hash 值babelParserPlugins?: ParserPlugin[]
refSugar?: boolean
使能ref
语法糖inlineTemplate?: boolean
内联模板???
compileScript:
|
|
feat(add): sfc->script, compileScript steps comment · gcclll/stb-vue-next@54ea72a
列出 compileScript() 将要完成的任务:
No. | Desc | Link |
---|---|---|
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
这个插件是如何使用的,输
出结果又是啥?
|
|
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/parser
的 parse 函数进行解析,因此后面
一坨代码在这种情况下(只有普通的 script
时)是不需要的。
新增代码:
|
|
测试:
|
|
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
示例:
|
|
+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
里面存在类型值:
name | type(BindingTypes ) | value |
---|---|---|
props | 'PROPS' | 'props' |
inject | 'PROPS' | 'props' |
computed | 'OPTIONS' | 'options' |
methods | 'OPTIONS' | 'options' |
setup | SETUP_MAYBE_REF | 'setup-maybe-ref' |
data | SETUP_MAYBE_REF | 'setup-maybe-ref' |
到这里还只是借助 @babel/parser
进行了解析,vue 自身的一些特性处理在
analyzeScriptBindings()
中,这个函数解析的类型是 ExportDefaultDeclaration
也
就是 export default {}
的代码部分。
然后调用 analyzeBindingsFromOptions(node.declaration)
解析对象成员,这里要处理
的主要有两种:
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
ObjectMethod
方法类型成员,且只处理setup
和data
方法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⃣ eb650ca 解析 <script>
feat(add): sfc->script, export default handle · gcclll/stb-vue-next@eb650ca
用到的插件:
Plugin |
---|
@babel/parser · Babel |
这一节中的普通 <script> 前提是,至少有一个 <script setup>
存在,否则会直接在
上一节 就退出解析了。
@babel/parser 解析 import 结果对照表
段 | 类型 | 值 |
---|---|---|
import { a } from './x' | ImportDeclaration | … |
a | ImportSpecifier | node.specifiers[i].imported.name |
'./x' | StringLiteral | node.source.value |
|
|
>>> node type > ImportDeclaration >>> node source type > StringLiteral >>> node source value > ./x >>> spec type > ImportSpecifier >>> spec imported type > Identifier >>> spec imported name > a
所以 vue-next 中新增的代码处理逻辑:
|
|
然后 compileScript 中有一段处理不明白:
|
|
这里是说如果有 <script setup>
但是类型不是 ts 就直接返回 scriptSetup
?
|
|
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 处理
|
|
上面处理:遍历 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 {}
语法的处理:
|
|
变量 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
|
|
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 }
|
|
测试
|
|
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()
中
|
|
下面是 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)
新增核心处理代码:
|
|
db7cb02 ref: { n = 1 } = useFoo()
feat(add): sfc->script, ref: ({ b: 1} = {}) · gcclll/stb-vue-next@db7cb02
对象解构语法支持。
|
|
---- 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
|
|
---- 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
|
|
---- 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 化。
|
|
---- 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 语法才算解析完成了。
借助
@babel/parser
得到 script[setup] ast 处理 ref 及解构语法将解构之后的变量进行 ref 语法化。
168041c ref: a = 1, b = 2 多条语句
feat(add): sfc->script, multiple statements after ref: · gcclll/stb-vue-next@168041c
|
|
---- 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
|
|
---- 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
|
|
3⃣ 5160a6d ref -> ref.value
feat(add): sfc->script, ref -> ref.value · gcclll/stb-vue-next@5160a6d
将对 ref 变量的访问转成对 ref.value
的访问。
|
|
{ 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
|
|
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 中声明的变量。
TODO 6⃣ 25662c6 删除非 script 内容
7⃣ deed8c1 analyze binding metadata(bindingMetadata)
feat(add): sfc->script, analyze binding metadata · gcclll/stb-vue-next@deed8c1
|
|
{ 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 变量
|
|
测试:
|
|
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 变量重写:
源码处理:
|
|
测试:
|
|
.foo { color: var(--test-color); font-size: var(--test-font_size); } undefined
isProd option 使用 hash 变量名
生产模式,使用随机 hash 值作为名字:
|
|
测试:
|
|
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
|
|
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
|
|
import { x } from './x' let a = 1 const b = 2 function c() {} class d {} return { a, b, c, d, x } } undefined
将所有变量都返回出去了。
|
|
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
如果要支持:
|
|
还需要实现 compiler-ssr
模块:
|
|
TODO 1⃣1⃣ cfca9de finalize default export
feat(add): sfc->script, finalize default export · gcclll/stb-vue-next@cfca9de
|
|
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
TODO 1⃣2⃣ 5810296 finalize Vue helper imports
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。
测试:
|
|
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'
测试结果:
|
|
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
选项:
|
|
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 模块的作用:
解析 Render 函数,替换 ref 变量
解析 <script> 标签
解析 <script setup>
ref: 解析,将 ref: 类型访问转成对 ref.value 的访问
将 ref: { … } 解构后的变量进行 ref(…) 化
defineProps({ foo: String }) 解析,合并到 export default { props: {…} }
defineEmit({ … }) 解析,合并到 export default { emits: {…} }
cssVar v-bind 变量使用,转换,包含 ref 变量引用转换
asset url 转换(相对路径,绝对路径,
~@path/...
,@path/..
转换)<img>, <source>
的srcset
URL转换
综合测试:
script[setup] 测试
|
|
>>> 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
结果简析:
<script setup>
标签内的代码都会被解析到setup() {...}
函数中即它和
<script>
中代码是不冲突的,比如<script>
里面的变量a
和<script setup>
中的同名变量a
互不影响。在
<style>
中使用的 v-bind 指令会使用_useCssVars
进行注册替换成实际的样 式值defineProps
中定义的变量对应 export default -> propsdefineEmits
中定义的变量对应 export default -> emitsref: height = 100
中的ref:
语法最终会转成对应的_ref(100)
reactive 变 量(语法糖,vue>compile-sfc解析)并且支持多种用法,解构/多个声明组合/解构默认值,对于解构后的变量进行
_ref(...)
然后 setup return 中返回。ref: 语句中如果有解构(对象解构)操作,那么后面的解构表达式必须用括号(
({ a } = useFoo())
)包起来。Imp. ref: 解构中的变量名不能以
$
开头。如果
script
和script setup
中的 export default 中同时包含同名属性,会被script setup
中的替换掉。因为生成的代码中是将 script setup 往 script 上合并。
PS. <script> 和 <script setup> 中 export default 的内容尽量不要重复。
typescript 语言
|
|
import { Options, Vue } from 'vue-class-component'; @Options({ components: { HelloWorld, }, props: ['foo', 'bar'] }) export default class Home extends Vue {} undefined
process normal <script> first if it exists