背景:
我們需要將React開發的應用部署到一個活動搭建平臺上,這意味我們只需要上傳源碼,沒有搭建服務器的環節,沒有配置Nginx的環節。具體步驟就是在該平臺新建一個活動,然後將自己的源碼傳到這個活動下,然後打開這個活動提供的地址,然後就能夠看到頁面。
我們上傳的js文件main.js會得到一個js/main.js的路徑,然後將這個路徑放到html的script標籤的src屬性上即可。main.css同理會得到一個css/main.css的路徑,然後放到link標籤的href屬性上即可。
這些做完查看頁面的時候我們會發現script標籤的src屬性被替換成了//xxx.com/xxx/xxx/main.js?t=xxx這種形式。同樣css也被換成了//xxx.com/xxx/xxx/main.css?t=xxx。需要注意的是這個路徑中並沒有js/和css/這種東西。
注:另外上傳的html文件存儲的位置和JS/CSS並不在一起。
現在我們知道上傳的資源除了HTML文件會被存在//xxx.com/xxx/xxx/目錄下。
當我們用createReactApp創建的腳手架打包React應用的時候可以修改publicPath爲//xxx.com/xxx/xxx/這樣打出來的包中HTML文件的script標籤和link標籤的對應屬性就絕對路徑(絕對路徑並不會被自動替換)。並且是正確的絕對路徑。
到目前爲止玩耍的很開心。
有一天的一個時間節點之後,發現傳上去的應用訪問不通,報錯了,JS和CSS文件找不到。排查之後發現活動平臺升級之後,將存儲JS和CSS這種靜態資源的域名換掉了。而我們打包的時候將域名寫死了這就出錯了。
解決的方法固然很簡單將publicPath換下就行了。
但是這種解決方法並不穩妥,因爲我們依賴服務端並不修改靜態資源存儲的域名。這顯然是不靠譜的。
那麼爲什麼我們不將打包出來的HTML中script標籤的src屬性變成相對的呢?例如平臺要求的js/main.js,css/main.css。
這顯然是可以的,我們試下。
結果是部分可以部分不可以–!
我們都知道webpack打包需要一個入口文件,webpack配置文件提供的entry字段提供的入口文件。
性能優化的時候,這個入口文件還會被分爲至少三個文件,一個是vendor.js這是入口文件中被引入的第三方庫,基本不怎麼變動可以緩存。一個是runtime.js這是每次打包都會改變的,不利於緩存,最好直接插入到HTML文件中。一個是業務代碼,基本每次發版都會變動的main.js。其中真正的入口文件是被插入到HTML中的runtime.js。
有了這些資源之後使用插件HtmlWebpackPlugin將這些資源注入到index.html中。
這些都是webpack打包的時候就確定下來的,在publicPath修改成’'並且將output.filename和output.chunkFilename改成’js/[name].[contenthash].js’之後,上面確定下來的部分生成的script標籤的src屬性爲 ‘js/xxxxx.js’ 現在將這個生成的HTML和其他靜態資源傳到活動平臺,發佈之後我們發現頁面正常。但是還有一部分有問題。
這部分是動態加載的,webpack優化的時候不出現在首屏的部分我們會現在稍後加載,或者說使用webpack切割代碼的功能讓它動態加載出來。具體方法就是 import('./a.js')
這種形式。
那麼這樣動態加載出來的文件有什麼問題呢?
首先動態加載的模塊是通過webpack後期自動構建一個script標籤然後插入的HTML中然後加載執行的。
既然是後期動態添加的,那麼活動平臺的統一替換標籤的src就沒有作用到這個動態添加的標籤上,這個標籤的src還是js/xxx.js
這種形式,這就報錯了。
小結:
爲了解決線上靜態資源地址會變的問題,將HTML文件中替換線上資源地址的工作重新交還給活動平臺自身。具體表現爲HTML中引入的資源都是相對地址’js/xxx.js’或者’css/xxx.css’這種形式,這樣就解決了我們寫死線上資源地址,但是活動平臺替換後我們不知道導致的問題。
具體配置修改:
publicPath從’//xxxx.com/xx/xxx/‘變成了’’
filename和chunkFilename從’xxx.js’變成了’js/xxx.js’
上面的配置得到的src就是 ‘’ + ‘js/xxx.js’ -> ‘js/xxx.js’ 這正是我想要的。
但是通過這個可以看出來還有一種方法可以做到。
將publicPath設置爲’js/’,filename還保持’xxx.js’這樣也能得到’js/xxx.js’。
但這是不行的,因爲publicPath的語義是所有靜態資源的公共路徑前綴。這麼一搞不僅得到了’js/xxx.js’還得到了’js/xxx.css’等。
注:HTML中被注入的script的src屬性和動態生成的script標籤的src屬性都是通過publicPath + filename得到的。
到這裏主要矛盾是活動平臺的地址替換並不能作用到動態添加的script標籤,導致js/xxx.js
這種形式並沒有被替換。其次編譯代碼的時候我們並不知道publicPath的值是什麼,只有等到代碼執行,也就是活動平臺替換script標籤的src屬性之後才能知道。但是publicPath是在編譯的時候寫死的。
怎麼讓publicPath變成動態的?讓publicPath在代碼執行的時候動態獲取被替換之後的script標籤的src屬性,然後解析出其中的path設置上,這樣後面動態生成的script標籤的src就可以正常訪問,得到正確的地址。
翻閱文檔可以找到這段描述:
在編譯時(compile time)無法知道輸出文件的 publicPath
的情況下,可以留空,然後在入口文件(entry file)處使用自由變量(free variable) __webpack_public_path__
,以便在運行時(runtime)進行動態設置。
__webpack_public_path__ = myRuntimePublicPath
// 應用程序入口的其他部分
這就給了我們動態修改publicPath的能力。
看到了可行的希望。
那麼這個原理是啥呢?翻看編譯過後未壓縮的代碼可以看到如下內容:
// __webpack_public_path__
__webpack_require__.p = "";
上面的屬性p就是我們配置的publicPath,在編譯後的代碼內部是被存在一個對象的屬性上的。如果webpack暴露給我們這個對象,我們自己是可以修改的。當然也可以看出,這個必須代碼一執行就需要修改,否則後面會有問題。這就是爲什麼修改publicPath的代碼要放在入口文件頂部的原因。
__webpack_public_path__ = myRuntimePublicPath
爲什麼這段代碼就能完成__webpack_require__.p
的修改呢?
例如:
__webpack_public_path__ = 'publicPath/'
會變成:
{
"./src/config.js":
/*!***********************!*\
!*** ./src/config.js ***!
\***********************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
eval("__webpack_require__.p = 'publicPath/'\n\n\n//# sourceURL=webpack:///./src/config.js?");
/***/ })
}
可以看到兩點,一個是模塊被編譯成被一個函數包裹的代碼塊,並且函數的最後一個入參是__webpack_require__
。第二是代碼__webpack_public_path__
變成了__webpack_require__.p
。
這就證明了webpack自身提供了我們動態修改publicPath的能力。
最後我們還發現瞭如下代碼:
// script path function
function jsonpScriptSrc(chunkId) {
return __webpack_require__.p + "js/" + chunkId + ".index.js"
}
這段代碼是用於獲取動態加載的script的src屬性的。這裏也看到了__webpack_require__.p
的身影。
注:爲啥是chunkId + “.index.js”?當沒有指定動態引入的模塊的名字的時候就是0.index.js, 1.index.js, 2.index.js…
好,現在我們知道動態的publicPath是可以實現的。
還有一個問題,我們怎麼在入口JS中獲取這個script標籤的src屬性呢?
並沒有手段直接獲取JS當前標籤的DOM對象,但是我們可以換個方法,JS可以通過屬性id獲取對應的DOM。
但是HtmlWebpackPlugin插入標籤的時候並沒有提供id屬性。
雖然沒有提供id,但是卻提供了開發相關插件的能力,這樣我們可以通過開發HtmlWebpackPlugin的插件來給對應的script標籤加上id。這樣我們就拿到了對應的src解析出了靜態資源存儲的path。
上面的方法我試了一下,是可以的。但是隻可以了一半。我們能做的這一半,做完了。剩下的一半也是無能爲力了。
通過上面的方法,我們得到了//xx.com/xx/xx/
這個線上的publicPath。然後動態生成的js的filename是js/xxx.js
,那麼拼起來就是//xx.com/xx/xx/js/xx.js
這種形式,這中間多了一個js/
。並且這個我們去除不了。所以還是破產了。
但我覺得這並不是我們的問題。
本身我們打包出來的文件是在文件夾js目錄下,並且傳上去之後得到的目錄也是js/xxx.js但是實際替換的時候,目錄結構卻變了,沒有了js/這一層。這有點怪了。
所以破產了。
但是如果url有能力去除後面部分倒還是可以的,但是明顯不行啊。例如 //xxx.com/a/b/…/c 表示的是不要b/這一層,但是不要b/這一層是由b/後面部分決定的,如果可以由b/這部分前面部分決定就好了。
還有如果我們可以實現上面提到的jsonpScriptSrc
這個函數到也可以,直接將js/這部分去掉就好了。
總結:
上面對於src="js/xxx.js"這部分js/的替換分爲了兩部分,一部分是靜態生成的,webpack打包出來的HTML模板就是這樣的。這部分會由活動平臺自動替換成線上的資源路徑。
第二部分是動態生成的資源部分,這部分我們需要自己手動替換,方法是,動態修改publicPath,拼接完成,但是因爲目錄層級的改變,我們失敗了。
對於這個問題的解決方案是上傳到活動平臺的代碼不要使用動態引入JS。