Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【看的见的思考】compiler-sfc #28

Open
cuixiaorui opened this issue Aug 26, 2021 · 1 comment
Open

【看的见的思考】compiler-sfc #28

cuixiaorui opened this issue Aug 26, 2021 · 1 comment

Comments

@cuixiaorui
Copy link
Owner

cuixiaorui commented Aug 26, 2021

sfc 这个模块是由 vue-loader 来调用的,目的是解析 SFC文件

主入口是 parse

接着来分析一下

一开始的时候还是基于 compiler.parse 去生成 ast 对象

  const ast = compiler.parse(source, {
    // there are no components at SFC parsing level
    isNativeTag: () => true,
    // preserve all whitespaces
    isPreTag: () => true,
    getTextMode: ({ tag, props }, parent) => {
      // all top level elements except <template> are parsed as raw text
      // containers
      if (
        (!parent && tag !== 'template') ||
        // <template lang="xxx"> should also be treated as raw text
        (tag === 'template' &&
          props.some(
            p =>
              p.type === NodeTypes.ATTRIBUTE &&
              p.name === 'lang' &&
              p.value &&
              p.value.content &&
              p.value.content !== 'html'
          ))
      ) {
        return TextModes.RAWTEXT
      } else {
        return TextModes.DATA
      }
    },
    onError: e => {
      errors.push(e)
    }
  })

