使用rerender-spa-plugin在构建时预渲染静态HTML文件优化SEO

前些时候,接到一个任务,我们给第三方开发的官网,一直无法在百度中搜索到,产品提出做个seo优化。由于此官网前端使用vue开发,打开网站只有首页有一个html页面,其余菜单打开并无html页面,更不用说给各个页面设置title、description、keywords(以下简称tdk)了,当时开发项目并没有考虑SEO,现在提出问题,我们技术也得做一些工作了。

考虑到已经使用vue开发,vue开发的应用本身对SEO就不是很友好,但是我们也不可能推倒原本的方案重做,只能在原本的项目中想办法。

针对此vue项目,我们前端在技术层面针对以下方面做了SEO优化:

1、默认语言设置为中文(en改为zh-CN),tdk重新合理布局关键词;

2、网站链接改为伪静态(路由mode模式由hash改为history);

3、网站各个页面设置各自独立的tdk;

4、页面中图片设置好合适的alt标签,便于搜索引擎理解;

5、完善sitemap和robots文件。

至于其他的提高应用seo的方案,比如持续更新网站内容,网站外链建设等,则不在我们前端开发的范围内,我们就不考虑了啊。

扯远了,今天议题是介绍rerender-spa-plugin,我们针对上面提到的SEO方案第3点(网站各个页面设置各自独立的tdk),发现目前市场上刚好有人针对Vue项目SEO的痛点,开发了一个预编译(或者称预渲染)插件prerender-spa-plugin。它可以在我们不改变原本项目代码和结构的基础上,只需要增加一些配置,在构建时,生成一些我们需要的静态页面,并按照我们的需要,合理设置tdk,省去了我们使用jsp、php、python重新开发项目,或者前端修改为SSR服务端渲染方案(比如 Vue.js 的 Nuxt、 React 的 Next)只为提高SEO而产生的巨大工作量。

好像又讲了太多废话了啊,我们现在直接上干货,参照从网上各个同行还有npmjs上对rerender-spa-plugin的介绍,具体步骤如下:

1、项目根目录下npm下载prerender-spa-plugin

js 复制代码
npm install prerender-spa-plugin --save-dev

2、在根目录下,创建seoMeta.js

js 复制代码
module.exports = {
    '/': {
        title : "首页title",
        keywords: '首页keywords1,首页keywords2,首页keywords3',
        description: '首页description1,首页description2,首页description3,首页description4'
    },
    '/caterer-list': {
        title : "美食店铺title",
        keywords: '美食店铺keywords1,美食店铺keywords2,美食店铺keywords3',
        description: '美食店铺description1,美食店铺description2,美食店铺description3,美食店铺description4'
    },
    '/weather': {
        title : "天气title",
        keywords: '天气keywords1,天气keywords2,天气keywords3',
        description: '天气description1,天气description2,天气description3,天气description4'
    },
    '/good-tags': {
        title : "推荐商品title",
        keywords: '推荐商品keywords1,推荐商品keywords2,推荐商品keywords3',
        description: '推荐商品description1,推荐商品description2,推荐商品description3,推荐商品description4'
    },
    '/scenic-list': {
        title : "景区title",
        keywords: '景区keywords1,景区keywords2,景区keywords3',
        description: '景区description1,景区description2,景区description3,景区description4'
    },
    // '/scenic-list?type=01': {
    //     title : "景点列表",
    //     keywords: '自然景观, 公园, 户外景点',
    //     description: '自然景观类景点列表,包含公园、山川等户外旅游资源'
    // },
    // '/scenic-list?type=02': {
    //     title : "人文景观",
    //     keywords: '人文景观, 历史遗迹, 文化景点',
    //     description: '人文景观类景点列表,探索历史遗迹和文化景点'
    // },
    '/scenicIntroDetail': {
        title : "景区介绍title",
        keywords: '景区介绍keywords1,景区介绍keywords2,景区介绍keywords3',
        description: '景区介绍description1,景区介绍description2,景区介绍description3,景区介绍description4'
    }
    // ,'/intro?id=1877252793975181313&type=article': {
    //     title : "详情页面title",
    //     keywords: '详情页面keywords1,详情页面keywords2,详情页面keywords3',
    //     description: '详情页面description1,详情页面description2,详情页面description3,详情页面description4'
    // },
};

3、在vue.config.js中,添加以下代码:

js 复制代码
/*** 下方代码中,与rerender-spa-plugin无关的代码已省略或者注释 ***/

const seoMetaConfig = require('./seoMeta.js')
const seoMetaKeyList = Object.keys(seoMetaConfig);  
const PrerenderSPAPlugin = require('prerender-spa-plugin');
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer;

