事情的起源是被人問到,一個以.vue結尾的文件,是如何被編譯而後運行在瀏覽器中的?忽然發現,對這一塊模糊的很,並且看mpvue的文檔,甚至小程序之類的都是實現了本身的loader,因此十分必要抽時間去仔細讀一讀源碼,順便總結一番。css
首先說結論:html
1、vue-loader是什麼vue
簡單的說,他就是基於webpack的一個的loader,解析和轉換 .vue 文件,提取出其中的邏輯代碼 script、樣式代碼 style、以及 HTML 模版 template,再分別把它們交給對應的 Loader 去處理,核心的做用,就是提取,劃重點。java
至於什麼是webpack的loader,其實就是用來打包、轉譯js或者css文件,簡單的說就是把你寫的代碼轉換成瀏覽器能識別的,還有一些打包、壓縮的功能等。node
這是一個.vue單文件的demo webpack
<template>
<div class=
"example"
>{{ msg }}</div>
</template>
<script>
export
default
{
data () {
return
{
msg:
'Hello world!'
}
}
}
</script>
<style>
.example {
color: red;
}
</style>
|
2、 vue-loader 的做用(引用自官網)web
- 容許爲 Vue 組件的每一個部分使用其它的 webpack loader,例如在
<style>
的部分使用 Sass 和在 <template>
的部分使用 Pug;
- 容許在一個
.vue
文件中使用自定義塊,並對其運用自定義的 loader 鏈;
- 使用 webpack loader 將
<style>
和 <template>
中引用的資源看成模塊依賴來處理;
- 爲每一個組件模擬出 scoped CSS;
- 在開發過程當中使用熱重載來保持狀態。
3、vue-loader的實現npm
先找到了vue-laoder在node_modules中的目錄,因爲源碼中有不少對代碼壓縮、熱重載之類的代碼,咱們定一個方向,看看一個.vue文件在運行時,是被vue-loader怎樣處理的小程序
既然vue-loader的核心首先是將覺得.vue爲結尾的組件進行分析、提取和轉換,那麼首先咱們要找到如下幾個loaderapi
- selector–將.vue文件解析拆分紅一個parts對象,其中分別包含style、script、template
- style-compiler–解析style部分
- template-compiler 解析template部分
- babel-loader-- 解析script部分,並轉換爲瀏覽器能識別的普通js
首先在loader.js這個總入口中,咱們不關心其餘的,先關心這幾個加載的loader,從名字判斷這事解析css、template的關鍵
3.1 首先是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)
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部分
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) {
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) {
return
map.toJSON()
}
|
parse.js其實也沒有真正解析.vue文件的代碼,只是包含一些熱重載以及生成sourceMap的代碼,最主要的仍是調用了compiler.parseComponent 這個方法,可是compiler並非vue-loader的方法,而是調用vue框架的parse,這個文件在vue/src/sfc/parser.js中,一層層的揭開面紗終於找到了解析.vue文件的真正處理方法parseComponent。
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)) {
}
else
{
}
function advance (n) {
}
function parseStartTag () {
}
function handleStartTag (match) {
if
(options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}
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這個模塊
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:
)
}
vueOptions = Object.assign({},
this
.options.vue,
this
.vue)
}
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)
if
(query.scoped) {
plugins.push(scopeId({ id: query.id }))
}
return
postcss(plugins)
.process(css, options)
.then(
function
(result) {
var
map = result.map && result.map.toJSON()
cb(
null
, result.css, map)
return
null
})
}).
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的做用
var
postcss = require(
'postcss'
)
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) {
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'
)
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函數貼到下邊
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);
hash = ((hash << 5) - hash) + chr;
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(
'!'
)
}
|