2. 「uniapp 源码分析」vue-loader@15.8.3 的整体流程

我心飞翔 分类:vue

通常我们会使用vue-cli来创建一个vue项目,由于vue-cli对常见的开发需求进行了预先配置,做到了开箱即用。但是阻碍碍我们窥探其真面目脚步。当然官方也提供了手动配置的方案。参考

安装依赖,下面库的作用后面都会分析到。

npm install -D vue-loader vue-template-compiler

webpack 配置,有loader有plugin ```js // webpack.config.js const { VueLoaderPlugin } = require('vue-loader')

module.exports = { module: { rules: [ // ... 其它规则 { test: /.vue$/, loader: 'vue-loader' } ] }, plugins: [ // 请确保引入这个插件! new VueLoaderPlugin() ] }

手动配置方式的一个简易[demo](https://github.com/yusongjohn/anaylyze-vue-loader),来调试看看vue-loader做了哪些事情。
# 构建前后对比
这里相关的库的版本和我们当前分析的uniapp中用到的版本保持一致
```json
// devDependecnies
"vue-loader": "15.8.3",
"vue-template-compiler": "2.6.11",
// dependecnies
"vue": "2.6.11"

demo

  • App.vue:就是要分析vue文件是如何被构建的,当然需要一个vue文件啦 ```html

export default { name: "App", data() { return { msg: "hello vue + webpack", }; } }; #id { background: red; }

- main.js:应用入口的js文件,为了App.vue构建后是独立的文件(因为构建后小程序也是独立的文件 js,wxml,wxss等),通过异步引用进行代码分割。
```js
import Vue from 'vue'

import ('./App.vue'/* webpackChunkName: 'App' */).then((App) => {
    new Vue({
        render: h => h(App.default),
    }).$mount('#app')
})

html,应用入口

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <script src="./runtime~main.js"></script>
</head>
<body>
  <div id="app"></div>
  <script src="./main.js"></script>
</body>
</html>

webpack.config.js ``` const path = require('path'); const CopyPlugin = require("copy-webpack-plugin"); const {VueLoaderPlugin} = require('vue-loader'); const MiniCssExtractPlugin = require('mini-css-extract-plugin') const {CleanWebpackPlugin} = require('clean-webpack-plugin');

module.exports = { mode: 'development', entry: './src/main.js', output: { path: path.resolve(__dirname, 'dist') }, module: { rules: [ { test: /.vue/, // 注意 loader: 'vue-loader' }, { test: /.css

1. optimizatin.runtimeChunk 是为了将运行时拆分(不要被你当前不需要关注的内容干扰啊)
2. MiniCssExtractPlugin 是为了拆分css内容为单独文件
3. 另外就是vue-loader的配置

![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/da29b388669143de859369ec5b898179~tplv-k3u1fbpfcp-watermark.image?)

## 产物
### App.css
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aa9d0c5144cc45ab843d0f7ab9f66418~tplv-k3u1fbpfcp-watermark.image?)

### App.js
![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6c8f63c81f9b42a18aefda3e507694bb~tplv-k3u1fbpfcp-watermark.image?)

## 小结
`App.vue`本身是三段式内容,分别是`<template></template>`、`<script>`、`<style>`。

而当前的构建结果只有两个部分:`App.vue` -> `App.js` + `App.css`。

实际上`App.vue`中`template`部分也被构建到了`App.js`中了,这是因为`vue`是基于`render`函数来构造虚拟DOM,而后将虚拟DOM渲染到界面中的,`template`部分实际是被转为了`render`函数了(可以参考[vue@2.6.11 源码分析](https://juejin.cn/column/7192880378015645752))。

![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/13e8f94b647b4733a472ef1300263faf~tplv-k3u1fbpfcp-watermark.image?)

# vue-loader的整体流程
需要从webpack的构建流程讲起,可以参考[webpack@4.46.0 源码分析](https://juejin.cn/column/7161609931563466766)。

进入webpack流程后,首先是注册插件,即调用插件的`apply`方法,通过插件`apply`方法中会拿到`compiler`实例,然后通过`compilation.hook.xxx`去监听自己关心的事件,从而参与构建流程,但是VueLoaderPlugin这里不是这么做的,而是重写了`module.rules`。

下面看下VueLoaderPlugin的逻辑

## VueLoaderPlugin
```js
const id = 'vue-loader-plugin'
const NS = 'vue-loader'

