本文主要讲如何优化前后端分离的系统(分析原因+解决方案)
为什么要做SEO,这个问题想必大家都知道,不知道的百度去。
发现问题
对开发工程师人员来说SEO并不陌生,8月初我们上线了新版本 智源社区 : https://hub.baai.ac.cn 待所有功能完善后(中间发了很多小版本修复bug),运营人员在Google上搜索并没有检索出我们社区的最新内容,最新的检索内容停留在了五月份,随后反馈到技术。
项目介绍
前端使用的是vue开发、后端是php,go, node.js 属于前后端分离的系统。 首页,个人主页采用的是vue开发,详情页是vue页面中嵌套的服务端渲染的页面。
问题分析
排查后发现爬虫对iframe 一点都不友好,恰好我们使用vue + iframe方式嵌入的内容页面这时候我就想到爬虫仅爬去页面源代码分析内容,此时还没等js的内容加载就已经返回了结果(下图所示),所以在Google上检索就没内容了
解决方案实践
我的想法是处理两种不同的页面:1,正常用户使用、2,爬虫使用。在网上冲浪一番后看到最多的是 Prerender 预渲染然后根据user-agent转发到Prerender 爬虫就能拿到正常数据了, 随后尝试本地容器化搭建进行测试。
方案一 :Prerender 预渲染
1,下载源码到本地
1https://github.com/halobug/prerender
2,制作Docker镜像,Dockerfile
1FROM node:16
2
3ENV LANG en_US.UTF-8
4ENV LANGUAGE en_US.UTF-8
5ENV LC_ALL=en_US.UTF-8
6
7WORKDIR /app
8
9COPY ./app/package.json /app/package.json
10RUN npm install --registry=https://registry.npm.taobao.org
11COPY ./app /app
12
13RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
14 && sh -c 'echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
15 && apt-get update -y \
16 && apt-get install google-chrome-stable -y \
17 && apt-get clean \
18 && rm -rf /tmp/* /var/lib/apt/lists/*
19
20# Add Chrome as a user
21RUN groupadd -r chrome && useradd -r -g chrome -G audio,video chrome \
22 && mkdir -p /home/chrome && chown -R chrome:chrome /home/chrome
23
24RUN chown -R chrome:chrome /home/chrome/
25
26# Run Chrome non-privileged
27USER chrome
28
29EXPOSE 3000
30
31CMD node /app/server.js
32
3,build 镜像
1docker build -t halobug/prerender:1.0 .
4,build 无异常后运行容器&进入容器
1docker run -it --privileged halobug/prerender:1.0 sh
5,运行测试
11,docker exec -it 容器ID sh
2
32,node /app/server.js
4
53,curl http://127.0.0.1:3000/https://hub.halobug.cn/v/10248
6
测试后的确是预加载了页面,但是严重的问题来了CPU 112% 飙升,响应5-8秒性能及差,无法提供正常服务(冲浪过没找到优化方案)。
果断放弃!继续第二种~
方案二 :使用Nginx的njs模块,根据api生成静态页面进行proxy
正常用户访问项目,爬虫访问进入njs生成静态,参考njs文章,在新版中的nginx 官方已经支持了njs模块,不需要重新安装了。
1,app.js 创建
目录 ./script/app.js
1// https://github.com/nginx/njs/issues/352#issuecomment-721126632
2function resolveAll(promises) {
3 return new Promise((resolve, reject) => {
4 var n = promises.length;
5 var rs = Array(n);
6 var done = () => {
7 if (--n === 0) {
8 resolve(rs);
9 }
10 };
11 promises.forEach((p, i) => {
12 p.then((x) => {
13 rs[i] = x;
14 }, reject).then(done);
15 });
16 });
17}
18
19function aggregation(req) {
20
21 var apis = ["/proxy/api-mysql/common/chat/chat-translation-data.json"];
22 resolveAll(apis.map((api) => req.subrequest(api)))
23 .then((responses) => {
24 var result = responses.reduce((prev, response) => {
25 var uri = response.uri;
26 var prop = uri.split("/proxy/api-")[1].split("/")[0];
27 try {
28 var parsed = JSON.parse(response.responseText);
29 if (response.status === 200) {
30 prev[prop] = parsed;
31 }
32 } catch (err) {
33 req.error(`Parse ${uri} failed.`);
34 }
35 return prev;
36 }, {});
37 req.headersOut["Content-Type"] = "application/json;charset=UTF-8";
38 req.return(200, JSON.stringify(result));
39 })
40 .catch((e) => req.return(501, e.message));
41}
42
43export default { aggregation };
2,nginx.conf 创建
目录 ./nginx.conf
1load_module modules/ngx_http_js_module.so;
2
3user nginx;
4worker_processes auto;
5
6error_log /var/log/nginx/error.log warn;
7pid /var/run/nginx.pid;
8
9events {
10 worker_connections 1024;
11}
12
13http {
14 include /etc/nginx/mime.types;
15 default_type application/json;
16
17 js_import app from script/app.js;
18
19 log_format main '$remote_addr - $remote_user [$time_local] "$request" '
20 '$status $body_bytes_sent "$http_referer" '
21 '"$http_user_agent" "$http_x_forwarded_for"';
22
23 access_log /var/log/nginx/access.log main;
24 sendfile on;
25 keepalive_timeout 65;
26
27 server {
28 listen 80;
29 server_name localhost;
30 charset utf-8;
31 gzip on;
32
33 location / {
34 js_content app.aggregation;
35 }
36 #这里不可少
37 subrequest_output_buffer_size 200k;
38 location /proxy/api-mysql {
39 internal;
40 gunzip on;
41 proxy_pass https://www.mysql.com/;
42 proxy_set_header Host www.mysql.com;
43 }
44
45 error_page 500 502 503 504 /50x.html;
46 location = /50x.html {
47 root /usr/share/nginx/html;
48 }
49 }
50}
3,编写 docker-compose.yml
1version: "3.6"
2
3services:
4 local-halobug:
5 image: nginx:1.19.8-alpine
6 restart: always
7 expose:
8 - 80
9 networks:
10 - traefik
11 volumes:
12 - ./logs:/var/log/nginx
13 - ./nginx.njs.conf:/etc/nginx/nginx.conf:ro
14 - ./script:/etc/nginx/script
15 labels:
16 - "traefik.enable=true"
17 - "traefik.docker.network=traefik"
18 - "traefik.http.routers.local_halobug.entrypoints=https"
19 - "traefik.http.routers.local_halobug.tls=true"
20 - "traefik.http.routers.local_halobug.rule=Host(`nginx.halobug.cn`)"
21 - "traefik.http.services.local_halobug-backend.loadbalancer.server.scheme=http"
22 - "traefik.http.services.local_halobug-backend.loadbalancer.server.port=80"
23 logging:
24 driver: "json-file"
25 options:
26 max-size: "1m"
27
28networks:
29 traefik:
30 external: true
31
浏览器访问出现以下结果代表成功(接口返回的内容自己组装静态吧)
4,运行成功后修改原项目的nginx 配置进行代理转发
1# 爬虫使用的静态页
2location ~ ^/view/(\d+)$ {
3 set $is_proxy 0;
4 # 部分爬虫的user-agent
5 if ($http_user_agent ~* "googlebot|bingbot|yandex|baiduspider|360Spider|Sogou Spider|Bytespider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest\/0\.|pinterestbot|slackbot|vkShare|W3C_Validator|whatsapp") {
6 ##
7 set $is_proxy 1;
8 }
9 if ($is_proxy = 1) {
10 # 容器内互通的容器名称 seo-njs.cn;
11 proxy_pass http://seo-njs.cn;
12 }
13 root /usr/share/nginx/html;
14 index index.html index.htm;
15 try_files $uri $uri/ /index.html;
16}
17location =/robots.txt {
18 default_type text/html;
19 add_header Content-Type "text/plain; charset=UTF-8";
20 return 200 "User-Agent: *\nAllow: /";
21}
5,模拟爬虫进行测试,不出意外的话展示组装后静态页面
1curl -A "Googlebot" https://hub.halobug.cn/view/9268
测试后nginx几乎没有压力,最终选择方案二。
实践过程中出现错误信息,请保持良好心态继续解决 哈哈哈~
以上测试均为本地,没有隐私内容。