理解vue-loader
阅读原文时间:2023年07月16日阅读:2

事情的起源是被人问到,一个以.vue结尾的文件,是如何被编译然后运行在浏览器中的?突然发现,对这一块模糊的很,而且看mpvue的文档,甚至小程序之类的都是实现了自己的loader,所以十分必要抽时间去仔细读一读源码,顺便总结一番。

首先说结论:

一、vue-loader是什么

简单的说,他就是基于webpack的一个的loader,解析和转换 .vue 文件,提取出其中的逻辑代码 script、样式代码 style、以及 HTML 模版 template,再分别把它们交给对应的 Loader 去处理,核心的作用,就是提取,划重点。

至于什么是webpack的loader,其实就是用来打包、转译js或者css文件,简单的说就是把你写的代码转换成浏览器能识别的,还有一些打包、压缩的功能等。

这是一个.vue单文件的demo

vue文件式例 折叠源码

<template>

<div class=``"example"``>{{ msg }}</div>

</template>

<script>

export default {

data () {

return {

msg: 'Hello world!'

}

}

}

</script>

<style>

.example {

color: red;

}

</style>

二、 vue-loader 的作用(引用自官网)

  • 允许为 Vue 组件的每个部分使用其它的 webpack loader,例如在 <style> 的部分使用 Sass 和在 <template> 的部分使用 Pug;
  • 允许在一个 .vue 文件中使用自定义块,并对其运用自定义的 loader 链;
  • 使用 webpack loader 将 <style> 和 <template> 中引用的资源当作模块依赖来处理;
  • 为每个组件模拟出 scoped CSS;
  • 在开发过程中使用热重载来保持状态。

三、vue-loader的实现

先找到了vue-laoder在node_modules中的目录,由于源码中有很多对代码压缩、热重载之类的代码,我们定一个方向,看看一个.vue文件在运行时,是被vue-loader怎样处理的

既然vue-loader的核心首先是将以为.vue为结尾的组件进行分析、提取和转换,那么首先我们要找到以下几个loader

  • selector–将.vue文件解析拆分成一个parts对象,其中分别包含style、script、template
  • style-compiler–解析style部分
  • template-compiler 解析template部分
  • babel-loader-- 解析script部分,并转换为浏览器能识别的普通js

首先在loader.js这个总入口中,我们不关心其他的,先关心这几个加载的loader,从名字判断这事解析css、template的关键

3.1 首先是selector

selector 折叠源码

var path = require(``'path'``)

var parse = require(``'./parser'``)

var loaderUtils = require(``'loader-utils'``)

module.exports = function (content) {

this``.cacheable()

var query = loaderUtils.getOptions(``this``) || {}

var filename = path.basename(``this``.resourcePath)

// 将.vue文件解析为对象parts,parts包含style, script, template

var parts = parse(content, filename, this``.sourceMap)

var part = parts[query.type]

if (Array.isArray(part)) {

part = part[query.index]

}

this``.callback(``null``, part.content, part.map)

}

selector的最主要的功能就是拆分parts,这个parts是一个对象,用来盛放将.vue文件解析出的style、script、template等模块,他调用了方法parse。

parse.js部分

parse.js 折叠源码

var compiler = require(``'vue-template-compiler'``)

var cache = require(``'lru-cache'``)(100)

var hash = require(``'hash-sum'``)

var SourceMapGenerator = require(``'source-map'``).SourceMapGenerator

var splitRE = /\r?\n/g

var emptyRE = /^(?:\/\/)?\s*$/

module.exports = function (content, filename, needMap) {

// source-map cache busting for hot-reloadded modules

// 省略部分代码

var filenameWithHash = filename + '?' + cacheKey

var output = cache.get(cacheKey)

if (output) return output

output = compiler.parseComponent(content, { pad: 'line' })

if (needMap) {

}

cache.set(cacheKey, output)

return output

}

function generateSourceMap (filename, source, generated) {

// 生成sourcemap

return map.toJSON()

}

parse.js其实也没有真正解析.vue文件的代码,只是包含一些热重载以及生成sourceMap的代码,最主要的还是调用了compiler.parseComponent 这个方法,但是compiler并不是vue-loader的方法,而是调用vue框架的parse,这个文件在vue/src/sfc/parser.js中,一层层的揭开面纱终于找到了解析.vue文件的真正处理方法parseComponent。

vue的parse.js 折叠源码

