如何使用prerender-spa-plugin外掛對頁面進行預渲染

hjava發表於2021-09-30

本文主要是介紹使用prerender-spa-plugin外掛在針對前端程式碼進行預渲染。

預渲染(SSG)和服務端(SSR)渲染有一定的區別,大家想要了解的話可以看:https://segmentfault.com/a/1190000023469150

背景

因為之前的網站是使用Vue開發的,這種前端JavaScript渲染的開發模式,對於搜尋引擎來說非常的不友好,沒有辦法抓取到有效的資訊。因此為了進行SEO,我們需要對頁面進行一些預渲染。

預渲染比較適合靜態或者變化不大的頁面,能夠通過部署前的一次靜態渲染,將頁面上大部分內容都渲染出來。這樣搜尋引擎在爬取的時候,就能夠爬到相關的內容資訊。

現狀

目前商企通官網情況列舉如下:

  • 技術棧使用的是Vue,腳手架使用的是vue-cli,使用JavaScript前端渲染方案(這個方案對技術棧沒有要求,相容所有方案)
  • 釋出工具使用的是公司的工具,打包過程中,HTML資源傳遞到A域名下,CSS、JS、Image等資源傳遞到B域名下。

目標

希望能夠通過預渲染,讓頁面在初次訪問沒有執行JavaScript時,就能夠攜帶足夠的資訊,即將JavaScript渲染的內容提前渲染到HTML中。

釋出期望不做過多的修改。

方案

我們本次方案主要採用的是prerender-spa-plugin這個webpack的外掛來實現的。

它的主要原理是啟動瀏覽器,渲染完成後抓取HTML,然後再替換掉原有HTML。

我們需要實現預渲染,那麼我們需要完成以下幾件事情:

  1. 外掛引入和配置。
  2. 本地驗證。
  3. 改造打包構建流程。
  4. 線上驗證。

下面,我們一個一個來說下,我們如何做這個事情的。

外掛引入和配置

首先,我們需要引入一個預渲染外掛,執行命令:

mnpm i prerender-spa-plugin -D

這個命令除了安裝外掛本身以外,依賴了puppeteer,然後puppeteer又依賴落地chromium,所以最後我們其實是需要在依賴中安裝一個chromium。

如果大家安裝puppeteer非常慢或者經常失敗,可以參考下這個文件中的方法:https://brickyang.github.io/2019/01/14/國內下載安裝-Puppeteer-的方法/,指定puppeteer下載映象。

安裝完成後,我們就可以在webpack的配置檔案中增加對應的配置了。

如果大家使用的也是vue-cli,那麼我們需要增加的配置是在vue.config.js中,如果是直接修改webpack的配置,那麼方法也是類似。

下面我們以vue.config.js的修改為例:

const PrerenderSPAPlugin = require('prerender-spa-plugin');

module.exports = {
  ...,
  configureWebpack: {
    ...,
    chainWebpack: config => {
      config.plugin('prerender').use(PrerenderSPAPlugin, [
        {
          staticDir: path.join(__dirname, 'build'),
          routes: [
            '/',
            '/product',
            '/case',
            '/about',
            '/register',
          ],
          renderer: new Renderer({
            headless: true,
            executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
            // 在 main.js 中 document.dispatchEvent(new Event('render-event')),兩者的事件名稱要對應上。
            renderAfterDocumentEvent: 'render-event',
          }),
        },
      ]);
    }
  }
}

因為我們在專案中使用了webpack-chain,所以我們的語法是上面類似鏈式呼叫的方法。如果大家直接修改的話,就是採用vue的原來的修改配置的方式。

下面我簡單的給大家介紹下,上面的一些配置的含義:

  • staticDir:這個指的是輸出預渲染檔案的目錄。
  • routes:這個指的是需要預渲染的路由。這裡需要注意的是,vue的hash路由策略是沒有辦法進行預渲染的,所以如果要進行預渲染,需要改成history路由,然後預渲染後會變成多個HTML檔案,每個檔案都帶全量路由功能,只是預設路由不一樣而已。
  • renderer:這個是可以傳入的puppeteer的配置,我說下我用過的這幾個配置:

    - headless:是否使用headless模式渲染,建議選擇true。

    - executablePath:指定chromium的路徑(也可以是chrome)。這個配置在talos中是需要指定的,talos中的chrome地址預設是/usr/bin/google-chrome。

    - renderAfterDocumentEvent:這個的意思是在哪個事件觸發後,進行預渲染的抓取。這個事件是需要在程式碼中自己使用dispatchEvent來觸發的,這樣自己可以控制預渲染的時機。一般我們都是在最外層的元件的mounted鉤子中觸發,如果大家有其他需求也可以自己指定。

更多的可以看外掛的官方文件

開發完成後,我們可以在本地構建一次,看看是否能夠生成符合我們預期的程式碼。

vue.config.js指定publicPath導致預渲染失敗問題

如果大家和我這個專案一樣,在vue.config.js中傳入publicPath指定第三方CDN域名,會將CSS、JavaScript、Image等資源傳遞到不同的域名上,類似配置如下:

module.exports = {
  ...,
  publicPath: `//awp-assets.cdn.net/${projectPath}`,
  ...,
};

如果沒有預渲染,這種方案會在打包完成後分別上傳至不同的CDN域名,線上上訪問是沒有問題的。

但是在本地,這個時候CSS和JS資源還沒有上傳到CDN中,瀏覽器無法載入對應的資源進行頁面的渲染,這樣的話會導致本地預渲染失敗。

