什么是 vue-loader?
vue-loader
是一個webpack
的loader
,它允許你以一種名為單文件組件的格式撰寫Vue
組件。
如何使用?
1. 安裝
npm install vue-loader vue-template-compiler --save-dev
2. 配置 webapck
// webpack.config.js
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
mode: 'development',
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
// 它會應用到普通的 `.js` 文件
// 以及 `.vue` 文件中的 `<script>` 塊
{
test: /\.js$/,
loader: 'babel-loader'
},
// 它會應用到普通的 `.css` 文件
// 以及 `.vue` 文件中的 `<style>` 塊
{
test: /\.css$/,
use: [
'vue-style-loader',
'css-loader'
]
}
]
},
plugins: [
// 請確保引入這個插件來施展魔法
new VueLoaderPlugin()
]
}
3. 創建一個 .vue 組件
一個標准的 .vue
組件可以分為三部分:
-
template: 模板
-
script: 腳本
-
stype: 樣式
<template>
<div id="app">
<div class="title">{{msg}}</div>
</div>
</template>
<script>
export default {
name: 'app',
data() {
return {
msg: 'Hello world',
};
},
}
</script>
<style lang="scss">
#app {
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
.title {
color: red;
}
</style>
4. 見證奇跡的時刻
打包完之后,這個 Vue
組件就會被解析到頁面上:
<head>
<style type="text/css">
#app {
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
.title {
color: red;
}
</style>
</head>
<body>
<div id="app">
<div class="title">Hello world</div>
</div>
<script type="text/javascript" src="/app.js"></script>
</body>
上面 Vue
組件里的 <template>
部分解析到 <body>
下,css
部分解析成<style>
標簽,<script>
部分則解析到 js
文件里。
簡單來說 vue-loader
的工作就是處理 Vue
組件,正確地解析各個部分。
vue-loader
的源碼較長,我們分幾個部分來解析。
源碼解析之主要流程
我們先從入口看起,從上往下看:
module.exports = function (source) {}
vue-loader
接收一個 source
字符串,值是 vue
文件的內容。
const stringifyRequest = r => loaderUtils.stringifyRequest(loaderContext, r)
loaderUtils.stringifyRequest
作用是將絕對路徑轉換成相對路徑。
接下來有一大串的聲明語句,我們暫且先不看,我們先看最簡單的情況。
const { parse } = require('@vue/component-compiler-utils')
const descriptor = parse({
source,
compiler: options.compiler || loadTemplateCompiler(loaderContext),
filename,
sourceRoot,
needMap: sourceMap
})
parse
方法是來自於 component-compiler-utils
,代碼簡略一下是這樣:
// component-compiler-utils parse
function parse(options) {
const { source, filename = '', compiler, compilerParseOptions = { pad: 'line' }, sourceRoot = '', needMap = true } = options;
// ...
output = compiler.parseComponent(source, compilerParseOptions);
// ...
return output;
}
可以看到,這里還不是真正 parse
的地方,實際上是調用了compiler.parseComponent
方法,默認情況下 compiler
指的是 vue-template-compiler
。
// vue-template-compiler parseComponent
function parseComponent (
content,
options
) {
var sfc = {
template: null,
script: null,
styles: [],
customBlocks: [],
errors: []
};
// ...
function start() {}
function end() {}
parseHTML(content, {
warn: warn,
start: start,
end: end,
outputSourceRange: options.outputSourceRange
});
return sfc;
}
這里可以看到,parseComponent
應該是調用了 parseHTML
方法,並且傳入了兩個方法:start
和 end
,最終返回 sfc
。
這一塊的源碼我們不多說,我們可以猜測 start
和 end
這兩個方法應該是會根據不同的規則去修改 sfc
,我們看一下 sfc
即 vue-loader
中 descriptor
是怎么樣的:
// vue-loader descriptor
{
customBlocks: [],
errors: [],
template: {
attrs: {},
content: "\n<div id="app">\n <div class="title">{{msg}}</div>\n</div>\n",
type: "template"
},
script: {
attrs: {},
content: "... export default {} ...",
type: "script"
},
style: [{
attrs: {
lang: "scss"
},
content: "... #app {} ...",
type: "style",
lang: "scss"
}],
}
vue
文件里的內容已經分別解析到對應的 type
去了,接下來是不是只要分別處理各個部分即可。
parseHTML
這個命名是不是有點問題。。。
vue-loader 如何處理不同 type
你們可以先思考五分鍾,這里的分別處理是如何處理的?比如,樣式內容需要通過style-loader
才能將其放到 DOM
里。
好了,就當作聰明的你已經有思路了。我們繼續往下看。
// template
let templateImport = `var render, staticRenderFns`
let templateRequest
if (descriptor.template) {
const src = descriptor.template.src || resourcePath
const idQuery = `&id=${id}`
const scopedQuery = hasScoped ? `&scoped=true` : ``
const attrsQuery = attrsToQuery(descriptor.template.attrs)
const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${inheritQuery}`
const request = templateRequest = stringifyRequest(src + query)
templateImport = `import { render, staticRenderFns } from ${request}`
}
// script
let scriptImport = `var script = {}`
if (descriptor.script) {
const src = descriptor.script.src || resourcePath
const attrsQuery = attrsToQuery(descriptor.script.attrs, 'js')
const query = `?vue&type=script${attrsQuery}${inheritQuery}`
const request = stringifyRequest(src + query)
scriptImport = (
`import script from ${request}\n` +
`export * from ${request}` // support named exports
)
}
// styles
let stylesCode = ``
if (descriptor.styles.length) {
stylesCode = genStylesCode(
loaderContext,
descriptor.styles,
id,
resourcePath,
stringifyRequest,
needsHotReload,
isServer || isShadow // needs explicit injection?
)
}
這三段代碼的結構很像,最終作用是針對不同的 type
分別構造一個 import
字符串:
templateImport = "import { render, staticRenderFns } from './App.vue?vue&type=template&id=7ba5bd90&'";
scriptImport = "import script from './App.vue?vue&type=script&lang=js&' \n export * from './App.vue?vue&type=script&lang=js&'";
stylesCode = "import style0 from './App.vue?vue&type=style&index=0&lang=scss&'";
這三個 import
語句有啥子用呢, vue-loader
是這樣做的:
let code = `
${templateImport}
${scriptImport}
${stylesCode}`.trim() + `\n`
code += `\nexport default component.exports`
return code
此時, code
是這樣的:
code = "
import { render, staticRenderFns } from './App.vue?vue&type=template&id=7ba5bd90&'
import script from './App.vue?vue&type=script&lang=js&'
export * from './App.vue?vue&type=script&lang=js&'
import style0 from './App.vue?vue&type=style&index=0&lang=scss&'
// 省略 ...
export default component.exports"
我們知道 loader
會導出一個可執行的 node
模塊,也就是說上面提到的 code
是會被 webpack
識別到然后執行的。
我們看到 code
里有三次的 import
,import
的文件都是 App.vue
,相當於又加載了一次觸發這次 vue-loader
的那個 vue
文件。不同的是,這次加載是帶參的,分別對應着 template
/ script
/ style
三種 type
的處理。
你們可以先思考五分鍾,這里的分別處理是如何處理的?
這個問題的答案就是,webpack
在加載 vue
文件時,會調用 vue-loader
來處理vue
文件,之后 return
一段可執行的 js
代碼,其中會根據不同 type
分別import
一次當前 vue
文件,並且將參數傳遞進去,這里的多次 import
也會被vue-loader
攔截,然后在 vue-loader
內部根據不同參數進行處理(比如調用style-loader
)。