接着遍历 ast 来处理 template、script、style

    switch (node.tag) {
      case 'template':
        if (!descriptor.template) {
          const templateBlock = (descriptor.template = createBlock(
            node,
            source,
            false
          ) as SFCTemplateBlock)
          templateBlock.ast = node

        break

是创建一个 block 然后存放到 descriptor.template 内

 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

也是创建一个 block 放到 descriptor.scriptSetup

      case 'style':
        const styleBlock = createBlock(node, source, pad) as SFCStyleBlock
       
        descriptor.styles.push(styleBlock)
        break

style 和上面不同的话,是可以有多个

最后是处理自定义的

        descriptor.customBlocks.push(createBlock(node, source, pad))

都不是的话,那么就是 custom类型的blocks

最后是返回了 result 对象

 const result = {
    descriptor,
    errors
  }
  sourceToSFC.set(sourceKey, result)
  return result

这个东西就需要和 vue-loader 结合去看了

接着我们从单元测试看

  test('nested templates', () => {
    const content = `
    <template v-if="ok">ok</template>
    <div><div></div></div>
    `
    const { descriptor } = parse(`<template>${content}</template>`)
    console.log(descriptor)

    expect(descriptor.template!.content).toBe(content)
  })

然后我们看看 descriptor 长什么样子

 {
      filename: 'anonymous.vue',
      source: '<template>\n' +
        '    <template v-if="ok">ok</template>\n' +
        '    <div><div></div></div>\n' +
        '    </template>',
      template: {
        type: 'template',
        content: '\n    <template v-if="ok">ok</template>\n    <div><div></div></div>\n    ',
        loc: {
          source: '\n    <template v-if="ok">ok</template>\n    <div><div></div></div>\n    ',
          start: [Object],
          end: [Object]
        },
        attrs: {},
        ast: {
          type: 1,
          ns: 0,
          tag: 'template',
          tagType: 0,
          props: [],
          isSelfClosing: false,
          children: [Array],
          loc: [Object],
          codegenNode: undefined
        },
        map: {
          version: 3,
          sources: [Array],
          names: [],
          mappings: ';IACI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC',
          file: 'anonymous.vue',
          sourceRoot: '',
          sourcesContent: [Array]
        }
      },
      script: null,
      scriptSetup: null,
      styles: [],
      customBlocks: [],
      cssVars: [],
      slotted: false
    }

compileTemplate

先看看是如何解析 template 的

先从单元测试看起

test('should work', () => {
  const source = `<div><p>{{ render }}</p></div>`

  const result = compile({ filename: 'example.vue', source })

  expect(result.errors.length).toBe(0)
  expect(result.source).toBe(source)
  // should expose render fn
  expect(result.code).toMatch(`export function render(`)
})

compile 就是调用的 compileTemplate

export function compileTemplate(
  options: SFCTemplateCompileOptions
): SFCTemplateCompileResults {
  const { preprocessLang, preprocessCustomRequire } = options

  const preprocessor = preprocessLang
    ? preprocessCustomRequire
      ? preprocessCustomRequire(preprocessLang)
      : require('consolidate')[preprocessLang as keyof typeof consolidate]
    : false
  if (preprocessor) {
    try {
      return doCompileTemplate({
        ...options,
        source: preprocess(options, preprocessor)
      })
    } catch (e) {
      return {
        code: `export default function render() {}`,
        source: options.source,
        tips: [],
        errors: [e]
      }
    }
  } else if (preprocessLang) {
    return {
      code: `export default function render() {}`,
      source: options.source,
      tips: [
        `Component ${options.filename} uses lang ${preprocessLang} for template. Please install the language preprocessor.`
      ],
      errors: [
        `Component ${options.filename} uses lang ${preprocessLang} for template, however it is not installed.`
      ]
    }
  } else {
    return doCompileTemplate(options)
  }
}

接着我们去把逻辑给拆分

  const { preprocessLang, preprocessCustomRequire } = options

是给用户做扩展用的接口,可以先忽略掉

接着是调用了 doCompileTemplate

else {
    return doCompileTemplate(options)
  }
function doCompileTemplate({
  filename,
  id,
  scoped,
  slotted,
  inMap,
  source,
  ssr = false,
  ssrCssVars,
  isProd = false,
  compiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM,
  compilerOptions = {},
  transformAssetUrls
}: SFCTemplateCompileOptions): SFCTemplateCompileResults {
  const errors: CompilerError[] = []
  const warnings: CompilerError[] = []

  let nodeTransforms: NodeTransform[] = []
  if (isObject(transformAssetUrls)) {
    const assetOptions = normalizeOptions(transformAssetUrls)
    nodeTransforms = [
      createAssetUrlTransformWithOptions(assetOptions),
      createSrcsetTransformWithOptions(assetOptions)
    ]
  } else if (transformAssetUrls !== false) {
    nodeTransforms = [transformAssetUrl, transformSrcset]
  }

  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
        ? genCssVarsFromList(ssrCssVars, shortId, isProd)
        : '',
    scopeId: scoped ? longId : undefined,
    slotted,
    ...compilerOptions,
    nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
    filename,
    sourceMap: true,
    onError: e => errors.push(e),
    onWarn: w => warnings.push(w)
  })

  // inMap should be the map produced by ./parse.ts which is a simple line-only
  // mapping. If it is present, we need to adjust the final map and errors to
  // reflect the original line numbers.
  if (inMap) {
    if (map) {
      map = mapLines(inMap, map)
    }
    if (errors.length) {
      patchErrors(errors, source, inMap)
    }
  }

  const tips = warnings.map(w => {
    let msg = w.message
    if (w.loc) {
      msg += `\n${generateCodeFrame(
        source,
        w.loc.start.offset,
        w.loc.end.offset
      )}`
    }
    return msg
  })

  return { code, ast, preamble, source, errors, tips, map }
}

继续拆分

  let nodeTransforms: NodeTransform[] = []
  if (isObject(transformAssetUrls)) {
    const assetOptions = normalizeOptions(transformAssetUrls)
    nodeTransforms = [
      createAssetUrlTransformWithOptions(assetOptions),
      createSrcsetTransformWithOptions(assetOptions)
    ]
  } else if (transformAssetUrls !== false) {
    nodeTransforms = [transformAssetUrl, transformSrcset]
  }

这里依然是给用户做扩展的

最终会影响 nodeTransforms 的值

也就是说 不同的 transformAssetUrls 会有不同的 transform

