背景
團(tuán)隊(duì)使用NX這一 monorepo 工具來(lái)搭建 React 應(yīng)用。NX 基于 React 應(yīng)用在 webpack 打包時(shí)添加了url-loader
的相關(guān)配置。但是同事反饋該url-loader
針對(duì)部分引用的圖片文件不起作用。
定位
url-loader 作用
url-loader
,簡(jiǎn)而言之,可以將應(yīng)用中引用到的一些資源文件(例如圖片)轉(zhuǎn)換成 base64 的數(shù)據(jù)格式,然后嵌入到我們的應(yīng)用中(例如 HTML 的 img src, css 中的 url 函數(shù))惠昔,這樣便無(wú)需針對(duì)該資源發(fā)起網(wǎng)絡(luò)請(qǐng)求,節(jié)省請(qǐng)求資源挑势。
以下是url-loader
的配置舉例:
{
"test": "/\\.(png|jpe?g|gif|webp)$/",
"loader": require.resolve("url-loader"),
"options": {
"limit": 10000, // 10kB
"name": "[name].[hash:7].[ext]"
}
}
上述配置的意思就是針對(duì) 10kB 以下大小的一些常見(jiàn)圖片格式文件使用url-loader
處理镇防,否則使用 fallback loader 處理,url-loader
默認(rèn)的 fallback loader 是file-loader
潮饱。超過(guò)大小的文件會(huì)被其處理营罢,相應(yīng)的 options 會(huì)傳給file-loader
,例如 name,最終處理后的文件名會(huì)包括 7 位 hash饲漾。
具體針對(duì)哪部分文件不起作用蝙搔?
由于 NX 并不是寫(xiě)好 webpack 配置,再使用 webpack 指令進(jìn)行打包考传。而是以Angular CLI builder的形式吃型,引入 webpack 并進(jìn)行代碼編寫(xiě)植袍,擁有高度定制化酗钞,因此一開(kāi)始并沒(méi)有細(xì)看其 builder 的源碼,很難找到具體是什么文件不起作用啸臀,而哪部分又沒(méi)有問(wèn)題泉褐。
一開(kāi)始同事與之前的一個(gè)應(yīng)用對(duì)比赐写,發(fā)現(xiàn)其打包的一部分圖片產(chǎn)物被file-loader
處理,沒(méi)有最終文件膜赃;而一部分則有最終文件挺邀,但是 hash 位數(shù)為默認(rèn)的 20 位而不是file-loader
處理后的 7 位;最后一部分文件則位數(shù)正確跳座。
通過(guò)一些比對(duì)后發(fā)現(xiàn)端铛,同一個(gè)圖片文件,如果在樣式文件(例如.scss)中引用則都會(huì)生成 20 位 hash 的文件名疲眷,而在 j(t)sx 中則配置生效禾蚕。
查看 webpack config,尤其是樣式文件那部分
找到對(duì)應(yīng)的文件后狂丝,便需要查找是哪個(gè) loader 處理了該文件换淆,最先想到的是直接輸出對(duì)應(yīng)的config.module
配置,由于 NX React 應(yīng)用使用的配置文件為@nrwl/react/plugins/webpack.js
几颜,編輯該文件产舞,增加如下部分:
PS: 由于正則表達(dá)式 JSON 沒(méi)有對(duì)應(yīng)的表達(dá)形式,因此 loader 的test
部分只會(huì)是一個(gè){}
菠剩,不便于確認(rèn)文件類(lèi)型,因此可以使用其toString()
方法作為JSON.stringfy()
時(shí)的處理函數(shù)耻煤,以此直觀(guān)顯示匹配的文件類(lèi)型具壮。
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;
打印出來(lái)的 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]" }
}
]
}
]
}
]
}
可以看到針對(duì).scss 文件處理的 loader 及順序?yàn)椋?code>sass-loader -> postcss-loader
-> style-loader
。
最疑惑的地方也就在這哈蝇,一般情況下棺妓,我們使用了postcss-loader
后還會(huì)使用css-loader
進(jìn)行處理,css-loader
會(huì)解析@import
以及url()
語(yǔ)法炮赦,將其轉(zhuǎn)化為import/require()
怜跑,這樣一來(lái)便能交給其他 loader 例如url-loader
進(jìn)行處理。而對(duì)于postcss-loader
,是因?yàn)樗?a target="_blank">autoprefixer非常出名性芬。
所以到此為止還是無(wú)法確認(rèn)具體是哪個(gè) loader 處理了url()
語(yǔ)法以至于url-loader
無(wú)法進(jìn)行處理峡眶。但是可以確定的是答案就在這三個(gè) loader 之一,由于這個(gè)時(shí)候還不了解postcss-loader
的插件機(jī)制植锉,我甚至懷疑是sass-loader
的某個(gè)配置使得url()
語(yǔ)法被提前解析辫樱。但是猜測(cè)始終不是辦法,需要找到一個(gè)更確定的方法俊庇,能夠知道對(duì)應(yīng)文件在 loader 處理前后的產(chǎn)物文件狮暑。
使用 stats.json 定位問(wèn)題
如果能 debugging webpack 打包這一過(guò)程就好了,查找官網(wǎng)辉饱,還真發(fā)現(xiàn)有針對(duì)webpack 打包過(guò)程的 debug 方法搬男,有兩種方案,我選用了較為簡(jiǎn)單的日志方案:查看數(shù)據(jù)報(bào)告 stats.json 文件
webpack 使用--json
指令可以輸出對(duì)應(yīng)文件彭沼,在 NX 中則是--statsJson
輸出后的日志報(bào)告條目很多缔逛,關(guān)于更多條目可以參考官網(wǎng)介紹,這里只需要搜索對(duì)應(yīng)文件溜腐,例如我這里是app.scss
译株,會(huì)得到如下信息(僅提取有效信息):
{
"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')}\""
}
]
}
]
}
先來(lái)說(shuō)幾個(gè)條目概念:
-
chunks
是我們這次打包的 chunk 列表,每個(gè) chunk 里的modules
對(duì)應(yīng)組成該 chunk 的 module 列表 -
identifier
為模塊內(nèi)部唯一標(biāo)識(shí) -
issuer
為該模塊的引用來(lái)源 -
source
為模塊的 stringfy 后的源碼 -
assets
為該模塊包含的靜態(tài)資源
依照issuer
挺益,我們可以得到調(diào)用順序?yàn)椋?/p>
-
app.tsx
-
/CXp(style-loader!postcss-loader!sass-loader!app.scss)
GAnJ(style-loader/injectStylesIntoStyleTag.js)
aZ7I(postcss-loader!sass-loader!app.scss)
-
可以理解為:app.tsx
中import './app.scss'
被匹配并解析為內(nèi)部模塊/CXp
歉糜,該模塊源碼source
format 后為:
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;
}
不難發(fā)現(xiàn),該模塊又引用了內(nèi)部模塊aZ7I
和第三方style-loader
的模塊GAnJ
第三方style-loader
的模塊GAnJ
望众,也就是injectStylesIntoStyleTag.js
的作用正如名字所述匪补,會(huì)將最終樣式產(chǎn)物插入到 HTML 的 style 標(biāo)簽。我們只需要繼續(xù)分析生成的內(nèi)部模塊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')}";
沒(méi)有再次引用夯缺,僅為最終樣式的字符串,將其提取為 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');
}
就是源碼中的三個(gè)url()
調(diào)用甘耿,但是里面的路徑已經(jīng)被解析踊兜,引用文件名包含 webpack 默認(rèn)的 20 位 hash 而不是url-loader
配置中的 7 位,而且前兩個(gè)圖片文件大小均小于 10KB佳恬,本應(yīng)該被url-loader
轉(zhuǎn)換為 base64 格式捏境。
看到這里,其實(shí)僅僅知道url()
被經(jīng)過(guò)模塊aZ7I
后被更改毁葱,aZ7I
的標(biāo)識(shí)字段為/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
垫言。
將標(biāo)識(shí)以!
分隔后,得到.../postcss-loader/src/index.js??embedded
倾剿,.../sass-loader/dist/cjs.js??ref--5-oneOf-3-2
以及app.scss
筷频,從標(biāo)識(shí)符中我們可以知道 webpack 處理 app.scss
文件使用的 loader 及順序,即經(jīng)過(guò)sass-loader
處理后再由postcss-loader
處理形成了上述產(chǎn)物。
這里我有一個(gè)疑問(wèn):
為什么 webpack 沒(méi)有把過(guò)程繼續(xù)拆分凛捏,也就是把上述模塊再細(xì)化成兩個(gè)模塊担忧,多出的一個(gè)模塊則單獨(dú)為
sass-loader
處理后的產(chǎn)物。如果能夠細(xì)分葵袭,則能知道具體是哪個(gè) loader 處理了url()
我暫時(shí)沒(méi)找到這個(gè)問(wèn)題的答案涵妥,所以繼續(xù)查找。
先是去搜索sass-loader
坡锡,發(fā)現(xiàn)其并沒(méi)有處理url()
蓬网;然后是postcss-loader
,發(fā)現(xiàn)了其支持第三方插件鹉勒,在這之前我還提了一個(gè)issue帆锋,通過(guò)逐步尋找,發(fā)現(xiàn)postcss-url插件
會(huì)處理url()
禽额,但是我在 NX 的依賴(lài)中卻沒(méi)找到它锯厢。
知道插件機(jī)制后,我同時(shí)也開(kāi)始閱讀 NX 源碼的 style loader 配置部分脯倒,終于發(fā)現(xiàn)原來(lái)是它們自己創(chuàng)建了一個(gè)插件用來(lái)處理@import
和url()
实辑,相當(dāng)于替代了css-loader
。但是針對(duì) react 的 webpack 配置中引入url-laoder
時(shí)卻沒(méi)有考慮這部分藻丢,導(dǎo)致圖片等文件無(wú)法再被url-loader
處理剪撬。
解決方案
盡管這個(gè)問(wèn)題的影響沒(méi)有那么大,但是一定要避免對(duì)同一個(gè)大于 10KB 的圖片文件同時(shí)在樣式文件和 j(t)sx 文件中引用悠反,這樣會(huì)導(dǎo)致同一個(gè)文件產(chǎn)生兩份產(chǎn)出残黑,一個(gè)文件名 7 位 hash,另一個(gè)則是 20 位 hash斋否,這會(huì)導(dǎo)致針對(duì)同一個(gè)文件發(fā)起兩次網(wǎng)絡(luò)請(qǐng)求梨水,如果文件較多將會(huì)浪費(fèi)很多資源。而且這確實(shí)也會(huì)讓人費(fèi)解茵臭,因此我提出了一個(gè)issue用于追蹤疫诽。
但是鑒于 NX 關(guān)于 style 部分的 webpack 配置是集成在定制化的 webpack builder 代碼文件中,而url-loader
等配置卻是 react 單獨(dú)的旦委,估計(jì) NX 也不好修改這部分奇徒。
給團(tuán)隊(duì)的臨時(shí)解決方案是:
- 如果想使用
url-loader
處理所有圖片文件,盡量使用<img />
標(biāo)簽而不是url()
來(lái)引入圖片 - 如果并不一定要使用
url-loader
處理社证,可以使用絕對(duì)路徑前綴例如/assets/xxx
引用,同時(shí)配置assets
選項(xiàng)评凝,個(gè)人認(rèn)為追葡,這才是assets
配置的正確使用方式 - 最后還是一定要避免對(duì)同一個(gè)大于 10KB 的圖片文件同時(shí)在樣式文件和 j(t)sx 文件中引用
最后的探索
給出方案后,問(wèn)題其實(shí)可以被較好地解決,但是我發(fā)現(xiàn) NX 使用postcss-loader
宜肉,除了最常見(jiàn)的 autoprefixer匀钧,還做的兩件事就是使用社區(qū)的postcss-import
來(lái)處理@import
,以及使用自己寫(xiě)的postcss-cli-resources
來(lái)處理url()
谬返。
其實(shí)社區(qū)處理url()
也有一個(gè)插件postcss-url
之斯,暫時(shí)無(wú)法得知為什么 NX 需要自己創(chuàng)建一個(gè)插件來(lái)處理。
做的這三件事中遣铝,url()
和@import
也可以交給css-loader
處理佑刷,不過(guò) NX 傳入了一些路徑參數(shù),如果這樣替換酿炸,我們便無(wú)法正常使用這些參數(shù)瘫絮,例如同時(shí)開(kāi)啟rebaseRootRelativeCssUrls
和deployUrl
后,NX 會(huì)在打包時(shí)將deployUrl
作為url()
聲明路徑的前綴填硕。
但是為了繼續(xù)探索url-loader
正常工作下這些文件解析的產(chǎn)物和順序麦萤,我還是對(duì)配置進(jìn)行了一些更改,主要包括覆蓋官方的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": "http:// 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": "..."
}
]
}
]
}
解讀之前姻檀,先來(lái)看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施敢。
解析順序?yàn)椋?/p>
-
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
中被解析僵娃,關(guān)于原因概作,暫時(shí)也沒(méi)有深入研究,如果有理解的同學(xué)歡迎留言默怨。
跟前面一樣讯榕,import './app.scss'
被匹配解析為內(nèi)部模塊/CXp(style-loader!css-loader!postcss-loader!sass-loader!app.scss)
,其source
format后和前面一致匙睹,唯一不同的是引用的子模塊多了一個(gè)css-loader
愚屁,對(duì)子模塊V1gc(css-loader!postcss-loader!sass-loader!app.scss)
的source
進(jìn)行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___;
可以清晰看到,除了引用兩個(gè)自身runtime模塊外痕檬,還引用了兩個(gè)圖片霎槐,由于banner2.png
事先已被解析,因此這里只需要解析arrow-right.svg
拆分banner2.png
和arrow-right.svg
的identifier
可以得出它們解析時(shí)使用的loader:
banner2.png
的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
arrow-right.svg
的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
兩者都使用了url-loader
進(jìn)行處理梦谜,不過(guò)要注意的是丘跌,banner2.png
其實(shí)使用的是url-loader
的默認(rèn)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,...\"
總結(jié)
這次踩坑之旅總體來(lái)說(shuō)效率不算高闭树,花了一些時(shí)間耸棒,主要是因?yàn)樽约翰皇煜?code>postcss-loader的插件機(jī)制以及NX的源碼結(jié)構(gòu)。而且最后也遺留了幾個(gè)問(wèn)題报辱,只算是對(duì)webpack debugging的一次初級(jí)入門(mén)与殃,希望今后能掌握第二種DevTools的方式,同時(shí)解決這些遺留問(wèn)題碍现。
本篇文章由一文多發(fā)平臺(tái)ArtiPub自動(dòng)發(fā)布