ssr.js 5.15 KB
const fs = require('fs');
const path = require('path');
const url = require('url');
const _ = require('lodash');
const os = require('os');
const md5 = require('yoho-md5');
const pkg = require('../../package.json');
const routes = require('../../config/ssr-routes');
const redis = require('../../utils/redis');
const routeEncode = require('../../utils/route-encode');
const {createBundleRenderer} = require('vue-server-renderer');
const logger = global.yoho.logger;
const config = global.yoho.config;

const isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV;

let renderer;
let serverBundle;
let degradeHtml;

if (!isDev) {
  const template = fs.readFileSync(path.join(__dirname, '../../index.html'), 'utf-8');

  degradeHtml = fs.readFileSync(path.join(__dirname, '../../degrade.html'), 'utf-8');

  serverBundle = require('../../manifest.server.json');
  const clientManifest = require('../../manifest.json');

  renderer = createBundleRenderer(serverBundle, {
    runInNewContext: false,
    template,
    clientManifest
  });
}

const getContext = (req) => {
  return {
    url: req.url,
    title: req.query.share_title || '',
    user: req.user,
    env: {
      isApp: req.yoho.isApp,
      isiOS: req.yoho.isiOS,
      isAndroid: req.yoho.isAndroid,
      isYohoApp: req.yoho.isYohoApp,
      clientIp: req.yoho.clientIp,
    },
    ua: req.get('user-agent'),
    hostname: os.hostname(),
    route: `[${req.method}]${_.get(req, 'route.path', '')}`, // 请求路由
    udid: _.get(req, 'cookies.udid', 'yoho'),
    path: `[${req.method}]${routeEncode.getRouter(req)}`,
  };
};

const handlerError = (err = {}, req, res, next) => {
  if (err.code === 404) {
    return res.redirect('/grass/error/404');
  } else if (err.code === 500) {
    return res.redirect('/grass/error/500');
  }
  return next(err);
};

const getCacheKey = (urlPath, cacheKey = '') => {
  const urlObj = url.parse(urlPath);

  return md5(cacheKey
    .replace('$url', urlObj.pathname)
    .replace('$params', urlObj.query));
};

const render = (route) => {
  return async(req, res, next) => {
    try {
      res.setHeader('X-YOHO-Version', pkg.version);
      const isDegrade = _.get(req.app.locals.wap, 'webapp.degrade', false);

      if (isDegrade) {
        return res.send(degradeHtml);
      }
      const ck = route.cacheKey ? getCacheKey(req.url, route.cacheKey) : void 0;

      if (config.useCache && route.cache && ck) {
        const html = await redis.getAsync(ck);

        res.set({
          'Cache-Control': 'max-age=' + route.cacheTime || 60
        });
        if (html) {
          logger.debug(`cached ${req.url}`);
          res.setHeader('X-YOHO-Cached', 'HIT');
          return res.send(html);
        }
        res.setHeader('X-YOHO-Cached', 'MISS');
      } else {
        res.set({
          'Cache-Control': 'no-cache',
          Pragma: 'no-cache',
          Expires: (new Date(1900, 0, 1, 0, 0, 0, 0)).toUTCString()
        });
      }
      let context = getContext(req);

      renderer.renderToString(context, (err, html) => {
        if (err) {
          return handlerError(err, req, res, next);
        }
        if (config.useCache && route.cache && ck) {
          redis.setex(ck, route.cacheTime || 60, html);
        }
        return res.send(html);
      });
    } catch (error) {
      return next(error);
    }
  };
};
const devRender = (route) => {
  return async(req, res, next) => {
    try {
      res.setHeader('X-YOHO-Version', pkg.version);
      const ck = route.cacheKey ? getCacheKey(req.url, route.cacheKey) : void 0;

      // return require('request-promise')({
      //   url: 'http://m.yohobuy.com:6005/degrade.html'
      // }).pipe(res);
      if (config.useCache && route.cache && ck) {
        const html = await redis.getAsync(ck);

        res.set({
          'Cache-Control': 'max-age=' + route.cacheTime || 60
        });
        if (html) {
          logger.debug(`cached ${req.url}`);
          res.setHeader('X-YOHO-Cached', 'HIT');
          return res.send(html);
        }
        res.setHeader('X-YOHO-Cached', 'MISS');
      } else {
        res.set({
          'Cache-Control': 'no-cache',
          Pragma: 'no-cache',
          Expires: (new Date(1900, 0, 1, 0, 0, 0, 0)).toUTCString()
        });
      }
      let context = getContext(req);

      process.send({action: 'ssr_request', context});
      let event = msg => {
        process.removeListener('message', event);
        if (msg.action === 'ssr_request') {
          if (msg.err) {
            let err = msg.err;

            try {
              err = JSON.parse(msg.err);
            } catch (error) {} // eslint-disable-line
            return handlerError(err, req, res, next);
          }
          if (config.useCache && route.cache && ck) {
            redis.setex(ck, route.cacheTime || 60, msg.html);
          }
          return res.end(msg.html);
        }
      };

      process.on('message', event);
    } catch (error) {
      return next(error);
    }
  };
};

const router = require('express').Router(); //eslint-disable-line

_.each(routes, r => {
  if (!r.disable) {
    router.get(r.route, isDev ? devRender(r) : render(r));
  }
});

exports.ssrRender = isDev ? devRender({}) : render({});

exports.routers = router;