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開頭:

nextjs中靜態資源上傳到CDN

如圖片中所示,帶有 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
歡迎關注蚊子的公眾號:
蚊子的前端公眾號