现在前端,React、Angular、Vue三足鼎立,再加上ES6的发布,大大改变了前端的开发方式,模块化、组件化的普及也给开发带来了极大的便利,它们的一些衍生技术,好比react-native, weex,也赋予了前端开发体验度良好的APP的能力。html
提到三大框架,不得不提的就是SPA(Single Page Application), 而SPA最大的问题就在于首屏渲染和SEO方面,所以为了补足SPA的这两个致命缺点,就要用到服务端渲染。那么服务端和客户端渲染有什么区别呢?大体总结了一下:前端
服务端渲染 | 客户端渲染 | |
---|---|---|
优势 | 一、首屏渲染快 二、利于SEO 三、缓存数据 |
一、先后端分离 二、局部刷新,用户体验好 三、节约服务器资源 |
缺点 | 一、用户体验差 二、不易于维护,客户端要改,服务端有时也要更改 三、占用服务器资源 |
一、SEO不友好 二、首屏渲染慢 |
针对SPA的SSR,网上有不少与该话题相关的文章,也有一些相关的框架,如next.js,nuxt.js等。但今天咱们所讲的ssr和这些没什么关系,而是利用另一个骚操做来完成。vue
是什么呢,就是puppeteer,关于puppeteer的相关介绍,我在以前的文章有介绍过 详见:node
puppeteer在开发过程当中的实践 react
puppeteer初探webpack
首先须要把你的react/vue项目打包到指定的文件目录(静态资源),如 "dist"目录 而后执行puppeteer-ssr后会进行以下操做git
话很少说,直接上代码github
index.ts
web
#!/usr/bin/env node
import * as puppeteer from "puppeteer";
import Server from "./server";
import * as fs from "fs";
import * as mkdirp from "mkdirp";
import chalk from "chalk";
import validate from "./validate";
let ssrConfigFile = process.cwd() + "/.ssrconfig.json";
const isSsrConfigExist = fs.existsSync(ssrConfigFile);
let ssrconfig = "{}";
if (isSsrConfigExist) {
ssrconfig = fs.readFileSync(ssrConfigFile, "utf8");
}
const log = console.log;
export interface Config {
PORT: number;
OUTPUTDIR: string;
INPUTDIR: string;
routes: Array<string>;
headless: boolean;
HASH: boolean;
}
/**
* params {string} PORT express服务端口, default 8888
* params {string} OUTPUTDIR 输出目录 default dist
* params {string} INPUTDIR 服务启动目录 default dist
* params {array} routes 须要ssr的路由 default ['/']
* params {boolean} headless headless mode default ture
* params {boolean} HASH 路由模式 default hash模式
*/
let {
PORT = 8888,
OUTPUTDIR = "dist",
INPUTDIR = "dist",
routes = ["/"],
headless = true,
HASH = true
}: Config = JSON.parse(ssrconfig);
class Ssr {
private index: number;
constructor() {
this.index = 0;
validate({
PORT,
OUTPUTDIR,
INPUTDIR,
routes,
headless,
HASH
});
}
async init() {
const server = new Server(PORT, INPUTDIR);
server.init();
log(chalk.greenBright("初始化browser"));
const browser = await puppeteer.launch({
headless
});
if (routes.length === 0 || !routes[0]) {
routes = ["/"];
}
const len = routes.length;
routes.map((v, i) => {
if (!v) routes.splice(i, 1);
});
routes.map(async (v: string) => {
const page = await browser.newPage();
const FRAGMENT = HASH ? "/#" : "";
const HISTORY = v.startsWith("/") ? v : `/${v}`;
const URL = `http://localhost:${PORT}${FRAGMENT}${HISTORY}`;
await page.goto(URL);
await page.waitForSelector("body");
const content = await page.content();
let DIR = `${process.cwd()}/${OUTPUTDIR}${HISTORY}`;
await mkdirp(DIR, err => {
if (err) {
console.error(err);
}
const filename = v.split("/").pop() || "index";
DIR = DIR.endsWith("/") ? DIR : DIR + "/";
fs.writeFile(`${DIR}${filename}.html`, content, err => {
if (err) {
console.error(err);
}
this.index++;
log(chalk.greenBright(`页面 ${DIR}${filename}.html 抓取完毕`));
if (len === this.index) {
log("");
log(chalk.greenBright("🎉 全部页面抓取完毕"));
log("");
log(chalk.redBright("npm install -g serve"));
log(chalk.redBright(`serve ${OUTPUTDIR}/`));
log("");
process.exit();
}
});
});
});
}
}
const ssr = new Ssr();
ssr.init();
复制代码
serve.ts
express
#!/usr/bin/env node
import * as express from "express";
import chalk from "chalk";
const log = console.log;
class Server {
private port: number;
private staticDir: string;
public app: any;
constructor(port: number, staticDir: string) {
this.port = port;
this.app = express();
this.staticDir = staticDir;
}
init() {
const { port, staticDir } = this;
this.app.use(express.static(staticDir));
this.app.listen(port, () => {
log(chalk.greenBright(`server running at http://localhost:${port}/`));
log("");
});
}
}
export default Server;
复制代码
validate.ts
import { Config } from "./index";
import chalk from "chalk";
const log = console.log;
export default function validate({
PORT,
OUTPUTDIR,
INPUTDIR,
routes,
headless,
HASH
}: Config) {
if (PORT < 0 || !Number.isInteger(PORT)) {
log("");
log(chalk.bgRedBright("PORT必须为正整数"));
process.exit;
}
if (!Array.isArray(routes)) {
log("");
log(chalk.bgRedBright("routes必须是一个数组"));
process.exit();
}
if (!typeof headless) {
log("");
log(chalk.bgRedBright("headless 必须是一个Boolean值"));
process.exit();
}
if (!typeof HASH) {
log("");
log(chalk.bgRedBright("HASH 必须是一个Boolean值"));
process.exit();
}
}
复制代码
puppeteer-ssr
会读取执行命令的根目录下读取.ssrconfig.json
,可按需配置参数(注: 若是没有.ssrconfig.json
,则以默认参数为准)
参数 | 类型 | 说明 |
---|---|---|
PORT | number | 服务端口号(default: 8888) |
OUTPUTDIR | string | ssr 输出目录(default: "dist") |
INPUTDIR | string | node 监听的静态资源目录(default: "dist") |
routes | Array | 须要 ssr 的路由(default: ["/"]) |
headless | boolean | headless mode(default: true) |
HASH | boolean | 路由模式(default: true) |
react项目通过webpack打包后的静态资源目录为: (点击查看如何快速生成react项目
其中index.html文件为
执行puppeteer-ssr
,会有以下提示(ps: 红字为能够全局装serve这个依赖,而后在输出目录下执行,能够查看效果, 下同)
咱们会发现此时的index.html文件以下
静态资源文件出现了和内容相关的dom节点, 而后咱们就能够把该文件放在服务端上进行渲染。
那么若是咱们在routes里面配了一个并不存在的路由, 如["/"", "/ssr/xixi"], puppeteer-ssr
是怎样处理的呢?一样, puppeteer-ssr
会根据你配置的路由,生成相对于的目录,以下
目录结构以下
经测试,vue也是能够达到该效果,具体就不演示了,相关代码可在github上查看。
这里只是我在学习puppeteer的过程当中对SSR理解后的一个简单实践,在写的过程当中不少地方没考虑的很清楚,只是和我以前写cas自动获取cookie(详见我以前写的关于puppeteer实践的文章)的一个大思路同样,就是简单的写一个无侵入的工具来知足我在开发过程当中对某些方面的需求,用工具来相对高效的完成个人开发任务,仅此而已。若有疑问或者写的有误,欢迎指正,😜