vue编译原理(1):vue-loader浅析
Author:zhoulujun Date:
vue-loader将 .vue 文件变成 .bundle.js,然后放入浏览器运行。
vue-loader整体分析
vue-loader 将 basic.vue 编译到最终输出的 bundle.js 的过程中,其实调用了四个小的 loader。它们分别是:
selector
style-compiler
template-compiler
babel-loader
以上四个 loader ,除了 babel-loader 是外部的package,其他三个都存在于 vue-loader 的内部(lib/style-compiler 和 lib/template-compiler 和 lib/selector)。
vue-loader 将 A.vue 编译成以下内容
/* script 从做右到左,被先后被 selector 和 babel-loader 处理过了。 selector(参数type=script) */ import __vue_script__ from "!!babel-loader!../../lib/selector?type=script&index=0&bustCache!./A.vue" /* template 从左到右,先后被 selector 和 template-compiler 处理过了。 selector (参数type=template) 的处理结果是将 basic.vue 中的 template 抽出来之后交给 template-compiler 处理,最终输出成可用的 HTML。*/ import __vue_template__ from "!!../../lib/template-compiler/index?{\"id\":\"data-v-793be54c\",\"hasScoped\":false,\"buble\":{\"transforms\":{}}}!../../lib/selector?type=template&index=0&bustCache!./A.vue" /* styles style 涉及的 loader 较多,一个一个来分析, 从上代码可知,basic.vue 先后要被 selector, style-compiler, css-loader 以及 vue-style-loader 处理。 selector (参数type=style) 的处理结果是将 basic.vue 中的 css 抽出来之后交给 style-compiler 处理成 css, 然后交给 css-loader 处理生成 module, 最后通过 vue-style-loader 将 css 放在 <style> 里面,然后注入到 HTML 里。 注意,这里之所以没有用 style-loader 是因为 vue-style-loader 是在 fork 了 style-loader 的基础上,增加了后端绘制 (SSR) 的支持。具体的不同,读者可以查看官方文档,笔者这里不再累述。 */ import __vue_styles__ from "!!vue-style-loader!css-loader!../../lib/style-compiler/index?{\"vue\":true,\"id\":\"data-v-793be54c\",\"scoped\":false,\"hasInlineConfig\":false}!../../lib/selector?type=styles&index=0&bustCache!./A.vue" var Component = normalizeComponent( __vue_script__, __vue_template__, __vue_template_functional__, __vue_styles__, __vue_scopeId__, __vue_module_identifier__ )
在三个 import 语句中,不管它们用了多少个不同的 loader 去加载,loader chain 的源头都是 A.vue。
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 比较合适。
参考文章:
vue-loader 源码解析系列之 selector https://segmentfault.com/a/1190000012336392
vue-loader 源码解析系列之 整体分析 https://nicholaslee119.github.io/2017/11/24/vueLoader源码解析/
转载本站文章《vue编译原理(1):vue-loader浅析》,
请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/vue/8445.html