基于VSCode Debug Terminal理解vue-loader原理

初始化项目

通过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放到了最前面:

根据pitcherresourceQuery,我们可以发现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-loadercache-loaderbabel-loadercache-loader以及HelloWorld.vue,其中loader得执行顺序是从后往前的。

总结

vue sfc文件实际上会经过三个阶段的处理:

  1. templatescriptstyle转换成对应的import请求。
  2. 通过pitch loader将所有来自vue模块儿的import请求转换成webpack 内联请求
  3. 根据内联请求上的query参数,选择项目中配置的loader对当前block代码进行处理。
相关推荐
小月鸭4 分钟前
如何理解HTML语义化
前端·html
jump68027 分钟前
url输入到网页展示会发生什么?
前端
诸葛韩信31 分钟前
我们需要了解的Web Workers
前端
brzhang36 分钟前
我觉得可以试试 TOON —— 一个为 LLM 而生的极致压缩数据格式
前端·后端·架构
yivifu1 小时前
JavaScript Selection API详解
java·前端·javascript
这儿有一堆花1 小时前
告别 Class 组件:拥抱 React Hooks 带来的函数式新范式
前端·javascript·react.js
十二春秋1 小时前
场景模拟:基础路由配置
前端
六月的可乐1 小时前
实战干货-Vue实现AI聊天助手全流程解析
前端·vue.js·ai编程
一 乐2 小时前
智慧党建|党务学习|基于SprinBoot+vue的智慧党建学习平台(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·学习
BBB努力学习程序设计2 小时前
CSS Sprite技术:用“雪碧图”提升网站性能的魔法
前端·html