為了解決這個問題,有兩個解決思路。

  1. 【推薦】調整打包的策略,將非HTML資源也上傳至同一個CDN域名下,這樣的話,我們就可以使用相對路徑來訪問這些資源,不需要傳遞新域名給publicPath,這樣我們在本地構建的時候就可以訪問到這些值。這個是個比較靠譜合理的方法,比較推薦。
  2. (如果上面那個方法實在無法實現,那麼可以考慮這個方案)在預渲染之前,資源是在本地可以通過相對路徑訪問到的,這個時候使用替換的方式把HTML中的資原始檔地址替換掉,然後預渲染完成後再替換回來。這個方法比較hack,但是經過實際驗證確實是可以生效。具體的做法是自己寫一個簡單的webpack外掛。

    首先,我們需要安裝一個新的NPM包,用來對檔案中的內容進行替換(自己寫正則也可以,不過用這個會方便一些),具體命令如下:

mnpm i replace-in-file

    安裝後,我們需要增加兩個webpack的外掛,分別作用在afterEmit和done這兩個鉤子節點上。如果想要了解為什麼是這兩個鉤子節點,那麼你可以閱讀下webpack外掛的開發章節。

const replace = require('replace-in-file');

let publicPath = `//awp-assets.cdn.net/${projectPath}`;

// 第1個替換外掛,主要是將原先打包過程中帶有CDN域名的路徑替換成相對路徑
function ReplacePathInHTMLPlugin1(cb) {
  this.apply = compiler => {
    if (compiler.hooks && compiler.hooks.afterEmit) {
      compiler.hooks.afterEmit.tap('replace-url', cb);
    }
  };
}

function replacePluginCallback1() {
  replace({
    files: path.join(__dirname, '/build/**/*.html'),
    from: new RegExp(
      publicPath.replace(/([./])/g, (match, p1) => {
        return `\\${p1}`;
      }),
      'g'
    ),
    to: '',
  })
    .then(results => {
      console.log('replace HTML static resources success', results);
    })
    .catch(e => {
      console.log('replace HTML static resources fail', e);
    });
}

// 第2個替換外掛,主要是將預渲染後的HTML檔案中的相對路徑替換成帶有CDN域名的路徑
function ReplacePathInHTMLPlugin2(cb) {
  this.apply = compiler => {
    if (compiler.hooks && compiler.hooks.done) {
      compiler.hooks.done.tap('replace-url', cb);
    }
  };
}

function replacePluginCallback2() {
  replace({
    files: path.join(__dirname, '/build/**/*.html'),
    from: [/href="\/css/g, /href="\/js/g, /src="\/js/g, /href="\/favicon.ico"/g],
    to: [
      `href="${publicPath}/css`,
      `href="${publicPath}/js`,
      `src="${publicPath}/js`,
      `href="${publicPath}/favicon.ico"`,
    ],
  })
    .then(results => {
      console.log('replace HTML static resources success', results);
    })
    .catch(e => {
      console.log('replace HTML static resources fail', e);
    });
}

    上述程式碼就是我們需要增加的兩個webpack的替換外掛和對應的回撥函式,接下來我們看下在webpack中怎麼配置。

module.exports = {
  publicPath,
  outputDir,
  crossorigin: 'anonymous',
  chainWebpack: config => {
    config.plugin('replaceInHTML').use(new ReplacePathInHTMLPlugin1(replacePluginCallback));
    config.plugin('prerender').use(PrerenderSPAPlugin, [
      {
        staticDir: path.join(__dirname, 'build'),
        // 我們應該只會使用根路徑,因為是hash路由,所以其他頁面預渲染沒有意義,因此不進行預渲染
        routes: ['/'],
        renderer: new Renderer({
          headless: true,
          executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
          // 在 main.js 中 document.dispatchEvent(new Event('render-event')),兩者的事件名稱要對應上。
          renderAfterDocumentEvent: 'render-event',
        }),
      },
    ]);
    config.plugin('replaceInHTML2').use(new ReplacePathInHTMLPlugin2(replacePluginCallback2));
  }

    我們第一個替換外掛,需要在預渲染外掛前執行,在預渲染外掛執行前,將HTML中的資源的地址替換成本地的相對路徑;第二個則需要在替換後執行,這樣將預渲染後端資源中的相對路徑,再替換成CDN地址。

    通過這兩個外掛,我們就可以完成在預渲染前替換掉路徑完成預渲染,然後在預渲染後再完成替換保證線上可用。

本地驗證

通過上面的方式,我們應該已經得到了一個預渲染完成的HTML,接下來我們就是要驗證下這個HTML是否符合預期了。

比較簡單的驗證方式,可以直接訪問那個HTML檔案,或者啟動一個HTTP靜態資源服務來驗證。

驗證的話,你可以使用curl來進行請求,這種情況下JavaScript不會執行,你可以看到HTML的原始檔是什麼。

FAQ

  1. 在chrome版本比較低的情況下(比如v73),會提示渲染失敗?

    這個是因為chrome的版本過低,導致預渲染失敗。解決方案是升級chrome/chromium版本到最新(目前v93無問題)版本即可。

總結

如果我們需要實現SSG(靜態站點生成),那麼我們可以使用prerender-spa-plugin這個外掛來做,這個外掛可以在本地啟動chromium來抓取HTML內容,再寫回HTML檔案中,如我們我們需要對其中的靜態資原始檔進行處理,我們可以使用替換的外掛,針對處理前後的內容進行替換,來達到我們的訴求。

直接替換壓縮後程式碼雖然看起來有效,但是這個強依賴壓縮的演算法和內容順序,強烈不推薦直接用指令碼修改替換壓縮後檔案,最好是在webpack的done鉤子回撥中處理。

相關文章