Blame view

doraemon/middleware/ssr.js 7.24 KB
陈峰 authored
1 2 3 4 5 6 7 8 9 10 11
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');
12
const Handlebars = require('handlebars');
陈峰 authored
13 14 15 16 17 18 19 20 21
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;
22 23 24 25
const hbs = fs.readFileSync(path.join(__dirname, '../views/index.hbs'), 'utf-8');

const template = Handlebars.compile(hbs);
陈峰 authored
26
if (!isDev) {
陈峰 authored
27
  degradeHtml = fs.readFileSync(path.join(__dirname, `../../degrade-${pkg.version}.html`), 'utf-8');
陈峰 authored
28 29 30 31 32 33 34


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

  renderer = createBundleRenderer(serverBundle, {
    runInNewContext: false,
35 36
    clientManifest,
    inject: false
陈峰 authored
37 38 39
  });
}
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
const REG_SCRIPT = /src="([^"]+)"/g;

const asyncLoadScripts = (renderScripts) => {
  let match;
  const scripts = [];

  while ((match = REG_SCRIPT.exec(renderScripts))) {
    scripts.push({
      src: match[1],
      index: scripts.length
    });
  }

  return scripts;
};
陈峰 authored
56 57 58
const getContext = (req) => {
  return {
    url: req.url,
yyq authored
59
    title: req.query.share_title || '',
陈峰 authored
60 61 62 63 64 65 66 67 68 69 70 71
    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'),
yyq authored
72
    path: `[${req.method}]${routeEncode.getRouter(req)}`
陈峰 authored
73 74 75 76 77
  };
};

const handlerError = (err = {}, req, res, next) => {
  if (err.code === 404) {
陈峰 authored
78
    return res.redirect('/grass/error/404');
陈峰 authored
79
  } else if (err.code === 500) {
陈峰 authored
80
    return res.redirect('/grass/error/500');
陈峰 authored
81 82 83 84
  }
  return next(err);
};
陈峰 authored
85
const getCacheKey = (req, route) => {
陈峰 authored
86
  const urlObj = url.parse(req.url);
陈峰 authored
87
  const isIos = req.yoho.isiOS;
陈峰 authored
88
  let ck = urlObj.pathname;
陈峰 authored
89
陈峰 authored
90 91
  if (route.query) {
    const qks = Object.keys(route.query);
陈峰 authored
92
陈峰 authored
93 94 95
    ck += `?${qks.map(qk => `${qk}=${req.query && req.query[qk] || ''}`).join('&')}`;
  }
yyq authored
96
  ck += `|${isIos ? 'ios' : 'android'}|${pkg.version}`;
yyq authored
97
陈峰 authored
98
  return md5(ck);
陈峰 authored
99 100 101 102 103 104
};

const render = (route) => {
  return async(req, res, next) => {
    try {
      res.setHeader('X-YOHO-Version', pkg.version);
陈峰 authored
105
      const isDegrade = _.get(req.app.locals.wap, `webapp.${config.appName}-degrade`, false);
陈峰 authored
106 107 108 109

      if (isDegrade) {
        return res.send(degradeHtml);
      }
陈峰 authored
110
      const ck = getCacheKey(req, route);
陈峰 authored
111 112

      if (config.useCache && route.cache && ck) {
陈峰 authored
113 114 115 116 117 118 119 120 121 122 123 124 125 126
        try {
          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');
        } catch (err) {
          logger.error(`get cache error: ${err.message}`);
陈峰 authored
127
        }
陈峰 authored
128 129 130 131 132 133
      } else {
        res.set({
          'Cache-Control': 'no-cache',
          Pragma: 'no-cache',
          Expires: (new Date(1900, 0, 1, 0, 0, 0, 0)).toUTCString()
        });
陈峰 authored
134 135 136 137 138 139 140
      }
      let context = getContext(req);

      renderer.renderToString(context, (err, html) => {
        if (err) {
          return handlerError(err, req, res, next);
        }
陈峰 authored
141
        let styles = context.renderStyles();
142
        let scripts = context.renderScripts();
陈峰 authored
143
        let resources = context.renderResourceHints();
144 145
        const states = context.renderState();
        let asyncScripts;
陈峰 authored
146 147 148
        let zk = {
          asyncJs: _.get(req.app.locals.wap, 'webapp.ios-async-js', true)
        };
yyq authored
149 150 151 152

        if (process.env.NODE_ENV === 'production') {
          zk.webperf = _.get(req.app.locals.wap, 'open.webperf', false);
        }
153
陈峰 authored
154
        scripts = scripts.replace(/defer/g, 'defer crossorigin="anonymous"');
yyq authored
155
        // resources = resources.replace(/link/g, 'link crossorigin="anonymous"');
陈峰 authored
156
        if (req.yoho.isiOS && zk.asyncJs) {
157 158 159 160 161 162 163 164 165
          asyncScripts = asyncLoadScripts(scripts);
        }

        const result = template({
          html,
          styles,
          scripts,
          asyncScripts,
          resources,
yyq authored
166 167
          states,
          zk,
yyq authored
168
          routeHash: routeEncode.getRouter(req)
169 170
        });
陈峰 authored
171
        if (config.useCache && route.cache && ck) {
172
          redis.setex(ck, route.cacheTime || 60, result);
陈峰 authored
173
        }
174
        return res.send(result);
陈峰 authored
175 176 177 178 179 180 181 182 183 184
      });
    } catch (error) {
      return next(error);
    }
  };
};
const devRender = (route) => {
  return async(req, res, next) => {
    try {
      res.setHeader('X-YOHO-Version', pkg.version);
陈峰 authored
185
      const ck = getCacheKey(req, route);
陈峰 authored
186 187

      const isDegrade = _.get(req.app.locals.wap, `webapp.${config.appName}-degrade`, false);
陈峰 authored
188
陈峰 authored
189 190 191 192 193
      if (isDegrade) {
        return require('request-promise')({
          url: 'http://m.yohobuy.com:6005/degrade.html'
        }).pipe(res);
      }
陈峰 authored
194 195 196
      if (config.useCache && route.cache && ck) {
        const html = await redis.getAsync(ck);
陈峰 authored
197 198 199
        res.set({
          'Cache-Control': 'max-age=' + route.cacheTime || 60
        });
陈峰 authored
200 201 202 203 204 205
        if (html) {
          logger.debug(`cached ${req.url}`);
          res.setHeader('X-YOHO-Cached', 'HIT');
          return res.send(html);
        }
        res.setHeader('X-YOHO-Cached', 'MISS');
陈峰 authored
206 207 208 209 210 211
      } else {
        res.set({
          'Cache-Control': 'no-cache',
          Pragma: 'no-cache',
          Expires: (new Date(1900, 0, 1, 0, 0, 0, 0)).toUTCString()
        });
陈峰 authored
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
      }
      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);
          }
227 228
          let {styles, scripts, resources, states, html} = msg;
陈峰 authored
229
          scripts = scripts.replace(/defer/g, 'defer crossorigin="anonymous"');
yyq authored
230
          // resources = resources.replace(/link/g, 'link crossorigin="anonymous"');
231 232 233 234 235
          const result = template({
            html,
            styles,
            scripts,
            resources,
yyq authored
236
            states,
yyq authored
237
            routeHash: routeEncode.getRouter(req)
238 239
          });
陈峰 authored
240
          if (config.useCache && route.cache && ck) {
241
            redis.setex(ck, route.cacheTime || 60, result);
陈峰 authored
242
          }
243
          return res.end(result);
陈峰 authored
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
        }
      };

      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;