現在前端,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實踐的文章)的一個大思路同樣,就是簡單的寫一個無侵入的工具來知足我在開發過程當中對某些方面的需求,用工具來相對高效的完成個人開發任務,僅此而已。若有疑問或者寫的有誤,歡迎指正,😜