class VueLoaderPlugin {
  apply (compiler) {
    // add NS marker so that the loader can detect and report missing plugin    
    // 第一步:找到 vue-loader,设置ident和options
    // use webpack's RuleSet utility to normalize user rules
    const rawRules = compiler.options.module.rules
    const { rules } = new RuleSet(rawRules) // 会将用户提供的规则标准化

    // find the rule that applies to vue files
    let vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue`))
    const vueRule = rules[vueRuleIndex]
    const vueUse = vueRule.use
    const vueLoaderUseIndex = vueUse.findIndex(u => {
      return /^vue-loader|(/|\|@)vue-loader/.test(u.loader)
    })
    
    const vueLoaderUse = vueUse[vueLoaderUseIndex]
    vueLoaderUse.ident = 'vue-loader-options'
    vueLoaderUse.options = vueLoaderUse.options || {}

    // 第二步:克隆除了vue-loader以外的其他规则
    // for each user rule (expect the vue rule), create a cloned rule
    // that targets the corresponding language blocks in *.vue files.
    const clonedRules = rules.filter(r => r !== vueRule).map(cloneRule)

    // global pitcher (responsible for injecting template compiler loader &amp; CSS post loader)
    const pitcher = {
      loader: require.resolve('./loaders/pitcher'),
      resourceQuery: query => {
        const parsed = qs.parse(query.slice(1))
        return parsed.vue != null
      },
      options: {
        cacheDirectory: vueLoaderUse.options.cacheDirectory,
        cacheIdentifier: vueLoaderUse.options.cacheIdentifier
      }
    }

    // replace original rules
    compiler.options.module.rules = [
      pitcher,
      ...clonedRules,
      ...rules
    ]
  }
}

VueLoaderPlugin.NS = NS
module.exports = VueLoaderPlugin

核心步骤如下:

找出应用在.vue文件上的rules,然后从找出vue-loader

vue-loader设置identoptions,目的是通过该标识来查找到对应的loader,并获取该loader上挂载的options,从而在不同的loader间进行信息传递,会在后面分析到。

克隆除了vue-loader以外的其他规则。为什么要克隆其他的所有规则呢?
因为vue文件的可能会包含多个,比如<template><style><script>,甚至是自定义块。而对于这些块比如style中的内容实际上应该要被用户提供的如css-loader等应用的,但是由于这些内容被嵌套在.vue文件中,并不能被用户提供的test规则匹配上。.vue文件经过vue-loader处理会产中间内容,比如会把style文件块转为如下形式,被克隆后的规则实际被添加了一个resourceQuery方法,该方法就是用来验证该块该不该应用用户提供的规则的,比如下面lang=css,实际就会应用用户提供的css相关的loader。所有cloneRule的作用就是将用户提供的rules尝试应用到.vue文件中的中。

import style0 from "./App.vue?vue&amp;type=style&amp;index=0&amp;id=4aa9bdb2&amp;prod&amp;lang=css&amp;"

创建一个pitching-loader,webpack中loader的类型和执行顺序,可以参考。这里需要知道pitching-loader会优先于normal-loader先执行,并且有熔断机制,一旦pitching-loader有内容返回,则后面的pitching-loader不会执行;转而执行上一个normal-loader

.

注意:新创建的pitcherresourceQuery,如下,只会匹配 ?vue....这样的路径

resourceQuery: query => {
  const parsed = qs.parse(query.slice(1))
  return parsed.vue != null
},

改写 module.rules,注意创建的pitching-loader位于第一个位置。注意,克隆的规则和原先的规则都是需要的,克隆的规则并不能代替原先的规则。因为克隆的规则仅仅针对.vue文件中的块的,但是项目中其他的文件如.css文件,依然是需要被处理的,此时还是用户自己提供的规则去处理。

小结

这部分的成果如下:

.
  1. 第一个pitcher匹配?vue...路径
  2. 第二个规则,是我们上面webpack.config.js中针对css的那个规则的克隆,引用在.vue文件中相应中,这里是<style />
  3. 后面的两个还是用户提供的规则(被规范化过了,开始new RuleSet()会做这个事情)

那下一步是什么,webpack会从entry配置项开始,我们这里是src/main.js,然后会找到依赖链上的所有模块并进行构建。这里忽略mian.js文件的构建,如果知道我们代码中的异步引用会自动将App.vue作为一个新的chunk就更好了,可以参考

下面看下App.vue文件的构建过程吧,首先会经过pitching阶段,由于这里没有匹配的pitching-loader,会从本地路径中读取该文件的内容(传递给第一个normal-loader哦,也就是我们下面vue-loader的入参source)进入normal阶段的执行,这里只有vue-loader,下面分析下vue-loader

vue-loader

处理 App.vue

module.exports = function (source) {
    const loaderContext = this;

    const {
        resourceQuery
    } = loaderContext

    const rawQuery = resourceQuery.slice(1)
    const inheritQuery = `&amp;${rawQuery}`

    //...

    const descriptor = parse({
        source,
        compiler: options.compiler || loadTemplateCompiler(loaderContext),
        filename,
        sourceRoot,
        needMap: sourceMap
    })

    // if the query has a type field, this is a language block request
    // e.g. foo.vue?type=template&amp;id=xxxxx
    // and we will return early
    if (incomingQuery.type) {
        // return selectBlock(...)
    }

    // template
    let templateImport = `var render, staticRenderFns`
    let templateRequest
    if (descriptor.template) {
        //...
    }

    // script
    let scriptImport = `var script = {}`
    if (descriptor.script) {
        //...
    }

    // styles
    let stylesCode = ``
    if (descriptor.styles.length) {
        //...
    }
    
    if (descriptor.customBlocks &amp;&amp; descriptor.customBlocks.length) {
        //...
    }

    //...
    return code
}

这里主要分为两种场景:query中是否有type

  • App.vue?type=template&id=xxxxx,query中有type,有type是则会走if(incomingQuery.type),并返回selectBlock(...)
  • App.vue,没有query(或者query中没有type),走后面的逻辑,清晰的看到后面的逻辑主要对descriptor进行处理,那descriptor是什么了,实际上就是.vue文件中的内容按照划分,每个块都有自己的内容,以App.vue为例,如下:
.

看到仅仅是获取每个块的内容,并没有进入每个块并进行处理,看到这里主要有四个关键的属性:scritpstyletemplatecustomBlock,最常用的就是前三个。

分别将三个标签的内容转化为如下:

import { render, staticRenderFns } from "./App.vue?vue&amp;type=template&amp;id=7ba5bd90&amp;"

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

import style0 from "./App.vue?vue&amp;type=style&amp;index=0&amp;lang=css&amp;"

这里还会注入一些运行时相关的逻辑,最终经过这部分处理,返回了如下内容

import { render, staticRenderFns } from "./App.vue?vue&amp;type=template&amp;id=7ba5bd90&amp;"
import script from "./App.vue?vue&amp;type=script&amp;lang=js&amp;"
export * from "./App.vue?vue&amp;type=script&amp;lang=js&amp;"
import style0 from "./App.vue?vue&amp;type=style&amp;index=0&amp;lang=css&amp;"


/* 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/App.vue"
export default component.exports

App.vue经过loader处理完后,会经过parser.parse来解析返回后的内容从而收集依赖。简单穿插下webpack这部分的大致逻辑,可以参考这里parser.parse的分析可以参考这里

// node_modules/webpack/lib/NormalModule.js

build(options, compilation, resolver, fs, callback) {
    //...
    this._source = null;
    this._ast = null;

    return this.doBuild(options, compilation, resolver, fs, err => {
        //...
        const result = this.parser.parse(this._ast || this._source.source(), { /*...*/ }, (err, result) => {/*...*/ });
    });
}

经过parser.parse处理完后,我们看下这部分有哪些依赖,这里会产生很多依赖,但大多数会被过滤掉(见webpack/lib/compilation.js中的processModuleDependencies方法),实际作为模块一步解析的是如下依赖(Dependency.request)

0: "./App.vue?vue&amp;type=template&amp;id=7ba5bd90&amp;"
1: "./App.vue?vue&amp;type=script&amp;lang=js&amp;"
2: "./App.vue?vue&amp;type=style&amp;index=0&amp;lang=css&amp;"
3: "!../node_modules/vue-loader/lib/runtime/componentNormalizer.js"

前面三个都会命中VueLoaderPlugin中创建的pitching-loader,和用户自己提供的vue-loader,先执行pitching-loader再执行vue-loader

pitching-loader: pitcher

// node_modules/vue-loader/lib/loaders/pitcher.js
module.exports.pitch = function (remainingRequest) {
    // 1. 获取当前模块的所有loaders,保存在loaderContext中,
    // 并过滤掉自己(pitcher)和eslint-loader
    // (因为整个vue文件应该是被eslint-loader处理了?),
    // 那在当前案例中就只剩下vue-loader了

    // 2. 将上一步过滤后的所有loaders,构造成内联loader形式,这里会区分type如script、template、style
}

内联loader,参考官方介绍inline loader,这里处理后的内联loader被添加了前缀-!

Prefixing with -! will disable all configured preLoaders and loaders but not postLoaders


./App.vue?vue&amp;type=template&amp;...为例,经过pitcher处理后的内容如下

export * from "-!../node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options
!../node_modules/vue-loader/lib/index.js??vue-loader-options
!./App.vue?vue&amp;type=template&amp;id=7ba5bd90&amp;"

注意,pitcher返回了内容,由于pitcher是第一个loader会结束当前模块的整个loader的执行,而后当前进入模块的依赖收集即进入webpack中的parser.parse()。然后会把上面pitcher返回的request作为新的模块进行解析。看到这里有两个loader

而是按照内联形式的loader的顺序和规则执行。

而后进入vue-loader的执行,由于./App.vue?vue&amp;type=template&amp;id=7ba5bd90&amp;中有type,因此会走下面:

// vue-loader/lib/index.js

if (incomingQuery.type) {
    return selectBlock(
        descriptor,
        loaderContext,
        incomingQuery,
        !!options.appendExtension
    )
}

selectBlock逻辑很简单,就是根据typedescriptor中的内容返回,比如template部分

<div id="app"> {{ msg }} </div>

然后返回的这部分内容会交给templateLoader.js处理,处理后的结果如下:

.

templateLoader 会在后面小节中单独分析


再看下:./App.vue?vue&amp;type=script&amp;...经过pitcher处理后的内容

import mod from "-!../node_modules/vue-loader/lib/index.js??vue-loader-options
!./App.vue?vue&amp;type=script&amp;lang=js&amp;"; 

export default mod; 
export * from "-!../node_modules/vue-loader/lib/index.js??vue-loader-options
!./App.vue?vue&amp;type=script&amp;lang=js&amp;"

由于我们当前demo中没有提供其他的处理jsloader,因此下次处理该模块时,会将.vue文件中script直接作为最终结果输出(注意: 模块化的处理是webpack内置能力,我们不需要关心),如下:

.

再看下:./App.vue?vue&amp;type=style&amp;...经过pitcher处理后的内容

import mod from "-!../node_modules/mini-css-extract-plugin/dist/loader.js
!../node_modules/css-loader/dist/cjs.js
!../node_modules/vue-loader/lib/loaders/stylePostLoader.js
!../node_modules/vue-loader/lib/index.js??vue-loader-options
!./App.vue?vue&amp;type=style&amp;index=0&amp;lang=css&amp;";

export default mod; export * from "-!../node_modules/mini-css-extract-plugin/dist/loader.js
!../node_modules/css-loader/dist/cjs.js
!../node_modules/vue-loader/lib/loaders/stylePostLoader.js
!../node_modules/vue-loader/lib/index.js??vue-loader-options
!./App.vue?vue&amp;type=style&amp;index=0&amp;lang=css&amp;"

.vue文件中style块,在当前案例中,会先后经过下面几个loader的处理

  1. vue-loader(->selectBlock直接返回原生内容)
  2. stylePostLoader.js
  3. css-loader
  4. mini-css-extract-plugin.loader

经过处理后的结果如下(被插件分离成单独的文件啦)

.

stylePostLoadercss-loadermini-css-extract-plugin 会在后面小节中单独分析。

构建后的产物的运行过程是怎样的

实际上构建过程中的中间内容,最终也都会被输出,我们当前案例中添加了soucemap配置,可以更清晰的验证这一点。

.

当然这些中间代码会被webpack再次处理(主要是模块化相关),因此看到App.js中定义了很多个模块,如下:

.

现在我们再来看看最终的产物是如何运行的吧,主要是引用关系。


index.html -> main.js -> App.js

main.js

// main.js
__webpack_require__.e(/*! import() | App */ "App").then(__webpack_require__.bind(null, /*! ./App.vue */ "./src/App.vue")).then((App) => {
  new vue__WEBPACK_IMPORTED_MODULE_0__["default"]({
      render: h => h(App.default),
  }).$mount('#app')
})

App.js "./src/App.vue"在App.js中定义的,如下: ``` // "./src/App.vue": /!*******************!
!** ./src/App.vue ***! *********************/ /! exports provided: default / /*/ (function(module, webpack_exports, webpack_require) {

"use strict"; 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_lang_css___WEBPACK_IMPORTED_MODULE_2_ = webpack_require(/! ./App.vue?vue&type=style&index=0&lang=css& / "./src/App.vue?vue&type=style&index=0&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 (false) { var api; } component.options.__file = "src/App.vue" / harmony default export */ webpack_exports["default"] = (component.exports);

/***/ }),

