index.js 5.57 KB
/**
 * 后端:改写和跳转:中间件。
 *
 * 说明:
 * 该中间件使用:使用文件夹下 rule 的所有的文件,以文件名的形式,载入该模块
 * 文件的名字建议以网站子域名的形式存在,如:guang, item
 * 每个文件夹是一个模块。
 *
 * 模块的导出形式:见 guang 模块的使用。
 * Created by TaoHuang on 2017/2/21.
 */

'use strict';

const fs = require('fs');
const _ = require('lodash');
const path = require('path');
const helpers = global.yoho.helpers;
const logger = global.yoho.logger;

const TYPE = require('./type');
const DIR = './rules/';
const curDir = path.resolve(__dirname, DIR);
const files = fs.readdirSync(curDir);

let domainRules = {};

// 启动时就载入模块
files.forEach((file) => {
    let info = fs.statSync(path.resolve(curDir, file));

    if (info.isFile()) {
        let module = path.basename(file, '.js');
        let loadPath = DIR + module;

        try {
            domainRules[module] = require(loadPath);
        } catch (e) {
            logger.error('load rules wrong', e);
        }
    }
});

// 选择模块
const loadRule = (domain) => {
    return domainRules[domain] || domain.default;
};

// 已处理完
class Done {
    constructor(val) {
        this.__val = val;
    }

    map(fn) {
        return new Done(fn(this.__val));
    }

    val() {
        return this.__val;
    }

    static of(val) {
        return new Done(val);
    }
}

const stepX = (fn) => {
    return (maybe) => {
        if (maybe instanceof Done) {
            return maybe;
        }
        return fn(maybe);
    };
};

// 判断是否需要处理
const isNeedHandle = (req, rule) => {
    return (_.isRegExp(rule.origin) && rule.origin.test(req.url)) ||
        (_.isFunction(rule.origin) && rule.origin(req) ||
        (_.isString(rule.origin) && _.isEqual(req.url, rule.origin)));
};

// 正则
const step1 = (req, rule, url) => {
    if (_.isRegExp(rule.origin)) {
        if (_.isFunction(rule.target)) {
            return Done.of(url.replace(rule.origin, _.partial(rule.target, req)));
        } else if (_.isString(rule.target)) {
            return Done.of(url.replace(rule.origin, rule.target));
        }
    }

    return url;
};

// 函数
const step2 = (req, rule, url) => {
    if (_.isFunction(rule.origin)) {
        if (_.isFunction(rule.target)) {
            return Done.of(rule.target(req));
        } else if (_.isString(rule.target)) {
            return Done.of(rule.target);
        }
    }

    return url;
};

// 字符
const step3 = (req, rule, url) => {
    if (_.isString(rule.origin)) {
        if (_.isFunction(rule.target)) {
            return Done.of(rule.target(req));
        } else if (_.isString(rule.target)) {
            return Done.of(rule.target);
        }
    }

    return url;
};

const getResultStatus = (req, rule, newUrl) => {
    if (newUrl instanceof Done) {
        if (newUrl.val() === req.url) {
            return {
                needNext: true,
                needRedirect: false,
                process: false
            };
        } else if (rule.type === TYPE.redirect) {
            return {
                needNext: false,
                needRedirect: true,
                process: true,
                url: newUrl.val()
            };
        } else if (rule.type === TYPE.rewrite) {
            return {
                needNext: true,
                needRedirect: false,
                process: true,
                url: newUrl.val()
            };
        }
    }

    return {
        needNext: true,
        needRedirect: false,
        process: false
    };
};

const isJsonp = (req) => {
    return _.includes(req.url, 'callback');
};

/**
 * 1. origin 可接受是 正则 , 函数, 纯字符串
 * 2. target 可接受是 匹配字符串 , 函数, 纯字符串
 * 3. 301: 跳转
 *    rewrite: 改写 url
 * @returns {Function}
 */
module.exports = () => {
    return (req, res, next) => {
        if (req.subdomains.length > 1 && req.subdomains[1] === 'www') {
            return res.redirect(301, helpers.urlFormat(req.path, req.query || '', req.subdomains[0]));
        }

        req.isMobile = /(nokia|iphone|android|ipad|motorola|^mot\-|softbank|foma|docomo|kddi|up\.browser|up\.link|htc|dopod|blazer|netfront|helio|hosin|huawei|novarra|CoolPad|webos|techfaith|palmsource|blackberry|alcatel|amoi|ktouch|nexian|samsung|^sam\-|s[cg]h|^lge|ericsson|philips|sagem|wellcom|bunjalloo|maui|symbian|smartphone|midp|wap|phone|windows ce|iemobile|^spice|^bird|^zte\-|longcos|pantech|gionee|^sie\-|portalmmm|jig\s browser|hiptop|^ucweb|^benq|haier|^lct|opera\s*mobi|opera\*mini|320x320|240x320|176x220)/i.test(req.get('user-agent')); // eslint-disable-line

        if (req.xhr || isJsonp(req)) {
            return next();
        }

        let rules = loadRule(req.subdomains[0]);
        let useRule = _.find(rules, rule => isNeedHandle(req, rule));

        if (!useRule) {
            return next();
        }

        let step1x = stepX(_.partial(step1, req, useRule, _));
        let step2x = stepX(_.partial(step2, req, useRule, _));
        let step3x = stepX(_.partial(step3, req, useRule, _));
        let processAfter = _.partial(getResultStatus, req, useRule, _);
        let processing = _.flow(step1x, step2x, step3x);

        let process = _.flow(processing, processAfter);
        let result = process(req.url);

        if (result.process) {
            if (result.needRedirect) {
                return res.redirect(result.url);
            }

            if (result.needNext) {
                req.url = result.url;
                return next();
            }
        }

        return next();
    };
};