事情的起源是被人问到,一个以.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 的作用(引用自官网)
<style>
的部分使用 Sass 和在 <template>
的部分使用 Pug;.vue
文件中使用自定义块,并对其运用自定义的 loader 链;<style>
和 <template>
中引用的资源当作模块依赖来处理;三、vue-loader的实现
先找到了vue-laoder在node_modules中的目录,由于源码中有很多对代码压缩、热重载之类的代码,我们定一个方向,看看一个.vue文件在运行时,是被vue-loader怎样处理的
既然vue-loader的核心首先是将以为.vue为结尾的组件进行分析、提取和转换,那么首先我们要找到以下几个loader
首先在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 [vue-loader] It seems you are using HappyPack
with
inline postcss ` +
`options
for
vue-loader. This is not supported because loaders running ` +
` ```in`
different threads cannot share non-serializable options. ` +``
`It is recommended to use a postcss config file instead.\n` +
`\n 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(``'!'``)
}
手机扫一扫
移动阅读更方便
你可能感兴趣的文章