笔者系 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 比较合适。