title | date | tags |
---|---|---|
vue-loader源码解析 |
2019-06-11 15:47:33 -0700 |
vue-loader作用:允许你以一种名为单文件组件(SFC)的格式撰写Vue组件
-
block(块):vue组件里包含
template
、script
、style
、custom blocks
这几部分,我们称之为“块”。 -
一个vue组件里可以包含多个
style
块、custom
块。 -
每个块都可以使用不同的loader来处理,比如:
<template lang="pug"></template>
<script type="text/vbscript"></script>
<style lang="scss"></style>
<style lang="less"></style>
<docs lang="xxx"></docs>
<foo></foo>
webpack里可以设置相应的loader来处理这些块,比如pug-plain-loader
、sass-loader
等。
- 支持函数式组件
<template functional>
<div>{{ props.foo }}</div>
</template>
vue-loader与webpack loader密切相关,我们首先看一下webpack loader的执行过程。
每个loader上都可以有一个.pitch
方法,loader的处理过程分为两个阶段,pitch阶段和normal执行阶段:
第一步先进行pitch阶段:会先按顺序执行每个loader的pitch方法;
第二步按相反顺序进行normal执行阶段
如果loader的pitch方法有返回值,则直接掉头往相反顺序执行。参考官网例子(pitching loader):
module.exports = {
//...
module: {
rules: [
{
//...
use: [
'a-loader',
'b-loader',
'c-loader'
]
}
]
}
};
上面例子中各个loader的执行顺序如下:
|- a-loader `pitch`
|- b-loader `pitch`
|- c-loader `pitch`
|- requested module is picked up as a dependency
|- c-loader normal execution
|- b-loader normal execution
|- a-loader normal execution
如果b-loader返回了内容,则执行顺序如下:
|- a-loader `pitch`
|- b-loader `pitch` returns a module
|- a-loader normal execution
图中黄色部分就是vue-loader所涉及的内容,也就是我们这篇文章要分析的。
接下来,我们将通过一个例子,来看vue-loader是怎么工作的(这个例子来自vue-loader/example/)。
// main.js
import Vue from 'vue'
import Foo from './source.vue'
new Vue({
el: '#app',
render: h => h(Foo)
})
// source.vue
<template lang="pug">
div(ok)
h1(:class="$style.red") hello
</template>
<script>
export default {
data () {
return {
msg: 'fesfff'
}
}
}
</script>
<style module>
.red {
color: red;
}
</style>
<foo>
export default comp => {
console.log(comp.options.data())
}
</foo>
// webpack.config.js
const path = require('path')
const VueLoaderPlugin = require('../lib/plugin')
module.exports = {
devtool: 'source-map',
mode: 'development',
entry: path.resolve(__dirname, './main.js'),
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: '/dist/'
},
devServer: {
stats: "minimal",
contentBase: __dirname,
writeToDisk: true,
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
resourceQuery: /blockType=foo/,
loader: 'babel-loader'
},
{
test: /\.pug$/,
oneOf: [
{
resourceQuery: /^\?vue/,
use: ['pug-plain-loader']
},
{
use: ['raw-loader', 'pug-plain-loader']
}
]
},
{
test: /\.css$/,
oneOf: [
{
resourceQuery: /module/,
use: [
'vue-style-loader',
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[local]_[hash:base64:8]'
}
}
]
},
{
use: [
'vue-style-loader',
'css-loader'
]
}
]
},
{
test: /\.scss$/,
use: [
'vue-style-loader',
'css-loader',
{
loader: 'sass-loader',
options: {
data: '$color: red;'
}
}
]
}
]
},
resolveLoader: {
alias: {
'vue-loader': require.resolve('../lib')
}
},
plugins: [
new VueLoaderPlugin()
]
}
对于上述例子,vue-loader的输出结果是:
// template
// 来自 import { render, staticRenderFns } from "./source.vue?vue&type=template&id=27e4e96e&lang=pug&" 的结果
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c(
"div",
{
attrs: {
ok: ""
}
},
[
_c(
"h1",
{
class: _vm.$style.red
},
[_vm._v("hello")
])
])
}
var staticRenderFns = []
render._withStripped = true
// script
// 来自 import script from "./source.vue?vue&type=script&lang=js&" 的结果
var script = {
data () {
return {
msg: 'fesfff'
}
}
}
// style
// 来自 import style0 from "./source.vue?vue&type=style&index=0&module=true&lang=css&" 的结果
.red {
color: red;
}
// 注入style 及 style热加载
var cssModules = {}
var disposed = false
function injectStyles (context) {
if (disposed) return
cssModules["$style"] = (style0.locals || style0)
Object.defineProperty(this, "$style", {
configurable: true,
get: function () {
return cssModules["$style"]
}
})
}
module.hot && module.hot.dispose(function (data) {
disposed = true
})
module.hot && module.hot.accept(["./source.vue?vue&type=style&index=0&module=true&lang=css&"], function () {
var oldLocals = cssModules["$style"]
if (oldLocals) {
var newLocals = require("./source.vue?vue&type=style&index=0&module=true&lang=css&")
if (JSON.stringify(newLocals) !== JSON.stringify(oldLocals)) {
cssModules["$style"] = newLocals
require("/Users/zhangxixi/knowledge collect/vue-loader/node_modules/_vue-hot-reload-api@2.3.3@vue-hot-reload-api/dist/index.js").rerender("27e4e96e")
}
}
})
// normalize component
import normalizer from "!../lib/runtime/componentNormalizer.js"
var component = normalizer(
script,
render,
staticRenderFns,
false,
injectStyles,
null,
null
)
// custom blocks
// 来自 import block0 from "./source.vue?vue&type=custom&index=0&blockType=foo" 的结果
var block0 = comp => {
console.log(comp.options.data())
}
if (typeof block0 === 'function') block0(component)
// hot reload
// script 和 template的热加载
if (module.hot) {
var api = require("/Users/zhangxixi/knowledge collect/vue-loader/node_modules/_vue-hot-reload-api@2.3.3@vue-hot-reload-api/dist/index.js")
api.install(require('vue'))
if (api.compatible) {
module.hot.accept()
if (!module.hot.data) {
api.createRecord('27e4e96e', component.options)
} else {
api.reload('27e4e96e', component.options)
}
module.hot.accept("./source.vue?vue&type=template&id=27e4e96e&lang=pug&", function () {
api.rerender('27e4e96e', {
render: render,
staticRenderFns: staticRenderFns
})
})
}
}
component.options.__file = "example/source.vue"
export default component.exports
首先看一下vue-loader源码结构:
vue-loader/lib/
│
├─── codegen/
│ ├─── customBlock.js/ 生成custom block的request
│ ├─── hotReload.js/ 生成热加载的代码
│ ├─── styleInjection.js/ 生成style的request
│ ├─── utils.js/ 工具函数
├─── loaders/ vue-loader内部定义的loaders
│ ├─── pitcher.js/ pitcher-loader,将所有的单文件组件里的block请求拦截并转成合适的请求
│ ├─── stylePostLoader.js/ style-post-loader, 处理scoped css的loader
│ ├─── templateLoader.js/ template-loader,编译 html 模板字符串,生成 render/staticRenderFns 函数
├─── runtime/
│ ├─── componentNormalizer.js/ 将组件标准化
├─── index.d.ts/
├─── index.js/ vue-loader的核心代码
├─── plugin.js/ vue-loader-plugin的核心代码
├─── select.js/ 根据不同query类型(script、template等)传递相应的content、map给下一个loader
在webpack开始执行后,会先合并webpack.config里的配置,接着实例化compiler,然后就去挨个执行所有plugin的apply方法。请看webpack这部分源码:
// webpack/lib/webpack.js
const Compiler = require("./Compiler")
const webpack = (options, callback) => {
...
options = new WebpackOptionsDefaulter().process(options) // 初始化 webpack 各配置参数
let compiler = new Compiler(options.context) // 初始化 compiler 对象,这里 options.context 为 process.cwd()
compiler.options = options // 往 compiler 添加初始化参数
new NodeEnvironmentPlugin().apply(compiler) // 往 compiler 添加 Node 环境相关方法
for (const plugin of options.plugins) {
plugin.apply(compiler);
}
...
}
从例子中看到,我们的webpack配置了vue-loader-plugin,也就是源码里的vue-loader/lib/plugin.js,这是vue-loader强依赖的,如果不配置vue-loader-plugin,就会抛出错误。根据上面wbepack执行过程,在执行vue-loader核心代码之前,会先经过vue-loader-plugin。那么它到底做了哪些事情?
// vue-loader/lib/plugin.js
class VueLoaderPlugin {
apply (compiler) {
// ...
// 事件注册(简化了源代码)
compiler.hooks.compilation.tap(id, compilation => {
let normalModuleLoader = compilation.hooks.normalModuleLoader
normalModuleLoader.tap(id, loaderContext => {
loaderContext[NS] = true
})
})
// ...
const rawRules = compiler.options.module.rules
const { rules } = new RuleSet(rawRules)
// ...
// 它的职责是将你定义过的其它规则复制并应用到 .vue 文件里相应语言的块。
// 例如,如果你有一条匹配 /\.js$/ 的规则,那么它会应用到 .vue 文件里的 <script> 块。
const clonedRules = rules
.filter(r => r !== vueRule)
.map(cloneRule)
// ...
// global pitcher (responsible for injecting template compiler loader & CSS
// post loader)
// 这个pitcher-loader的作用之一就是给template块添加template-loader,给style块添加style-post-loader,并分别导出一个新的js module request
const pitcher = {
loader: require.resolve('./loaders/pitcher'),
resourceQuery: query => {
const parsed = qs.parse(query.slice(1))
return parsed.vue != null
},
options: {
cacheDirectory: vueLoaderUse.options.cacheDirectory,
cacheIdentifier: vueLoaderUse.options.cacheIdentifier
}
}
// replace original rules
compiler.options.module.rules = [
pitcher,
...clonedRules,
...rules
]
}
}
function createMatcher (fakeFile) {/*...*/}
function cloneRule (rule) {/*...*/}
VueLoaderPlugin.NS = NS
module.exports = VueLoaderPlugin
从上面源码可以看出,vue-loader-plugin导出的是一个类,并且只包含了一个apply方法。这个apply方法就是被webpack调用的。
apply方法其实就做了3件事:
- 事件监听:在normalModuleLoader钩子执行前调用代码:loaderContext[NS] = true (每解析一个module,都会用到normalModuleLoader,由于每解析一个module都会有一个新的loaderContext,vue-loader/lib/index.js会判断loaderContext[NS]的值,为保证经过vue-loader执行时不报错,需要在这里标记loaderContext[NS] = true。)
说明:loader中的this是一个叫做loaderContext的对象,这是webpack提供的,是loader的上下文对象,里面包含loader可以访问的方法或属性。
- 将webpack中配置的rules利用webpack的new RuleSet进行格式化(rules配置),并clone一份rules给.vue文件里的每个block使用。
rules = [{
resource: f (),
use: [{
loader: "vue-loader",
options: undefined
}]
}, {
resourceQuery: f (),
use: [{
loader: "babel-loader",
options: undefined
}]
}, {
resourceQuery: f (),
use: [{
loader: "babel-loader",
options: undefined
}]
}, {
resource: ƒ (),
oneOf: [{
resourceQuery: ƒ (),
use: [{
loader: "pug-plain-loader", options: undefined
}]
}, {
use: [{
loader: "raw-loader",
options: undefined
}, {
loader: "pug-plain-loader",
options: undefined
}]
}]
}]
- 在rules里加入vue-loader内部提供的rule(暂且称为pitcher-rule),其对应的loader是pitcher-loader,同时将原始的rules替换成pitcher-rule、cloneRules、rules。至于pitcher-rule、pitcher-loader做了什么,我们后面再讲。
放一张图总结下vue-loader-plugin的整体流程:
当webpack加载入口文件main.js时,依赖到了source.vue,webpack内部会匹配source.vue的loaders,发现是vue-loader,然后就会去执行vue-loader(vue-loader/lib/index.js)。接下来,我们分析vue-loader的实现过程。
// vue-loader/lib/index.js
module.exports = function (source) {
const loaderContext = this
// 会先判断是否加载了vue-loader-plugin,没有则报错
if (!errorEmitted && !loaderContext['thread-loader'] && !loaderContext[NS]) {
// 略
}
// 从loaderContext获取信息
const {
target, // 编译的目标,是从webpack配置中传递过来的,默认是'web',也可以是'node'等
request, // 请求的资源的路径(每个资源都有一个路径)
minimize, // 是否压缩:true/false,现在已废弃
sourceMap, // 是否生成sourceMap: true/false
rootContext, // 当前项目绝对路径,对本例子来说是:/Users/zhangxixi/knowledge collect/vue-loader
resourcePath, // 资源文件的绝对路径,对本例子来说是:/Users/zhangxixi/knowledge collect/vue-loader/example/source.vue
resourceQuery // 资源的 query 参数,也就是问号及后面的,如 ?vue&type=custom&index=0&blockType=foo
} = loaderContext
// 开始解析SFC,其实就是根据不同的 block 来拆解对应的内容
// parse函数返回的是compiler.parseComponent()的结果
// 如果没有自定义compiler,compiler对应的就是vue-template-compiler。
const descriptor = parse({
source,
compiler: options.compiler || loadTemplateCompiler(loaderContext), // 如果loader的options没有配置compiler, 则使用vue-template-compiler
filename,
sourceRoot,
needMap: sourceMap
})
// 如果是语言块,则直接返回
if (incomingQuery.type) {
return selectBlock(
descriptor,
loaderContext,
incomingQuery,
!!options.appendExtension
)
}
// 接下来分别对不同block的请求进行处理
// template
// 处理template,根据descriptor.template,生成template的js module(生成import语句)
/* 生成的template请求
import { render, staticRenderFns } from "./source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&"
*/
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` : ``
// 把attrs转成query格式:{lang: pug} => &lang=pug
const attrsQuery = attrsToQuery(descriptor.template.attrs)
// 如果css有scope,那么template就需要加上scoped=true,这是why??
const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${inheritQuery}`
const request = templateRequest = stringifyRequest(src + query)
// 这个request会经过pug-plain-loader、template-loader
// 最终template-loader会返回render, staticRenderFns这两个函数
templateImport = `import { render, staticRenderFns } from ${request}`
}
// script
// 处理script,与template类似
/* 生成的script请求:
import script from "./source.vue?vue&type=script&lang=js&"
export * from "./source.vue?vue&type=script&lang=js&"
*/
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)
/* script不会再经过其他loader处理,所以从request里import的script就是对应的源码,如
{
data () {
return {
msg: 'fesfff'
}
}
}
*/
scriptImport = (
`import script from ${request}\n` +
`export * from ${request}` // support named exports
)
}
// styles
// 处理styles
/*
genStylesCode做了3件事情:
1. 生成import语句(这一步与template生成import语句类似)
2. 如果需要热加载,添加热加载代码
3.如果需要注入样式,则添加样式注入函数injectStyles
*/
/* 生成的style请求:
import style0 from "./source.vue?vue&type=style&index=0&id=27e4e96e&scoped=true&lang=css&"
*/
let stylesCode = ``
if (descriptor.styles.length) {
stylesCode = genStylesCode(
loaderContext,
descriptor.styles, // vue单文件组件支持多个style标签,故descriptor.styles是数组
id,
resourcePath,
stringifyRequest,
needsHotReload,
isServer || isShadow // needs explicit injection?
)
}
// 将由 .vue 提供 render函数/staticRenderFns,js script,style样式,并交由 normalizer 进行统一的格式化,最终导出 component.exports
// 如果stylesCode里含有injectStyles,则表明是需要注入style的,因此可以使用这个正则来判断:/injectStyles/.test(stylesCode)
let code = `
${templateImport}
${scriptImport}
${stylesCode}
/* normalize component */
import normalizer from ${stringifyRequest(`!${componentNormalizerPath}`)}
var component = normalizer(
script,
render,
staticRenderFns,
${hasFunctional ? `true` : `false`},
${/injectStyles/.test(stylesCode) ? `injectStyles` : `null`},
${hasScoped ? JSON.stringify(id) : `null`},
${isServer ? JSON.stringify(hash(request)) : `null`}
${isShadow ? `,true` : ``}
)
`.trim() + `\n`
if (descriptor.customBlocks && descriptor.customBlocks.length) {
code += genCustomBlocksCode(
descriptor.customBlocks,
resourcePath,
resourceQuery,
stringifyRequest
)
}
if (needsHotReload) {
code += `\n` + genHotReloadCode(id, hasFunctional, templateRequest)
}
// Expose filename. This is used by the devtools and Vue runtime warnings.
if (!isProduction) {
// Expose the file's full path in development, so that it can be opened
// from the devtools.
code += `\ncomponent.options.__file = ${JSON.stringify(rawShortFilePath.replace(/\\/g, '/'))}`
} else if (options.exposeFilename) {
// Libraies can opt-in to expose their components' filenames in production builds.
// For security reasons, only expose the file's basename in production.
code += `\ncomponent.options.__file = ${JSON.stringify(filename)}`
}
code += `\nexport default component.exports`
// console.log(code)
return code
}
module.exports.VueLoaderPlugin = plugin
vue-loader整体流程图:
可以看出,整个过程大体可以分为3个阶段。
这一阶段是将.vue文件解析成js代码。
-
会先判断是否加载了vue-loader-plugin,没有则报错
if (!errorEmitted && !loaderContext['thread-loader'] && !loaderContext[NS]) { // 略 }
-
从loaderContext中获取到模块的信息,比如request、resourcePath、resourceQuery等
const { target, // 编译的目标,是从webpack配置中传递过来的,默认是'web',也可以是'node'等 request, // 请求的资源的路径(每个资源都有一个路径) minimize, // 是否压缩:true/false,现在已废弃 sourceMap, // 是否生成sourceMap: true/false rootContext, // 当前项目绝对路径,对本例子来说是:/Users/zhangxixi/knowledge collect/vue-loader resourcePath, // 资源文件的绝对路径,对本例子来说是:/Users/zhangxixi/knowledge collect/vue-loader/example/source.vue resourceQuery // 资源的 query 参数,也就是问号及后面的,如 ?vue&type=custom&index=0&blockType=foo } = loaderContext
-
对.vue文件进行parse,其实就是把.vue分成template、script、style、customBlocks这几部分
const descriptor = parse({ source, compiler: options.compiler || loadTemplateCompiler(loaderContext), // 如果loader的options没有配置compiler, 则使用vue-template-compiler filename, sourceRoot, needMap: sourceMap })
这一阶段最关键的就是parse过程,parse前后对比如下:
// parse之前 source是: '<template lang="pug">\ndiv(ok)\n h1(:class="$style.red") hello\n</template>\n\n<script>\nexport default {\n data () {\n return {\n msg: \'fesfff\'\n }\n }\n}\n</script>\n\n<style scoped>\n.red {\n color: red;\n}\n</style>\n\n<foo>\nexport default comp => {\n console.log(comp.options.data())\n}\n</foo>\n' // parse之后 得到的结果 { template: { type: 'template', content: '\ndiv(ok)\n h1(:class="$style.red") hello\n', start: 21, attrs: { lang: 'pug' }, lang: 'pug', end: 62 }, script: { type: 'script', content: '//\n//\n//\n//\n//\n\nexport default {\n data () {\n return {\n msg: \'fesfff\'\n }\n }\n}\n', start: 83, attrs: {}, end: 158, map: { version: 3, sources: [Array], names: [], mappings: ';;;;;;AAMA;AACA;AACA;AACA;AACA;AACA;AACA', file: 'source.vue', sourceRoot: 'example', sourcesContent: [Array] } }, styles: [ { type: 'style', content: '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n.red {\n color: red;\n}\n', start: 183, attrs: [Object], scoped: true, end: 207, map: [Object] } ], customBlocks: [ { type: 'foo', content: '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nexport default comp => {\n console.log(comp.options.data())\n}\n', start: 222, attrs: {}, end: 285 } ], errors: [] }
-
然后区分.vue请求与block请求(请求source.vue就是.vue请求,source.vue里依赖了template、script等block,那么这些依赖会被解析成.source.vue?vue&type=template这种带query的,我们称之为block请求)。如果是.vue请求,则需要生成js module。否则就执行selectBlock。第一阶段是.vue请求,因此会生成js module:分别生成template、script、style、customBlock的请求路径(这里会在query上添加'vue',比如./source.vue?vue&type=script&lang=js,这会在第二阶段用到);添加热加载逻辑。
vue-loader第一阶段生成的js代码如下:
import { render, staticRenderFns } from "./source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&"
import script from "./source.vue?vue&type=script&lang=js&"
export * from "./source.vue?vue&type=script&lang=js&"
import style0 from "./source.vue?vue&type=style&index=0&id=27e4e96e&scoped=true&lang=css&"
import normalizer from "!../lib/runtime/componentNormalizer.js"
var component = normalizer(
script,
render,
staticRenderFns,
false,
null,
"27e4e96e",
null
)
import block0 from "./source.vue?vue&type=custom&index=0&blockType=foo"
if (typeof block0 === 'function') block0(component)
if (module.hot) {
var api = require("/Users/zhangxixi/knowledge collect/vue-loader/node_modules/_vue-hot-reload-api@2.3.3@vue-hot-reload-api/dist/index.js")
api.install(require('vue'))
if (api.compatible) {
module.hot.accept()
if (!module.hot.data) {
api.createRecord('27e4e96e', component.options)
} else {
api.reload('27e4e96e', component.options)
}
module.hot.accept("./source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&", function () {
api.rerender('27e4e96e', {
render: render,
staticRenderFns: staticRenderFns
})
})
}
}
component.options.__file = "example/source.vue"
export default component.exports
第一阶段返回的js代码交与webpack继续解析,代码里的这几个import请求就会被接着进行依赖解析,这样就会接着请求所依赖的template、script、style、customBlock。
我们以template的请求为例:
import { render, staticRenderFns } from "./source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&"
,webpack解析出这个module需要的loaders是:pitcher-loader、pug-plain-loader、vue-loader。需要哪些loader是webpack内部根据rules来匹配的,这里的rules是经过vue-loader-plugin处理后的。之所以能解析出pitcher-loader,是因为query里含有vue,此时,是时候回过头来看一下vue-loader-plugin中pitcher-rule和pitcher-loader的代码了。
// vue-loader/lib/plugin.js
// ...
// global pitcher (responsible for injecting template compiler loader & CSS
// post loader)
const pitcher = {
loader: require.resolve('./loaders/pitcher'),
resourceQuery: query => {
const parsed = qs.parse(query.slice(1))
return parsed.vue != null
},
options: {
cacheDirectory: vueLoaderUse.options.cacheDirectory,
cacheIdentifier: vueLoaderUse.options.cacheIdentifier
}
}
// ...
从上面代码可以看到,pitcher-rule是通过resourceQuery中是否有vue进行匹配的。从第一阶段返回的代码中可以看到,template、script、style、custom-block的请求query中都带有vue。所以这个rule就是匹配这几个block请求的。如果匹配到了,那么pitcher-loader就会加入到这些请求需要的loader数组中。pitcher-loader来自于vue-loader/lib/loaders/pitcher.js。那我们来看一下这个pitcher-loader到底做了什么:
// vue-loader/lib/loaders/pitcher.js
module.exports = code => code
module.exports.pitch = function (remainingRequest) {
// ...
const query = qs.parse(this.resourceQuery.slice(1))
let loaders = this.loaders
// if this is a language block request, eslint-loader may get matched
// multiple times
if (query.type) {
// if this is an inline block, since the whole file itself is being linted,
// remove eslint-loader to avoid duplicate linting.
if (/\.vue$/.test(this.resourcePath)) {
loaders = loaders.filter(l => !isESLintLoader(l))
} else {
// This is a src import. Just make sure there's not more than 1 instance
// of eslint present.
loaders = dedupeESLintLoader(loaders)
}
}
// remove self
loaders = loaders.filter(isPitcher)
// ...
// Inject style-post-loader before css-loader for scoped CSS and trimming
if (query.type === `style`) {
const cssLoaderIndex = loaders.findIndex(isCSSLoader)
if (cssLoaderIndex > -1) {
const afterLoaders = loaders.slice(0, cssLoaderIndex + 1)
const beforeLoaders = loaders.slice(cssLoaderIndex + 1)
const request = genRequest([
...afterLoaders,
stylePostLoaderPath,
...beforeLoaders
])
return `import mod from ${request}; export default mod; export * from ${request}`
}
}
// for templates: inject the template compiler & optional cache
if (query.type === `template`) {
const path = require('path')
const cacheLoader = cacheDirectory && cacheIdentifier
? [`cache-loader?${JSON.stringify({
// For some reason, webpack fails to generate consistent hash if we
// use absolute paths here, even though the path is only used in a
// comment. For now we have to ensure cacheDirectory is a relative path.
cacheDirectory: (path.isAbsolute(cacheDirectory)
? path.relative(process.cwd(), cacheDirectory)
: cacheDirectory).replace(/\\/g, '/'),
cacheIdentifier: hash(cacheIdentifier) + '-vue-loader-template'
})}`]
: []
const preLoaders = loaders.filter(isPreLoader)
const postLoaders = loaders.filter(isPostLoader)
const request = genRequest([
...cacheLoader,
...postLoaders,
templateLoaderPath + `??vue-loader-options`,
...preLoaders
])
// the template compiler uses esm exports
return `export * from ${request}`
}
// if a custom block has no other matching loader other than vue-loader itself
// or cache-loader, we should ignore it
if (query.type === `custom` && shouldIgnoreCustomBlock(loaders)) {
return ``
}
// When the user defines a rule that has only resourceQuery but no test,
// both that rule and the cloned rule will match, resulting in duplicated
// loaders. Therefore it is necessary to perform a dedupe here.
const request = genRequest(loaders)
return `import mod from ${request}; export default mod; export * from ${request}`
}
pitcher-loader做了三件事情,最关键的是第三件事情:
- 剔除eslint-loader
- 剔除pitcher-loader自身
- 根据不同的query进行拦截处理,返回对应的内容,跳过后面的loader执行部分
对于style的处理,先判断是否有css-loader,有的话就生成一个新的request,这个过程会将vue-loader内部的style-post-loader添加进去,然后返回一个js请求。根据pitch的规则,pitcher-loader后面的loader都会被跳过,然后就开始解析这个返回的js请求,它的的内容是:
import mod from "-!../node_modules/_vue-style-loader@4.1.2@vue-style-loader/index.js!../node_modules/_css-loader@1.0.1@css-loader/index.js!../lib/loaders/stylePostLoader.js!../lib/index.js??vue-loader-options!./source.vue?vue&type=style&index=0&id=27e4e96e&scoped=true&lang=css&";
export default mod; export * from "-!../node_modules/_vue-style-loader@4.1.2@vue-style-loader/index.js!../node_modules/_css-loader@1.0.1@css-loader/index.js!../lib/loaders/stylePostLoader.js!../lib/index.js??vue-loader-options!./source.vue?vue&type=style&index=0&id=27e4e96e&scoped=true&lang=css&"
对于template的处理类似,也会生成一个新的request,这个过程会将vue-loader内部提供的template-loader加进去,并返回一个js请求:
export * from "-!../lib/loaders/templateLoader.js??vue-loader-options!../node_modules/_pug-plain-loader@1.0.0@pug-plain-loader/index.js!../lib/index.js??vue-loader-options!./source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&"
其他block也是类似的。
经过第二阶段后,webpack会继续解析每个block对应的js请求。根据这些请求,webpack会匹配到相应的loaders。
对于style,对应的loader是vue-style-loader、css-loader、style-post-loader、vue-loader。执行顺序就是:
vue-style-loader的pitch、css-loader的pitch、style-post-loader的pitch、vue-loader的pitch、vue-loader(分离出style block)、style-post-loader(处理scoped css)、css-loader(处理相关资源的引入路径)、vue-style-loader(动态创建style标签插入css)。
对于template,对应的loader是template-loader、pug-plain-loader、vue-loader,执行顺序是:
template-loader的pitch、pug-plain-loader的pitch、vue-loader的pitch、vue-loader(分离出template block)、pug-plain-loader(将pug模板转化为html字符串)、template-loader(编译 html 模板字符串,生成 render/staticRenderFns 函数并暴露出去)。
其他模块类似。
会发现,在不考虑pitch函数的时候,第三阶段里最先执行的都是vue-loader,此时query是有值的,所以会进入到selecBlock阶段。(这就是vue-loader执行时与第一阶段不同的地方)
// vue-loader/lib/index.js
// ...
// 如果是语言块,则直接返回
if (incomingQuery.type) {
return selectBlock(
descriptor,
loaderContext,
incomingQuery,
!!options.appendExtension
)
}
// ...
selectBlock来自select.js,那么我们来看看select.js做了什么:
module.exports = function selectBlock (
descriptor,
loaderContext,
query,
appendExtension
) {
// template
if (query.type === `template`) {
if (appendExtension) {
loaderContext.resourcePath += '.' + (descriptor.template.lang || 'html')
}
loaderContext.callback(
null,
descriptor.template.content,
descriptor.template.map
)
return
}
// script
if (query.type === `script`) {
if (appendExtension) {
loaderContext.resourcePath += '.' + (descriptor.script.lang || 'js')
}
loaderContext.callback(
null,
descriptor.script.content,
descriptor.script.map
)
return
}
// styles
if (query.type === `style` && query.index != null) {
const style = descriptor.styles[query.index]
if (appendExtension) {
loaderContext.resourcePath += '.' + (style.lang || 'css')
}
loaderContext.callback(
null,
style.content,
style.map
)
return
}
// custom
if (query.type === 'custom' && query.index != null) {
const block = descriptor.customBlocks[query.index]
loaderContext.callback(
null,
block.content,
block.map
)
return
}
}
select.js其实就是根据不同的query类型,将相应的content和map传递给下一个loader。
最终生成的代码长什么样?
template最终解析代码:
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c(
"div",
{
attrs: {
ok: ""
}
},
[
_c(
"h1",
{
class: _vm.$style.red
},
[_vm._v("hello")
])
])
}
var staticRenderFns = []
render._withStripped = true
export { render, staticRenderFns }
style最终解析代码:
.red[data-v-27e4e96e] {
color: red;
}
vue-loader-plugin的cloneRules源码:
// vue-loader/lib/plugin.js
// ...
function cloneRule (rule) {
const { resource, resourceQuery } = rule
// Assuming `test` and `resourceQuery` tests are executed in series and
// synchronously (which is true based on RuleSet's implementation), we can
// save the current resource being matched from `test` so that we can access
// it in `resourceQuery`. This ensures when we use the normalized rule's
// resource check, include/exclude are matched correctly.
let currentResource
const res = Object.assign({}, rule, {
resource: {
test: resource => {
currentResource = resource
// 始终返回true,是为了能让ruleSet在执行时能够进入resourceQuery的判断规则
// 同时提供currentResource给resourceQuery使用
return true
}
},
resourceQuery: query => {
const parsed = qs.parse(query.slice(1))
// 如果query里没有vue,则说明不是.vue的block,不进行匹配
if (parsed.vue == null) {
return false
}
// .vue里给block匹配loader时需要通过lang来匹配。如果没有指定lang,也不进行匹配。
// 会发现,在为block生成request时,都会用到attrsToQuery,而style和script会给attrsToQuery分别传递'css', 'js'作为langFallback, customBlock和template则不需要传递
// 这是因为我们写代码时style和script可以不写lang,不写默认是css、js。这时候vue-loader需要把默认的加上。
// 而customBlock和template没有默认的lang,所以vue-loader不用提供默认的lang。
if (resource && parsed.lang == null) {
return false
}
// 这里需要在原资源路径后拼接一个假的后缀,如source.vue.css,这是为了执行resource时,能够通过资源名后缀匹配到loader
/* 比如,我们配置了一个规则是:
{
test: /\.css$/,
use: [
'vue-style-loader',
'css-loader'
]
}
经过new RuleSet后,会变成:
{
resource: (resource) => {
return /\.css$/.test(resource)
},
use: [
'vue-style-loader',
'css-loader'
]
}
resource是一个函数,此时利用拼接的fakeResourcePath,resource(fakeResourcePath)就可以匹配成功了
*/
const fakeResourcePath = `${currentResource}.${parsed.lang}`
if (resource && !resource(fakeResourcePath)) {
return false
}
if (resourceQuery && !resourceQuery(query)) {
return false
}
return true
}
})
if (rule.oneOf) {
res.oneOf = rule.oneOf.map(cloneRule)
}
return res
}
// ...
// replace original rules
compiler.options.module.rules = [
pitcher,
...clonedRules,
...rules
]
// ...
本文只是梳理了vue-loader的整体流程,具体源码细节请参考我写的源码注释