vue-loader都做了些什么

本文主要内容:

  1. vue-loader解析vue单文件组件的过程分析
  2. VueLoaderPlugin的作用

1. import一个vue单文件组件时,实际引入是什么

新建一个index.vue文件,内容如下:

<template>
  <ul @click="handleClick">
    <li v-for="item in list" :key="item">{{ item }}</li>
  </ul>
</template>
<script>
export default {
  data() {
    return {
      list:  ['a', 'b', 'c', 'd', 'e']
    };
  },
  methods: {
    handleClick() {
      this.list =  ['h', 'i', 'a', 'j', 'k']
    },
  }
};
</script>
<style>
  ul {
    color: red;
  }
</style>

在其他文件中导入 index.vue,并查看输出结果

import app from './index.vue';
console.log(app);

2i7PBj.png

看图可以知道,import 一个单文件组件其实就是导入了这样一个对象。

2. webpack中的loader和plugin

webpack 中 loader 的作用是将匹配后缀名的文件从源文件导出为 js 模块,是源文件到 js 模块的转换。

plugin 可以在 webpack 构建过程中插入自定义行为,插件的原型对象上都有一个 apply 方法,这个 apply 方法在安装插件时会被 webpack 编译器调用一次。

众所周知,要想使用 vue 单文件组件,必须在 webpack 中配置 vue-loader 和 vueLoaderPlugin 插件

const VueLoaderPlugin = require('vue-loader/lib/plugin');

module: {
  rules: [
    {
      test: /\.vue$/,
      loader: 'vue-loader',
    },
  ];
}

plugins: [new VueLoaderPlugin()];

3. vue-loader的解析流程

1. 从字符串到AST抽象语法树

import .vue文件时,会命中vue-loader,第一次调用 vue-loader,原文件首先会被从字符串解析为 AST 抽象语法树,即用普通 js 对象来描述组件中的内容:

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

const descriptor = parse({
  source, //source是loader接收到的源代码字符串
  compiler: options.compiler || loadTemplateCompiler(loaderContext),
  filename,
  sourceRoot,
  needMap: sourceMap,
});

调用结果:

{
  template: {
    type: 'template',
    content: '\n' +
      '<ul @click="handleClick">\n' +
      '  <li v-for="item in list" :key="item">{{ item }}</li>\n' +
      '</ul>\n',
    start: 54,
    attrs: {},
    end: 148
  },
  script: {
    type: 'script',
    content: '//\n' +
      '//\n' +
      '//\n' +
      '//\n' +
      '//\n' +
      '//\n' +
      '//\n' +
      '//\n' +
      '//\n' +
      '//\n' +
      '\n' +
      'export default {\n' +
      '  data() {\n' +
      '    return {\n' +
      "      list:  ['a', 'b', 'c', 'd', 'e']\n" +
      '    };\n' +
      '  },\n' +
      '  methods: {\n' +
      '    handleClick() {\n' +
      "      this.list =  ['h', 'i', 'a', 'j', 'k']\n" +
      '    },\n' +
      '  }\n' +
      '};\n',
    start: 168,
    attrs: {},
    end: 353,
    map: {
      version: 3,
      sources: [Array],
      names: [],
      mappings: ';;;;;;;;;;;AAWA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA',
      file: 'index.vue',
      sourceRoot: 'src/vue',
      sourcesContent: [Array]
    }
  },
  styles: [
    {
      type: 'style',
      content: '\nul {\n  color: red;\n}\n',
      start: 7,
      attrs: {},
      end: 35,
      map: [Object]
    }
  ],
  customBlocks: [],
  errors: []
}
2. 将AST各部分转换为特殊的引用路径

对这个AST的每部分进行处理,.vue文件会被转换为下面这样的代码:

//代码1
import {
  render,
  staticRenderFns,
} from './index.vue?vue&type=template&id=21fec300&';
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,
);

component.options.__file = 'src/vue/index.vue';
export default component.exports;

关注最前面的import语句,在原本的index.vue后面加入了查询条件,包括vue,type,id,lang等,之后将通过不同的查询条件来处理不同部分的代码

3. VueLoaderPlugin插件处理rules

VueLoaderPlugin的主要作用就是对vue不同模块配置不同的loader,在webpack安装插件时,也就是预处理阶段,VueLoaderPlugin的apply会被调用,该方法中拦截了用户自定义的rules属性,加入对 vue 单文件模块处理的规则后,返回调整后的rules列表,看下面的例子:

自定义rules:

[
  {
    test: /\.vue$/,
    loader: 'vue-loader',
  },
  {
    test: /\.css$/,
    use: ['style-loader', 'css-loader'],
  },
];

VueLoaderPlugin处理后的rules:

