Today, React, Angular, Vue, and the release of ES6 have greatly changed the way of front-end development. The popularization of modularization and componentization has also brought great convenience to development. Some of their derivative technologies, React-native and Weex, for example, also give the front-end the ability to develop good apps.

When it comes to the three frameworks, SPA(Single Page Application) has to be mentioned, and SPA’s biggest problem lies in the first screen rendering and SEO, so in order to make up for these two fatal shortcomings of SPA, we need to use server-side rendering. So what’s the difference between server-side and client-side rendering? Summary:

Server side rendering Client-side rendering
advantages 1. Fast rendering of the first screen

2, conducive to SEO

3. Cache data
1. Separation of front and rear ends

2, partial refresh, good user experience

3. Save server resources
disadvantages 1. Poor user experience

2, not easy to maintain, the client to change, the server sometimes also need to change

3. Occupy server resources
1. SEO is not friendly

2. Slow rendering of the first screen

For SPA SSR, there are many articles related to this topic on the Internet, as well as some related frameworks, such as next. Js, nuxt.js, etc. But today we are talking about SSR has nothing to do with these, but using another SAO operation to do.

What is it, the puppeteer, which I have described in a previous article:

The practice of puppeteer development

Puppeteer que

Implementation approach

First you need to package your React /vue project into a specified file directory (static resource) such as “dist” and then execute puppeteer-SSR to do the following

  • A Node service is started under the project. The service address is http://localhost:8888
  • Run puppeteer, start an instance of Browser, follow the configured route, such as [‘/’,’/login’], then jump to the route (http://localhost:8888/# and http://localhost:8888/#/login), Gets the content of the forward route and writes it to the specified directory

Relevant logic

Without further ado, get right to the code

index.ts

#! /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;

exportinterface Config { PORT: number; OUTPUTDIR: string; INPUTDIR: string; routes: Array<string>; headless: boolean; HASH: boolean; } /** * params {string} PORT Express service PORT, Default dist * params {string} OUTPUTDIR Output directory Default dist * params {string} INPUTDIR Service startup directory Default dist * params {array} routes Required SSR routes default ['/'] * params {Boolean} headless headless mode default ture * params {Boolean} HASH routing mode defaulthashModel * /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("Initialize 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 (` page${DIR}${filename}.html fetching finished '));if (len === this.index) {
            log("");
            log(chalk.greenBright("🎉 all pages captured"));
            log("");
            log(chalk.redBright("npm install -g serve"));
            log(chalk.redBright(`serve ${OUTPUTDIR}/ `));log(""); process.exit(); }}); }); }); } } const ssr = new Ssr(); ssr.init();Copy the code

serve.ts

#! /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;
Copy the code

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 must be a positive integer"));
    process.exit;
  }
  if(! Array.isArray(routes)) {log("");
    log(chalk.bgRedBright("Routes must be an array"));
    process.exit();
  }
  if(! typeof headless) {log("");
    log(chalk.bgRedBright("Headless must be a Boolean value"));
    process.exit();
  }
  if(! typeof HASH) {log("");
    log(chalk.bgRedBright("HASH must be a Boolean value")); process.exit(); }}Copy the code

Puppeteer-ssr will obtain. Ssrconfig. json from the root directory of the puppeteer-SSR command. You can configure parameters as required.

parameter type instructions
PORT number Service port number (default: 8888)
OUTPUTDIR string SSR output directory (default: “dist”)
INPUTDIR string Static resource directory that Node listens to (default: “dist”)
routes Array SSR routes (default: [“/”])
headless boolean headless mode(default: true)
HASH boolean Routing mode (default: true)

validation

The React project is webpacked with static resources

The index. HTML file is

If puppeteer-SSR is executed, the following message is displayed: (ps: the red character indicates that the serve dependency can be installed globally. Run the puppeteer-SSR command in the output directory to view the effect.)

We’ll find the index.html file at this point as follows

The static resource file appears with a dom node associated with the content, which can then be rendered on the server.

If we add a non-existent route to routes, such as [“/””, “/ SSR /xixi”], what happens to puppeteer-SSR? Similarly, puppeteer-SSR will generate relative directories based on the route you configured, as shown below

The directory structure is as follows

After testing, Vue can also achieve this effect, specific will not demonstrate, related code can be viewed on Github.

This is just a simple practice after I understand SSR in the process of learning Puppeteer. Many aspects were not clearly considered in the writing process. It is just the same as my general idea of cas automatic cookie acquisition (see my previous article on puppeteer practice). It is simple to write a non-invasive tool to meet my needs in some aspects of the development process, and use the tool to relatively efficient completion of my development tasks, nothing more. If you have any questions or write wrong, welcome to correct, 😜