前言
单页应用(SPA)是流行的一种应用模式,它支持在同一页面下通过hash
或history
实现不刷新式切换视图,既实现了动态路由的变化,也实现了历史记录的保持,然而,相比于静态页面和动态页面(ASP/PHP/JSP)而言,缺点也是显而易见的,那就是对SEO不友好。
不管是Vue、React还是angular等实现路由的框架,无一不是加载原始html代码,再通过JavaScript动态渲染页面,但对于爬虫而已,JavaScript代码是不会执行的,从而导致爬取不到合适的内容。
解决方案
搜索爬虫不会去执行js代码,只会从原有的html中读取相关信息,所以总的解决方案是尽可能保证原始html代码包含希望被收录的信息,有下面几种方案:
1、页面中使用隐藏式元素 在html源码上写入希望被收录的静态信息,但不显示出来,仅为爬虫服务。
2、专门为爬虫开一个服务 在后端做http拦截,检查请求代理User-Agent是不是爬虫,如果检测到是爬虫类型,动态生产html片段,仅为爬虫服务。
3、使用预渲染(preredner) 在构建项目的同时,自动在浏览器中渲染一遍页面,然后生成相应路由下的静态html,这些路由只有在不执行javascript时才会显示(正是爬虫的情况),如果执行了js,将触发常规逻辑。
4、使用服务端渲染(SSR) 并非动态页面,而是在node端执行vue代码,翻译转换得出相应的html片段/全文,最后在响应体中返回信息(后续文章介绍)。
综合考虑后,方式一适合简单修改,方式二修改难度高,方式三和方式四最为合适。从简易难度而已,预渲染是最简单最快捷的,所以这里先介绍预渲染的seo优化方式。
工作原理
webpack插件prerender-spa-plugin
在第一次打包之后通过 puppeteer 打开浏览器访问之前配置好的路由地址。通过puppeteer我们可以向页面中注入js代码执行,从而得到相应页面的前端代码写入到本地文件中生成预渲染内容。
工作流程
npm run build
进行打包生成第一次编译文件- 启动
prerender-spa-plugin
插件 - 插件启动本地
node
服务器和puppeteer
浏览器 - 浏览器根据vue.config.js中创建PrerenderSPAPlugin对象时提供的routes配置访问相应的路由地址
- puppeteer监听创建PrerenderSPAPlugin对象时提供的renderer对象中配置的renderAfterDocumentEvent事件,该事件会在入口文件main.js(或其它类似文件)中的相应时机里触发
- 当puppeteer监听的事件被触发时开始获取页面代码
- 当代码爬取完毕进入到创建PrerenderSPAPlugin对象时提供的postProcess函数中,该函数是在预渲染内容输出到本地文件前最后一个钩子对输出内容进行修改
使用
先安装prerender-spa-plugin模块,然后修改路由为 history 模式,紧接着在打包配置中(webpack.prod.config.js或vue.config.js)添加PrerenderSPAPlugin插件,最后在入口文件中触发相关事件即可。 ```javascript //config.js路由配置文件
const autoTitle = ‘同驿商城(TOYEE MALL)-同程艺龙旗下住宿业供应链平台-酒店用品运营物资耗材采购’; const autoKeywords = ‘同驿商城,TOYEE MALL,同程艺龙,酒店用品,运营耗材,装修建材,供应链,物资采购’; const autoDescription = ‘同驿商城(TOYEE MALL)是同程艺龙(股票代码:0780.HK)旗下住宿业一站式供应链平台,于 2020 年 7 月 1 日上线,总部位于广州。通过同程艺龙优势资源整合,全球布局的强大互联网战略,严格甄选上千家优质品牌供应商和服务商,为全球住宿业态提供专业供应链平台服务,打造自有、开放、共赢的住宿业生态体系。’;
module.exports = {
‘/‘: {
titleName: ‘首页’,
title: ${autoTitle}-首页
,
keywords: autoKeywords,
description: autoDescription
},
‘/lineIntro.html’: {
titleName: ‘平台介绍’,
title: ${autoTitle}-平台介绍
,
keywords: autoKeywords,
description: autoDescription
},
‘/ticketChangeFoods.html’: {
titleName: ‘房券换物’,
title: ${autoTitle}-房券换物
,
keywords: autoKeywords,
description: autoDescription
},
‘/bzAgree.html’: {
titleName: ‘采购保障’,
title: ${autoTitle}-采购保障
,
keywords: ‘1111111’,
description: autoDescription
},
‘/classify.html’: {
titleName: ‘商品分类’,
title: ${autoTitle}-商品分类
,
keywords: autoKeywords,
description: autoDescription
},
‘/detail.html’: {
titleName: ‘商品详情’,
title: ${autoTitle}-商品详情
,
keywords: autoKeywords,
description: autoDescription
}
};
```javascript
//cnpm install prerender-spa-plugin -D
//webpack.prod.conf.js
new PrerenderSPAPlugin({
staticDir: path.join(__dirname, '../dist'),
// indexPath: path.join(__dirname, '../dist', 'index.html'),
routes: Object.keys(routesConfig),
// Optional - Allows you to customize the HTML and output path before
// writing the rendered contents to a file.
// renderedRoute can be modified and it or an equivelant should be returned.
// renderedRoute format:
// {
// route: String, // Where the output file will end up (relative to outputDir)
// originalRoute: String, // The route that was passed into the renderer, before redirects.
// html: String, // The rendered HTML for this route.
// outputPath: String // The path the rendered HTML will be written to.
// }
postProcess(ctx) {
ctx.route = ctx.originalRoute;
ctx.html = ctx.html.split(/>[\s]+</gim).join('><');
ctx.html = ctx.html.replace(
/<title>(.*?)<\/title>/gi,
`<title>${
routesConfig[ctx.route].title
}</title><meta name="keywords" content="${
routesConfig[ctx.route].keywords
}" /><meta name="description" content="${
routesConfig[ctx.route].description
}" />`
);
if (ctx.route.endsWith('.html')) {
ctx.outputPath = path.join(__dirname, '../dist', ctx.route);
}
return ctx;
},
// Optional - Uses html-minifier (https://github.com/kangax/html-minifier)
// To minify the resulting HTML.
// Option reference: https://github.com/kangax/html-minifier#options-quick-reference
minify: {
collapseBooleanAttributes: true,
collapseWhitespace: true,
decodeEntities: true,
keepClosingSlash: true,
sortAttributes: true
},
// The actual renderer to use. (Feel free to write your own)
// Available renderers: https://github.com/Tribex/prerenderer/tree/master/renderers
renderer: new Renderer({
// Optional - Any values you'd like your app to have access to via `window.injectProperty`.
inject: {
// foo: 'bar'
},
// Display the browser window when rendering. Useful for debugging.
headless: true,
// Optional - Wait to render until a certain amount of time has passed.
// NOT RECOMMENDED
renderAfterTime: 5000, // Wait 5 seconds.
renderAfterDocumentEvent: 'render-event'
})
})
//main.js
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>',
mounted() {
document.dispatchEvent(new Event('render-event')); // 预渲染
}
})
注意点
1.路由模式history
模式,因为预渲染的静态文件都会同步到服务器上,hash不会带到服务器,路由信息会丢失
2.预渲染只适用于简单的不变化的页面,对于页面数据需要实时变化,动态路由的页面,需要微信授权页面,需要登录的页面不太适用
3.页面加载闪现首页。
部分路由是没有做预渲染的,这部分路由在nginx配置的时候往往默认指向index.html的配置。
由于对首页做了预渲染,所以index.html默认有很多内容的。
解决方案有两种:
- 默认根节点隐藏,合适时机再显式出来:https://blog.csdn.net/Christiano_Lee/article/details/94569119。(感觉思路可行,但是本人没有实践,后面实践后再加上评论)
- 新增一个空页面,路由为’/empty’,并为这个路由做预渲染,nginx配置中没有匹配的路由默认指向加载此页面。nginx配置改为
4.还有其他问题,请查看https://www.cnblogs.com/chuaWeb/p/prerender-plugin.htmllocation / {
try_files $uri $uri/index.html /empty/index.html; # /index.html;
#root /static/front; #站点目录 已经配置了全局root
}
5.prerender-spa-plugin插件和vue-meta-info插件配合使用效果更佳!参考:
https://www.npmjs.com/package/prerender-spa-plugin
https://zhuanlan.zhihu.com/p/29148760
http://blog.ideacome.com/2020/03/31/vue-prerendercai-keng-bi-ji/