2

如何使用prerender-spa-plugin插件对页面进行预渲染

 2 years ago
source link: https://segmentfault.com/a/1190000040758967
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

如何使用prerender-spa-plugin插件对页面进行预渲染

本文主要是介绍使用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的源文件是什么。

  1. 在chrome版本比较低的情况下(比如v73),会提示渲染失败?

    这个是因为chrome的版本过低,导致预渲染失败。解决方案是升级chrome/chromium版本到最新(目前v93无问题)版本即可。

如果我们需要实现SSG(静态站点生成),那么我们可以使用prerender-spa-plugin这个插件来做,这个插件可以在本地启动chromium来抓取HTML内容,再写回HTML文件中,如我们我们需要对其中的静态资源文件进行处理,我们可以使用替换的插件,针对处理前后的内容进行替换,来达到我们的诉求。

直接替换压缩后代码虽然看起来有效,但是这个强依赖压缩的算法和内容顺序,强烈不推荐直接用脚本修改替换压缩后文件,最好是在webpack的done钩子回调中处理。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK