vue-loader 原始碼解析系列之 selector

再見尼克發表於2019-03-04

筆者系 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<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 裡面有以下變數

  • 處理物件 sfc

    把 .vue 裡的 css, javaScript, html 抽離出來之後,存放到找個這個物件裡面

  • 變數 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

作者微博

相關文章