利用 webpack stats.json 定位 @nrwl/react webpack 配置问题

利用 webpack stats.json 定位 @nrwl/react webpack 配置问题。

团队使用NX这一 monorepo 工具来搭建 React 应用。NX 基于 React 应用在 webpack 打包时添加了url-loader的相关配置。但是同事反馈该url-loader针对部分引用的图片文件不起作用。

url-loader,简而言之,可以将应用中引用到的一些资源文件(例如图片)转换成 base64 的数据格式,然后嵌入到我们的应用中(例如 HTML 的 img src, css 中的 url 函数),这样便无需针对该资源发起网络请求,节省请求资源。

以下是url-loader的配置举例:

1
2
3
4
5
6
7
8
{
  "test": "/\\.(png|jpe?g|gif|webp)$/",
  "loader": require.resolve("url-loader"),
  "options": {
    "limit": 10000, // 10kB
    "name": "[name].[hash:7].[ext]"
  }
}

上述配置的意思就是针对 10kB 以下大小的一些常见图片格式文件使用url-loader处理,否则使用 fallback loader 处理,url-loader默认的 fallback loader 是file-loader。超过大小的文件会被其处理,相应的 options 会传给file-loader,例如 name,最终处理后的文件名会包括 7 位 hash。

由于 NX 并不是写好 webpack 配置,再使用 webpack 指令进行打包。而是以Angular CLI builder的形式,引入 webpack 并进行代码编写,拥有高度定制化,因此一开始并没有细看其 builder 的源码,很难找到具体是什么文件不起作用,而哪部分又没有问题。

一开始同事与之前的一个应用对比,发现其打包的一部分图片产物被file-loader处理,没有最终文件;而一部分则有最终文件,但是 hash 位数为默认的 20 位而不是file-loader处理后的 7 位;最后一部分文件则位数正确。

通过一些比对后发现,同一个图片文件,如果在样式文件(例如.scss)中引用则都会生成 20 位 hash 的文件名,而在 j(t)sx 中则配置生效。

找到对应的文件后,便需要查找是哪个 loader 处理了该文件,最先想到的是直接输出对应的config.module配置,由于 NX React 应用使用的配置文件为@nrwl/react/plugins/webpack.js,编辑该文件,增加如下部分:

PS: 由于正则表达式 JSON 没有对应的表达形式,因此 loader 的test部分只会是一个{},不便于确认文件类型,因此可以使用其toString()方法作为JSON.stringfy()时的处理函数,以此直观显示匹配的文件类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const fs = require("fs");

RegExp.prototype.toJSON = RegExp.prototype.toString;

function getWebpackConfig(config) {
  // ...
  fs.writeFile(
    "./webpack-config.json",
    JSON.stringify(config.module),
    null,
    () => {}
  );
  return config;
}

module.exports = getWebpackConfig;