[
    {
        loader: 'xxx/xx/xx/pitcher.js',
        resourceQuery: [Function: resourceQuery],
        options: { cacheDirectory: undefined, cacheIdentifier: undefined }
    },
    {
        use: [
            { loader: 'style-loader', options: undefined, ident: undefined },
            { loader: 'css-loader', options: undefined, ident: undefined }
        ],
        resource: [Function: resource],
        resourceQuery: [Function: resourceQuery]
    },
    {
        test: /\.vue$/,
        use: [
                { loader: 'vue-loader', options: {}, ident: 'vue-loader-options' }
            ]
    },
    {
        test: /\.css$/,
        use: [
            { loader: 'style-loader', options: undefined, ident: undefined },
            { loader: 'css-loader', options: undefined, ident: undefined }
        ]
    }
]

可以看出自定义的两个 rules 只是对格式进行统一,主要的修改是添加了 pitcher loader, 复用了处理 css 的 loader,重定义了 它们的resource 和 resourceQuery 属性,resourceQuery属性是通过匹配链接中的查询参数来判断链接是否命中。

resourceQuery: 此选项用于测试请求字符串的查询部分(即从问号开始)
resource: 简单理解就是匹配到的资源文件的绝对路径

挨个看一下新加的rule:

pitcher loader的resourceQuery属性只是简单的匹配第一个查询条件是否是vue

resourceQuery: query => {
    if (!query) { return false }

    const parsed = qs.parse(query.slice(1))
    return parsed.vue != null
}

复用的rule,resource 和 resourceQuery具体内容如下

resource: resources => {
  currentResource = resources
  return true
},
resourceQuery: query => {
  if (!query) { return false }
  const parsed = qs.parse(query.slice(1))
  if (parsed.vue == null) {
    return false
  }
  if (!conditions) {
    return false
  }
  const fakeResourcePath = `${currentResource}.${parsed.lang}`
  for (const condition of conditions) {
    // add support for resourceQuery
    const request = condition.property === 'resourceQuery' ? query :fakeResourcePath
    if (condition && !condition.fn(request)) {
      return false
    }
  }
  return true
}

resource只是为了获取当前资源文件的绝对路径,赋值给currentResource

resourceQuery简单来说就是将.+查询条件中lang属性作为后缀名添加到当前请求的最末尾,判断这个后缀名是否在rules中配置了对应的loader,如果有loader返回true,否则返回false。

4. 执行pitching loader,生成内联loader

rules列表中目前配置了四个rule,正常情况下loader 总是 从右到左被调用,即import文件时,匹配的顺序是.css -> .vue -> resourceQuery -> pitcher resourceQuery,但这组的第一个loader定义了pitch方法:

//pitcher.js
module.exports = code => code 

module.exports.pitch = function (remainingRequest) {
    //code
}

查看文档上关于pitching loader的介绍

loader 总是 从右到左被调用。有些情况下,loader 只关心 request 后面的 元数据(metadata),并且忽略前一个 loader 的结果。在实际(从右到左)执行 loader 之前,会先 从左到右 调用 loader 上的 pitch 方法。

重点是会先从左到右调用 loader 上的 pitch 方法,举个例子,当执行这句代码时,

import { render, staticRenderFns, } from './index.vue?vue&type=template&id=21fec300&';

执行顺序可以看做是:

|- pitcher-loader `pitch`
  |- resourceQuery-loader `pitch` //匹配了resourceQuery
    |- vue-loader `pitch`
      |- css-loader `pitch`
        |- requested module is picked up as a dependency
      |- css-loader normal execution
    |- vue-loader normal execution
  |- resourceQuery-loader normal execution
|- pitcher-loader normal execution

在这里参数?vue命中了pitcher loader的resourceQuery,执行pitcher loader的pitch方法,这个方法根据参数type来生成内联lodaer,如type=template执行完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=21fec300&"

这个阶段中,如果某个 loader 在 pitch 方法中返回一个结果,那么这个过程会回过身来,并跳过剩下的 loader。
也就是只会执行

|- pitcher-loader `pitch`

其他import语句中带有?vue&type=xxx&lang=xxx&'的链接都是同理,最终都会被转换为如上所示的一段内联loader

5. 执行内联loader

内联loader按照从右到左的顺序执行,还看上面的例子,export './index.vue?vue&type=template&id=21fec300&'的loader是vue-loader/lib/index.js,因为链接查询条件中带有type属性,直接命中下面这段代码

// 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
    )
  }

返回template部分的代码

<ul @click="handleClick">
  <li v-for="item in list" :key="item">{{ item }}</li>
</ul>

这段代码会传入loaders/templateLoader.js被解析,templateLoader使用vue-template-compilerhtml代码转换为renderstaticRenderFns,最终用exports 导出,这也是为什么vue-cli文档上要求将vue-loadervue-template-compiler一起安装的原因。

你应该将 vue-loader 和 vue-template-compiler 一起安装——除非你是使用自行 fork 版本的 Vue 模板编译器的高阶用户

vue单文件组件中的不同模块都是按照这种模式使用对应的loader来处理,回看代码1import都导入完成之后,执行normalizer方法,将script标签中定义的data,methods等对象和render,staticRenderFns组装在同一个对象中返回,这就是导入一个单文件组件的大致流程。