SPA应用部署方便,直接扔到CDN即可。 直到有一天发现搜索引擎除了首页之外其他页面都没有收录~傻逼了吧
这可怎么办,SSR框架重写一遍?工程太大了吧。。
如果只是针对SEO,爬虫的访问量完全可以在响应的时候实时利用Headless browser去本地打开网页。 再把渲染过后的HTML返回给爬虫。
正常用户访问还是前端渲染。
缺点就是需要额外引入node作为后端webserver,需要自己保证这些服务的稳定性,没有直接扔CDN来的那么省事。 但是为了SEO的流量…
Puppeteer
自从Puppteteer发布后,PhantomJS就宣布停止维护了,前两年爬虫换上了puppeteer性能飞起。
Puppeteer的原理是通过dev protocol websokct协议去控制浏览器的各种操作。 我在15年写过一个类似的,利用chrome的扩展api,通过建立websock连接和node通讯来控制浏览器,当时是为了替代缓慢的phantomjs。
安装Puppeteer 1 cnpm install puppeteer --save
Centos依赖 1 2 3 4 5 # 依赖库yum install pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64 cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 GConf2.x86_64 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 -y # 字体yum install ipa-gothic-fonts xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi xorg-x11-utils xorg-x11-fonts-cyrillic xorg-x11-fonts-Type1 xorg-x11-fonts-misc -y
目录结构 1 2 3 4 5 6 7 8 9 10 project │ server.js | ssr.js │ └───dist │ index.html │ └───static │ xx.js │ ...
依赖
isbot
爬虫uA判断库
connect-history-api-fallback
express vue history兼容中间件
1 npm install express isbot connect-history-api-fallback --save
ssr.js 接受一个URL参数,返回渲染后的HTML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const puppeteer = require ('puppeteer' );async function ssr (url ) { const start = Date .now(); const browser = await puppeteer.launch(); const page = await browser.newPage(); try { await page.goto(url, {waitUntil : 'networkidle0' }); await page.waitForSelector('#posts' ); } catch (err) { console .error(err); throw new Error ('page.goto/waitForSelector timed out.' ); } const html = await page.content(); await browser.close(); const ttRenderMs = Date .now() - start; console .info(`Headless rendered page in: ${ttRenderMs} ms` ); return {html, ttRenderMs}; } export {ssr as default };
server.js
启动一个webserver
每次请求判断客户端UA是否是爬虫,调用puppeteer获取渲染后的html
利用express
的static
中间件来处理资源请求
利用connect-history-api-fallback
库来对请求重定向到index.html
前端Vue路由改为history
模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 var express = require ('express' );var app = express();var server = require ('http' ).createServer(app);var history = require ('connect-history-api-fallback' );var isBot = reuqire('isbot' );var SSR = require ('./ssr.js' );var listenPort = 8088 ;const staticFileMiddleware = express.static('dist' );app.use(function (req, res, next ) { var UA = req.headers['user-agent' ]; var isStaticDir = req.url.indexOf('static/' ) > -1 ; if (UA && isBot(UA) && !isStaticDir){ var requestUrl = 'http://localhost:' +listenPort+req.url; (async () => { try { var results = await SSR(requestUrl); res.send(results.html); }catch (e){ console .log('ssr failed' , e); res.status(500 ).send('Server error' ); } })(); return ; } next(); }); app.use(staticFileMiddleware); app.use(history({ disableDotRule: true , verbose: true })); app.use(staticFileMiddleware); server.listen(listenPort);
性能优化
浏览器单次启动
减少不必要的资源请求如图片等等..
缓存每个URL的HTML结构 生命周期1个小时
单次启动 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 let browserWSEndpoint = null ;async function ssr (renderUrl ) { let browser = null ; if (browserWSEndpoint){ try { browser = await puppeteer.connect({browserWSEndpoint}); }catch (e){ browserWSEndpoint = null ; browser = null ; } } if (!browserWSEndpoint){ browser = await puppeteer.launch({ headless: true , ignoreHTTPSErrors: true , args: [ '--no-sandbox' , '--disable-setuid-sandbox' ] }); browserWSEndpoint = await browser.wsEndpoint(); } ...
资源请求 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 let browserWSEndpoint = null ;async function SSR (renderUrl ) { let browser = null ; if (browserWSEndpoint){ try { browser = await puppeteer.connect({browserWSEndpoint}); }catch (e){ browserWSEndpoint = null ; browser = null ; } } if (!browserWSEndpoint){ browser = await puppeteer.launch({ headless: true , ignoreHTTPSErrors: true , args: [ '--no-sandbox' , '--disable-setuid-sandbox' ] }); browserWSEndpoint = await browser.wsEndpoint(); } const page = await browser.newPage(); await page.setRequestInterception(true ); page.on('request' , req => { const whitelist = ['document' , 'script' , 'xhr' , 'fetch' ]; if (!whitelist.includes(req.resourceType())) { return req.abort(); } req.continue(); }); await page.goto(renderUrl, {waitUntil : 'networkidle0' }); const html = await page.content(); let results = { html } return results; }
缓存 安装依赖
1 npm install cacheman cacheman-file --save
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 const FilecCache = new Cacheman('htmls' , { ttl: 60 * 60 * 3 , engine: 'file' , }); let browserWSEndpoint = null ;async function SSR (renderUrl ) { let browser = null ; let urlMd5 = md5(renderUrl); var hitByCache = await FilecCache.get(urlMd5); if (hitByCache){ return hitByCache; } if (browserWSEndpoint){ try { browser = await puppeteer.connect({browserWSEndpoint}); }catch (e){ browserWSEndpoint = null ; browser = null ; } } if (!browserWSEndpoint){ browser = await puppeteer.launch({ headless: true , ignoreHTTPSErrors: true , args: [ '--no-sandbox' , '--disable-setuid-sandbox' , ] }); browserWSEndpoint = await browser.wsEndpoint(); } const page = await browser.newPage(); await page.setRequestInterception(true ); page.on('request' , req => { const whitelist = ['document' , 'script' , 'xhr' , 'fetch' ]; if (!whitelist.includes(req.resourceType())) { return req.abort(); } req.continue(); }); await page.goto(renderUrl, {waitUntil : 'networkidle0' }); const html = await page.content(); let results = { html } await FilecCache.set(urlMd5, results); return results; }
统计代码排除 避免统计错误,可以把ga,百度统计之类的代码请求block掉