打印出来的 config 如下(仅提取匹配部分):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
{
  "rules": [
    {
      "test": "/\\.css$|\\.scss$|\\.sass$|\\.less$|\\.styl$/",
      "oneOf": [
        {
          "exclude": [
            "/Users/tianzhi/dev/nx-examples/libs/shared/styles/src/index.scss",
            "/Users/tianzhi/dev/nx-examples/libs/shared/header/index.scss",
            "/Users/tianzhi/dev/nx-examples/node_modules/normalize.css/normalize.css"
          ],
          "test": "/\\.scss$|\\.sass$/",
          "use": [
            {
              "loader": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/node_modules/style-loader/dist/index.js"
            },
            {
              "loader": "/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src/index.js",
              "options": { "ident": "embedded", "sourceMap": false }
            },
            {
              "loader": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/node_modules/sass-loader/dist/cjs.js",
              "options": {
                "implementation": {
                  "info": "dart-sass\t1.26.10\t(Sass Compiler)\t[Dart]\ndart2js\t2.8.4\t(Dart Compiler)\t[Dart]",
                  "types": {},
                  "NULL": {},
                  "TRUE": { "value": true },
                  "FALSE": { "value": false }
                },
                "sourceMap": false,
                "sassOptions": { "precision": 8, "includePaths": [] }
              }
            }
          ]
        }
      ]
    },
    {
      "test": "/\\.(png|jpe?g|gif|webp)$/",
      "loader": "/Users/tianzhi/dev/nx-examples/node_modules/url-loader/dist/cjs.js",
      "options": { "limit": 10000, "name": "[name].[hash:7].[ext]" }
    },
    {
      "test": "/\\.svg$/",
      "oneOf": [
        {
          "issuer": { "test": "/\\.[jt]sx?$/" },
          "use": [
            "@svgr/webpack?-svgo,+titleProp,+ref![path]",
            {
              "loader": "/Users/tianzhi/dev/nx-examples/node_modules/url-loader/dist/cjs.js",
              "options": {
                "limit": 10000,
                "name": "[name].[hash:7].[ext]",
                "esModule": false
              }
            }
          ]
        },
        {
          "use": [
            {
              "loader": "/Users/tianzhi/dev/nx-examples/node_modules/url-loader/dist/cjs.js",
              "options": { "limit": 10000, "name": "[name].[hash:7].[ext]" }
            }
          ]
        }
      ]
    }
  ]
}

可以看到针对.scss 文件处理的 loader 及顺序为:sass-loader -> postcss-loader -> style-loader

最疑惑的地方也就在这,一般情况下,我们使用了postcss-loader后还会使用css-loader进行处理,css-loader会解析@import以及url()语法,将其转化为import/require(),这样一来便能交给其他 loader 例如url-loader进行处理。而对于postcss-loader,是因为它的autoprefixer非常出名。

所以到此为止还是无法确认具体是哪个 loader 处理了url()语法以至于url-loader无法进行处理。但是可以确定的是答案就在这三个 loader 之一,由于这个时候还不了解postcss-loader的插件机制,我甚至怀疑是sass-loader的某个配置使得url()语法被提前解析。但是猜测始终不是办法,需要找到一个更确定的方法,能够知道对应文件在 loader 处理前后的产物文件。

如果能 debugging webpack 打包这一过程就好了,查找官网,还真发现有针对webpack 打包过程的 debug 方法,有两种方案,我选用了较为简单的日志方案:查看数据报告 stats.json 文件

webpack 使用--json指令可以输出对应文件,在 NX 中则是--statsJson

