171 lines
5.2 KiB
JavaScript
171 lines
5.2 KiB
JavaScript
|
|
/**
|
|||
|
|
* SEO中间件
|
|||
|
|
* 用于在Node.js服务器中检测爬虫并返回静态HTML
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
const fs = require('fs')
|
|||
|
|
const path = require('path')
|
|||
|
|
const CrawlerDetector = require('./crawler-detector')
|
|||
|
|
|
|||
|
|
class SEOMiddleware {
|
|||
|
|
constructor(options = {}) {
|
|||
|
|
this.detector = new CrawlerDetector()
|
|||
|
|
this.templateDir = options.templateDir || path.join(__dirname, '../public/seo-templates')
|
|||
|
|
this.defaultTemplate = options.defaultTemplate || 'index.html'
|
|||
|
|
this.fallbackToSPA = options.fallbackToSPA !== false
|
|||
|
|
this.debug = options.debug || false
|
|||
|
|
|
|||
|
|
// 路由到模板的映射(与 useSEO.js 的 routeConfigs 保持一致)
|
|||
|
|
this.routeTemplateMap = {
|
|||
|
|
'/': 'index.html',
|
|||
|
|
'/agent': 'agent.html',
|
|||
|
|
'/help/guide': 'help-guide.html',
|
|||
|
|
'/help': 'help.html',
|
|||
|
|
'/example': 'example.html',
|
|||
|
|
'/service': 'service.html'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 初始化模板缓存
|
|||
|
|
this.templateCache = new Map()
|
|||
|
|
this.cacheTemplates()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 缓存所有模板文件
|
|||
|
|
*/
|
|||
|
|
cacheTemplates() {
|
|||
|
|
try {
|
|||
|
|
if (!fs.existsSync(this.templateDir)) {
|
|||
|
|
console.warn(`[SEOMiddleware] 模板目录不存在: ${this.templateDir}`)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const files = fs.readdirSync(this.templateDir)
|
|||
|
|
files.forEach(file => {
|
|||
|
|
const filePath = path.join(this.templateDir, file)
|
|||
|
|
if (fs.statSync(filePath).isFile()) {
|
|||
|
|
this.templateCache.set(file, fs.readFileSync(filePath, 'utf-8'))
|
|||
|
|
if (this.debug) {
|
|||
|
|
console.log(`[SEOMiddleware] 已缓存模板: ${file}`)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
console.log(`[SEOMiddleware] 已缓存 ${this.templateCache.size} 个模板文件`)
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('[SEOMiddleware] 缓存模板失败:', error)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取对应的模板文件名
|
|||
|
|
* @param {String} path - 请求路径
|
|||
|
|
* @returns {String} 模板文件名
|
|||
|
|
*/
|
|||
|
|
getTemplatePath(requestPath) {
|
|||
|
|
// 完全匹配
|
|||
|
|
if (this.routeTemplateMap[requestPath]) {
|
|||
|
|
return this.routeTemplateMap[requestPath]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 模糊匹配(处理动态路由)
|
|||
|
|
const matchedKey = Object.keys(this.routeTemplateMap).find(route => {
|
|||
|
|
return requestPath.startsWith(route)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
return matchedKey ? this.routeTemplateMap[matchedKey] : this.defaultTemplate
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取模板内容
|
|||
|
|
* @param {String} templateName - 模板文件名
|
|||
|
|
* @returns {String|null} 模板内容
|
|||
|
|
*/
|
|||
|
|
getTemplate(templateName) {
|
|||
|
|
// 首先尝试缓存
|
|||
|
|
let content = this.templateCache.get(templateName)
|
|||
|
|
|
|||
|
|
// 如果缓存中没有,尝试从磁盘读取
|
|||
|
|
if (!content) {
|
|||
|
|
try {
|
|||
|
|
const filePath = path.join(this.templateDir, templateName)
|
|||
|
|
if (fs.existsSync(filePath)) {
|
|||
|
|
content = fs.readFileSync(filePath, 'utf-8')
|
|||
|
|
this.templateCache.set(templateName, content)
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(`[SEOMiddleware] 读取模板失败: ${templateName}`, error)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return content || null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Express中间件
|
|||
|
|
*/
|
|||
|
|
express() {
|
|||
|
|
return (req, res, next) => {
|
|||
|
|
// 检测是否为爬虫
|
|||
|
|
if (this.detector.isCrawler(req)) {
|
|||
|
|
const templateName = this.getTemplatePath(req.path)
|
|||
|
|
const template = this.getTemplate(templateName)
|
|||
|
|
|
|||
|
|
if (template) {
|
|||
|
|
// 设置响应头
|
|||
|
|
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
|||
|
|
res.setHeader('X-SEOMiddleware', 'prerendered')
|
|||
|
|
|
|||
|
|
// 返回静态HTML
|
|||
|
|
if (this.debug) {
|
|||
|
|
console.log(`[SEOMiddleware] 返回SEO模板: ${templateName} for ${req.path}`)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return res.send(template)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 不是爬虫或模板不存在,继续处理SPA
|
|||
|
|
next()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Koa中间件
|
|||
|
|
*/
|
|||
|
|
koa() {
|
|||
|
|
return async (ctx, next) => {
|
|||
|
|
// 检测是否为爬虫
|
|||
|
|
if (this.detector.isCrawler(ctx.req)) {
|
|||
|
|
const templateName = this.getTemplatePath(ctx.path)
|
|||
|
|
const template = this.getTemplate(templateName)
|
|||
|
|
|
|||
|
|
if (template) {
|
|||
|
|
ctx.type = 'text/html; charset=utf-8'
|
|||
|
|
ctx.set('X-SEOMiddleware', 'prerendered')
|
|||
|
|
|
|||
|
|
if (this.debug) {
|
|||
|
|
console.log(`[SEOMiddleware] 返回SEO模板: ${templateName} for ${ctx.path}`)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ctx.body = template
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await next()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 重新加载模板缓存
|
|||
|
|
*/
|
|||
|
|
reloadCache() {
|
|||
|
|
this.templateCache.clear()
|
|||
|
|
this.cacheTemplates()
|
|||
|
|
console.log('[SEOMiddleware] 模板缓存已重新加载')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
module.exports = SEOMiddleware
|