筆者系 vue-loader 貢獻者之一(#16)css
vue-loader 源碼解析系列之一,閱讀該文章以前,請你們首先參考大綱 vue-loader 源碼解析系列之 總體分析html
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
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
/** * 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
把 .vue 裏的 css, javaScript, html 抽離出來以後,存放到找個這個對象裏面npm
當前正在處理的節點的深度,比方說,對於 <template><div><p>foo</p></div></template>
來講,處理到 foo
時,當前深度就是 3, 處理到 </div>
時,當前深度就是 2 。數組
當前正在處理的節點,以及該節點的 attr 和 content 等信息。函數
遇到 openTag 節點時,對 openTag 的相關處理。邏輯不是很複雜,讀者能夠直接看源碼。有一點值得注意的是,style 是用 array 形式存儲的
遇到 closeTag 節點時,對 closeTag 的相關處理。
對當前節點的 attrs 的相關處理
這是和一個外部的函數,傳入了 content (其實也就是 .vue 的內容)以及由 start和 end 兩個函數組成的對象。看來,這個 parseHTML 之纔是分解分析 .vue 的關鍵
跟以前同樣,咱們要繼續深刻 parseHTML 函數來分析,它到底對 .vue 作了些什麼,源碼以下
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 循環中,存在兩個大的分支,一個用來分析 template ,一個是用來分析 script 和 style。
向前跳過文本
判斷當前的 node 是否是 openTag
處理 openTag, 這裏就用到了以前提到的 start() 函數
判斷當前的 node 是否是 closeTag,同時這裏也用到了 end() 函數
經過以上各個函數的組合,在while循環中就將 sfc 分割成了三個不一樣的部分,讀者能夠對比個人註釋和源碼自行解讀源碼邏輯。
順便在這裏吐個槽,很明顯這裏的 parseHTML 是函數名是有問題的,parseHTML 應該叫作 parseSFC 比較合適。