「.vue文件的编译」1. vue-loader@15.8.3 的整体流程
通常我们会使用vue-cli
来创建一个vue
项目,由于vue-cli
对常见的开发需求进行了预先配置,做到了开箱即用。但是阻碍碍我们窥探其真面目脚步。当然官方也提供了手动配置的方案。参考
- 安装依赖,下面库的作用后面都会分析到。
npm install -D vue-loader vue-template-compiler
- webpack 配置,有loader有plugin
// webpack.config.js
const { VueLoaderPlugin } = require('vue-loader')
module.exports = {
module: {
rules: [
// ... 其它规则
{
test: /.vue$/,
loader: 'vue-loader'
}
]
},
plugins: [
// 请确保引入这个插件!
new VueLoaderPlugin()
]
}
手动配置方式的一个简易demo,来调试看看vue-loader做了哪些事情。
构建前后对比
这里相关的库的版本和我们当前分析的uniapp中用到的版本保持一致
// devDependecnies
"vue-loader": "15.8.3",
"vue-template-compiler": "2.6.11",
// dependecnies
"vue": "2.6.11"
demo
- App.vue:就是要分析vue文件是如何被构建的,当然需要一个vue文件啦
<template>
<div id="app">
{{ msg }}
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
msg: "hello vue + webpack",
};
}
};
</script>
<style>
#id {
background: red;
}
</style>
- main.js:应用入口的js文件,为了App.vue构建后是独立的文件(因为构建后小程序也是独立的文件 js,wxml,wxss等),通过异步引用进行代码分割。
import Vue from 'vue'
import ('./App.vue'/* webpackChunkName: 'App' */).then((App) => {
new Vue({
render: h => h(App.default),
}).$mount('#app')
})
- html,应用入口
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="./runtime~main.js"></script>
</head>
<body>
<div id="app"></div>
<script src="./main.js"></script>
</body>
</html>
- webpack.config.js
const path = require('path');
const CopyPlugin = require("copy-webpack-plugin");
const {VueLoaderPlugin} = require('vue-loader');
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
module.exports = {
mode: 'development',
entry: './src/main.js',
output: {
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /.vue$/, // 注意 $
loader: 'vue-loader'
},
{
test: /.css$/,
use: [MiniCssExtractPlugin.loader]
}
]
},
optimization: {
runtimeChunk: true
},
devtool: "cheap-source-map",
plugins: [
new CleanWebpackPlugin(),
new CopyPlugin([
{from: './src/index.html', to: '.'}
]),
new MiniCssExtractPlugin({
filename: "[name].css",
}),
new VueLoaderPlugin()
]
};
- optimizatin.runtimeChunk 是为了将运行时拆分(不要被你当前不需要关注的内容干扰啊)
- MiniCssExtractPlugin 是为了拆分css内容为单独文件
- 另外就是vue-loader的配置
产物
App.css
App.js
小结
App.vue
本身是三段式内容,分别是<template></template>
、<script>
、<style>
。
而当前的构建结果只有两个部分:App.vue
-> App.js
+ App.css
。
vue-loader的整体流程
进入webpack流程后,首先是注册插件,即调用插件的apply
方法,通过插件apply
方法中会拿到compiler
实例,然后通过compilation.hook.xxx
去监听自己关心的事件,从而参与构建流程,但是VueLoaderPlugin这里不是这么做的,而是重写了module.rules
。
下面看下VueLoaderPlugin的逻辑
VueLoaderPlugin
const id = 'vue-loader-plugin'
const NS = 'vue-loader'
class VueLoaderPlugin {
apply (compiler) {
// add NS marker so that the loader can detect and report missing plugin
// 第一步:找到 vue-loader,设置ident和options
// use webpack's RuleSet utility to normalize user rules
const rawRules = compiler.options.module.rules
const { rules } = new RuleSet(rawRules) // 会将用户提供的规则标准化
// find the rule that applies to vue files
let vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue`))
const vueRule = rules[vueRuleIndex]
const vueUse = vueRule.use
const vueLoaderUseIndex = vueUse.findIndex(u => {
return /^vue-loader|(/|\|@)vue-loader/.test(u.loader)
})
const vueLoaderUse = vueUse[vueLoaderUseIndex]
vueLoaderUse.ident = 'vue-loader-options'
vueLoaderUse.options = vueLoaderUse.options || {}
// 第二步:克隆除了vue-loader以外的其他规则
// for each user rule (expect the vue rule), create a cloned rule
// that targets the corresponding language blocks in *.vue files.
const clonedRules = rules.filter(r => r !== vueRule).map(cloneRule)
// 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
}
}
// replace original rules
compiler.options.module.rules = [
pitcher,
...clonedRules,
...rules
]
}
}
VueLoaderPlugin.NS = NS
module.exports = VueLoaderPlugin
核心步骤如下:
小结
这部分的成果如下:
- 第一个pitcher匹配
?vue...
路径 - 第二个规则,是我们上面webpack.config.js中针对css的那个规则的克隆,引用在
.vue
文件中相应块
中,这里是<style />
- 后面的两个还是用户提供的规则(被规范化过了,开始
new RuleSet()
会做这个事情)
下面看下App.vue
文件的构建过程吧,首先会经过pitching阶段,由于这里没有匹配的pitching-loader,会从本地路径中读取该文件的内容(传递给第一个normal-loader
哦,也就是我们下面vue-loader
的入参source
)进入normal阶段的执行,这里只有vue-loader
,下面分析下vue-loader
vue-loader
处理 App.vue
module.exports = function (source) {
const loaderContext = this;
const {
resourceQuery
} = loaderContext
const rawQuery = resourceQuery.slice(1)
const inheritQuery = `&${rawQuery}`
//...
const descriptor = parse({
source,
compiler: options.compiler || loadTemplateCompiler(loaderContext),
filename,
sourceRoot,
needMap: sourceMap
})
// if the query has a type field, this is a language block request
// e.g. foo.vue?type=template&id=xxxxx
// and we will return early
if (incomingQuery.type) {
// return selectBlock(...)
}
// template
let templateImport = `var render, staticRenderFns`
let templateRequest
if (descriptor.template) {
//...
}
// script
let scriptImport = `var script = {}`
if (descriptor.script) {
//...
}
// styles
let stylesCode = ``
if (descriptor.styles.length) {
//...
}
if (descriptor.customBlocks && descriptor.customBlocks.length) {
//...
}
//...
return code
}
这里主要分为两种场景:query
中是否有type
-
App.vue?type=template&id=xxxxx,
query
中有type
,有type
是则会走if(incomingQuery.type)
,并返回selectBlock(...)
; -
App.vue,没有query(或者
query
中没有type
),走后面的逻辑,清晰的看到后面的逻辑主要对descriptor
进行处理,那descriptor
是什么了,实际上就是.vue
文件中的内容按照块
划分,每个块都有自己的内容,以App.vue
为例,如下:看到仅仅是获取每个块的内容,并没有进入每个块并进行处理,看到这里主要有四个关键的属性:
scritp
、style
、template
、customBlock
,最常用的就是前三个。
分别将三个标签的内容转化为如下:
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=css&"
这里还会注入一些运行时相关的逻辑,最终经过这部分处理,返回了如下内容
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=css&"
/* normalize component */
import normalizer from "!../node_modules/vue-loader/lib/runtime/componentNormalizer.js"
var component = normalizer(
script,
render,
staticRenderFns,
false,
null,
null,
null
)
component.options.__file = "src/App.vue"
export default component.exports
// node_modules/webpack/lib/NormalModule.js
build(options, compilation, resolver, fs, callback) {
//...
this._source = null;
this._ast = null;
return this.doBuild(options, compilation, resolver, fs, err => {
//...
const result = this.parser.parse(this._ast || this._source.source(), { /*...*/ }, (err, result) => {/*...*/ });
});
}
经过parser.parse
处理完后,我们看下这部分有哪些依赖,这里会产生很多依赖,但大多数会被过滤掉(见webpack/lib/compilation.js
中的processModuleDependencies
方法),实际作为模块一步解析的是如下依赖(Dependency.request)
0: "./App.vue?vue&type=template&id=7ba5bd90&"
1: "./App.vue?vue&type=script&lang=js&"
2: "./App.vue?vue&type=style&index=0&lang=css&"
3: "!../node_modules/vue-loader/lib/runtime/componentNormalizer.js"
前面三个都会命中VueLoaderPlugin中创建的pitching-loader
,和用户自己提供的vue-loader
,先执行pitching-loader
再执行vue-loader
。
pitching-loader: pitcher
// node_modules/vue-loader/lib/loaders/pitcher.js
module.exports.pitch = function (remainingRequest) {
// 1. 获取当前模块的所有loaders,保存在loaderContext中,
// 并过滤掉自己(pitcher)和eslint-loader
// (因为整个vue文件应该是被eslint-loader处理了?),
// 那在当前案例中就只剩下vue-loader了
// 2. 将上一步过滤后的所有loaders,构造成内联loader形式,这里会区分type如script、template、style
}
内联loader,参考官方介绍inline loader,这里处理后的内联loader被添加了前缀-!
Prefixing with
-!
will disable all configured preLoaders and loaders but not postLoaders
以./App.vue?vue&type=template&...
为例,经过pitcher
处理后的内容如下
export * from "-!../node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options
!../node_modules/vue-loader/lib/index.js??vue-loader-options
!./App.vue?vue&type=template&id=7ba5bd90&"
注意,pitcher
返回了内容,由于pitcher
是第一个loader
会结束当前模块的整个loader
的执行,而后当前进入模块的依赖收集即进入webpack
中的parser.parse()
。然后会把上面pitcher
返回的request
作为新的模块进行解析。看到这里有两个loader
而是按照内联形式的loader的顺序和规则执行。
而后进入vue-loader
的执行,由于./App.vue?vue&type=template&id=7ba5bd90&
中有type
,因此会走下面:
// vue-loader/lib/index.js
if (incomingQuery.type) {
return selectBlock(
descriptor,
loaderContext,
incomingQuery,
!!options.appendExtension
)
}
selectBlock
逻辑很简单,就是根据type
将descriptor
中的内容返回,比如template
部分
<div id="app"> {{ msg }} </div>
templateLoader
会在后面小节中单独分析
再看下:./App.vue?vue&type=script&...
经过pitcher处理后的内容
import mod from "-!../node_modules/vue-loader/lib/index.js??vue-loader-options
!./App.vue?vue&type=script&lang=js&";
export default mod;
export * from "-!../node_modules/vue-loader/lib/index.js??vue-loader-options
!./App.vue?vue&type=script&lang=js&"
由于我们当前demo中没有提供其他的处理js
的loader
,因此下次处理该模块时,会将.vue
文件中script
直接作为最终结果输出(注意: 模块化的处理是webpack
内置能力,我们不需要关心),如下:
再看下:./App.vue?vue&type=style&...
经过pitcher处理后的内容
import mod from "-!../node_modules/mini-css-extract-plugin/dist/loader.js
!../node_modules/css-loader/dist/cjs.js
!../node_modules/vue-loader/lib/loaders/stylePostLoader.js
!../node_modules/vue-loader/lib/index.js??vue-loader-options
!./App.vue?vue&type=style&index=0&lang=css&";
export default mod; export * from "-!../node_modules/mini-css-extract-plugin/dist/loader.js
!../node_modules/css-loader/dist/cjs.js
!../node_modules/vue-loader/lib/loaders/stylePostLoader.js
!../node_modules/vue-loader/lib/index.js??vue-loader-options
!./App.vue?vue&type=style&index=0&lang=css&"
.vue
文件中style
块,在当前案例中,会先后经过下面几个loader的处理
- vue-loader(->selectBlock直接返回原生内容)
- stylePostLoader.js
- css-loader
- mini-css-extract-plugin.loader
stylePostLoader
,css-loader
,mini-css-extract-plugin
会在后面小节中单独分析。
构建后的产物的运行过程是怎样的
实际上构建过程中的中间内容,最终也都会被输出,我们当前案例中添加了soucemap配置,可以更清晰的验证这一点。
当然这些中间代码会被webpack再次处理(主要是模块化相关),因此看到App.js
中定义了很多个模块,如下:
现在我们再来看看最终的产物是如何运行的吧,主要是引用关系。
index.html -> main.js -> App.js
- main.js
// main.js
__webpack_require__.e(/*! import() | App */ "App").then(__webpack_require__.bind(null, /*! ./App.vue */ "./src/App.vue")).then((App) => {
new vue__WEBPACK_IMPORTED_MODULE_0__["default"]({
render: h => h(App.default),
}).$mount('#app')
})
- App.js "./src/App.vue"在App.js中定义的,如下:
/***/ "./src/App.vue":
/*!*********************!*\
!*** ./src/App.vue ***!
*********************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__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_lang_css___WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./App.vue?vue&type=style&index=0&lang=css& */ "./src/App.vue?vue&type=style&index=0&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 (false) { var api; }
component.options.__file = "src/App.vue"
/* harmony default export */ __webpack_exports__["default"] = (component.exports);
/***/ }),
总结
补充下 !../node_modules/vue-loader/lib/runtime/componentNormalizer.js ,同样没有额外的处理js的loader,因此这个文件也是原始内容直接输出。 该文件的作用是标准化组件选项的,主要是挂载render
和staticRenderFns
,这两个方法来自template
部分处理结果,vue
运行时在创建虚拟DOM时依赖render
方法(就是通过render方法来创建虚拟DOM的)
vue-loader
作用大致过程:
VueLoaderPlugin
,修改module.rules
,注入pitcher
和克隆的rules
App.vue
首先会被vue-loader
解析成如下内容
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=css&"
/* normalize component */
import normalizer from "!../node_modules/vue-loader/lib/runtime/componentNormalizer.js"
var component = normalizer(
script,
render,
staticRenderFns,
false,
null,
null,
null
)
component.options.__file = "src/App.vue"
export default component.exports
- webpack会从上述内容中解析出依赖,并将这些依赖构造成模块,并进行解析
- ./App.vue?vue&type=script&lang=js&
- ./App.vue?vue&type=script&lang=js&
- ./App.vue?vue&type=style&index=0&lang=css&
- !../node_modules/vue-loader/lib/runtime/componentNormalizer.js
./App.vue?vue&type=...
会匹配到pitcher和vue-loader,但是由于pitcher有返回内容,此次的vue-loader
并不会执行。
pitcher针对type=template/script/style返回的内容如下
- type=template
export * from "-!../node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options
!../node_modules/vue-loader/lib/index.js??vue-loader-options
!./App.vue?vue&type=template&id=7ba5bd90&"
- type=script
import mod from "-!../node_modules/vue-loader/lib/index.js??vue-loader-options
!./App.vue?vue&type=script&lang=js&";
export default mod;
export * from "-!../node_modules/vue-loader/lib/index.js??vue-loader-options
!./App.vue?vue&type=script&lang=js&"
- type=style
import mod from "-!../node_modules/mini-css-extract-plugin/dist/loader.js
!../node_modules/css-loader/dist/cjs.js
!../node_modules/vue-loader/lib/loaders/stylePostLoader.js
!../node_modules/vue-loader/lib/index.js??vue-loader-options
!./App.vue?vue&type=style&index=0&lang=css&";
export default mod; export * from "-!../node_modules/mini-css-extract-plugin/dist/loader.js
!../node_modules/css-loader/dist/cjs.js
!../node_modules/vue-loader/lib/loaders/stylePostLoader.js
!../node_modules/vue-loader/lib/index.js??vue-loader-options
!./App.vue?vue&type=style&index=0&lang=css&"
- 而后会对上面的内容进行依赖解析收集依赖,并创建对应的模块,对新的模块进行解析,此时解析模块的loader主要来自内联路径中。经过这些内联loader的解析生成各个块的内容。
转载自:https://juejin.cn/post/7201444843137335333