nextjs:如何將靜態資源發布到 CDN
- 2019 年 10 月 17 日
- 筆記
nextjs 是基於 react 的服務端同構指出框架,在使用的過程中也多多少少遇到過幾個問題,其中最大的問題就是靜態資源的發布了。
1. 如何基於文件內容進行 hash 命名
Next.js uses a constant generated at build time to identify which version of your application is being served. This can cause problems in multi-server deployments when next build is ran on every server. In order to keep a static build id between builds you can provide the generateBuildId function:
按照官網上的說法,每次發布都會生成新的 hash 路徑,即使當前沒有任何的變動。例如某次發布的路徑是/_next/static/tZonUgEY-GPCEExGbFapL/pages/index.js
,那麼下次的 hash 必然不是這個值。這樣導致的一個問題是:如果在多台機器上發布並 build 時,會導致每次 build 產生的值不同。如果想固定某個值或者使用某個值,一個是可以先 build 完成後後再分發,或者,可以在next.config.js
中自定義generateBuildId
:
// 來自官網上的例子 // next.config.js module.exports = { generateBuildId: async () => { return 'my-build-id'; } };
npm 上也有提供相應的安裝包,可以使用當前 git 提交的 hash 值作為 buildId:next-build-id。
可是這種存在的一個問題就是:即使文件沒有發生變動,或者我只修改了首頁的程式碼,發布完成後,pages 下所有的資源都需要重新載入,有用戶建議使用內容的 hash 值作為每個資源的路徑,但官方好像好像不太情願,說實現起來比較困難,詳情可以看這個 issue: use content hash in pages chunk name。在這條 issue 中,有用戶自己實現一個插件,不過我還沒用過,有興趣的同學可以嘗試下。
2. 路徑的拼接規則
靜態資源上傳到 CDN,這是存在目前存在的最大的問題,雖然在next.config.js
中可以配置assetPrefix
欄位,但實際使用起來還是非常困難。
打包後的 js 和 css,引用路由均為/_next/static
開頭:
如圖片中所示,帶有 data-next-page 屬性的,實際上訪問的是.next/server/static/[hash]/pages/_app.js;不帶這個屬性的,訪問的路徑是.next/static/runtime/webpack-[hash].js
我們以 2019/09/16 提交的 nextjs 源碼為例:pages_document,裡面有全局脫水數據的注入,頁面相關的 js 和靜態資源的 js 的拼接:
// 頁面相關的js // assetPrefix為我們在next.config.js中配置的前綴 // ${buildId}即為每次打包生成的hash值,在本地環境下值為development // _devOnlyInvalidateCacheQueryString: 變動的時間戳,正式環境中為空, _devOnlyInvalidateCacheQueryString: process.env.NODE_ENV !== 'production' ? '?ts=' + Date.now() : '' src={assetPrefix + encodeURI(`/_next/static/${buildId}/pages${getPageFile(page)}`) + _devOnlyInvalidateCacheQueryString} // 靜態資源的js src={`${assetPrefix}/_next/${file}${_devOnlyInvalidateCacheQueryString}`}
全局脫水數據的注入
<script id="__NEXT_DATA__" type="application/json" nonce={this.props.nonce} crossOrigin={this.props.crossOrigin || process.crossOrigin} dangerouslySetInnerHTML={{ __html: NextScript.getInlineScriptSource(this.context._documentProps) }} data-ampdevmode />
上面的頁面編譯後的路徑是.next/server/static/{hash}/pages/_document.js
,這些 js 讀取的路徑是分別由 2 個 json 文件控制的。
sever/pages-manifest.json
:載入頁面相關的 js,nextjs 是服務端渲染+客戶端渲染兩種方式,刷新頁面時使用的服務端渲染(使用server/static/{hash}/pages/
中的文件),切換路由時使用的是客戶端渲染(使用static/{hash}/pages/
中的文件),這裡載入的 js,是用於在路由切換時使用客戶端渲染的方式;static/build-manifest.json
:載入靜態資源的 js,使用static/
里除 hash 路徑外的資源;
我們了解這些,主要是為了理解 js 的路徑是怎樣拼接完成的。
3. 如何發布靜態資源到 CDN
靜態資源發布到 CDN 其實很簡單,只要把.next/static
下目錄的資源上傳上去即可。最困難的是如何替換程式碼中的路徑。把這個目錄下的靜態文件上傳到 CDN 後,生成的地址會變成:
<!-- 假設我們的CDN地址是 http://static.qq.com --> <script src="http://static.qq.com/runtime/webpack-4b444dab214c6491079c.js"></script>
從第 2 部分中能看到,程式碼中使用assetPrefix
作為靜態資源的前綴時,只是單純的拼接到了最前面而已,拼接後的地址是:
<script src="http://static.qq.com/_next/static/runtime/webpack-4b444dab214c6491079c.js"></script>
中間多出了/_next/static
的路徑,最後的結果是頁面需要載入的資源和上傳的資源路徑不一致,就會各種 404。這裡我的解決方案很簡單粗暴,讀取編譯後的文件,然後執行 node 程式,將裡面的字元替換掉:
const fs = require('fs'); // 獲取文件夾中所有的文件 function readDirAll(path) { // 獲取字元串的最後一個字元 var getLastCode = function(str) { return str.substr(str.length - 1, 1); }; var result = []; // 存儲獲取到的文件 var stats = fs.statSync(path); // 獲取當前文件的狀態 if (stats.isFile()) { result.push(path); } else if (stats.isDirectory()) { // 若當前路徑是文件夾,則獲取路徑下所有的資訊,並循環 var files = fs.readdirSync(path); for (var i = 0, len = files.length; i < len; i++) { var item = files[i], itempath = getLastCode(path) == '/' ? path + item : path + '/' + item; // 拼接路徑 var st = fs.statSync(itempath); if (st.isFile()) { result.push(itempath); } else if (st.isDirectory() && item !== 'cache') { // 當前是文件夾,則遞歸檢索,將遞歸獲取到的文件列表與當前result進行拼接 var s = readDirAll(itempath); result = result.concat(s); } } } return result; } const list = readDirAll('.next'); list.forEach(file => { let data = fs.readFileSync(file, 'utf8'); if (file.indexOf('_document.js') > -1) { data = data .replace(//_next//g, '/') .replace(/static/" + buildId/g, '" + buildId'); fs.writeFileSync(file, data); console.log(file, 'success'); } else if (file.indexOf('build-manifest.json') > -1) { data = data.replace(/static//g, ''); fs.writeFileSync(file, data); console.log(file, 'success'); } else if (data.indexOf('/_next/static') > -1) { data = data.replace(//_next/static//g, '/'); fs.writeFileSync(file, data); console.log(file, 'success'); } });
這樣就能就可以保證項目的 CDN 地址和真正上傳的地址是一致的了。
歡迎訪問蚊子的前端部落格: https://www.xiabingbao.com
歡迎關注蚊子的公眾號: