基于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代码进行处理。
相关推荐
TurtleOrange44 分钟前
VSCode编辑前端快速开发模板
前端
陪你去流浪_1 小时前
Vue el-input密码输入框 按住显示密码,松开显示*;阻止浏览器密码回填,自写密码输入框;校验输入非汉字内容;文本框聚焦到内容末尾;
前端·javascript·vue.js
肖老师xy1 小时前
vue3新建组件库项目并上传到私库
vue.js
星糖曙光1 小时前
基于HTML生成网页有什么优势
前端·经验分享·笔记·html·ai编程
计算机-秋大田2 小时前
云上考场微信小程序的设计与实现(LW+源码+讲解)
java·前端·spring boot·微信小程序·小程序·课程设计
大厂在职_Xbg2 小时前
Dagger2进阶学习
前端·python·学习
m0_748246612 小时前
2024最新版Node.js详细安装教程(含npm配置淘宝最新镜像地址)
前端·npm·node.js
哟哟耶耶2 小时前
npm-npm ERR! missing script: serve
前端·npm·node.js
萌萌哒草头将军2 小时前
2025年了,令人唏嘘的Angular,现在怎么样了🚀🚀🚀
前端·javascript·vue.js
蓝瑟2 小时前
🔥 当AI遇上费曼技巧:用魔法打败魔法の闭包完全指南
前端·javascript·deepseek