资讯专栏INFORMATION COLUMN

vue-loader 源码解析系列之 selector

miqt / 2971人阅读

摘要:当前正在处理的节点,以及该节点的和等信息。源码解析之一整体分析源码解析之三写作中源码解析之四写作中作者博客作者作者微博

笔者系 vue-loader 贡献者之一(#16)

前言

vue-loader 源码解析系列之一,阅读该文章之前,请大家首先参考大纲 vue-loader 源码解析系列之 整体分析

selector 做了什么
const path = require("path")
const parse = require("./parser")
const loaderUtils = require("loader-utils")

module.exports = function (content) {
  // 略
  const query = loaderUtils.getOptions(this) || {}
  // 略
  const parts = parse(content, filename, this.sourceMap, sourceRoot, query.bustCache)
  let part = parts[query.type]
  // 略
  this.callback(null, part.content, part.map)
}

大家可以看到,selector的代码非常简单,
通过 parser 将 .vue 解析成对象 parts, 里面分别有 style, script, template。可以根据不同的 query, 返回对应的部分。
很明显那么这个 parser 完成了分析分解 .vue 的工作,那么让我们继续深入 parser

parser 做了什么
const compiler = require("vue-template-compiler")
const cache = require("lru-cache")(100)

module.exports = (content, filename, needMap, sourceRoot, bustCache) => {
  const cacheKey = hash(filename + content)
  // 略
  let output = cache.get(cacheKey)
  if (output) return output
  output = compiler.parseComponent(content, { pad: "line" })
  if (needMap) {
    // 略去了生成 sourceMap 的代码
  }
  cache.set(cacheKey, output)
  return output
}

同样的,为了方便读者理解主要流程,笔者去掉了部分代码。

从上面代码可以看到,.vue 解析的工作其实是交给了 compiler.parseComponent 去完成,那么我们需要继续深入 compiler。
注意,这里 vue-template-compiler 并不是 vue-loader 的一部分,从 vue-template-compiler 的 npm 主页可以了解到, vue-template-compiler 原来是 vue 本体的一部分
并不是一个多带带的 package。通过查看文档可知,compiler.parseComponent 的逻辑在 vue/src/sfc/parser.js 里。

源码如下

parseComponent 做了什么
/**
 * Parse a single-file component (*.vue) file into an SFC Descriptor Object.
 */
export function parseComponent (
  content: string,
  options?: Object = {}
 ): SFCDescriptor {
  const sfc: SFCDescriptor = {
    template: null,
    script: null,
    styles: [],
    customBlocks: []
  }
  let depth = 0
  let currentBlock: ?(SFCBlock | SFCCustomBlock) = null

  function start (
    tag: string,
    attrs: Array,
    unary: boolean,
    start: number,
    end: number
  ) {
    // 略
  }

  function checkAttrs (block: SFCBlock, attrs: Array) {
    // 略
  }

  function end (tag: string, start: number, end: number) {
    // 略
  }

  function padContent (block: SFCBlock | SFCCustomBlock, pad: true | "line" | "space") {
    // 略
  }

  parseHTML(content, {
    start,
    end
  })

  return sfc
}

parseComponent 里面有以下变量

处理对象 sfc

把 .vue 里的 css, javaScript, html 抽离出来之后,存放到找个这个对象里面

变量 depth

当前正在处理的节点的深度,比方说,对于 来说,处理到 foo 时,当前深度就是 3, 处理到

时,当前深度就是 2 。

currentBlock

当前正在处理的节点,以及该节点的 attr 和 content 等信息。

函数 start

遇到 openTag 节点时,对 openTag 的相关处理。逻辑不是很复杂,读者可以直接看源码。有一点值得注意的是,style 是用 array 形式存储的

函数 end

遇到 closeTag 节点时,对 closeTag 的相关处理。

函数 checkAttrs

对当前节点的 attrs 的相关处理

函数 parseHTML

这是和一个外部的函数,传入了 content (其实也就是 .vue 的内容)以及由 start和 end 两个函数组成的对象。看来,这个 parseHTML 之才是分解分析 .vue 的关键

跟之前一样,我们要继续深入 parseHTML 函数来分析,它到底对 .vue 做了些什么,源码如下

parseHTML 做了什么
export function parseHTML (html, options) {
  const stack = []
  const expectHTML = options.expectHTML
  const isUnaryTag = options.isUnaryTag || no
  const canBeLeftOpenTag = options.canBeLeftOpenTag || no
  let index = 0
  let last, lastTag
  while (html) {
    last = html
    if (!lastTag || !isPlainTextElement(lastTag)) {
      // 这里分离了template
    } else {
      // 这里分离了style/script
  }

  // 略

  // 前进n个字符
  function advance (n) {
    // 略
  }

  // 解析 openTag 比如