Svelte 如何编译Script标签

116 阅读4分钟

上一篇我们已经能够根据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)

输出如下

svelte-js.png

完整代码点击

解析标签内属性

解析属性的名字相对比较容易,我们只要在解析完标签名字之后尝试在空格,=和 >之前读取任意字符串,如果没有读取到,标签没有属性。

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>对于这个字符串,解析出如下结果

codered.png

因此我们就可以用它来解析属性值为{}内的内容,具体的代码如下

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)

输出如下

attribute.png

对比以前的变化可以点击查看

接下来我们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来构建组件了。