输出后的日志报告条目很多,关于更多条目可以参考官网介绍,这里只需要搜索对应文件,例如我这里是app.scss,会得到如下信息(仅提取有效信息):

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
{
  "chunks": [
    {
      "names": ["main"],
      "files": ["main.48c162c6863d37cba541.es5.js"],
      "hash": "48c162c6863d37cba541",
      "modules": [
        {
          "id": "/CXp",
          "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/node_modules/style-loader/dist/index.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src/index.js??embedded!/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/node_modules/sass-loader/dist/cjs.js??ref--5-oneOf-3-2!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.scss",
          "name": "./app/app.scss",
          "issuer": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.tsx",
          "issuerId": null,
          "issuerName": "./app/app.tsx",
          "issuerPath": [
            {
              "id": 0,
              "identifier": "multi /Users/tianzhi/dev/nx-examples/apps/cart/src/main.tsx",
              "name": "multi ./main.tsx"
            },
            {
              "id": null,
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/main.tsx",
              "name": "./main.tsx"
            },
            {
              "id": null,
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.tsx",
              "name": "./app/app.tsx"
            }
          ],
          "source": "var content = require(\"!!../../../../node_modules/postcss-loader/src/index.js??embedded!../../../../node_modules/@nrwl/web/node_modules/sass-loader/dist/cjs.js??ref--5-oneOf-3-2!./app.scss\");\n\nif (typeof content === 'string') {\n  content = [[module.id, content, '']];\n}\n\nvar options = {}\n\noptions.insert = \"head\";\noptions.singleton = false;\n\nvar update = require(\"!../../../../node_modules/@nrwl/web/node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\")(content, options);\n\nif (content.locals) {\n  module.exports = content.locals;\n}\n"
        },
        {
          "id": "GAnJ",
          "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js",
          "name": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js",
          "issuer": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/node_modules/style-loader/dist/index.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src/index.js??embedded!/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/node_modules/sass-loader/dist/cjs.js??ref--5-oneOf-3-2!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.scss",
          "issuerId": "/CXp",
          "issuerName": "./app/app.scss",
          "issuerPath": [
            {
              "id": 0,
              "identifier": "multi /Users/tianzhi/dev/nx-examples/apps/cart/src/main.tsx",
              "name": "multi ./main.tsx"
            },
            {
              "id": null,
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/main.tsx",
              "name": "./main.tsx"
            },
            {
              "id": null,
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.tsx",
              "name": "./app/app.tsx"
            },
            {
              "id": "/CXp",
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/node_modules/style-loader/dist/index.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src/index.js??embedded!/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/node_modules/sass-loader/dist/cjs.js??ref--5-oneOf-3-2!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.scss",
              "name": "./app/app.scss"
            }
          ],
          "source": "..."
        },
        {
          "id": null,
          "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.tsx",
          "name": "./app/app.tsx",
          "issuer": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/main.tsx",
          "issuerId": null,
          "issuerName": "./main.tsx",
          "issuerPath": [
            {
              "id": 0,
              "identifier": "multi /Users/tianzhi/dev/nx-examples/apps/cart/src/main.tsx",
              "name": "multi ./main.tsx"
            },
            {
              "id": null,
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/main.tsx",
              "name": "./main.tsx"
            }
          ],
          "source": "... import './app.scss';\n ..."
        },
        {
          "id": "aZ7I",
          "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src/index.js??embedded!/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/node_modules/sass-loader/dist/cjs.js??ref--5-oneOf-3-2!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.scss",
          "name": "/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src??embedded!/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/node_modules/sass-loader/dist/cjs.js??ref--5-oneOf-3-2!./app/app.scss",
          "issuer": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/node_modules/style-loader/dist/index.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src/index.js??embedded!/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/node_modules/sass-loader/dist/cjs.js??ref--5-oneOf-3-2!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.scss",
          "issuerId": "/CXp",
          "issuerName": "./app/app.scss",
          "issuerPath": [
            {
              "id": 0,
              "identifier": "multi /Users/tianzhi/dev/nx-examples/apps/cart/src/main.tsx",
              "name": "multi ./main.tsx"
            },
            {
              "id": null,
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/main.tsx",
              "name": "./main.tsx"
            },
            {
              "id": null,
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.tsx",
              "name": "./app/app.tsx"
            },
            {
              "id": "/CXp",
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/node_modules/style-loader/dist/index.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src/index.js??embedded!/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/node_modules/sass-loader/dist/cjs.js??ref--5-oneOf-3-2!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.scss",
              "name": "./app/app.scss"
            }
          ],
          "assets": [
            "banner2.50acf29cb9b5f2021724.png",
            "arrow-right.d9910f497ca75d78bcee.svg",
            "add-big@2x.ebff90ff08575204ca08.png"
          ],
          "source": "module.exports = \".image-png{background-image:url('add-big@2x.ebff90ff08575204ca08.png')}.image-svg{background-image:url('arrow-right.d9910f497ca75d78bcee.svg')}.image-big-png{background-image:url('banner2.50acf29cb9b5f2021724.png')}\""
        }
      ]
    }
  ]
}

先来说几个条目概念:

  1. chunks是我们这次打包的 chunk 列表,每个 chunk 里的modules对应组成该 chunk 的 module 列表
  2. identifier为模块内部唯一标识
  3. issuer为该模块的引用来源
  4. source为模块的 stringfy 后的源码
  5. assets为该模块包含的静态资源

依照issuer,我们可以得到调用顺序为:

  • app.tsx
    • /CXp(style-loader!postcss-loader!sass-loader!app.scss)
      • GAnJ(style-loader/injectStylesIntoStyleTag.js)
      • aZ7I(postcss-loader!sass-loader!app.scss)

