UNPKG

santi

Version:

Isomorphic framework for base on create-react-app and jsdom

225 lines (192 loc) 5.85 kB
const httpProxy = require('koa-server-http-proxy') const compress = require('koa-compress') const LRU = require('lru-cache') // https://github.com/isaacs/node-lru-cache const qs = require('qs') // https://github.com/ljharb/qs const micromatch = require('micromatch') // https://github.com/micromatch/micromatch const { isArray, isUndefined, isFunction, isNumber, isObject, isString, isPromiseLike, } = require('szfe-tools') const Renderer = require('./Renderer') const koaFallbackStatic = require('../koaFallbackStatic') const renderTaskMap = new Map() const defaultCacheMap = new LRU() const defaultCacheEngine = { get: (key) => defaultCacheMap.get(key), set: (key, value, maxAge) => { if (isObject(maxAge)) { maxAge = maxAge.maxAge } if (isNumber(maxAge) && maxAge > 0) { defaultCacheMap.set(key, value, maxAge) } else { defaultCacheMap.set(key, value) } }, } module.exports = function ssr({ devMode = false, staticDir, publicPath, server, log: useLog = true, renderConfig: renderConfigTable = [], cacheEngine = defaultCacheEngine, ...rendererConfig } = {}) { let count = 0 const redirect = staticDir ? koaFallbackStatic(staticDir, { maxAge: 365 * 24 * 60 * 60 * 1000, immutable: true, // 需要存在 .gz 文件时才能生效 // https://github.com/koajs/send/blob/5.0.0/index.js#L80 gzip: true, fallback: '__root.html', publicPath, }) : server ? httpProxy({ target: server, onProxyReq: (proxyReq, req, res) => { proxyReq.setHeader('x-ssr-redirect', true) }, }) : undefined if (!redirect) { throw new Error('"staticDir" or "server" must exist!') } const applyCompress = compress() const { render } = new Renderer({ staticDir, publicPath, server, ...rendererConfig, }) const log = useLog ? (...args) => { console.log(...args) } : () => null return async (ctx, next) => { // 不处理非 html 或 .html 结尾的请求 if ( /\.html$/.test(ctx.request.url) || !/text\/html/.test(ctx.header.accept) // /\..*$/.test(ctx.request.url) ) { return redirect(ctx, next) } let times if (useLog) { times = ++count console.time(`[${times}] "${ctx.request.url}" finished after`) } const __REQUEST__ = { href: ctx.request.href, url: ctx.request.url, path: ctx.request.path, query: ctx.request.query, cookie: qs.parse(ctx.request.headers.cookie, { delimiter: '; ' }), headers: ctx.request.headers, URL: ctx.request.URL, } const getRenderConfigEntries = renderConfigTable.find(([key]) => micromatch.isMatch(ctx.request.url, key) ) const getRenderConfig = !!getRenderConfigEntries ? getRenderConfigEntries[1] : undefined const renderConfig = isFunction(getRenderConfig) ? getRenderConfig(__REQUEST__) : getRenderConfig const useSsr = isUndefined(renderConfig) || isUndefined(renderConfig.ssr) || !!renderConfig.ssr const { key } = renderConfig || {} if (useLog) { if (isArray(getRenderConfigEntries) && getRenderConfigEntries[0]) { log( `[${times}] "${ ctx.request.url }" matched render config: ${JSON.stringify( getRenderConfigEntries[0] )}` ) } else { log(`[${times}] "${ctx.request.url}" match no any render config`) } } try { if (useSsr && isUndefined(key)) { const html = await render(ctx.request.url, { uncss: rendererConfig.uncss, timeout: rendererConfig.timeout, cookie: ctx.request.headers.cookie, inject: { __REQUEST__, }, }) log(`[${times}] "${ctx.request.url}" no render config, forced rendered`) ctx.body = html return applyCompress(ctx, next) } if (!useSsr) { log( `[${times}] "${ctx.request.url}" don't use ssr, return static entry` ) return redirect(ctx, next) } const cacheConfig = renderConfig.cache === true ? {} : renderConfig.cache || {} const useCache = !!renderConfig.cache && !cacheConfig.forceUpdate && !devMode const cache = useCache ? await cacheEngine.get(key) : undefined if (isString(cache)) { ctx.body = cache log(`[${times}] "${ctx.request.url}" from cache with key: ${key}`) return applyCompress(ctx, next) } let renderTask = renderTaskMap.get(key) if (isPromiseLike(renderTask)) { const htmlContent = await renderTask ctx.body = htmlContent log( `[${times}] "${ctx.request.url}" from exist render task with key: ${key}` ) return applyCompress(ctx, next) } renderTask = render(ctx.request.url, { uncss: renderConfig.uncss || rendererConfig.uncss, timeout: renderConfig.timeout || rendererConfig.timeout, cookie: ctx.request.headers.cookie, inject: { __REQUEST__, ...(renderConfig.inject || {}), }, }) renderTaskMap.set(key, renderTask) const htmlContent = await renderTask renderTaskMap.delete(key) if (useCache) { await cacheEngine.set(key, htmlContent, cacheConfig.maxAge) } ctx.body = htmlContent log(`[${times}] "${ctx.request.url}" render with key: ${key}`) return applyCompress(ctx, next) } catch (err) { console.error(`[${times}] ssr error!`, err) return redirect(ctx, next) } finally { if (useLog) { console.timeEnd(`[${times}] "${ctx.request.url}" finished after`) } } } }