網上有好幾種單頁應用轉seo的方案,有服務端渲染ssr、有預渲染prerender、google抓AJAX、靜態化。。。這些方案都各有優劣,開發者能夠根據不一樣的業務場景和環境決定用哪種方案。本文將介紹另外一種思路比較清奇的SEO方案,這個方案也是有優有劣,就看讀者以爲適不適合了。javascript
個人項目是用react+ts+dva技術棧搭建的單頁應用,目前在線上已經有幾十個頁面,若干個sdk和插件在裏面。html
之前寫過一種單頁應用seo的方案,就是本身先在本地用爬蟲作預渲染,生成一樣目錄結構的靜態化的html,前端項目服務器判斷請求的UA是搜索引擎蜘蛛的話就會轉發到我事先靜態化過的html頁面前端
當時的項目只是一個簡單的只有幾個頁面的企業官網,預渲染沒啥問題。java
跟着這個思路,只要判斷搜索引擎蜘蛛讓蜘蛛看到另外一個有數據的頁面不就好了。react
至於頁面長什麼樣,蜘蛛🕷纔不會管呢,就像是你找廣告商投放廣告,廣告商不會要求你要怎樣的主題什麼色調,只要你按照他的尺寸和要求來作,而後給錢給貨就完事了🤑。ios
因此能夠針對SEO作另外一套網站,沒有樣式,只有符合seo規範的html標籤和對應的數據,不須要在原有項目上改造,開發成本也不會很高,體積小加載速度更快。web
缺點也有,就是須要另外維護一套網站,主網站界面變化不會影響,若是展現數據有變化就須要同步修改seo版的網站。express
先建個單獨的seo文件夾,不須要動到原有項目,下面是代碼結構:
axios
代碼實現很是之簡單,只要寫一箇中間件攔截請求,鑑別蜘蛛,返回對應路徑的seo頁面便可。api
個人前端服務器是用express,能夠寫個express的中間件, 新建server.js:
// seo/server.js const routes = require('./routes') const layout_render = require('./src/layout'); module.exports = (req, res, next) => { // 各大搜索引擎蜘蛛UA const spiderUA = /Baiduspider|bingbot|Googlebot|360spider|Sogou|Yahoo! Slurp/ var isSpider = spiderUA.test(req.get('user-agent')) // 獲取路由表的路徑 var seoPath = Object.keys(routes) if (isSpider) { for (let i=0,route; route = seoPath[i]; i++) { if (new RegExp(route).test(req.path)) { routes[route](req).then((result) => { // 返回對應的模板結果給蜘蛛 res.set({'Content-Type': 'text/html','charset': 'utf-8mb4'}).status(200).send(layout_render(result)) }) break; } } } else { // 未匹配到蜘蛛則繼續後面的中間件 return next() } }
而後在前端的啓動服務器里加入這個中間件,記得要放在其餘中間件以前
// 前端啓動服務器的server文件 var express = require('express') var app = express() // seo app.use(require('seo/server')); ...... app.listen(xxxx)
接下來就是寫模板和對應的解析了, 新建一個home文件夾,文件夾下再建一個index.ejs和index.js
<!-- seo/src/home/index.ejs --> <div> <h1>官網首頁</h1> <p>友情連接:</p> <p><a href="https://www.baidu.com/" target="_blank">百度</a></p> <p><a href="https://www.gogole.com/" target="_blank">谷歌</a></p> </div>
index.js用於解析對應的ejs模板
// seo/src/home/index.js const ejs = require('ejs') const fs = require('fs') const path = require('path') const template = fs.readFileSync(path.resolve(__dirname, './index.ejs'), 'utf8'); // 這裏爲何會有個async關鍵字,日後面看就能夠知道。 module.exports = async (req) => { const result = ejs.render(template) return result }
咱們還能夠建多個layout模板來管理head、title和導航欄這些公有的元素
<!-- seo/layout.ejs --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="content-type" content="text/html;charset=utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name=」renderer」 content=」webkit」> <meta content="網站關鍵字"" name="keywords"/> <meta content="網站描述" name="description"/> <title>網站標題</title> </head> <body> <div id="root"> <ul> <li><a href="/">首頁</a></li> <li><a href="/community">社區</a></li> </ul> <%- children -%> </div> </body> </html>
解析layout.ejs,套入內容的layout_render:
// seo/layout.js const ejs = require('ejs') const fs = require('fs') const path = require('path') const template = fs.readFileSync(path.resolve(__dirname, './layout.ejs'), 'utf8'); const layout_render = (children) => { return ejs.render(template, {children: children}) } module.exports = layout_render
路由表用簡單的鍵值對就能夠了,鍵名用字符串形式的正則來表示路徑的匹配規則:
// seo/routes.js const home_route = require('./src/home/index') module.exports = { '^(/?)$': home_route, }
那麼數據如何作請求並展現到對應的模板內呢?數據請求是異步的,怎樣等到請求完成再渲染模板呢?
咱們能夠用async/await來實現,如今來作一個社區的帖子列表頁面,須要先請求社區下帖子列表數據再把數據渲染到模板,新建一個community文件夾,一樣再建一個index.ejs做爲帖子列表頁面模板:
<!-- seo/src/community/index.ejs --> <div> <h1>帖子列表</h1> <ul> <% forum_list.map((item) => { %> <li><a href="/community/<%= item.id%>" target="_blank"><%= item.title-%></a></li> <% })%> </ul> </div>
相關的接口請求及數據操做寫在同級的index.js:
// seo/src/community/index.js const ejs = require('ejs') const fs = require('fs') const path = require('path') const template = fs.readFileSync(path.resolve(__dirname, './index.ejs'), 'utf8'); const axios = require('axios'); module.exports = async (req) => { const res = await axios.get('http://xxx.xx/api/community/list') const result = ejs.render(template, {forum_list: res.data.list}) return result }
再加上對應的路由配置:
// seo/routes.js const home_route = require('./src/home/index') const community_route = require('./src/community/index') module.exports = { '^(/?)$': home_route, '^/community$': community_route, }
這樣就實現了先取接口數據再作渲染,保證了蜘蛛訪問能給到完整的數據和html結構。
繼續實現一個帖子詳情的頁面:
<!-- seo/src/community_detail/index.ejs --> const community_route = require('./src/community/index') <div> <h1><%= forum_data.title%></h1> <p><%= forum_data.content%></p> <p>做者:<%= forum_data.user.nickname%></p> </div>
// seo/src/community_detail/index.js const ejs = require('ejs') const fs = require('fs') const path = require('path') const template = fs.readFileSync(path.resolve(__dirname, './index.ejs'), 'utf8'); const axios = require('axios'); module.exports = async (req) => { // 獲取路徑裏的id /community/:id const forum_id = req.path.split('/')[2] const res = await axios.get(`http://xxx.xx/api/community/${forum_id}/details?offset=1&limit=10`) const result = ejs.render(template, {forum_data: res.data}) return result }
一樣加上對應的路由配置:
// seo/routes.js const home_route = require('./src/home/index') const community_route = require('./src/community/index') const community_detail_route = require('./src/community_detail/index') module.exports = { '^(/?)$': home_route, '^/community$': community_route, '^/community/\\d+$': community_detail_route, }
這樣就實現了一個簡單的seo版網站,不須要任何樣式,不須要js作彈框之類的後續交互,只要蜘蛛訪問網址的第一個請求有它要的數據便可,是否是很是的清奇😝。。。
總結來講呢,就是若是你的項目處在線上運營階段而且開發到了必定的集成度了,迫於ssr的改形成本太大,又須要讓一些數據(好比每一篇文章帖子)可以被收錄,就能夠考慮一下個人這個方法🤓。
可是我不保證蜘蛛的防做弊機制,會不會過濾掉我這種跟瀏覽器正常訪問主站差別較大的seo版小網站🤔。目前這個方案還在試驗階段。
瞭解到有一種黑客攻擊手段叫「搜索引擎劫持」,原理也是在網站裏植入惡意代碼,利用小蜘蛛訪問時引導到另外一個網站,從而小蜘蛛爬到的是另外一個網站,瀏覽器直接訪問域名則是正常的,成功的利用了蜘蛛自己只會收集爬到的網站內容並不會驗證與瀏覽器訪問時的內容是否一致這一特色。
測試也很簡單,寫個模擬蜘蛛請求便可,curl、爬蟲、postman均可以模擬蜘蛛的UA來測試。或者改一下搜索引擎蜘蛛的的判斷條件就能夠直接用瀏覽器訪問的呢。
若是有朋友用了我這個方法而且真的有用可以被搜索引擎收錄的話,請記得我😎,要是能打賞就更好了哈哈🤑。