可以理解为:app.tsximport './app.scss'被匹配并解析为内部模块/CXp,该模块源码sourceformat 后为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
var content = require("!!../../../../node_modules/postcss-loader/src/index.js??embedded!../../../../node_modules/@nrwl/web/node_modules/sass-loader/dist/cjs.js??ref--5-oneOf-3-2!./app.scss");

if (typeof content === "string") {
  content = [[module.id, content, ""]];
}

var options = {};

options.insert = "head";
options.singleton = false;

var update =
  require("!../../../../node_modules/@nrwl/web/node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js")(
    content,
    options
  );

if (content.locals) {
  module.exports = content.locals;
}

不难发现,该模块又引用了内部模块aZ7I和第三方style-loader的模块GAnJ

第三方style-loader的模块GAnJ,也就是injectStylesIntoStyleTag.js的作用正如名字所述,会将最终样式产物插入到 HTML 的 style 标签。我们只需要继续分析生成的内部模块aZ7I,其source如下:

1
2
module.exports =
  ".image-png{background-image:url('add-big@2x.ebff90ff08575204ca08.png')}.image-svg{background-image:url('arrow-right.d9910f497ca75d78bcee.svg')}.image-big-png{background-image:url('banner2.50acf29cb9b5f2021724.png')}";

没有再次引用,仅为最终样式的字符串,将其提取为 CSS:

1
2
3
4
5
6
7
8
9
.image-png {
  background-image: url("add-big@2x.ebff90ff08575204ca08.png");
}
.image-svg {
  background-image: url("arrow-right.d9910f497ca75d78bcee.svg");
}
.image-big-png {
  background-image: url("banner2.50acf29cb9b5f2021724.png");
}

就是源码中的三个url()调用,但是里面的路径已经被解析,引用文件名包含 webpack 默认的 20 位 hash 而不是url-loader配置中的 7 位,而且前两个图片文件大小均小于 10KB,本应该被url-loader转换为 base64 格式。

看到这里,其实仅仅知道url()被经过模块aZ7I后被更改,aZ7I的标识字段为/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src/index.js??embedded!/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/node_modules/sass-loader/dist/cjs.js??ref--5-oneOf-3-2!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.scss

将标识以!分隔后,得到.../postcss-loader/src/index.js??embedded.../sass-loader/dist/cjs.js??ref--5-oneOf-3-2以及app.scss,从标识符中我们可以知道 webpack 处理 app.scss文件使用的 loader 及顺序,即经过sass-loader处理后再由postcss-loader处理形成了上述产物。

这里我有一个疑问:

为什么 webpack 没有把过程继续拆分,也就是把上述模块再细化成两个模块,多出的一个模块则单独为sass-loader处理后的产物。如果能够细分,则能知道具体是哪个 loader 处理了url()

我暂时没找到这个问题的答案,所以继续查找。

先是去搜索sass-loader,发现其并没有处理url();然后是postcss-loader,发现了其支持第三方插件,在这之前我还提了一个issue,通过逐步寻找,发现postcss-url插件会处理url(),但是我在 NX 的依赖中却没找到它。

知道插件机制后,我同时也开始阅读 NX 源码的 style loader 配置部分,终于发现原来是它们自己创建了一个插件用来处理@importurl(),相当于替代了css-loader。但是针对 react 的 webpack 配置中引入url-laoder时却没有考虑这部分,导致图片等文件无法再被url-loader处理。

尽管这个问题的影响没有那么大,但是一定要避免对同一个大于 10KB 的图片文件同时在样式文件和 j(t)sx 文件中引用,这样会导致同一个文件产生两份产出,一个文件名 7 位 hash,另一个则是 20 位 hash,这会导致针对同一个文件发起两次网络请求,如果文件较多将会浪费很多资源。而且这确实也会让人费解,因此我提出了一个issue用于追踪。

但是鉴于 NX 关于 style 部分的 webpack 配置是集成在定制化的 webpack builder 代码文件中,而url-loader等配置却是 react 单独的,估计 NX 也不好修改这部分。

