vue的SSR方案探讨
最近有一个项目要求SEO,于是摸索了一下vue的ssr方案,顺便总结一下。
先简单说一下ssr(Server-Side Rendering),官方是这样说的:
Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序
其实也就是说把在客户端生成的HTML节点,改成在服务端生成
这样做的好处是,可以加快首屏的渲染(加快内容到达时间),对seo更友好。缺点是开发条件所限,需要更多的服务器端负载等,详情内容请移步官网https://ssr.vuejs.org/zh
本文总结了四种ssr方案,各有利弊
一. vue官网SSR方案
先上一张构建流程图
大概就是分别定义客户端入口(client entry)和服务端入口(server entry),然后通过webpack分别打包生成 server bundle 和client bundle
server bundle 用于服务器端渲染(SSR), client bundle发送给浏览器,用于混合静态标记
服务端的渲染,需要用到‘vue-server-renderer’插件进行处理,同时还要对vue入口文件和vue router 进行改造,具体实现请看demo,这里不再展开。
https://github.com/chenpeiguang/vue-cli-ssr-demo
几个需要注意的地方
1.不要把有副作用的代码放到created中,比如调用浏览器的api,因为created钩子会在服务端渲染的时候触发,避免找不到相关api报错
2.最好不要使用第三方的插件(需要调用浏览器api),比如vue toast,原因同上
3.官方提到的
二. nuxt.js
nuxt是vue的ssr框架,为ssr提供整套的解决方案。可以很好的解决上面第一种存在的一些问题,缺点就是增加学习成本,具体实现看官方教程,https://zh.nuxtjs.org/guide,一笔带过。
三. 利用puppeteer
Google Chrome 团队官方的无界面(Headless)Chrome 工具,它是一个 Node 库,提供了一个高级的 API 来控制 DevTools协议上的无头版 Chrome。
思路就是,利用puppeteer抓取SPA并生成预先呈现的内容,也就是ssr
实现:
1.根目录新建app.js,作为静态服务器,为不需要ssr的静态资源提供访问
const Koa = require('koa')
const app = new Koa()
const server = require('koa-static')
const cors = require('@koa/cors')
app.use(cors())
app.use(server(`${__dirname}/dist`, {
maxage: 7200 * 1000,
}))
const port = process.env.PORT || 5001
app.listen(port)
console.log(`is working on port ${port}`)
2.根目录新建server.js,通过puppeteer进行ssr
const Koa = require('koa');
const app = new Koa();
const server = require('koa-static');
const puppeteer = require('puppeteer');
const { URL } = require('url');
const cors = require('@koa/cors');
const RENDER_CACHE = new Map();
app.use(cors());
app.use(async (ctx, next) => {
const url = new URL(ctx.request.href);
url.hostname = 'localhost'
url.port = process.env.PORT || 5001;
console.log('url: ', url.href);
if (!isNeedSsrPath(url.pathname)) {
console.log('is not need ssr path: ', url.pathname);
await next();
} else {
const { html, ttRenderMs } = await ssr(url.href);
ctx.set('Server-Timing', `Prerender;dur=${ttRenderMs};desc="Headless render time (ms)"`);
ctx.body = html;
}
});
app.use(server(`${__dirname}/dist`, {
maxAge: 7200 * 1000,
}));
const port = process.env.SSR_PORT || 5009;
app.listen(port);
console.log(`ssr is working on port ${port}`);
async function ssr (url) {
if (RENDER_CACHE.has(url)) {
return {html: RENDER_CACHE.get(url), ttRenderMs: 0};
}
const start = Date.now();
const browser = await puppeteer.launch();
const page = await browser.newPage();
try {
await page.goto(url, {waitUntil: ['domcontentloaded', 'load', 'networkidle0', 'networkidle2']});
} catch (err) {
console.error(err);
}
const html = await page.content(); // serialized HTML of page DOM.
await browser.close();
const ttRenderMs = Date.now() - start;
console.info(`Headless rendered page in: ${ttRenderMs}ms`);
RENDER_CACHE.set(url, html);
return {html, ttRenderMs};
}
function isNeedSsrPath (path) {
let flag = true;
const dontNeedSsrPathArray = ['/static', '/js', '/css'];
dontNeedSsrPathArray.forEach(item => {
if (path.startsWith(item)) {
flag = false;
}
});
return flag;
}
这种方案的好处,就是不像上面两种方案,需要改动原来的代码,在项目的任何一个阶段,都可以很好的进行ssr改造。并且可以很好的控制哪些页面需要ssr,哪些页面不需要ssr。
再想一下,其实这种方案也是有缺点的,无论爬虫还是用户正常访问都需要server.js入口访问,遇到静态资源或者不需要ssr的页面,还需要转到另一个静态服务器。增加访问成本。
这里还可以优化一下。通过请求,判断一下爬虫,如果是爬虫,就按照上面的使用puppeteer进行ssr ,返回html结构。如果是正常用户,可以直接build一个版本访问。
就是区分爬虫