笔者系 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
处理对象 sfcnpm
把 .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 作了些什么,源码以下
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 比较合适。