vue-loader 的执行原理
如果一个框架(如 weex 和快应用)需要支持 vue dsl,那么就离不开 vue-loader。
vue-loader 负责将 vue 文件转为单文件组件 (SFCs)对象,并调用 vue-template-compiler 将模板编译成 render 函数。
本文主要分析其执行原理。
首先还是贴一下官方文档:Vue Loader
下面主要分为这两个部分说明
- 构建示例项目
- 分析 vue-loader 的执行
初始化项目
根据文档说明先快速构建一个项目,并安装上依赖
安装依赖
新建项目
npm init
安装 vue-loader、vue-template-compiler,编译 vue 文件
npm install -D vue-loader vue-template-compiler
安装 webpack 4 和 webpack-cli,打包静态资源
npm install -D webpack@4 webpack-cli
安装 css-loader 和 babel-loader,编译 css 和模拟对 js 的处理
npm install -D css-loader babel-loader
然后在根目录下新建 webpack.config.js
和 src/index.vue
文件。
最终目录应该是这样的
├─package-lock.json
├─package.json
├─webpack.config.js
├─src
| └index.vue
└dist
添加单组件文件的内容
往 index.vue 中添加如下内容,也可以是其他任意的组件内容
<template>
<!-- template里只能有一个根节点 -->
<div class="demo-page">
<text class="title">欢迎打开{{ title }}</text>
</div>
</template>
<script>
export default {
data: function () {
return {
title: "vue-loader示例",
};
},
};
</script>
<style>
.demo-page {
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
}
</style>
添加 webpack 配置
往 webpack.config.js 中添加如下配置,其中 vue-style-loader 是跟随 vue-loader 安装的,不用自行再安装。
const path = require("path");
const VueLoaderPlugin = require("vue-loader/lib/plugin");
module.exports = {
mode: "development",
entry: "./src/index.vue",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
},
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(),
],
};
添加脚本
往 package.json 中添加脚本 "build": "webpack"
{
...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack"
},
...
}
打包
最后执行 npm run build
即可正常打包,在 dist/main.js 看到输出的文件。
后面的调试方法参考该文章:使用 vscode 的调试功能
分析执行
plugin 会在 webpack 启动后不久就开始执行,所以我们先从 VueLoaderPlugin 处开始看起。
VueLoaderPlugin 的执行
本文基于 webpack 4 编写,对应的 VueLoaderPlugin 定义在 vue-loader/lib/plugin-webpack4.js 中,在 apply 方法处打断点,并开始调试。
VueLoaderPlugin 的执行主要就是处理 loader 中的 rules。VueLoaderPlugin 的作用是将定义过的其它规则复制并应用到 .vue 文件里相应语言的块(js、style)。在 webpack 启动时会立即执行。
首先看一下第一部分的代码,可以看到就是在 webpack 配置中想尽办法找到和 .vue
或者 .vue.html
匹配的 rules,获取到后存储在 vueRule
中
class VueLoaderPlugin {
apply (compiler) {
.
...
// 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`))
if (vueRuleIndex < 0) {
vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue.html`))
}
const vueRule = rules[vueRuleIndex]
...
}
然后就是克隆一份所有除了 vue rules 以外的 rules,此处也暂不展开说明
const clonedRules = rules.filter((r) => r !== vueRule).map(cloneRule);
然后会准备一个 pitcher-loader,该 loader 只要资源文件路径的?后有 vue 就会执行(如 xxx/filename.vue?vue
&xxxx)。
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 文件,通过代码就可以知道,pitcher 只是为了负责拦截所有 vue 块请求,并将其转化为适当的请求。module.exports 并没有其他逻辑实现。
module.exports = code => code
// This pitching loader is responsible for intercepting all vue block requests
// and transform it into appropriate requests.
module.exports.pitch = function (remainingRequest) {
...
}
最后就是修改 rules,重写 rules 为 pitcher-loader,克隆的 loader(clonedRules)和原本定义的 loader(rules)
// replace original rules
compiler.options.module.rules = [pitcher, ...clonedRules, ...rules];
根据 rules 可以知道默认的执行顺序:
pitch
- pitcher-loader 中 pitch,对有 vue 参数的请求进行转化
loader
- 用户自己定义的 rules
- 经过克隆的用户定义的 rules(没有 vue-loader)
- pitcher-loader(无用)
loader 第一轮执行
根据 webpack 的配置,从下到上开始第一轮的 vue 文件处理。
此处不贴完整代码,建议打开vue-loader github 仓库对照阅读。
首先 vue-loader 会检测是否在 webpack config 中正确配置了 VueLoaderPlugin
然后 vue 文件会迎来他的第一次转换,即下图中的 parse 方法,该方法实际调用的是 vue-template-compiler/build.js 中的 parseComponent 方法
该方法将 vue 文件的内容转换成了 SFC 描述符对象,即文件内容(字符串)转换成下面这个对象,将每个部分(script、styles、template)明确分了开来。
接着会有一个 incomingQuery.type 的判断,现在为 false 暂时忽略。
最后的解析结果:
import {
render,
staticRenderFns,
} from "./index.vue?vue&type=template&id=2964abc9&";
import script from "./index.vue?vue&type=script&lang=js&";
export * from "./index.vue?vue&type=script&lang=js&";
import style0 from "./index.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
);
/* hot reload 代码省略 */
component.options.__file = "src/index.vue";
export default component.exports;
可以看到经过第一轮的解析,vue 文件被拆分为了几个请求,主要为三个:
- ./index.vue?vue&type=template
- ./index.vue?vue&type=script
- ./index.vue?vue&type=style
loader 第二轮执行
根据上面第一轮 vue-loader 的解析结果,开始第二轮的解析。
import {
render,
staticRenderFns,
} from "./index.vue?vue&type=template&id=2964abc9&";
因为请求中有 vue,故该请求会给 VueLoaderPlugin 定义的 pitcher 拦截,重新分发请求。让我们直接看一下 pitch 重写后的请求。
export * from "-!../node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!../node_modules/vue-loader/lib/index.js??vue-loader-options!./index.vue?vue&type=template&id=2964abc9&";
可以看到该请求已经转换成这 2 个 loader 的事情了
- vue-loader
- templateLoader
直接看 vue-loader,关键是这块刚才没有提的逻辑
此处的 selectBlock 方法会根据 url 参数中的 type 获取对应模块的内容(template 等)给下一个 loader。
最后就是 templateLoader,该 loader 会会拿到 template 字符串。vue-loader 就是通过这个 loader 调用 vue-template-compiler 对模板进行编译,得到最终的 render 函数。
这是最后编译获得的结果:
var render = function () {
var _vm = this;
var _h = _vm.$createElement;
var _c = _vm._self._c || _h;
return _c("div", { staticClass: "demo-page" }, [
_c("text", { staticClass: "title" }, [
_vm._v("欢迎打开" + _vm._s(_vm.title)),
]),
]);
};
var staticRenderFns = [];
render._withStripped = true;
export { render, staticRenderFns };