之前写了两个 demo 解说了如何实现 SSRSSG,明天再写个 demo 说在 ISR 如何实现。

什么是 ISR

ISRIncremental Static Regeneration 增量动态再生,是指在 SSG 的前提下,能够在收到申请时断定页面是否须要刷新,如果须要则从新构建该页面,这样既领有了动态页面的劣势又能够防止页面长时间未更新导致信息过期。且因为在页面维度验证,所以每次能够只构建特定的页面。

ISR 个别实用于合乎 SSG 场景,然而却对页面的时限性有肯定要求时。

如何实现

简略的 ISR 实现也很简略,只须要在收到页面申请时依照更新策略判断是否须要须要从新生成页面,如果须要触发页面的构建更新。须要留神个别状况下生成页面不会影响页面的响应,而是后盾去做构建。

当初就基于之前写的 SSG demo,做一下革新让其反对 ISR

批改构建脚本

因为 ISR 构建会同时在构建脚本和服务器中触发,所以须要对之前的代码做一些小小的改变。

首先抽离出一个通用的构建函数(因为服务器会应用到尽量避免同步代码):

import fs from 'fs/promises';import { renderToString } from 'react-dom/server';import React from 'react';import Post from './ui/Post';import List from './ui/List';async function build(type: 'list'): Promise<void>;async function build(type: 'post', name: string): Promise<void>;async function build(type: 'list' | 'post', name?: string) {    if (type === 'list') {        const posts = await fs.readdir('posts');        await fs.writeFile(            'dist/index.html',            `<div id="root">${renderToString(                <List                    list={posts.map(post => {                        delete require.cache['posts/' + post];                        return { ...require('./posts/' + post), key: post.replace('.json', '') };                    })}                />            )}</div>`        );    } else {        delete require.cache['posts/' + name];        const postInfo = require('./posts/' + name);        const fileName = `dist/posts/${name}.html`;        await fs.writeFile(fileName, `<div id="root">${renderToString(<Post data={postInfo} />)}</div>`);    }}export default build;

这样就能够通过 build 函数来构建指定的 post 或者 list 页面。

而后再将原先的构建脚本做一下简略的批改:

import fs from 'fs';import build from './build-util';// make sure the dir existsif (!fs.existsSync('dist')) {    fs.mkdirSync('dist');}if (!fs.existsSync('dist/posts')) {    fs.mkdirSync('dist/posts');}// get all the files in postsconst posts = fs.readdirSync('posts');(async () => {    for await (const post of posts) {        await build('post', post.replace('.json', ''));    }    await build('list');})();

服务器

因为 ISR 须要在申请时做是否构建的断定,所以原先的动态服务器计划无奈持续应用,咱们换成 express 来实现:

import express from 'express';import path from 'path';import fs from 'fs';import build from '../build-util';const app = express();const expiresTime = 1000 * 60 * 10;app.use(function (req, res, next) {    setTimeout(() => {        const filename = req.path.indexOf('.html') >= 0 ? req.path : req.path + 'index.html';        // get the file's create timestamps        fs.stat(path.join('./dist', filename), function (err, stats) {            if (err) {                console.error(err);                return;            }            if (Date.now() - +stats.mtime > expiresTime) {                console.log(filename, 'files expired, rebuilding...');                if (filename === '/index.html') {                    build('list');                } else {                    build('post', path.basename(filename).replace('.html', ''));                }            }        });    });    next();});app.use(express.static('dist'));app.listen(4000, () => {    console.log('Listening on port 4000');});

咱们减少一个 express 的中间件,让其来断定文件是否过期,这里以十分钟为例,理论场景可按需定义过期断定。这里过期后就会调用 build 文件来从新构建该文件。要留神此处先返回再构建,所以用户不会期待构建,并且此次拜访仍旧是旧的内容,构建实现后拜访的才是新的内容。

更多细节

  • 留神给构建工作加锁,防止一个页面过期后多个申请同时触发多个同样的构建工作
  • 给构建工作加队列,防止申请过多时同时呈现过多的后盾构建工作导致服务器资源问题
  • 能够为每个文件制订特定的过期断定条件,比方 post 源文件的批改工夫等等

总结

ISR 比照 SSG 能够无效的管制页面的时效性,但也要付出额定的代价:

  • 须要额定的开发成本
  • 须要额定的服务器资源投入
  • 无奈应用个别的动态文件服务器

没有最佳,只有最适宜,所以理论场景下还是按需选用。

最初

本文的 demo 代码搁置在 React ISR Demo 中,可自行取阅。