给团队的临时解决方案是:

  1. 如果想使用url-loader处理所有图片文件,尽量使用<img />标签而不是url()来引入图片
  2. 如果并不一定要使用url-loader处理,可以使用绝对路径前缀例如/assets/xxx引用,同时配置assets选项,个人认为,这才是assets配置的正确使用方式
  3. 最后还是一定要避免对同一个大于 10KB 的图片文件同时在样式文件和 j(t)sx 文件中引用

给出方案后,问题其实可以被较好地解决,但是我发现 NX 使用postcss-loader,除了最常见的 autoprefixer,还做的两件事就是使用社区的postcss-import来处理@import,以及使用自己写的postcss-cli-resources来处理url()

其实社区处理url()也有一个插件postcss-url,暂时无法得知为什么 NX 需要自己创建一个插件来处理。

做的这三件事中,url()@import也可以交给css-loader处理,不过 NX 传入了一些路径参数,如果这样替换,我们便无法正常使用这些参数,例如同时开启rebaseRootRelativeCssUrlsdeployUrl后,NX 会在打包时将deployUrl作为url()声明路径的前缀。

但是为了继续探索url-loader正常工作下这些文件解析的产物和顺序,我还是对配置进行了一些更改,主要包括覆盖官方的postcss-loader配置,仅使用其 autoprefixer 功能,以及在其后添加css-loader,完成后输出的stats.json如下(仅提取有效信息):

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
{
  "chunks": [
    {
      "names": ["main"],
      "files": ["main.413a212ab2c713a5f9c4.es5.js"],
      "hash": "413a212ab2c713a5f9c4",
      "modules": [
        {
          "id": "/CXp",
          "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/style-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src/index.js??ref--8-2!/Users/tianzhi/dev/nx-examples/node_modules/sass-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.scss",
          "name": "./app/app.scss",
          "issuer": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.tsx",
          "issuerId": null,
          "issuerName": "./app/app.tsx"
        },
        {
          "id": "JDPv",
          "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/url-loader/dist/cjs.js??ref--7-oneOf-1-0!/Users/tianzhi/dev/nx-examples/apps/cart/src/assets/arrow-right.svg",
          "name": "./assets/arrow-right.svg",
          "issuer": "/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src/index.js??ref--8-2!/Users/tianzhi/dev/nx-examples/node_modules/sass-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.scss",
          "issuerId": "V1gc",
          "issuerName": "/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src??ref--8-2!/Users/tianzhi/dev/nx-examples/node_modules/sass-loader/dist/cjs.js!./app/app.scss",
          "issuerPath": [
            {
              "id": 0,
              "identifier": "multi /Users/tianzhi/dev/nx-examples/apps/cart/src/main.tsx",
              "name": "multi ./main.tsx"
            },
            {
              "id": null,
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/main.tsx",
              "name": "./main.tsx"
            },
            {
              "id": null,
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.tsx",
              "name": "./app/app.tsx"
            },
            {
              "id": "/CXp",
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/style-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src/index.js??ref--8-2!/Users/tianzhi/dev/nx-examples/node_modules/sass-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.scss",
              "name": "./app/app.scss"
            },
            {
              "id": "V1gc",
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src/index.js??ref--8-2!/Users/tianzhi/dev/nx-examples/node_modules/sass-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.scss",
              "name": "/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src??ref--8-2!/Users/tianzhi/dev/nx-examples/node_modules/sass-loader/dist/cjs.js!./app/app.scss"
            }
          ],
          "source": "export default \"data:image/svg+xml;base64,...\""
        },
        {
          "id": "LPAU",
          "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js",
          "name": "/Users/tianzhi/dev/nx-examples/node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js",
          "issuerId": "/CXp",
          "issuerName": "./app/app.scss"
        },
        {
          "id": null,
          "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.tsx",
          "name": "./app/app.tsx",
          "issuer": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/main.tsx"
        },
        {
          "id": "V1gc",
          "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src/index.js??ref--8-2!/Users/tianzhi/dev/nx-examples/node_modules/sass-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.scss",
          "name": "/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src??ref--8-2!/Users/tianzhi/dev/nx-examples/node_modules/sass-loader/dist/cjs.js!./app/app.scss",
          "issuer": "/Users/tianzhi/dev/nx-examples/node_modules/style-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src/index.js??ref--8-2!/Users/tianzhi/dev/nx-examples/node_modules/sass-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.scss",
          "issuerId": "/CXp",
          "issuerName": "./app/app.scss",
          "issuerPath": [
            {
              "id": 0,
              "identifier": "multi /Users/tianzhi/dev/nx-examples/apps/cart/src/main.tsx",
              "name": "multi ./main.tsx"
            },
            {
              "id": null,
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/main.tsx",
              "name": "./main.tsx"
            },
            {
              "id": null,
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.tsx",
              "name": "./app/app.tsx"
            },
            {
              "id": "/CXp",
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/style-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src/index.js??ref--8-2!/Users/tianzhi/dev/nx-examples/node_modules/sass-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.scss",
              "name": "./app/app.scss"
            }
          ],
          "source": "// Imports\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../../node_modules/css-loader/dist/runtime/api.js\";\nimport ___CSS_LOADER_GET_URL_IMPORT___ from \"../../../../node_modules/css-loader/dist/runtime/getUrl.js\";\nimport ___CSS_LOADER_URL_IMPORT_0___ from \"../assets/add-big@2x.png\";\nimport ___CSS_LOADER_URL_IMPORT_1___ from \"../assets/arrow-right.svg\";\nimport ___CSS_LOADER_URL_IMPORT_2___ from \"../assets/banner2.png\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(true);\nvar ___CSS_LOADER_URL_REPLACEMENT_0___ = ___CSS_LOADER_GET_URL_IMPORT___(___CSS_LOADER_URL_IMPORT_0___);\nvar ___CSS_LOADER_URL_REPLACEMENT_1___ = ___CSS_LOADER_GET_URL_IMPORT___(___CSS_LOADER_URL_IMPORT_1___);\nvar ___CSS_LOADER_URL_REPLACEMENT_2___ = ___CSS_LOADER_GET_URL_IMPORT___(___CSS_LOADER_URL_IMPORT_2___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \".image-png{background-image:url(\" + ___CSS_LOADER_URL_REPLACEMENT_0___ + \")}.image-svg{background-image:url(\" + ___CSS_LOADER_URL_REPLACEMENT_1___ + \")}.image-big-png{background-image:url(\" + ___CSS_LOADER_URL_REPLACEMENT_2___ + \")}.example>.example-inner{font-size:14px}\", \"\",{\"version\":3,\"sources\":[\"webpack://app/app.scss\"],\"names\":[],\"mappings\":\"AAAA,WAAW,wDAAgD,CAAC,WAAW,wDAAiD,CAAC,eAAe,wDAA6C,CAAC,wBAAwB,cAAc\",\"sourcesContent\":[\".image-png{background-image:url(\\\"../assets/add-big@2x.png\\\")}.image-svg{background-image:url(\\\"../assets/arrow-right.svg\\\")}.image-big-png{background-image:url(\\\"../assets/banner2.png\\\")}.example>.example-inner{font-size:14px}\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n"
        },
        {
          "id": "VNgF",
          "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/runtime/api.js",
          "name": "/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/runtime/api.js",
          "issuer": "/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src/index.js??ref--8-2!/Users/tianzhi/dev/nx-examples/node_modules/sass-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.scss",
          "issuerId": "V1gc",
          "issuerName": "/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src??ref--8-2!/Users/tianzhi/dev/nx-examples/node_modules/sass-loader/dist/cjs.js!./app/app.scss",
          "issuerPath": [
            {
              "id": 0,
              "identifier": "multi /Users/tianzhi/dev/nx-examples/apps/cart/src/main.tsx",
              "name": "multi ./main.tsx"
            },
            {
              "id": null,
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/main.tsx",
              "name": "./main.tsx"
            },
            {
              "id": null,
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.tsx",
              "name": "./app/app.tsx"
            },
            {
              "id": "/CXp",
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/style-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src/index.js??ref--8-2!/Users/tianzhi/dev/nx-examples/node_modules/sass-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.scss",
              "name": "./app/app.scss"
            },
            {
              "id": "V1gc",
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src/index.js??ref--8-2!/Users/tianzhi/dev/nx-examples/node_modules/sass-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.scss",
              "name": "/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src??ref--8-2!/Users/tianzhi/dev/nx-examples/node_modules/sass-loader/dist/cjs.js!./app/app.scss"
            }
          ],
          "source": "..."
        },
        {
          "id": "m1aJ",
          "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/url-loader/dist/cjs.js??ref--6!/Users/tianzhi/dev/nx-examples/apps/cart/src/assets/banner2.png",
          "name": "./assets/banner2.png",
          "issuer": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.tsx",
          "issuerId": null,
          "issuerName": "./app/app.tsx",
          "issuerPath": [
            {
              "id": 0,
              "identifier": "multi /Users/tianzhi/dev/nx-examples/apps/cart/src/main.tsx",
              "name": "multi ./main.tsx"
            },
            {
              "id": null,
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/main.tsx",
              "name": "./main.tsx"
            },
            {
              "id": null,
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.tsx",
              "name": "./app/app.tsx"
            }
          ],
          "source": "export default __webpack_public_path__ + \"banner2.e11abd9.png\";"
        },
        {
          "id": "q/iR",
          "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/runtime/getUrl.js",
          "name": "/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/runtime/getUrl.js",
          "issuer": "/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src/index.js??ref--8-2!/Users/tianzhi/dev/nx-examples/node_modules/sass-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.scss",
          "issuerId": "V1gc",
          "issuerName": "/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src??ref--8-2!/Users/tianzhi/dev/nx-examples/node_modules/sass-loader/dist/cjs.js!./app/app.scss",
          "issuerPath": [
            {
              "id": 0,
              "identifier": "multi /Users/tianzhi/dev/nx-examples/apps/cart/src/main.tsx",
              "name": "multi ./main.tsx"
            },
            {
              "id": null,
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/main.tsx",
              "name": "./main.tsx"
            },
            {
              "id": null,
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/@nrwl/web/src/utils/web-babel-loader.js??ref--4!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.tsx",
              "name": "./app/app.tsx"
            },
            {
              "id": "/CXp",
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/style-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src/index.js??ref--8-2!/Users/tianzhi/dev/nx-examples/node_modules/sass-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.scss",
              "name": "./app/app.scss"
            },
            {
              "id": "V1gc",
              "identifier": "/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src/index.js??ref--8-2!/Users/tianzhi/dev/nx-examples/node_modules/sass-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/apps/cart/src/app/app.scss",
              "name": "/Users/tianzhi/dev/nx-examples/node_modules/css-loader/dist/cjs.js!/Users/tianzhi/dev/nx-examples/node_modules/postcss-loader/src??ref--8-2!/Users/tianzhi/dev/nx-examples/node_modules/sass-loader/dist/cjs.js!./app/app.scss"
            }
          ],
          "source": "..."
        }
      ]
    }
  ]
}

