前端
DNS分别解析实现前端渲染+SEO

背景

本文源自对知乎回答 《有用nuxt的公司吗?公司的项目需要seo又想前后端分离,想知道你们怎么解决性能问题的?》 的实践,即【如何实现即能前端渲染又满足SEO的需求】

回答本身让我眼前一亮,打破了我前端渲染无法SEO的固有认知。其中最关键的步骤是:DNS解析后端渲染的线路可以选择搜索引擎

以下是在实践过程中遇到的问题和解决

现状和架构思路

本博客架构出自《node+express+mongdb教程》 ,是后端渲染的项目,地址:https://www.gjx.zone  

把原来的项目1分为2:

前端项目fe,负责页面

后端项目node-blog,负责api数据(/api/前缀)和渲染ejs,。跑在ESC服务器上

普通用户对应的fe流程是:获取前端页面后,再请求api数据。打包后的静态页存在OSS,并开启CDN加速

搜索引擎对应的是:获取后端ejs的html

步骤1:准备一份基于api的后端服务+前端项目

这里我选择从原来的项目基础更改,除了常规的改动:

router.options('/api/count', AccessCros);
router.get('/api/count', AccessCros, function (req, res) {
        // ...
        return res.json({count: 100})

});
// 取代原来的render-ejs模式
// app.get('/post', checkLogin, function (req, res) {
//   res.render('post', {
//     title: '发表',
//     user: req.session.user,
//   });
// });

鉴权部分由session更改为了jwt:

// function checkLogin(req, res, next) {
//   if (!req.session.user) {
//     req.flash('error', '未登录!');
//     res.redirect('/login');
//   }
//   next();
// }
router.post('/api/login', AccessCros, function (req, res) {
    // ...
    const tokenStr = jwt.sign({ userName: req.body.name }, 'SECRET', { expiresIn: 3600 * 24 * 3 })
    res.send({
        status: 200,
        message: '登录成功!',
        token: tokenStr // 要发送给客户端的 token 字符串
    })
})
const tokenMid = (req, res, next) => {
    try {
        req.user = verf(req.headers.authorization)
    } catch (error) {
        // console.log(error, 'verf error')
    } finally {
        next()
    }
}
router.post('/api/edit', tokenMid, function (req, res) {
    const user = req.user
    if (!user) {
        res.json({ message: '更新失败' });
    } else {
        res.json({ message: '-' });
    }
})
问题1:生成的token不能直接传

一开始用的【express-jwt】,发现接口会返回401:UnauthorizedError: Format is Authorization: Bearer

这个插件源码意图是指需要持票人开头的规范,即: Bearer [token], 所以还得传入getToken直接返回请求头解决

const expressJWT = require('express-jwt')
const SECRET = 'gjxzone'
const exjwt = expressJWT.expressjwt({ 
    secret: SECRET, 
    algorithms: ['HS256'],
    // credentialsRequired: false,
    getToken: function noPreBearer(req) {
        // 传入使前端不必传[Bearer xxx]
        return req.headers.authorization||null
    },
})

router.get('/api/auth', exjwt, function (req, res) {
    if (req.auth) {
        res.send({
            status: 200,
            message: '获取用户信息成功!',
            data: req.auth // 要发送给客户端的用户信息
        })
    } else {
        res.send({
            status: 200,
            message: '-',
        })
    }
})

既然是前后端拆分,那绕不过的就是跨域,中间件的鉴权:    

function AccessCros(req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "content-type,Authorization");
    res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
    res.header("Content-Type", "application/json;charset=utf-8");
    next();
}

// 最终是两个中间件,一个鉴权,一个跨域
router.options('/auth', AccessCros);
router.get('/auth', exjwt, AccessCros, function (req, res) {
    if (req.user) {
        res.send({
            status: 200,
            message: '获取用户信息成功!',
            data: req.user // 要发送给客户端的用户信息
        })
    } else {
        res.send({
            status: 200,
            message: '-',
        })
    }
})
问题2: 线上的nginx会和express的跨域设置冲突

