likes
comments
collection
share

浅析vue-loader

作者站长头像
站长
· 阅读数 28

环境配置

// webpack.config.js
const path = require("path");
const { VueLoaderPlugin } = require("vue-loader");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: "./src/index.js",
  mode: "development",
  devtool: false,
  output: {
    filename: "[name].js",
    path: path.resolve(__dirname, "dist"),
  },
  devServer: {
    hot: true,
    open: true,
  },
  module: {
    rules: [
      { test: /\.vue$/, use: ["vue-loader"] },
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"],
      },
    ],
  },
  plugins: [
    new VueLoaderPlugin(),
    new HtmlWebpackPlugin({
      template: 'index.html'
    }),
  ],
};

// index.js
import { createApp } from 'vue'
import App from "./App.vue";

const app = createApp(App)
app.mount('#app')

代码调试

VueLoaderPlugin

ruleSetCompiler && BasicMatcherRulePlugin

const ruleSetCompiler = new RuleSetCompiler([
    new BasicMatcherRulePlugin('test', 'resource'),
    new BasicMatcherRulePlugin('mimetype'),
    new BasicMatcherRulePlugin('dependency'),
    new BasicMatcherRulePlugin('include', 'resource'),
    new BasicMatcherRulePlugin('exclude', 'resource', true),
    new BasicMatcherRulePlugin('conditions'),
    new BasicMatcherRulePlugin('resource'),
    new BasicMatcherRulePlugin('resourceQuery'),
    new BasicMatcherRulePlugin('resourceFragment'),
    new BasicMatcherRulePlugin('realResource'),
    new BasicMatcherRulePlugin('issuer'),
    new BasicMatcherRulePlugin('compiler'),
    ...objectMatcherRulePlugins,
    new BasicEffectRulePlugin('type'),
    new BasicEffectRulePlugin('sideEffects'),
    new BasicEffectRulePlugin('parser'),
    new BasicEffectRulePlugin('resolve'),
    new BasicEffectRulePlugin('generator'),
    new UseEffectRulePlugin(),
]);

BasicEffectRulePlugin主要是对rule的改造,它删除了rule中的"test","mimetype"等等属性,并增加了conditions

浅析vue-loader

RuleSetCompiler创建了hooks.rule这个SyncHook,然后后面match的时候会call这个hook,由于vue-loader是第一个,所以它最先进入match,最后vue-loader的rule的处理结果为

浅析vue-loader

test变为了一个函数放在conditions之中,use放在了effects之中

function match(rule, fakeFile) {
    let ruleSet = matcherCache.get(rule);
    if (!ruleSet) {
        // skip the `include` check when locating the vue rule
        const clonedRawRule = Object.assign({}, rule);
        delete clonedRawRule.include;
        ruleSet = ruleSetCompiler.compile([clonedRawRule]);
        matcherCache.set(rule, ruleSet);
    }
    return ruleSet.exec({
        resource: fakeFile,
    });
}

比如执行vueRules = match(rawRule, 'foo.vue');会进入上文处理过的rule,然后conditions判断为true,就返回对应的loader

NormalModuleFactory

NormalModuleFactory这个生命周期中,就会进行上述ruleSetCompiler的调用

之前很奇怪给resourceQuery打断点的时候会卡在这个生命周期,原因是matchWhenEmpty: condition("")

在生成rules的时候,给matchWhenEmpty赋值会调用condition

之后就执行compile,会创建一个NormalModuleFactory

接着html-webpack-plugin会创建一个childCompiler,也会创建一次NormalModuleFactory

生成rules

首先VueLoaderPlugin会对module.rule除了vue-loader的rule进行复制并在clone的过程中同样用了ruleSetCompiler进行rule的改写,这里只有css的rule,命中条件为xx.vue?vue&lang=css

{
    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 (!ruleCheck(parsed, compiledRule)) {
        return false
      }
      const fakeResourcePath = ruleResource(parsed, currentResource)
      for (const condition of compiledRule.conditions) {
        // add support for resourceQuery
        const request =
          condition.property === 'resourceQuery' ? query : fakeResourcePath
        if (condition && !condition.fn(request)) {
          return false
        }
      }
      return true
    },
    use: [{
        loader: "style-loader",
    }, {
        loader: "css-loader"
    }]
}

之后会创建一个pitcher-loader,命中条件为xx.vue?vue

const pitcher = {
  loader: require.resolve('./loaders/pitcher'),
  resourceQuery: (query) => {
    if (!query) {
      return false
    }
    const parsed = qs.parse(query.slice(1))
    return parsed.vue != null
  },
  options: {
    cacheDirectory: vueLoaderUse.options.cacheDirectory,
    cacheIdentifier: vueLoaderUse.options.cacheIdentifier
  }
}

还有templateCompilerRule,命中条件为xx.vue?vue&type=template

const templateCompilerRule = {
    loader: require.resolve('./templateLoader'),
    resourceQuery: (query) => {
        if (!query) {
            return false;
        }
        const parsed = qs.parse(query.slice(1));
        return parsed.vue != null && parsed.type === 'template';
    },
    options: vueLoaderOptions,
};

所以最后的rules共有5个

compiler.options.module.rules = [
    pitcher,
    templateCompilerRule,
    ...clonedRules, // 拷贝的css
    ...rules, // vue-loader和css2个
];

这就是VueLoaderPlugin所做的工作,只是添加了几个loader

vue-loader

文件的加载顺序是:index.js->index.html->app.vue这几个都没命中对应loader,略过

首先根据入口文件import App from "./App.vue";处理App.vue文件,命中vue-loader

浅析vue-loader

source为App.vue文件的内容

这个文件会根据路径通过hash生成id

浅析vue-loader

给App.vue文件的scrpit部分增加query参数,使它能够命中pitch-loader