// 定义在这个位置,方面在后面的configureWebpack中,plugins按条件动态添加 
const PrerenderSPAPluginFn = new PrerenderSPAPlugin({
    // 生成文件的路径,也可以与webpakc打包的一致。
    // 下面这句话非常重要!!!
    // 这个目录只能有一级,如果目录层次大于一级,在生成的时候不会有任何错误提示,在预渲染的时候只会卡着不动。
    staticDir: path.join(__dirname,'dist'),
    // // 对应自己的路由文件,比如a有参数,就需要写成 /a/param1。
    // routes: ['/', '/caterer-list','/weather','/good-tags','/scenic-list?type=01','/scenic-list?type=02','/intro?id=1877252793975181313&type=article'],
    routes: seoMetaKeyList,
    // 添加路由meta信息处理
    postProcessHtml: (context) => {
        let { html, route } = context;
        // 获取当前路由的meta配置
        const { title="", keywords="", description="" } = seoMetaConfig[route] || { title:'', keywords: '', description: '' };
        if(route=='/'){
            return html
        }else{
            // 1. 移除已存在的title和keywords、description meta标签(如果有)
            html = html.replace(/<title>[^<]*<\/title>/i, '');  // 注意这里不能用gi
            html = html.replace(/<meta\s+name=(?:"keywords"|keywords)[^>]*>/gi, '');
            html = html.replace(/<meta\s+name=(?:"description"|description)[^>]*>/gi, '');
            // 2. 在head结束前添加新的meta标签
            return html.replace(/<\/head>/i,
                `   <title>${title}</title>
                    <meta name="keywords" content="${keywords}" />
                    <meta name="description" content="${description}" />
                </head>`     
            );
        }
    },
    // 这个很重要,如果没有配置这段,也不会进行预编译
    renderer: new Renderer({
        headless: false,
        // 在 main.js 中 document.dispatchEvent(new Event('render-event')),两者的事件名称要对应上。
        renderAfterDocumentEvent: 'render-event'
    })
})

module.exports = {
    publicPath: '/',
    configureWebpack: () => {
        const plugins = []
        
        // 直接将PrerenderSPAPluginFn写在plugins中,会导致开发调试阶段(执行npm run dev、npm run dev:test)时候,触发PrerenderSPAPlugin的逻辑,
        // 导致改一点代码,就会触发弹出页面,无法进行正常的开发操作,
        // 同时修改的还有main.js中new Vue时的`document.dispatchEvent(new Event('render-event'))`,同样加入了条件判断
        if(process.env.NODE_ENV === 'production' && process.argv.includes('--preRender')){
            plugins.push(PrerenderSPAPluginFn)
        }
        return {
            //resolve: {
                //alias: {
                    //'@': resolve('src')
                //}
            //},
            plugins
        }
    }
}

4、根目录下,找到package.json

js 复制代码
  "scripts": {
    "build:prod": "vue-cli-service build --mode production --preRender",
  },

5、根目录下,找到main.js

js 复制代码
const renderEvent= new Event('render-event')
if(process.env.NODE_ENV === 'production' && renderEvent){
  new Vue({
    router,
    store,
    render: h => h(App),
    mounted () {
      document.dispatchEvent(renderEvent)
    }
  }).$mount('#app')
}else{
  new Vue({
    router,
    store,
    render: h => h(App)
  }).$mount('#app')
}
// 或者
const renderEvent= new Event('render-event')
new Vue({
  router,
  store,
  render: h => h(App),
  mounted () {
    if(process.env.NODE_ENV === 'production' && renderEvent)
      document.dispatchEvent(renderEvent)
  }
}).$mount('#app')

在步骤2中,我创建了一个名为seoMeta.js的js文件,用于统一配置各个路由页面的tdk,或许有些同行已经意识到了,为什么不在router中直接配置tdk呢,其实肯定可以了,而且肯定是最好的,我这里之所以没有采用,主要是我们的这个项目比较特殊,根据当时业主方的需求,我们的页面的每一个菜单都是通过后台配置出来,除了部分列表页面路由地址固定不变之外,大部分页面类似一个详情页,完全是可配置的,在我们的路由js中,根本没有配置各个页面的路由,所以我没有在路由js中设置,而是另外创建了一个seoMeta.js文件。

在步骤3中,我声明了一个插件函数PrerenderSPAPluginFn,在 postProcessHtml 中,将从seoMeta.js获取到的各个路由页面tdk,替换title、description、keywords生成在构建的html中。在PrerenderSPAPluginFn函数中,只有在routes中指定包含的路由地址,prerender-spa-plugin的PuppeteerRenderer方法才会帮我们构建静态HTML页面,简单来讲,就是只有指定的路由才会生成HTML。

