原文地址javascript
webpack loaders 从上手到理解系列
还有这些:css
vue-loader
是一个webpack
的loader
,它容许你以一种名为单文件组件的格式撰写Vue
组件。html
npm install vue-loader vue-template-compiler --save-dev
复制代码
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()
]
}
复制代码
Vue
组件一个标准的 Vue
组件能够分为三部分:vue
<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>
复制代码
打包完以后,这个 Vue
组件就会被解析到页面上:java
<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
文件里。node
简单来讲 vue-loader
的工做就是处理 Vue
组件,正确地解析各个部分。webpack
vue-loader
的源码较长,咱们分几个部分来解析。git
咱们先从入口看起,从上往下看:github
module.exports = function (source) {}
复制代码
vue-loader
接收一个 source
字符串,值是 vue
文件的内容。web
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&'
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
)。
后续还有 vue-loader
的第二篇文章,讲解 VueLoaderPlugin
的代码以及如何处理不一样 type
。