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
歡迎關注蚊子的公眾號: