fis3源码解读系类—本地数据模拟原理及应用
发布于 12 天前 作者 younth 235 次浏览 来自 分享

https://github.com/younth/fis3-analysis/issues/3

fis3重度用户,最近在看fis3源码,与大家分享下。fis3源码注释写的很好,学到了不少黑魔法~~

  • fis3 server start --qrcode 启动本地server的时候生成一个二维码
  • fis.requre方法用于加载内置插件 …

近期遇到了fis3 proxy 只支持get请求,无法Post,于是研究了一番。

fis3提供了强大的本地数据模拟的能力,与业务解耦的接口mock,在本地开发时候可谓非常方便。mock能力依赖基础的server.conf及数据文件,首先下面我们来看看基本用法。

目录结构

  • server.conf支持的目录名称:config mock
  • test数据支持的目录:test mock

统一用mock目录作为数据模拟目录:

└── mock
    ├── sample.json
    └── server.conf

server.conf 配置语法

指令名称 正则规则 目标文件
  • 指令名称 支持 rewrite 、 redirect 和 proxy。
  • 正则规则 用来命中需要作假的请求路径
  • 目标文件 设置转发的目标地址,需要配置一个可请求的 url 地址。

mock 静态假数据

rewrite ^\/api\/user$ /mock/sample.json

// sample.json内容

{
	"error": 0,
	"message": "ok",
	"data": {
		"username": "younth",
		"uid": 1,
		"age": 25,
		"company": "waimai"
	}
}

mock 动态假数据

node 服务器可以通过 js 的方式提供动态假数据。,动态数据本质是 express 的 route.

rewrite ^\/api\/dynamic\/time$ /mock/dynamic.js

// dynamic.js内容

module.exports = function(req, res, next) {

  res.write('Hello world ');

  // set custom header.
  // res.setHeader('xxxx', 'xxx');

  res.end('The time is ' + Date.now());
};

// 更复杂的,可以直接引用其他模块,发送请求


var http = require('http');
var url = require('url');
var util = require('util');
var querystring = require('querystring');

// 通过nodejs来抓取线上的结果。这样就完成了动态获取线上数据的功能

module.exports = function(request, response, next) {

    var method = request.method;
    ...

};

proxy 到其他服务的 api 地址

// 支持正则分组,不支持post请求转发,后面有解决方案
proxy ^\/wmall\/privilege\/(.*)$  http://10.19.161.92:8059/wmall/privilege/$1

以上就是mock的基础用法。fis3的mock能力实现的并不复杂,依赖fis3-server-node模块。该模块的详细讲解放在后面。下面讲下post请求转发问题。

如何proxy 在处理post请求时候的bug

proxy能力在联调阶段非常方便,但是目前fis3的proxy能力只能处理get请求。参见proxy bug issues。看起来可能是http-proxy模块的问题,但目前fis团队还未修复,下面提供两种方法办法。

1. 利用动态数据能力

rewrite ^\/wmall.*$ /mock/data.js

data.js在是一个expreses的路由,我们可以自己处理所有的请求。目前我自己项目中动态数据数据请求的代码,供参考:

// 注意这里面的.js,不是一般的.js 文件,而是相当于 express 的 route.

var http = require('http');
var url = require('url');
var util = require('util');
var querystring = require('querystring');

var host = 'http://10.19.161.92:8059';

// 通过nodejs来抓取线上的结果。这样就完成了动态获取线上数据的功能
module.exports = function(request, response, next) {

    // // 最简单的测试
    // response.writeHead(200, {
    //     'Content-type': 'text/html'
    // });
    // response.end('Date.now() ' + Date.now());

    // 获取 GET/POST
    var method = request.method;

    // 获取 post 时的参数
    var postParams = request.body;

    // 解析参数
    postParams = querystring.stringify(postParams)

    // 获取匹配到的
    var originalUrl = request.originalUrl;

    // API 符合 /wmall... 格式
    if (/^\/wmall.*$/.test(originalUrl) === false) {

        // API不是 /news?tn=... 格式,给出提示
        response.writeHead(200, {
            'Content-type': 'text/html'
        });
        response.end('url format is not /wmall....');
        return;
    }

    var apiUrl = host + originalUrl;
    var parsedUrl = url.parse(apiUrl, true);

    var options = {
        host: parsedUrl.hostname,
        port: parsedUrl.port || 80,
        path: parsedUrl.pathname,
        method: method,
        headers: {}
    };
    // 携带cookie
    options.headers.cookie = request.headers.cookie;

    if (parsedUrl.search) {
        options.path += parsedUrl.search;
    }

    if (method.toLowerCase() === 'post') {
        // 增加 header参数
        options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
        options.headers['Content-Length'] = postParams.length;
    }

    // 发送线上的数据请求,此时请求没有携带cookie
    var req = http.request(options, function (res) {
        res.setEncoding('utf8');
        var str = '';
        res.on('data', function (d) {
            // data 响应事件中先一步一步拼接数据
            str += d;
        });
        res.on('end', function () {
            // end 响应事件中,返回数据
            var data;
            try {
                data = JSON.parse(str);
            } catch (ex) {
                data = str;
            }

            response.json(data);
        });
        res.on('error',function (err) {
            // error 处理
            response.end('reponse error: ' + err.message);
        })
    });
    req.on('error', function (err) {
        // error 处理
        response.end('request error' + err.message);
    });
    // 写入post参数
    req.write(postParams);
    // 请求结束,告诉 response 可以返回了
    req.end();
};

2. 利用fis server配置文件

    // fis3-server-node index.js
  var script = path.join(opt.root, 'server.js');

  // www目录下不存在server.js 则走当前模块的app.js作为入口
  if (!fis.util.exists(script)) {
    script = path.join(__dirname, 'app.js');
  }

可以看出,server start的时候支持使用目录下的server.js, 在发布的目录下创建一个server.js, 就能自己实现server,使用http-proxy-middleware代理。我们完全可以用自己的server.js。

var express = require('express');
var path = require('path');
var url = require('url');
var fs = require('fs');
var bodyParser = require('body-parser')
var proxy = require('http-proxy-middleware');

var args = process
  .argv
  .join('|');

var program = require('commander');
var port = /\-\-port\|(\d+)(?:\||$)/.test(args)
  ? ~~ RegExp.$1
  : 8080;

var https = /\-\-https\|(true)(?:\||$)/.test(args)
  ? !!RegExp.$1
  : false;

var DOCUMENT_ROOT = path.resolve(/\-\-root\|(.*?)(?:\||$)/.test(args)
  ? RegExp.$1
  : process.cwd());

var app = express();

// logger
app.use(require('morgan')('short'));
console.log(args)
// add proxy support
var apiAddress = 'http://api.test.com';
app.use('/api', proxy({
  target: apiAddress,
  changeOrigin: true,
  pathRewrite: {
    '^/api': '/nakedhub'
  },
}));

// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({extended: false}));

// parse application/json
app.use(bodyParser.json());

// 静态文件输出
app.use(express.static(DOCUMENT_ROOT, {
  index: [
    'index.html', 'index.htm', 'default.html', 'default.htm'
  ],
  extensions: ['html', 'htm']
}));

// 静态文件列表。
app.use((function() {
  return function(req, res, next) {
    var pathname = url
      .parse(req.url)
      .pathname;
    var fullpath = path.join(DOCUMENT_ROOT, pathname);

    if (/\/$/.test(pathname) && fs.existsSync(fullpath)) {
      var stat = fs.statSync(fullpath);

      if (stat.isDirectory()) {
        var html = '';

        var files = fs.readdirSync(fullpath);

        html = '<!doctype html>';
        html += '<html>';
        html += '<head>';
        html += '<title>' + pathname + '</title>';
        html += '</head>';
        html += '<body>';
        html += '<h1> - ' + pathname + '</h1>';
        html += '<div id="file-list">';
        html += '<ul>';

        if (pathname != '/') {
          html += '<li><a href="' + pathname + '..">..</a></li>';
        }

        files
          .forEach(function(item) {
            var s_url = path.join(pathname, item);
            html += '<li><a href="' + s_url + '">' + item + '</a></li>';
          });

        html += '</ul>';
        html += '</div>';
        html += '</body>';
        html += '</html>';

        res.send(html);
        return;
      }
    }

    next();
  };
})());

// utf8 support
app.use(function(req, res, next) {

  // attach utf-8 encoding header to text files.
  if (/\.(?:js|json|text|css)$/i.test(req.path)) {
    res.charset = 'utf-8';
  }

  next();
});

// 错误捕获。
app.use(function(err, req, res, next) {
  console.log(err);
});

// Bind to a port
var server;

if (https) {
  server = require('https').createServer({
    key: fs.readFileSync(path.join(__dirname, 'key.pem'), 'utf8'),
    cert: fs.readFileSync(path.join(__dirname, 'cert.pem'), 'utf8')
  }, app);
} else {
  server = require('http').createServer(app);
}

server
  .listen(port, '0.0.0.0', function() {
    console.log(' Listening on ' + (https
      ? 'https'
      : 'http') + '://127.0.0.1:%d', port);
  });

// 在接收到关闭信号的时候,关闭所有的 socket 连接。
(function() {
  var sockets = [];

  server.on('connection', function(socket) {
    sockets.push(socket);

    socket.on('close', function() {
      var idx = sockets.indexOf(socket);
      ~idx && sockets.splice(idx, 1);
    });
  });

  var finalize = function() {
    // Disconnect from cluster master
    process.disconnect && process.disconnect();
    process.exit(0);
  }

  // 关掉服务。
  process.on('SIGTERM', function() {
    console.log(' Recive quit signal in worker %s.', process.pid);
    sockets.length
      ? sockets.forEach(function(socket) {
        socket.destroy();
        finalize();
      })
      : server.close(finalize);
  });
})(server);

fis3 mock server 源码解读

fis3/node_modules/fis3-server-node

下周给大家分享下,fis3-server 如何做到daemon与热加载。

回到顶部