利用 stats.json 定位 @nrwl/react webpack 配置問題

背景

團隊使用NX這一 monorepo 工具來搭建 React 應用。NX 基於 React 應用在 webpack 打包時添加了url-loader的相關配置。可是同事反饋該url-loader針對部分引用的圖片文件不起做用。css

定位

url-loader 做用

url-loader,簡而言之,能夠將應用中引用到的一些資源文件(例如圖片)轉換成 base64 的數據格式,而後嵌入到咱們的應用中(例如 HTML 的 img src, css 中的 url 函數),這樣便無需針對該資源發起網絡請求,節省請求資源。node

如下是url-loader的配置舉例:react

{
  "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。webpack

具體針對哪部分文件不起做用?

因爲 NX 並非寫好 webpack 配置,再使用 webpack 指令進行打包。而是以Angular CLI builder的形式,引入 webpack 並進行代碼編寫,擁有高度定製化,所以一開始並無細看其 builder 的源碼,很難找到具體是什麼文件不起做用,而哪部分又沒有問題。git

一開始同事與以前的一個應用對比,發現其打包的一部分圖片產物被file-loader處理,沒有最終文件;而一部分則有最終文件,可是 hash 位數爲默認的 20 位而不是file-loader處理後的 7 位;最後一部分文件則位數正確。github

經過一些比對後發現,同一個圖片文件,若是在樣式文件(例如.scss)中引用則都會生成 20 位 hash 的文件名,而在 j(t)sx 中則配置生效。web

查看 webpack config,尤爲是樣式文件那部分

找到對應的文件後,便須要查找是哪一個 loader 處理了該文件,最早想到的是直接輸出對應的config.module配置,因爲 NX React 應用使用的配置文件爲@nrwl/react/plugins/webpack.js,編輯該文件,增長以下部分:正則表達式

PS: 因爲正則表達式 JSON 沒有對應的表達形式,所以 loader 的test部分只會是一個{},不便於確認文件類型,所以可使用其toString()方法做爲JSON.stringfy()時的處理函數,以此直觀顯示匹配的文件類型。json

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 以下(僅提取匹配部分):api

{
  "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的方式,同時解決這些遺留問題。

相關文章
相關標籤/搜索