然后又会去引入其他模块(这些都是vue-loader生成的中间模块),就不往下继续了。如果你了解webpack自己的模块化机制,看到上述`__webpack_require__`等方法时就不会一脸懵逼,可以参考[这里](https://juejin.cn/post/7161621727905054734),粗略的介绍了webpack自己的模块化机制。

# 总结
补充下 !../node_modules/vue-loader/lib/runtime/componentNormalizer.js ,同样没有额外的处理js的loader,因此这个文件也是原始内容直接输出。 该文件的作用是标准化组件选项的,主要是挂载`render`和`staticRenderFns`,这两个方法来自`template`部分处理结果,`vue`运行时在创建虚拟DOM时依赖`render`方法(就是通过render方法来创建虚拟DOM的)

`vue-loader`作用大致过程:
1. `VueLoaderPlugin`,修改`module.rules`,注入`pitcher`和`克隆的rules`
2. `App.vue`首先会被`vue-loader`解析成如下内容
```js
import { render, staticRenderFns } from "./App.vue?vue&amp;type=template&amp;id=7ba5bd90&amp;"
import script from "./App.vue?vue&amp;type=script&amp;lang=js&amp;"
export * from "./App.vue?vue&amp;type=script&amp;lang=js&amp;"
import style0 from "./App.vue?vue&amp;type=style&amp;index=0&amp;lang=css&amp;"


/* 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/App.vue"
export default component.exports
  1. webpack会从上述内容中解析出依赖,并将这些依赖构造成模块,并进行解析
  • ./App.vue?vue&type=script&lang=js&
  • ./App.vue?vue&type=script&lang=js&
  • ./App.vue?vue&type=style&index=0&lang=css&
  • !../node_modules/vue-loader/lib/runtime/componentNormalizer.js

./App.vue?vue&amp;type=...会匹配到pitcher和vue-loader,但是由于pitcher有返回内容,此次的vue-loader并不会执行。

pitcher针对type=template/script/style返回的内容如下

type=template

export * from "-!../node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options
!../node_modules/vue-loader/lib/index.js??vue-loader-options
!./App.vue?vue&amp;type=template&amp;id=7ba5bd90&amp;"

type=script ```text import mod from "-!../node_modules/vue-loader/lib/index.js??vue-loader-options !./App.vue?vue&type=script&lang=js&";

export default mod; export * from "-!../node_modules/vue-loader/lib/index.js??vue-loader-options !./App.vue?vue&type=script&lang=js&"

- type=style
```text
import mod from "-!../node_modules/mini-css-extract-plugin/dist/loader.js
!../node_modules/css-loader/dist/cjs.js
!../node_modules/vue-loader/lib/loaders/stylePostLoader.js
!../node_modules/vue-loader/lib/index.js??vue-loader-options
!./App.vue?vue&amp;type=style&amp;index=0&amp;lang=css&amp;";

export default mod; export * from "-!../node_modules/mini-css-extract-plugin/dist/loader.js
!../node_modules/css-loader/dist/cjs.js
!../node_modules/vue-loader/lib/loaders/stylePostLoader.js
!../node_modules/vue-loader/lib/index.js??vue-loader-options
!./App.vue?vue&amp;type=style&amp;index=0&amp;lang=css&amp;"
  1. 而后会对上面的内容进行依赖解析收集依赖,并创建对应的模块,对新的模块进行解析,此时解析模块的loader主要来自内联路径中。经过这些内联loader的解析生成各个块的内容。

回复

我来回复
  • 暂无回复内容