下面是核心代码

  let { code, ast, preamble, map } = compiler.compile(source, {
    mode: 'module',
    prefixIdentifiers: true,
    hoistStatic: true,
    cacheHandlers: true,
    ssrCssVars:
      ssr && ssrCssVars && ssrCssVars.length
        ? genCssVarsFromList(ssrCssVars, shortId, isProd)
        : '',
    scopeId: scoped ? longId : undefined,
    slotted,
    ...compilerOptions,
    nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
    filename,
    sourceMap: true,
    onError: e => errors.push(e),
    onWarn: w => warnings.push(w)
  })

这里的 compiler 是来自 compiler-dom ,执行 compile 开始编译

所以 template 就是收集一些特有的数据 ,然后给到 compiler.compile 进行编译,最后得到数据完事。


看看 template 如果是 pug 的话,是如何处理的

test('preprocess pug', () => {
  const template = parse(
    `
<template lang="pug">
body
  h1 Pug Examples
  div.container
    p Cool Pug example!
</template>
`,
    { filename: 'example.vue', sourceMap: true }
  ).descriptor.template as SFCTemplateBlock

  const result = compile({
    filename: 'example.vue',
    source: template.content,
    preprocessLang: template.lang
  })

  expect(result.errors.length).toBe(0)
})

如果template 是 pug 的话,那么 vue3 会调用 consolidate 这个库来处理解析

consolidate 是个template 大杂烩,做了一个中间层

那么换个角度来讲的话,只要是 consolidate 支持的template ,vue3 的template 就会支持

compileScript

接着看看如何处理 script 的

还是先从单元测试入手

  test('should expose top level declarations', () => {
    const { content, bindings } = compile(`
      <script setup>
      import { x } from './x'
      let a = 1
      const b = 2
      function c() {}
      class d {}
      </script>

      <script>
      import { xx } from './x'
      let aa = 1
      const bb = 2
      function cc() {}
      class dd {}
      </script>
      `)
    expect(content).toMatch('return { aa, bb, cc, dd, a, b, c, d, xx, x }')
    expect(bindings).toStrictEqual({
      x: BindingTypes.SETUP_MAYBE_REF,
      a: BindingTypes.SETUP_LET,
      b: BindingTypes.SETUP_CONST,
      c: BindingTypes.SETUP_CONST,
      d: BindingTypes.SETUP_CONST,
      xx: BindingTypes.SETUP_MAYBE_REF,
      aa: BindingTypes.SETUP_LET,
      bb: BindingTypes.SETUP_CONST,
      cc: BindingTypes.SETUP_CONST,
      dd: BindingTypes.SETUP_CONST
    })
    assertCode(content)
  })

这里的 compile 实际是调用了 compileSFCScript

这里最终实现的就是把 script 代码编译成可以让 runtime 执行的js代码

import { x } from './x'\n      \nexport default {\n  setup(__props, { expose }) {\n  expose()\n\n      let a = 1\n      const b = 2\n      function c() {}\n      class d {}\n      \nreturn { aa, bb, cc, dd, a, b, c, d, xx, x }\n}\n\n}\n      import { xx } from './x'\n      let aa = 1\n      const bb = 2\n      function cc() {}\n      class dd {}

接着看看调用的主流程

会调用compileScript,代码量很大,我们分拆这来看