目前网上还有一个插件vue-meta-info或者vue-meta,也能实现和我目前方案类似的效果,不过不同的是,使用vue-meta-info是在各自的路由页面按它的规则配置tdk,并且vue-meta-info是在最终构建的页面后面追加tdk,从而我们可以看到,最终构建出来的html页面代码中最先会出现首页的tdk,后面又有一个本页面的tdk,vue-meta-info的核心是通过在它后面追加的tdk覆盖前面的首页tdk,从而达到给各个路由页面设置tdk的目的。

显而易见,vue-meta-info可以根据页面内容合理的设置tdk,虽然最终构建的html静态页面会生成两个tdk,但是这个瑕疵是可以忽略的。

本人实际尝试使用vue-meta-info或者vue-meta,使用vue-meta-info一直并未成功生成tdk,vue-meta可以,但是生成的title偶尔会出现为空的情况,同时针对'/scenic-list?type=01''/scenic-list?type=02'这种带query查询参数的路由,rerender-spa-plugin结合vue-meta,并不能生成两种不同的tdk的页面,个人认为应该是rerender-spa-plugin的机制引起,生成的 scenic-list 文件中并无区分查询参数的文件,只有一个index.html,如下图 这在预渲染方案中是无法实现区分对不同query参数来生成不同tdk的静态html页面的。所以使用rerender-spa-pluginvue-meta组合同样无法正确处理带query查询参数路由的页面。毕竟rerender-spa-plugin只是预渲染,不是SSR服务端渲染方案。我们不能苛求太高太多,能实现对部分页面(固定内容不变的页面、固定路由的列表页面)生成一些静态html设置tdk用于seo,已经比只有一个主页面有html强了,您说是不是呢?

因为结合vue-meta同样无法实现对带query查询参数路由的页面有个正确的预渲染效果,所以最终我并没有采用vue-meta,所以在步骤3中的PrerenderSPAPluginFn函数中,postProcessHtml的逻辑就是您前面看到的那种通过替换tdk字符串来实现的方案。

js 复制代码
    // 添加路由meta信息处理
    postProcessHtml: (context) => {
        let { html, route } = context;
        // 获取当前路由的meta配置
        const { title="", keywords="", description="" } = seoMetaConfig[route] || { title:'', keywords: '', description: '' };
        if(route=='/'){
            return html
        }else{
            // 1. 移除已存在的title和keywords、description meta标签(如果有)
            html = html.replace(/<title>[^<]*<\/title>/i, '');  // 注意这里不能用gi
            html = html.replace(/<meta\s+name=(?:"keywords"|keywords)[^>]*>/gi, '');
            html = html.replace(/<meta\s+name=(?:"description"|description)[^>]*>/gi, '');
            // 2. 在head结束前添加新的meta标签
            return html.replace(/<\/head>/i,
                `   <title>${title}</title>
                    <meta name="keywords" content="${keywords}" />
                    <meta name="description" content="${description}" />
                </head>`     
            );
        }
    },

如果vue-meta 是用在SSR服务端渲染方案 Nuxt 中,个人觉的应该是可行的,没实际实验过,如果有误,欢迎大家指正。

言归正传,在步骤3和步骤4中,我定义了一个变量--preRender,至于为什么定义这个变量,在步骤3中的注释中已经说明,我在这里就不做说明了。

在步骤5中加上判断,原由同样和上面提到的--preRender一样,在步骤3和步骤4中增加了--preRender后,只会在npm run执行build:prod命令时会触发prerender-spa-plugin的预编译,npm run 执行devdev:test等命令时候并不会,所以我才加了--preRender这个条件。

好了,关于使用rerender-spa-plugin预渲染vue应用,我就啰嗦到这了。本文没有具体介绍rerender-spa-plugin的定义和用法,有不清楚的请自己查询资料啊。

相关推荐
KongHen3 小时前
完美解决请求跨域问题
前端
前端开发爱好者3 小时前
弃用 uni-app!Vue3 的原生 App 开发框架来了!
前端·javascript·vue.js
再吃一根胡萝卜3 小时前
VS Code Ctrl+/ 注释失效:两套快速修复与冲突排查方案(含可复制配置)
前端
支付宝体验科技3 小时前
Rokid 许德刚确认出席 SEE Conf 2025,带来《AI + AR 的实践与趋势》演讲
前端
PairsNightRain3 小时前
React.lazy 和 suspense 如何使用?
前端·javascript·react.js
猪哥帅过吴彦祖3 小时前
第 7 篇:交互的乐趣 - 响应用户输入
前端·webgl
渣哥3 小时前
三级缓存揭秘:Spring 如何优雅地处理循环依赖问题
javascript·后端·面试
渣哥3 小时前
为什么几乎所有 Java 项目都离不开 IoC?Spring 控制反转的优势惊人!
javascript·后端·面试
用户17592342150284 小时前
D3.js - 选择集方法(Selection Methods)
前端