vue-loader-15.9.8代码分析


loader 执行顺序

先左到右执行 loader 上的 pitch 方法,如果 pitch 方法有实际的返回值会跳过后续的 pitch 方法 在右到左执行 loader

vue-loader

source => parse()转化 => descriptor => Vue-loader 生成 code

const { parse } = require("@vue/component-compiler-utils");

module.exports = function (source) {
  //....
  const descriptor = parse({
    source,
    compiler: options.compiler || loadTemplateCompiler(loaderContext),
    filename,
    sourceRoot,
    needMap: sourceMap,
  });
  /**
  descriptor {
    template: { ... },
    script: { ... },
    styles: [ ... ],
    customBlocks: [],
    errors: []
  }
  */
  // 第二次进入 vue-loader 执行 匹配到.vue?type=template
  // 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(
      descriptor,
      loaderContext,
      incomingQuery,
      !!options.appendExtension
    );
  }

  // ....
  // 开启热更新
  if (needsHotReload) {
    code += `\n` + genHotReloadCode(id, hasFunctional, templateRequest);
  }
  return code;
};
console.log(source)
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <p>
      For a guide and recipes on how to configure / customize this project,<br>
      check out the
      <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
    </p>
    <h3>Installed CLI Plugins</h3>
    <ul>
      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
    </ul>
    <h3>Essential Links</h3>
    <ul>
      <li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
      <li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
      <li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
      <li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
      <li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
    </ul>
    <h3>Ecosystem</h3>
    <ul>
      <li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
      <li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
      <li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
      <li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
      <li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  props: {
    msg: String
  },
    data(){
    return {
      pageName:'App'
    }
  },
  create(){
      console.log('helloWorld --create')
  },
  mounted(){
    console.log('app -- mounted')
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>

console.log(descriptor)
  template: {
    type: 'template',
    content: '\n' +
      '<div class="hello">\n' +
      '  <h1>{{ msg }}</h1>\n' +
      '  <p>\n' +
      '    For a guide and recipes on how to configure / customize this project,<br>\n' +
      '    check out the\n' +
      '    <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.\n' +
      '  </p>\n' +
      '  <h3>Installed CLI Plugins</h3>\n' +
      '  <ul>\n' +
      '    <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>\n' +
      '    <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>\n' +
      '  </ul>\n' +
      '  <h3>Essential Links</h3>\n' +
      '  <ul>\n' +
      '    <li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>\n' +
      '    <li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>\n' +
      '    <li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>\n' +
      '    <li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>\n' +
      '    <li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>\n' +
      '  </ul>\n' +
      '  <h3>Ecosystem</h3>\n' +
      '  <ul>\n' +
      '    <li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>\n' +
      '    <li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>\n' +
      '    <li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>\n' +
      '    <li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>\n' +
      '    <li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>\n' +
      '  </ul>\n' +
      '</div>\n',
    start: 10,
    attrs: {},
    end: 1681
  },
  script: {
    type: 'script',
    content: '//\n' +
      'export default {\n' +
      "  name: 'HelloWorld',\n" +
      '  props: {\n' +
      '    msg: String\n' +
      '  },\n' +
      '    data(){\n' +
      '    return {\n' +
      "      pageName:'App'\n" +
      '    }\n' +
      '  },\n' +
      '  create(){\n' +
      "      console.log('helloWorld --create')\n" +
      '  },\n' +
      '  mounted(){\n' +
      "    console.log('app -- mounted')\n" +
      '  }\n' +
      '}\n',
    start: 1702,
    attrs: {},
    end: 1942
  },
  styles: [
    {
      type: 'style',
      content: '\n' +
        'h3 {\n' +
        '  margin: 40px 0 0;\n' +
        '}\n' +
        'ul {\n' +
        '  list-style-type: none;\n' +
        '  padding: 0;\n' +
        '}\n' +
        'li {\n' +
        '  display: inline-block;\n' +
        '  margin: 0 10px;\n' +
        '}\n' +
        'a {\n' +
        '  color: #42b983;\n' +
        '}\n',
      start: 2035,
      attrs: [Object],
      scoped: true,
      end: 2183
    }
  ],
  customBlocks: [],
  errors: [ 'tag <br> has no matching end tag.' ]

console.log(code)
import {
  render,
  staticRenderFns,
} from "./HelloWorld.vue?vue&type=template&id=469af010&scoped=true&";
import script from "./HelloWorld.vue?vue&type=script&lang=js&";
export * from "./HelloWorld.vue?vue&type=script&lang=js&";
import style0 from "./HelloWorld.vue?vue&type=style&index=0&id=469af010&scoped=true&lang=css&";

/* normalize component */
import normalizer from "!../../source-code/vue-loader-15.9.8/lib/runtime/componentNormalizer.js";
var component = normalizer(
  script,
  render,
  staticRenderFns,
  false,
  null,
  "469af010",
  null
);

/* hot reload */
if (module.hot) {
  var api = require("F:\\github\\vue2-test\\node_modules\\vue-hot-reload-api\\dist\\index.js");
  api.install(require("vue"));
  if (api.compatible) {
    module.hot.accept();
    if (!api.isRecorded("469af010")) {
      api.createRecord("469af010", component.options);
    } else {
      api.reload("469af010", component.options);
    }
    module.hot.accept(
      "./HelloWorld.vue?vue&type=template&id=469af010&scoped=true&",
      function () {
        api.rerender("469af010", {
          render: render,
          staticRenderFns: staticRenderFns,
        });
      }
    );
  }
}
component.options.__file = "src/components/HelloWorld.vue";
export default component.exports;

VueLoaderPlugin

webpack 插件是一个具有 apply 方法的 JavaScript 对象。apply 方法会被 webpack compiler 调用,并且在 整个 编译生命周期都可以访问 compiler 对象。

Plugin 的作用,主要有以下两条:

  • 能够 hook 到在每个编译(compilation)中触发的所有关键事件。
  • 在插件实例的 apply 方法中,可以通过 compiler.options 获取 Webpack 配置,并进行修改。

class VueLoaderPlugin {
  apply (compiler) {
    // 对 Webpack 配置进行修改
    const rules = compiler.options.module.rules

    const clonedRules = rules
      .filter(r => r !== rawVueRules)
      .map((rawRule) => cloneRule(rawRule, refs))

    // pitcher
    const pitcher = {
      loader: require.resolve('./loaders/pitcher'),
      resourceQuery: query => {
        if (!query) { return false }
        const parsed = qs.parse(query.slice(1))
        // 带有?vue的都会匹配到
        return parsed.vue != null
      },
      options: { ... }
    }

    // 替换初始 module.rules,在原有 rule 上,增加 pitcher、clonedRules
    compiler.options.module.rules = [
       pitcher,
       ...clonedRules,
       ...rules
     ];
  }
}

pitchLoader


// vue-loader lib/loaders/pitcher.js
module.exports.pitch = function (remainingRequest) {
    // this.resourceQuery import文件?后的内容
    // ?vue&type=script&lang=js&
    // ?vue&type=template&id=3942140e&scoped=true&
    //
  const query = qs.parse(this.resourceQuery.slice(1))
  if (query.type === 'style') { ... }
  if (query.type === 'template') { ... }

  // 处理 script 块和 custom 块
  return `import mod from ${request}; export default mod; export * from ${request}`;
}
pitch 处理结果
//  template ?vue&type=template&id=39502b8f&scoped=true&
export * from "-!../../source-code/vue-loader-15.9.8/lib/loaders/templateLoader.js??vue-loader-options!../../source-code/vue-loader-15.9.8/lib/index.js??vue-loader-options!./HelloWorld.vue?vue&type=template&id=469af010&scoped=true&";

//  style vue&type=style&index=0&id=469af010&scoped=true&lang=css&
export * from "-!../../node_modules/style-loader/dist/cjs.js!../../node_modules/css-loader/dist/cjs.js!../../source-code/vue-loader-15.9.8/lib/loaders/stylePostLoader.js!../../node_modules/postcss-loader/dist/cjs.js!../../node_modules/less-loader/dist/cjs.js!../../source-code/vue-loader-15.9.8/lib/index.js??vue-loader-options!./HelloWorld.vue?vue&type=style&index=0&id=469af010&scoped=true&lang=css&";
//  script vue&type=script&lang=js&
// lonedRuleSet-1[0].rules[0].use 使用配置的js rule处理
import mod from "-!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0].use!../../source-code/vue-loader-15.9.8/lib/index.js??vue-loader-options!./HelloWorld.vue?vue&type=script&lang=js&";
export default mod;
export * from "-!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0].use!../../source-code/vue-loader-15.9.8/lib/index.js??vue-loader-options!./HelloWorld.vue?vue&type=script&lang=js&";

匹配过程

template 匹配过程
//  1. Vue-loader处理 ?vue 匹配到pitch loader
"./HelloWorld.vue?vue&type=template&id=469af010&scoped=true&";
// 2. pitch Loader 匹配 type=template 进行处理 结果
export * from "-!../../source-code/vue-loader-15.9.8/lib/loaders/templateLoader.js??vue-loader-options!../../source-code/vue-loader-15.9.8/lib/index.js??vue-loader-options!./HelloWorld.vue?vue&type=template&id=469af010&scoped=true&";
// 3. -! 禁用所有已配置的 preLoader 和 loader,但是不禁用 postLoaders 再次进入vue-loader处理后续.vue文件  type= template
if (incomingQuery.type) {
  return selectBlock(
    descriptor,
    loaderContext,
    incomingQuery,
    !!options.appendExtension
  );
}
// 4. 匹配 templateLoader 的postLoader
return code + `\nexport { render, staticRenderFns }`;
// 5. Vue-loader 输出
import {
  render,
  staticRenderFns,
} from "./HelloWorld.vue?vue&type=template&id=469af010&scoped=true&";
// ....
var component = normalizer(
  script,
  render,
  staticRenderFns,
  false,
  null,
  "469af010",
  null
);

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.13.0