/**

* 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

}

但是令人窒息的是parseHTML才是核心的方法,翻了一下文件,parseHTML是调用的vue源码中的compiler/parser/html-parser.js

 折叠源码

export function parseHTML (html, options) {

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)

}

}

}

}

这个parseHTML的主要组成部分就是解析传入的template标签,同时分离style和script

3.2 解析了template 接下来再看style样式部分的解析,在源码中调用的是style-compiler这个模块

style-compiler模块 折叠源码

var postcss = require(``'postcss'``)

module.exports = function (css, map) {

var query = loaderUtils.getOptions(``this``) || {}

var vueOptions = this``.options.__vueOptions__

if (!vueOptions) {

if (query.hasInlineConfig) {

this``.emitError(

`\n&nbsp; [vue-loader] It seems you are using HappyPack with inline postcss ` +

`options for vue-loader. This is not supported because loaders running ` +

` ```in`&nbsp;different threads cannot share non-serializable options. ` +``

`It is recommended to use a postcss config file instead.\n` +

`\n&nbsp; See http:````//vue-loader.vuejs.org/en/features/postcss.html#using-a-config-file for more details.\n`

)

}

vueOptions = Object.assign({}, this``.options.vue, this``.vue)

}

// use the same config loading interface as postcss-loader

loadPostcssConfig(vueOptions.postcss).then(config => {

var plugins = [trim].concat(config.plugins)

var options = Object.assign({

to: this``.resourcePath,

from: this``.resourcePath,

map: false

}, config.options)

// add plugin for vue-loader scoped css rewrite

if (query.scoped) {

plugins.push(scopeId({ id: query.id }))

}

// souceMap略

return postcss(plugins)

.process(css, options)

.then(``function (result) {

var map = result.map && result.map.toJSON()

cb(``null``, result.css, map)

return null // silence bluebird warning

})

}).``catch``(e => {

console.log(e)

cb(e)

})

}

简单的说,这一部分其实是调用了webpack原有的postcss这个loader,不过值得注意的是在vue中style标签scope的实现

实现的效果,在加了scope的style的文件中,为所设置的样式添加私有属性data,同时css中也加入单独的id,起到不同组件之间css私有的作用

这里调用了scopeId这个方法,是在postcss的基础上自定义的插件,调用postcss-selector-parser这个插件,在css转译后的选择器上生成特殊的id,从而起到隔离css的作用

vue-loader针对postcss的拓展 折叠源码

var postcss = require(``'postcss'``)

// 调用postcss-selector-parser 这个基于postcss的css选择器解析插件

var selectorParser = require(``'postcss-selector-parser'``)

module.exports = postcss.plugin(``'add-id'``, function (opts) {

return function (root) {

root.each(``function rewriteSelector (node) {

if (!node.selector) {

// handle media queries

if (node.type === 'atrule' && node.name === 'media'``) {

node.each(rewriteSelector)

}

return

}

node.selector = selectorParser(``function (selectors) {

selectors.each(``function (selector) {

var node = null

selector.each(``function (n) {

if (n.type !== 'pseudo'``) node = n

})

selector.insertAfter(node, selectorParser.attribute({

attribute: opts.id

}))

})

}).process(node.selector).result

})

}

})

同时在对应的组件标签上,添加自定义的data属性,在vue-loader下的loader.js中

而genId则是生成scopeId的方法,其中调用了基于npm的hash-sum插件,快速生成唯一的哈希值

 折叠源码

var path = require(``'path'``)

var hash = require(``'hash-sum'``) //此处引用了hash-sum插件

var cache = Object.create(``null``)

var sepRE = new RegExp(path.sep.replace(``'\\'``, '\\\\'``), 'g'``)

module.exports = function genId (file, context, key) {

var contextPath = context.split(path.sep)

var rootId = contextPath[contextPath.length - 1]

file = rootId + '/' + path.relative(context, file).replace(sepRE, '/'``) + (key || ''``)

return cache[file] || (cache[file] = hash(file))

}

而hash-sum生成唯一hash值的基本函数也比较有意思,通过charCodeAt 以及左移运算符产生新的值,最基本的一个fold函数贴到下边

hash-sum 折叠源码

function fold (hash, text) {

var i;

var chr;

var len;

if (text.length === 0) {

return hash;

}

for (i = 0, len = text.length; i < len; i++) {

chr = text.charCodeAt(i); // 调用了charCodeAt()这个方法转换为unicode编码

hash = ((hash << 5) - hash) + chr; // 左移运算符改变hash值

hash |= 0; // hash = hash | 0;

}

return hash < 0 ? hash * -2 : hash;

}

hash-sum还通过嵌套多层fold函数,以及pad、foldObject、foldValue等函数进一步混淆保证hash值的唯一不重复,感兴趣的可以翻看下hash-sum的源码。

3.3 script的处理

vue-loader对于script的处理则要简单一些,因为相对于自定义的程度,需要学习的v-指令,以及vue css中划分的scope,js反而是最通用的。

如果script标签有lang的标签,确保解析方式

根据属性lang的内容,加载使用对应的loader

 折叠源码

function ensureLoader (lang) {

return lang.split(``'!'``).map(``function (loader) {

return loader.replace(/^([\w-]+)(\?.*)?/, function (_, name, query) {

return (/-loader$/.test(name) ? name : (name + '-loader'``)) + (query || ''``)

})

}).join(``'!'``)

}