美文网首页
利用 stats.json 定位 @nrwl/react web

利用 stats.json 定位 @nrwl/react web

作者: 小志Chris | 来源:发表于2020-12-06 18:22 被阅读0次

    背景

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

    定位

    url-loader 作用

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

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

    {
      "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 中则配置生效。

    查看 webpack config,尤其是样式文件那部分

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

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

    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 如下(仅提取匹配部分):

    {
      "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 处理前后的产物文件。

    使用 stats.json 定位问题

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

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

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

    {
      "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 后为:

    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如下:

    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:

    .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如下(仅提取有效信息):

    {
      "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文件(仅提取引用信息):

    $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;
      }
    }
    
    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后得到:

    // 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的方式,同时解决这些遗留问题。

    本篇文章由一文多发平台ArtiPub自动发布

    相关文章

      网友评论

          本文标题:利用 stats.json 定位 @nrwl/react web

          本文链接:https://www.haomeiwen.com/subject/kbzowktx.html