解读之前,先来看app.scss源文件和app.tsx文件(仅提取引用信息):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$font: 14px;

.image-svg {
  background-image: url("../assets/arrow-right.svg");
}
.image-big-png {
  background-image: url("../assets/banner2.png");
}

.example {
  & > .example-inner {
    font-size: $font;
  }
}
1
2
3
import "./app.scss";
import banner from "../assets/banner2.png";
import arrow from "../assets/arrow-right.svg";

其中,arrow-right.svg体积小于 10KB,而banner2.png大于 10KB。

解析顺序为:

  • app.tsx
    • m1aJ(banner2.png)
    • /CXp(style-loader!css-loader!postcss-loader!sass-loader!app.scss)
      • LPAU(style-loader/injectStylesIntoStyleTag.js)
      • V1gc(css-loader!postcss-loader!sass-loader!app.scss)
        • JDPv(arrow-right.svg)
        • VNgF(css-loader/api.js)
        • q/iR(css-loader/getUrl.js)

可以看到,banner2.png先在app.tsx中被解析,而arrow-right.svg却在app.scss中被解析,关于原因,暂时也没有深入研究,如果有理解的同学欢迎留言。

跟前面一样,import './app.scss'被匹配解析为内部模块/CXp(style-loader!css-loader!postcss-loader!sass-loader!app.scss),其sourceformat 后和前面一致,唯一不同的是引用的子模块多了一个css-loader,对子模块V1gc(css-loader!postcss-loader!sass-loader!app.scss)source进行 format 后得到:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Imports
import ___CSS_LOADER_API_IMPORT___ from "../../../../node_modules/css-loader/dist/runtime/api.js";
import ___CSS_LOADER_GET_URL_IMPORT___ from "../../../../node_modules/css-loader/dist/runtime/getUrl.js";
import ___CSS_LOADER_URL_IMPORT_0___ from "../assets/arrow-right.svg";
import ___CSS_LOADER_URL_IMPORT_1___ from "../assets/banner2.png";
var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(true);
var ___CSS_LOADER_URL_REPLACEMENT_0___ = ___CSS_LOADER_GET_URL_IMPORT___(
  ___CSS_LOADER_URL_IMPORT_0___
);
var ___CSS_LOADER_URL_REPLACEMENT_1___ = ___CSS_LOADER_GET_URL_IMPORT___(
  ___CSS_LOADER_URL_IMPORT_1___
);
// Module
___CSS_LOADER_EXPORT___.push([
  module.id,
  ".image-svg{background-image:url(" +
    ___CSS_LOADER_URL_REPLACEMENT_0___ +
    ")}.image-big-png{background-image:url(" +
    ___CSS_LOADER_URL_REPLACEMENT_1___ +
    ")}.example>.example-inner{font-size:14px}",
  "",
  {
    version: 3,
    sources: ["webpack://app/app.scss"],
    names: [],
    mappings:
      "AAAA,WAAW,wDAAgD,CAAC,WAAW,wDAAiD,CAAC,eAAe,wDAA6C,CAAC,wBAAwB,cAAc",
    sourcesContent: [
      '.image-svg{background-image:url("../assets/arrow-right.svg")}.image-big-png{background-image:url("../assets/banner2.png")}.example>.example-inner{font-size:14px}',
    ],
    sourceRoot: "",
  },
]);
// Exports
export default ___CSS_LOADER_EXPORT___;