由于源站nginx(本地开发node是3000端口,线上为nginx代理https://www.gjx.zone)也开启了跨域:

location / {
		#网站主页路径,代理3000的node项目
		proxy_read_timeout 300s;
		proxy_connect_timeout 75s;
		proxy_pass http://localhost:3000;
		add_header Access-Control-Allow-Origin *;
        add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
}

nginx和node共同对返回头设置跨域会冲突:Access to XMLHttpRequest at 'https://gjx.zone/b-h' from origin 'http://127.0.0.1:5173' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header contains multiple values '*, *', but only one is allowed.

解决比较简单,区分环境,线上才走nginx,开发走express:

function AccessCros(req, res, next) {
    if (req.headers.origin.includes('localhost')) {
        res.header("Access-Control-Allow-Origin", "*");
        res.header("Access-Control-Allow-Headers", "content-type,Authorization");
        res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
        res.header("Content-Type", "application/json;charset=utf-8");
    }
    // 线上走nginx
    next();
}

步骤2:买服务器(阿里ESC),CDN,OSS

问题:ping通,但无法访问

购买并开通后,从腾讯云的DNS域名解析管理页绑定了阿里云ESC:

也配好了nginx,ping也能通:

但站点就是访问不了,最终还是定位到问题了,是阿里云的规则:  接入备案对网站访问的影响备案信息未成功接入阿里云之前,如果将域名解析指向阿里云中国内地服务器,会导致网站无法正常访问

解决:阿里云需要重新下载模板,按手印

之前为了备案,特意花100大洋买了腾讯云服务器(没办法gjx.zone域名是从腾讯云买的),没想到到了阿里云这边也需要重新备案,并且也需要拥有一个三个月以上的ESC。。。😔

这里感谢小姨子的大学生身份,让预算不足的我蹭上了阿里云对大学生7个月的优惠活动

备案流程都是一步一步按官网步骤来,跟我首次在腾讯云备案一样,阿里云也会人工核实初审,期间我被两次打回,一次是身份证照片反光,一次是不能用旧的腾讯云备案模板,得重新按手印。

值得一提的是,阿里云初审一过,域名就能正式访问了,不用等到ICP备案通过,那可是整整20天啊


步骤3: 前端项目实现DNS域名解析+cdn加速

前端工程上传了oss,开了自定义域名和静态托管,其中404页仍是index.html是为了能实现前端history类型的路由跳转:

这点上跟nginx的try_files是一个作用

server {
        listen       8080;
        server_name localhost;
        location / {
            root   C:/project/fe-project/dist;
            index index.html;
            try_files $uri $uri/ /index.html;
        }
}

由于文件只放ossping域名只有一个ip节点 所以开cdn加速

这里是各种跟DNS解析打交道:

1. 之前的oss自定义域名的CNAME校验

这步的目的应该是验证域名确实是本人使用,且具备解析权。也即实现下图步骤2

oss自定义域名

2. 开cdn后,需要在cdn绑定域名,而背后的源站指向oss域名

这步也是需要CNAME的DNS解析

3. 配置HTTPS证书,需要提前申请SSL,申请SSL时也需要做DNS解析

对于以上三步都需要的DNS解析,我个人理解无论是CNAME还是TXT目的都是为了验证所有权

步骤4:前端项目设置cdn回源策略

之前对CDN策略比较陌生,只是听运维的同事提起回源。

回源就是指用户身边的服务器没有留存真正想要的文件(比如最新的css),需要触发【回到源站获取文件】的动作

问题: 静态托管的文件可以自动刷新cdn,但其他路由不能

前文说到静态托管的404页 = 前端的其他路由

3.13上传了一份html:

过了几分钟后,只有首页可以成功回源刷新:

其他路由都没有办法刷新

提了工单,售后给的方案是把那些为止的统统设置缓存时间0,即统统回源

分析了下,本身前端的其他路由也确实没有对应上oss的真正文件,那还凭借什么【回源】呢,也难怪售后会用‘非正常’来描述了

到这里让我有点怀疑这个 oss静态托管+cdn加速+api动态请求博客内容的架构到底合不合理了,有一种“假静态化”的感觉

目前的解决是每次上传oss后,手动刷新cdn的缓存(所有的前端路由,可以用正则提交)

步骤5: DNS针对SEO,开启对后端渲染项目的解析

选择【搜索引擎】线路直接指向原来的站点,也即ESC服务器ip

至此,所有步骤都完成了,可以验证了

步骤6: 验证google

针对前端用户:

针对搜索引擎(这里需要翻墙到google search console):

再看看google搜索:


总结

通过dns分别对搜索引擎和普通用户的解析,能实现针对SEO抓包的后端渲染页面+用户看到的前端渲染页面

我认为即便明白知乎答主的流程设计,实践起来这个过程还是比较曲折的。知行合一确实也能让我们学习更多,也希望本篇可以帮助到一些伙伴。


日期:2024-03-18 17:21:08 | 阅读:222 | 评论:0