export function compileScript(
  sfc: SFCDescriptor,
  options: SFCScriptCompileOptions
): SFCScriptBlock {
  const { descriptor } = parse(src)

先看看 input ,这里的 sfc 是通过 parse 过得到的对象,是已经把src也就是string代码编译成对象了

接着就是通过这个对象上面的数据信息来进行处理就可以了

在一开始的时候先收集数据,进行初始化

  const scopeId = options.id ? options.id.replace(/^data-v-/, '') : ''
  const cssVars = sfc.cssVars
  const scriptLang = script && script.lang
  const scriptSetupLang = scriptSetup && scriptSetup.lang
  const isTS =
    scriptLang === 'ts' ||
    scriptLang === 'tsx' ||
    scriptSetupLang === 'ts' ||
    scriptSetupLang === 'tsx'
  const plugins: ParserPlugin[] = [...babelParserDefaultPlugins]
  if (!isTS || scriptLang === 'tsx' || scriptSetupLang === 'tsx') {
    plugins.push('jsx')
  }
  if (options.babelParserPlugins) plugins.push(...options.babelParserPlugins)
  if (isTS) plugins.push('typescript', 'decorators-legacy')

然后先处理的是 普通的 scirpt

处理普通的 script

    try {
      let content = script.content
      let map = script.map
      const scriptAst = _parse(content, {
        plugins,
        sourceType: 'module'
      }).program
      const bindings = analyzeScriptBindings(scriptAst.body)
      if (enableRefTransform && shouldTransformRef(content)) {
        const s = new MagicString(source)
        const startOffset = script.loc.start.offset
        const endOffset = script.loc.end.offset
        const { importedHelpers } = transformRefAST(scriptAst, s, startOffset)
        if (importedHelpers.length) {
          s.prepend(
            `import { ${importedHelpers
              .map(h => `${h} as _${h}`)
              .join(', ')} } from 'vue'\n`
          )
        }
        s.remove(0, startOffset)
        s.remove(endOffset, source.length)
        content = s.toString()
        map = s.generateMap({
          source: filename,
          hires: true,
          includeContent: true
        }) as unknown as RawSourceMap
      }
  1. 先解析 script里面的代码,搞成 ast 对象

  2. 这里是基于 babel 来解析的 JS 代码

  3. 基于 ast.body 生成bindings

  4. 看看有没有开启 enableRefTransform,开启的话处理

      if (cssVars.length) {
        content = rewriteDefault(content, `__default__`, plugins)
        content += genNormalScriptCssVarsCode(
          cssVars,
          bindings,
          scopeId,
          !!options.isProd
        )
        content += `\nexport default __default__`
      }
      return {
        ...script,
        content,
        map,
        bindings,
        scriptAst: scriptAst.body
      }
  1. 处理 cssVars

完事就直接返回了,可以看到普通的 script 的处理过程是比较简单的

处理 script setup

处理 setup 代码就多了去了

  // metadata that needs to be returned
  const bindingMetadata: BindingMetadata = {}
  const defaultTempVar = `__default__`
  const helperImports: Set<string> = new Set()
  const userImports: Record<string, ImportBinding> = Object.create(null)
  const userImportAlias: Record<string, string> = Object.create(null)
  const setupBindings: Record<string, BindingTypes> = Object.create(null)

  let defaultExport: Node | undefined
  let hasDefinePropsCall = false
  let hasDefineEmitCall = false
  let hasDefineExposeCall = false
  let propsRuntimeDecl: Node | undefined
  let propsRuntimeDefaults: Node | undefined
  let propsTypeDecl: TSTypeLiteral | TSInterfaceBody | undefined
  let propsTypeDeclRaw: Node | undefined
  let propsIdentifier: string | undefined
  let emitsRuntimeDecl: Node | undefined
  let emitsTypeDecl:
    | TSFunctionType
    | TSTypeLiteral
    | TSInterfaceBody
    | undefined
  let emitsTypeDeclRaw: Node | undefined
  let emitIdentifier: string | undefined
  let hasAwait = false
  let hasInlinedSsrRenderFn = false
  // props/emits declared via types

先初始化后面需要用到的数据


总结

目标:compiler-sfc 是把 sfc 里面的string生成对应的用 js 表达的组件代码

基本的逻辑是先解析 SFC 文件,生成对应的对象。这个对象里面包含了所有 SFC 的信息

然后在把 template script style 分别交给各自的编译器来编译成对象

template 就是用的compiler-dom 来解析的

script 用的是 babel 来解析的

style 暂时还不知道 TODO

然后基于ast 对象就可以对代码做手术了,比如获取到某些信息,然后基于这些信息生成对应的 js 代码

@LeeCong98
Copy link

This is a good article

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants