ssr.js 4.32 KB
const fs = require('fs');
const path = require('path');
const url = require('url');
const sourceMap = require('source-map');
const _ = require('lodash');
const md5 = require('yoho-md5');
const pkg = require('../../package.json');
const routes = require('../../config/ssr-routes');
const redis = require('../../utils/redis');
const {createBundleRenderer} = require('vue-server-renderer');
const logger = global.yoho.logger;
const config = global.yoho.config;

const REG_STACK = /at ([^:]+):(\d+):(\d+)/;

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

let renderer;
let serverBundle;

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

  serverBundle = require(`../../dist/yohoblk-wap/bundle/yoho-ssr-server-${pkg.version}.json`);
  const clientManifest = require(`../../dist/yohoblk-wap/bundle/yoho-ssr-client-${pkg.version}.json`);

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

const getContext = (req) => {
  return {
    url: req.url,
    title: 'BLK!',
    user: req.user,
    env: {
      isApp: req.yoho.isApp,
      isiOS: req.yoho.isiOS,
      isAndroid: req.yoho.isAndroid,
      isYohoApp: req.yoho.isYohoApp,
      clientIp: req.yoho.clientIp
    }
  };
};

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

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

const parseError = async({stack = ''}) => {
  try {
    const splits = stack.split('\n');
    const lastError = splits.map(str => {
      const match = str.match(REG_STACK);

      if (match) {
        return {file: match[1], line: parseInt(match[2], 10), column: parseInt(match[3], 10)};
      }
      return false;
    }).find(match => match);

    if (lastError && lastError.file) {
      const consumer = await new sourceMap.SourceMapConsumer(serverBundle.maps[lastError.file]);

      const origin = consumer.originalPositionFor({
        line: lastError.line,
        column: 342
      });

      console.log(origin);
    }
  } catch (error) {
    logger.error(error);
  }
};

const render = (route) => {
  return async(req, res, next) => {
    res.setHeader('X-YOHO-Version', pkg.version);
    const ck = getCacheKey(req.url, route.cackeKey);

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

      if (html) {
        logger.debug(`cached ${req.url}`);
        res.setHeader('X-YOHO-Cached', 'HIT');
        return res.send(html);
      }
      res.setHeader('X-YOHO-Cached', 'MISS');
    }
    let context = getContext(req);

    renderer.renderToString(context, (err, html) => {
      if (err) {
        parseError(err);
        return next(err.message);
      }
      if (config.useCache && route.cache && ck) {
        redis.setex(ck, route.cacheTime || 60, html);
      }
      return res.send(html);
    });
  };
};
const devRender = (route) => {
  return async(req, res, next) => {
    res.setHeader('X-YOHO-Version', pkg.version);
    const ck = getCacheKey(req.url, route.cackeKey);

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

      if (html) {
        logger.debug(`cached ${req.url}`);
        res.setHeader('X-YOHO-Cached', 'HIT');
        return res.send(html);
      }
      res.setHeader('X-YOHO-Cached', 'MISS');
    }
    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) {
          try {
            const err = JSON.parse(msg.err);

            if (err.code === 404) {
              return next();
            }
            return next(err);
          } catch (e) {
            return next({
              code: 500,
              message: msg.err
            });
          }
        }
        if (config.useCache && route.cache && ck) {
          redis.setex(ck, route.cacheTime || 60, msg.html);
        }
        return res.end(msg.html);
      }
    };

    process.on('message', event);
  };
};

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

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

module.exports = router;