上一篇我们已经能够根据html来生成我们的ast,但是我们跳过了如何解析script内的代码,以及如何解析属性,今天我们就来完善它,请确保已经看过上一篇了在看今天的内容上一篇的内容
解析script标签。
因为script标签内包含的代码,我们会在template有可能会引用到,因此我们要编译script内部的代码构建标准es语法树,但是这次我们就不用自己手写了,直接使用code_red这个类库,我们只要输入代码字符串就能得到ESTree,Svelte也是使用这个类库。
import * as code_red from 'code-red'
const regex_not_newline_character = /[^\n]/g
const regex_closing_script_tag = /<\/script\s*>/
/**
*
* @param {} parser
* @param {script标签开始的位置} start
*/
export function read_script(parser, start) {
const scriptStart = parser.index // 保存代码开始的位置。
const data = parser.read_util(regex_closing_script_tag) // 读取script内的代码。
if (parser.index > parser.template.length) {
throw Error('超出代码字符串的长度')
}
// 保留代码字符串开始到script标签开始之间的/n换行符,具体作用以后再解释。
const source = parser.template.slice(0, scriptStart).replace(regex_not_newline_character, ' ') + data
let ast
try {
ast = parse(source)
} catch (error) {
throw Error(error)
}
// 消耗</script>
parser.expect(regex_closing_script_tag)
const node = {
type: 'Script',
start,
end: parse.index,
content: ast
}
return node
}
function parse(code) {
return code_red.parse(code, {
sourceType: 'module',
ecmaVersion: 12,
locations: true
})
}
接下来我们再向parser添加一个字段专门来保存解析出来的Estree。同时修改parser.expect的实现,使其支持通过Regexp匹配,消耗字符。
class Parser {
// 其他代码省略
js: []
expect(c, require) {
if (this.index > this.template.length) {
throw Error('超过代码字符串长度')
}
const result = c instanceof RegExp ? c.exec(this.template.slice(this.index))
: null
if (result && result.index === 0) { // 匹配成功,并且是在字符串开头开始匹配
this.index += result[0].length - 1 // 消耗匹配的字符长度
return true
}
if (c === this.template[this.index]) {
this.index++
return true
}
if (require) {
throw Error(`消耗字符或者reg失败`)
}
return false
}
}
然后我们在思考一下如何把read_script添加进我们的项目里面,因为script也是标签所以它也是会走函数element的逻辑的,我们可以在函数解析完名字之后,判断一下是否是script如果是的话,就调用我们的read_script来进行处理。
import { read_script } from './read/script.js'
/**
* 专门来处理标签的特殊情况
* specials对象的每一项都是对象
* key位特殊标签的名字,value也是一个对象。
* 对象内有特殊标签的解析函数和在Parser对象的字段名称
*/
const specials = {
script: {
read: read_script,
property: 'js'
}
}
export function element(parser) {
// 其他代码省略
// 处理特殊标签script
if (specials[tagName]) {
// 期待script后面跟着>, 否则就报错。
parser.expect('>', true)
const readObj = specials[tagName]
const node = readObj.read(parser, start)
parser[readObj.property].push(node)
return
}
parser.current().children.push(element)
// 其他代码省略
}
注意if(specials[tagName]){...}在element中的位置,必须是在解析完名字之后,parser.current().chidlren.push(element)之前。
我们来测试一下我们的代码
const str = `
<script>
let count = 0
</script>
<button>{count}</button>
`
const parser = new Parser(str)
console.info(parser.js)
输出如下
解析标签内属性
解析属性的名字相对比较容易,我们只要在解析完标签名字之后尝试在空格,=和 >之前读取任意字符串,如果没有读取到,标签没有属性。
export function element(parser) {
// 省略其他代码
const unique_names = new Set() // 用来保证标签内的属性名字是唯一的。
while (attribute = readAttribute(parser, unique_names)) {
element.attribute.push(attribute)
parser.skip_white_space()
}
}
// 属性名字会中断在 空格,/ ,= > 四种字符
const regex_token_ending_character = /[\s/\>=]/
function readAttribute(parser, unique_names) {
const start = parser.index
function checkUniqueName(name) {
if (unique_names.has(name)) {
throw Error('不能出现相同的属性名')
}
unique_names.add(name)
}
const name = parser.read_util(regex_token_ending_character)
if (!name)
return null
checkUniqueName(name)
parser.skip_white_space()
// 先保存end,因为有可能只有属性名,没有属性值。
let end = parser.index
let value
if (parser.expect('=')) {
parser.skip_white_space()
value = read_attribute_value(parser)
parser.skip_white_space()
parser.expect('}', true)
end = parser.index
}
return {
type: 'Attribute',
start,
end,
name,
value
}
}
对于解析属性名,我们要先看一下code_red.parseExpressionAt的用法,该函数接受三个参数,分别是字符串,开始解析的位置,和配置,只要我们的输入指定开始的位置,有一段是合法的Estree内容,该函数就能够解析出来。例如handleClick}</button>对于这个字符串,解析出如下结果
因此我们就可以用它来解析属性值为{}内的内容,具体的代码如下
function read_attribute_value(parser) {
if (parser.expect('{')) {
return readExpression(parser)
}
throw Error('Todo 属性值为其他情况,暂时还没有处理')
}
function readExpression(parser) {
let node
try {
node = code_red.parseExpressionAt(
parser.template,
parser.index,
{
sourceType: 'module',
ecmaVersion: 12,
locations: true
}
)
} catch (error) {
throw Error('解析表达式失败')
}
parser.index += node.end
return node
}
现在我们来写一些代码测试一下,看实现是否正确
const source = `
<script>
let count = 0
let handleClick = () => {
count += 1
}
</script>
<button on:click={handleClick}>{count}</button>
`
const parser = new Parser(source)
console.info(parser.html)
输出如下
接下来我们ast的数据结构,改成svelte那样。
export function parse(template) {
const parser = new Parser(template)
return {
html: parser.html,
instance: parser.js[0],
css: null,
module: null
}
}
这样子parse返回的数据结构跟svelte就一样了,当然了组件中的css,module,还有其他的一大堆细节我们没有处理,不过我们不要心急毕竟要模仿Svelte也不是一天两天的事情。
下一章我们要根据生产的ast来构建组件了。