vue-loader 源碼解析系列之 selector

筆者系 vue-loader 貢獻者之一(#16)css

前言

vue-loader 源碼解析系列之一,閱讀該文章以前,請你們首先參考大綱 vue-loader 源碼解析系列之 總體分析html

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 的工做,那麼讓咱們繼續深刻 parservue

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
}

一樣的,爲了方便讀者理解主要流程,筆者去掉了部分代碼。java

從上面代碼能夠看到,.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 裏。node

源碼以下git

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<Attribute>,
    unary: boolean,
    start: number,
    end: number
  ) {
    // 略
  }

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

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

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

  parseHTML(content, {
    start,
    end
  })

  return sfc
}

parseComponent 裏面有如下變量github

  • 處理對象 sfc

    把 .vue 裏的 css, javaScript, html 抽離出來以後,存放到找個這個對象裏面npm

  • 變量 depth

    當前正在處理的節點的深度,比方說,對於 <template><div><p>foo</p></div></template>來講,處理到 foo 時,當前深度就是 3, 處理到 </div> 時,當前深度就是 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 好比 <template>
  function parseStartTag () {
    // 略
  }

  // 處理 openTag
  function handleStartTag (match) {
    // 略
    if (options.start) {
      options.start(tagName, attrs, unary, match.start, match.end)
    }
  }

  // 處理 closeTag
  function parseEndTag (tagName, start, end) {
    // 略
    if (options.start) {
      options.start(tagName, [], false, start, end)
    }
    if (options.end) {
      options.end(tagName, start, end)
    }
  }
}

深刻到這一步,我想再提醒一下讀者,selector的目的是將 .vue 中的 template, javaScript, css 分離出來。帶着這個目的意識,咱們再來審視這個 parseHTML。

parseHTML 整個函數的組成是:

  • 一個 while 循環

    在 while 循環中,存在兩個大的分支,一個用來分析 template ,一個是用來分析 script 和 style。

  • 函數 advance

    向前跳過文本

  • 函數 parseStartTag

    判斷當前的 node 是否是 openTag

  • 函數 handleStartTag

    處理 openTag, 這裏就用到了以前提到的 start() 函數

  • 函數 parseEndTag

    判斷當前的 node 是否是 closeTag,同時這裏也用到了 end() 函數

經過以上各個函數的組合,在while循環中就將 sfc 分割成了三個不一樣的部分,讀者能夠對比個人註釋和源碼自行解讀源碼邏輯。

順便在這裏吐個槽,很明顯這裏的 parseHTML 是函數名是有問題的,parseHTML 應該叫作 parseSFC 比較合適。

做者博客

做者github

做者微博

相關文章
相關標籤/搜索