可以清晰看到,除了引用两个自身 runtime 模块外,还引用了两个图片,由于banner2.png事先已被解析,因此这里只需要解析arrow-right.svg

拆分banner2.pngarrow-right.svgidentifier可以得出它们解析时使用的 loader:

  1. banner2.pngidentifier为:/Users/tianzhi/dev/nx-examples/node_modules/url-loader/dist/cjs.js??ref--6!/Users/tianzhi/dev/nx-examples/apps/cart/src/assets/banner2.png

  2. arrow-right.svgidentifier为:/Users/tianzhi/dev/nx-examples/node_modules/url-loader/dist/cjs.js??ref--7-oneOf-1-0!/Users/tianzhi/dev/nx-examples/apps/cart/src/assets/arrow-right.svg

两者都使用了url-loader进行处理,不过要注意的是,banner2.png其实使用的是url-loader的默认 fallback loader 也就是file-loader处理。可以看到banner2.png模块的source为:

export default __webpack_public_path__ + \"banner2.e11abd9.png\";

arrow-right.svg模块则为:

export default \"data:image/svg+xml;base64,...\"

这次踩坑之旅总体来说效率不算高,花了一些时间,主要是因为自己不熟悉postcss-loader的插件机制以及 NX 的源码结构。而且最后也遗留了几个问题,只算是对 webpack debugging 的一次初级入门,希望今后能掌握第二种DevTools的方式,同时解决这些遗留问题。

相关内容