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 比较合适。
- vue-loader 源码解析之一 整体分析
- vue-loader 源码解析之三 style-compiler (写作中)
- vue-loader 源码解析之四 template-compiler (写作中)
作者微博
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。