浅析vue-loader

给App.vue文件的template也同样增加query参数

浅析vue-loader

样式文件也一样

浅析vue-loader

经过处理,生成了三个import

import { render } from "./App.vue?vue&type=template&id=7ba5bd90"
import script from "./App.vue?vue&type=script&lang=js"
export * from "./App.vue?vue&type=script&lang=js"

import "./App.vue?vue&type=style&index=0&id=7ba5bd90&lang=css"

后面又加上了热更新的代码

import { render } from "./App.vue?vue&type=template&id=7ba5bd90"
import script from "./App.vue?vue&type=script&lang=js"
export * from "./App.vue?vue&type=script&lang=js"

import "./App.vue?vue&type=style&index=0&id=7ba5bd90&lang=css"

import exportComponent from "/Users/g/webpack-book-samples/5-1_use-vue/node_modules/vue-loader/dist/exportHelper.js"
const __exports__ = /*#__PURE__*/exportComponent(script, [['render',render],['__file',"src/App.vue"]])
/* hot reload */
if (module.hot) {
  __exports__.__hmrId = "7ba5bd90"
  const api = __VUE_HMR_RUNTIME__
  module.hot.accept()
  if (!api.createRecord('7ba5bd90', __exports__)) {
    api.reload('7ba5bd90', __exports__)
  }
  
  module.hot.accept("./App.vue?vue&type=template&id=7ba5bd90", () => {
    api.rerender('7ba5bd90', render)
  })

}


export default __exports__

第一次进入vue-loader就会调用sfc,存储的键值就是App.vue的内容,并且缓存下来,下次剩下两个就可以直接用缓存了

sfc的结果就是将template,script,style分开

浅析vue-loader

loader命中

由于这三个import的query都带有vue,所以都会进入pitch-loader

按照代码顺序,首先处理template

浅析vue-loader

template import会进入专门处理template的template-loader

浅析vue-loader

后面进行其他rule的校验,由于此时webpack只配置了css的校验,所以只有css的import才会进入clonedRules

  • template -〉 pitch-loader -〉template-loader -〉vue-loader
  • script -〉 pitch-loader -〉vue-loader
  • css -〉 pitch-loader-〉css-loader -〉style-loader -〉vue-loader

pitch-loader

有一个很重要的一点是,normalModuleFactory处理import的时候,并不是同步的!!!这就是import和require的差别;import是能够tree-shaking的,所以虽然按照顺序是template->script->style这个顺序来导入的,loader也是这么命中的,但是加载module的时候,是按照使用的顺序,若是没有引用,可以three-shaking掉,所以第一个处理的模块是script

pitch-loader只是将import转成了行内loader的写法,类似style-loader

// script
import script from "./App.vue?vue&type=script&lang=js"
export * from "./App.vue?vue&type=script&lang=js"

/* 转换成了下面的写法 */

export { default } from "-!../node_modules/vue-loader/dist/index.js??ruleSet[1].rules[3].use[0]!./App.vue?vue&type=script&lang=js";

export * from "-!../node_modules/vue-loader/dist/index.js??ruleSet[1].rules[3].use[0]!./App.vue?vue&type=script&lang=js"
// template
import { render } from "./App.vue?vue&type=template&id=7ba5bd90"

/* 转换成了下面的写法 */

export * from "-!../node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[1]!../node_modules/vue-loader/dist/index.js??ruleSet[1].rules[3].use[0]!./App.vue?vue&type=template&id=7ba5bd90"
// style
import "./App.vue?vue&type=style&index=0&id=7ba5bd90&lang=css"

/* 转换成了下面的写法 */

export * from "-!../node_modules/style-loader/dist/cjs.js!../node_modules/css-loader/dist/cjs.js!../node_modules/vue-loader/dist/stylePostLoader.js!../node_modules/vue-loader/dist/index.js??ruleSet[1].rules[3].use[0]!./App.vue?vue&type=style&index=0&id=7ba5bd90&lang=css"

注意style多了个stylePostLoader

第二次vue-loader

由于这次携带query,会进入selectBlock

function selectBlock(descriptor, scopeId, options, loaderContext, query, appendExtension) {
    // template
    if (query.type === `template`) {
        // if we are receiving a query with type it can only come from a *.vue file
        // that contains that block, so the block is guaranteed to exist.
        const template = descriptor.template;
        if (appendExtension) {
            loaderContext.resourcePath += '.' + (template.lang || 'html');
        }
        loaderContext.callback(null, template.content, template.map);
        return;
    }
    // script
    if (query.type === `script`) {
        const script = (0, resolveScript_1.resolveScript)(descriptor, scopeId, options, loaderContext);
        if (appendExtension) {
            loaderContext.resourcePath += '.' + (script.lang || 'js');
        }
        loaderContext.callback(null, script.content, script.map);
        return;
    }
    // styles
    if (query.type === `style` && query.index != null) {
        const style = descriptor.styles[Number(query.index)];
        if (appendExtension) {
            loaderContext.resourcePath += '.' + (style.lang || 'css');
        }
        loaderContext.callback(null, style.content, style.map);
        return;
    }
    // custom
    if (query.type === 'custom' && query.index != null) {
        const block = descriptor.customBlocks[Number(query.index)];
        loaderContext.callback(null, block.content, block.map);
    }
}

script会将sfc对script的处理结果返回

template还会进入template-loader

返回结果是一个render函数,能渲染vnode

import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("h3", null, _toDisplayString($data.message), 1 /* TEXT */)
  ]))
}

css会进入stylePostLoader,给带scoped字段的style文件增加data-v-[id],后续处理就和之前讲的css-loader和style-loader的步骤一样了

转载自:https://juejin.cn/post/7205208559263514680
评论
请登录