初始化项目
通过vue-cli初始化你的项目:
bash
vue create vue-demo
此时项目是基于@vue/cli-service启动的,有关vue-loader等webpack的相关配置都在node_modules/@vue/cli-service/lib/config/base.js
中:
配置Debug Terminal
完成项目初始化后,点击如下按钮:
选择More Node.js options ,然后点击JavaScript Debug Terminal vue-demo:
然后你就得到了一个debug控制台:
断点调试
每次执行命令之前记得删除缓存,不然无法走到对应的逻辑:
第一阶段
在node_modules/vue-loader/lib/index.js
的第228行
打一个断点,获取vue-loader第一阶段处理的内容:
js
import {
render,
staticRenderFns
} from './App.vue?vue&type=template&id=7ba5bd90'
import script from './App.vue?vue&type=script&lang=js'
import style0 from './App.vue?vue&type=style&index=0&id=7ba5bd90&lang=css'
/* normalize component */
import normalizer from '!../node_modules/vue-loader/lib/runtime/componentNormalizer.js'
export * from './App.vue?vue&type=script&lang=js'
var component = normalizer(
script,
render,
staticRenderFns,
false,
null,
null,
null
) /* hot reload */
if (module.hot) {
var api = require('/Users/xxx/Desktop/playground/vue-demo/node_modules/vue-hot-reload-api/dist/index.js')
api.install(require('vue'))
if (api.compatible) {
module.hot.accept()
if (!api.isRecorded('7ba5bd90')) {
api.createRecord('7ba5bd90', component.options)
} else {
api.reload('7ba5bd90', component.options)
}
module.hot.accept('./App.vue?vue&type=template&id=7ba5bd90', function () {
api.rerender('7ba5bd90', {
render: render,
staticRenderFns: staticRenderFns
})
})
}
}
component.options.__file = 'src/App.vue'
export default component.exports
第二阶段
由于第一阶段将vue原文件的template、script、style等block转换成了对应的import请求,因此webpack会对这些请求应用对应的loader进行处理。
在node_modules/vue-loader/lib/loaders/pitcher.js
的第222行
打一个断点,获取第二阶段的内容(这里以script为例):
js
import mod from "-!../node_modules/cache-loader/dist/cjs.js??ref--12-0!../node_modules/babel-loader/lib/index.js!../node_modules/cache-loader/dist/cjs.js??ref--0-0!../node_modules/vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=script&lang=js";
export default mod; export * from "-!../node_modules/cache-loader/dist/cjs.js??ref--12-0!../node_modules/babel-loader/lib/index.js!../node_modules/cache-loader/dist/cjs.js??ref--0-0!../node_modules/vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=script&lang=js"
关于pitch loader的相关原理后面会介绍,上述请求方式为webpack 内联请求,可以指定loader来处理特定的模块儿。
第三阶段
经过第二阶段处理后,因为在内联请求上指定了vue-loader
,此时会再次应用vue-loader
对vue源文件进行处理。由于query参数上存在type=template
等属性,此时会通过selectBlock
获取对应block的内容:
处理template
在node_modules/vue-loader/lib/select.js
的第21行
打断点,获取template
的原始内容:
html
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
接着调用loaderContext.callback
将上述内容传递给templateLoader
来处理,得到如下内容:
最终输出一个单独的模块儿,通过__webpack_require__
进行加载:
js
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */
__webpack_require__.d(__webpack_exports__, "render", function() {
return render;
});
/* harmony export (binding) */
__webpack_require__.d(__webpack_exports__, "staticRenderFns", function() {
return staticRenderFns;
});
var render = function render() {
var _vm = this
, _c = _vm._self._c;
return _c("div", {
attrs: {
id: "app"
}
}, [_c("img", {
attrs: {
alt: "Vue logo",
src: __webpack_require__(/*! ./assets/logo.png */
"./src/assets/logo.png")
}
}), _c("HelloWorld", {
attrs: {
msg: "Welcome to Your Vue.js App"
}
})], 1);
};
var staticRenderFns = [];
render._withStripped = true;
处理script
在第31行
打断点,获取script
的原始内容:
js
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
},
mounted() {
const a = 1
console.log(a)
}
}
接着调用loaderContext.callback
将上述内容传递给babel-loader
来处理,得到如下内容:
如果babel-loader没有对const进行转换,注意配置好项目package.json中的browserslist
字段:
json
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie <= 11",
"Android >= 4.0",
"iOS >= 8"
]
最终输出一个单独的模块儿,通过__webpack_require__
进行加载:
js
__webpack_require__.r(__webpack_exports__);
/* harmony import */
var _components_HelloWorld_vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./components/HelloWorld.vue */
"./src/components/HelloWorld.vue");
/* harmony default export */
__webpack_exports__["default"] = ({
name: 'App',
components: {
HelloWorld: _components_HelloWorld_vue__WEBPACK_IMPORTED_MODULE_0__["default"]
},
mounted: function mounted() {
var a = 1;
console.log(a);
}
});
处理style
在第41行
打断点,获取style
的原始内容:
css
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
接着调用loaderContext.callback
将上述内容传递给stylePostLoader
来处理:
上图的代码为HelloWorld.vue
的<style scoped>
转换后的代码。可以看到因为有scoped
标识,处理后的代码被加上了hash。最终会通过style-loader将css代码通过style标签插入到dom中。
输出最终vue模块儿
最后将所有的内容转换成对应的模块儿进行输出:
js
__webpack_require__.r(__webpack_exports__);
/* harmony import */
var _App_vue_vue_type_template_id_7ba5bd90__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./App.vue?vue&type=template&id=7ba5bd90 */
"./src/App.vue?vue&type=template&id=7ba5bd90");
/* harmony import */
var _App_vue_vue_type_script_lang_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./App.vue?vue&type=script&lang=js */
"./src/App.vue?vue&type=script&lang=js");
/* empty/unused harmony star reexport */
/* harmony import */
var _App_vue_vue_type_style_index_0_id_7ba5bd90_lang_css__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./App.vue?vue&type=style&index=0&id=7ba5bd90&lang=css */
"./src/App.vue?vue&type=style&index=0&id=7ba5bd90&lang=css");
/* harmony import */
var _node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../node_modules/vue-loader/lib/runtime/componentNormalizer.js */
"./node_modules/vue-loader/lib/runtime/componentNormalizer.js");
/* normalize component */
var component = Object(_node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_3__["default"])(_App_vue_vue_type_script_lang_js__WEBPACK_IMPORTED_MODULE_1__["default"], _App_vue_vue_type_template_id_7ba5bd90__WEBPACK_IMPORTED_MODULE_0__["render"], _App_vue_vue_type_template_id_7ba5bd90__WEBPACK_IMPORTED_MODULE_0__["staticRenderFns"], false, null, null, null
)
/* hot reload */
if (true) {
var api = __webpack_require__(/*! ./node_modules/vue-hot-reload-api/dist/index.js */
"./node_modules/vue-hot-reload-api/dist/index.js")
api.install(__webpack_require__(/*! vue */
"./node_modules/vue/dist/vue.runtime.esm.js"))
if (api.compatible) {
module.hot.accept()
if (!api.isRecorded('7ba5bd90')) {
api.createRecord('7ba5bd90', component.options)
} else {
api.reload('7ba5bd90', component.options)
}
module.hot.accept(/*! ./App.vue?vue&type=template&id=7ba5bd90 */
"./src/App.vue?vue&type=template&id=7ba5bd90", function(__WEBPACK_OUTDATED_DEPENDENCIES__) {
/* harmony import */
_App_vue_vue_type_template_id_7ba5bd90__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./App.vue?vue&type=template&id=7ba5bd90 */
"./src/App.vue?vue&type=template&id=7ba5bd90");
(function() {
api.rerender('7ba5bd90', {
render: _App_vue_vue_type_template_id_7ba5bd90__WEBPACK_IMPORTED_MODULE_0__["render"],
staticRenderFns: _App_vue_vue_type_template_id_7ba5bd90__WEBPACK_IMPORTED_MODULE_0__["staticRenderFns"]
})
}
)(__WEBPACK_OUTDATED_DEPENDENCIES__);
}
.bind(this))
}
}
component.options.__file = "src/App.vue"
/* harmony default export */
__webpack_exports__["default"] = (component.exports);
如果对上述输出内容还是不理解,可以启动项目,然后打开控制台查看如下内容(development环境):
其中,第一个App.vue会请求下面三个App.vue?vue&xxx
模块儿,分别对应template、script、style,然后在这三个模块儿里又会请求对应node_modules下的cache-loader/dist
或者vue-style-loader/index.js
中的内容。
这里的webpack-internal实际上是将vue源文件处理后得到多个模块儿,他是保存在内存里的,最终还是会经过webpack处理成一个立即执行函数,通过eval的方式加载这些单独的block代码。这些模块儿通过map的形式保存为module graph,如:
js
{
"./src/App.vue": (function(module, __webpack_exports__, __webpack_require__) {"use strict";eval("__webpack_require__.r(__webpack_exports__);xxx")}),
"./src/App.vue?vue&type=script&lang=js": (function(module, __webpack_exports__, __webpack_require__) {"use strict";eval("__webpack_require__.r(__webpack_exports__);xxx")}),
"./src/App.vue?vue&type=style&index=0&id=7ba5bd90&lang=css": (function(module, __webpack_exports__, __webpack_require__) {"use strict";eval("__webpack_require__.r(__webpack_exports__);xxx")}),
"./src/App.vue?vue&type=template&id=7ba5bd90": (function(module, __webpack_exports__, __webpack_require__) {"use strict";eval("__webpack_require__.r(__webpack_exports__);xxx")})
}
具体见app.js
:
VueLoaderPlugin
在执行上述三步逻辑之前,实际上还需要对plugin、pitcher进行初始化。
初始化plugin
在执行模块儿转换之前(loader处理),vue-loader
会在webpack中注册一个插件,插件的代码位于plugin.js
中,具体逻辑如下:
js
const { testWebpack5 } = require('./codegen/utils')
const NS = 'vue-loader'
class VueLoaderPlugin {
apply(compiler) {
let Ctor = null
if (testWebpack5(compiler)) {
// webpack5 and upper
Ctor = require('./plugin-webpack5')
} else {
// webpack4 and lower
Ctor = require('./plugin-webpack4')
}
new Ctor().apply(compiler)
}
}
VueLoaderPlugin.NS = NS
module.exports = VueLoaderPlugin
我这个项目是基于webpack4的,因此在plugin-webpack4.js
的这个位置打一个断点:
拷贝loader
在Debug Terminal
执行跑一下项目:
bash
npm run serve
在这儿可以看到我们配置的所有的针对不同文件后缀的loader
:
下一个断点我们打在第75行
,并进入cloneRule
逻辑:
这里的逻辑是拷贝所有的loader
,并根据import
请求上的lang
,过滤出对应的loader进行处理。之所以要这样做,是因为原有的loader无法处理.vue
文件,我们拷贝一份然后根据lang
进行处理就能够对文件进行正常处理了。
在cloneRule
函数中,vue-loader
对拷贝的loader
进行了放行(默认返回true ),让所有配置的loader
都能处理.vue
文件:
js
resource: {
test: (resource) => {
currentResource = resource
return true
}
}
第二步,根据resourceQuery,过滤掉当前import
请求需要匹配的loader
:
js
const fakeResourcePath = `${currentResource}.${parsed.lang}`
if (resource && !resource(fakeResourcePath)) {
return false
}
这一步就和我们写的.rule('images').test(/\.(png|jpe?g|gif|webp)(\?.*)?$/)
一样,根据文件后缀匹配loader
。
配置pitcher
关于pitch loader
是什么,可以看你不知道的「pitch loader」应用场景。
下一行断点我们打到第116行
,可以看到vue-loader
将项目配置的rules
进行了重写,并且把pitcher
放到了最前面:
根据pitcher
的resourceQuery
,我们可以发现pitcher loader
仅针对?vue
类型的文件有效。
我们看看pitcher
的大致逻辑:
主要就是根据query
参数上不同的type
生成不同的内联方式的请求,从而使用对应的loader,如:
js
-!../../node_modules/cache-loader/dist/cjs.js??ref--12-0!../../node_modules/babel-loader/lib/index.js!../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./HelloWorld.vue?vue&type=script&lang=js
经过!
split之后,可以得到:
从下标1-5
,分别对应了vue-loader
、cache-loader
、babel-loader
、cache-loader
以及HelloWorld.vue
,其中loader得执行顺序是从后往前的。
总结
vue sfc文件实际上会经过三个阶段的处理:
- 将
template
、script
、style
转换成对应的import
请求。 - 通过
pitch loader
将所有来自vue模块儿的import
请求转换成webpack 内联请求。 - 根据内联请求上的
query参数
,选择项目中配置的loader
对当前block代码进行处理。