├── .gitignore ├── README.md ├── app.js ├── bin ├── publish.js └── www ├── imitator.js ├── init.js ├── package.json ├── test ├── Imitatorfile.js ├── myfile.txt └── targetHostImitatorfile.js └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | mock/static/ 5 | webpack.config.js 6 | webpack.production.config.js 7 | webpack.test.config.js 8 | npm-debug.log* 9 | 10 | **/*.swp 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | imitator 2 | ======== 3 | 4 | 一个简单易用的 nodejs 服务器, 主要用于模拟 HTTP 接口数据, 请求代理与转发 。 5 | 使用imitator,可以解决前后端分离开发中的数据模拟问题,也可以作为代理服务器与静态资源服务器使用。 6 | 7 | ### 为什么会有 imitator? 8 | 9 | 前后端开发协作的过程中,为了不依赖于后端环境,我们常常会和后端童鞋定好接口,然后采用前后端分离的开发模式。 10 | 但是这样的模式,需要前端自己来实现接口数据的模拟。通常使用 nginx 可以满足我们绝大部分场景的需求了。 11 | 但是,nginx 的配置文件相对前端同学来说还是不够友好,而且有些个性的接口格式无法满足。 12 | imitator 使用 nodejs 并基于 express.js 实现, 配置文件相当简单, 而且易于订制,前端同学使用起来非常顺手。 13 | 14 | ### 快速上手 15 | 16 | 1. 安装——首先你要先安装 nodejs 和 npm, 然后全局安装imitator。 17 | 18 | npm install imitator -g 19 | 20 | 2. 编写配置文件——在你的用户目录(比如我的是/User/hanan)下新建一个名为 Imitatorfile.js 的文件(这是 imitator 的默认配置文件), 内容如下。 21 | 22 | module.exports = function(imitator) { 23 | // 返回一个json 24 | imitator('/json', {name: 'hello world'}); 25 | } 26 | 27 | 3. 启动服务——命令行输入以下命令,启动 imitator server. 28 | 29 | imitator 30 | 31 | 4. 浏览器访问 127.0.0.1:8888/json , 将会看到: 32 | 33 | {"name":"hello world"} 34 | 35 | 36 | ### 命令行参数 37 | 38 | imitator 命令接受2参数: 39 | 40 | -p 设置 imitator server 的端口号,默认是8888。 41 | 42 | -f 设置配置文件的路径,支持相对路径和绝对路径,默认为:用户目录/Imitatorfile.js 。 43 | 44 | 下面的命令将使用 9000 端口, /home/myconfig.js 这个文件作为配置文件来启动 imitator server 。 45 | 46 | imitator -p 9000 -f /home/myconfig.js 47 | 48 | ### 配置文件 49 | 50 | imitator 的配置文件是其实就是一个 nodejs 模块, module.exports 是一个函数,接受一个参数:imitator 。 通过调用 imitator(option) 来设置一条规则。 51 | 其中 option 是规则的参数对象。如: 52 | 53 | module.exports = function(imitator) { 54 | imitator({ 55 | url: '/json', // 匹配的url 56 | result: {name: 'json test'} // 返回的内容 57 | }); 58 | } 59 | 60 | 如上,当请求地址匹配到 /json 这个路径的时候,就会返回 {name: 'json test'} 的json字符串。 61 | 62 | 当 option 中只包含 url,result 两个参数时,可以简写成 imitator(url, result) 的形式,上面的例子可以写成: 63 | 64 | module.exports = function(imitator) { 65 | imitator('/json', {name: 'json test'}); 66 | } 67 | 68 | 69 | ### 规则参数(option) 70 | 71 | 配置文件中可以通过 imitator(option) 来制定一条规则,其中参数对象包含以下属性: 72 | 73 | #### option.url 74 | 75 | 必填,设置请求的匹配模式,支持正则。如: 76 | 77 | module.exports = function(imitator) { 78 | 79 | imitator({ 80 | url: '/json', 81 | …… 82 | }); 83 | 84 | imitator({ 85 | url: /\/\d{1,3}/, // 支持正则 86 | …… 87 | }); 88 | } 89 | 90 | #### option.result 91 | 92 | 必填,设置请求的返回内容,如果是一个 object 或者 array,将会被 JSON.stringify 后返回;如果是一个 function,将会接受 req 和 res 两个参数执行,可用于实现一些个性化的返回内容。如: 93 | 94 | module.exports = function(imitator) { 95 | 96 | imitator({ 97 | …… 98 | result: 'my result', //普通字符串 99 | }); 100 | 101 | imitator({ 102 | …… 103 | result: {name: 'json test'}, //json 104 | }); 105 | 106 | imitator({ 107 | …… 108 | result: function (req, res) { // 自定义内容 109 | if (req.param('name') === 'hanan') { 110 | res.send('中年痴呆症患者'); 111 | } 112 | else { 113 | res.send('i do not know .'); 114 | } 115 | }, 116 | }); 117 | } 118 | 119 | #### option.type 120 | 121 | 设置通过 mime.lookup() 转化的 Content-Type HTTP header。如: 122 | 123 | module.exports = function(imitator) { 124 | 125 | imitator({ 126 | …… 127 | type: 'json', ==> 'application/json' 128 | …… 129 | }); 130 | 131 | imitator({ 132 | …… 133 | type: 'html', ==> 'text/html' 134 | …… 135 | }); 136 | 137 | } 138 | 139 | #### option.headers 140 | 141 | 设置 HTTP header。如: 142 | 143 | module.exports = function(imitator) { 144 | 145 | imitator({ 146 | …… 147 | headers: { 148 | myheadername: 'myheader value' 149 | } 150 | …… 151 | }); 152 | 153 | } 154 | 155 | #### option.cookies 156 | 157 | 设置 cookie,如: 158 | 159 | module.exports = function(imitator) { 160 | 161 | imitator({ 162 | …… 163 | cookies: [ 164 | {name: 'myname', value: 'hanan', maxAge: 900000, httpOnly: true} 165 | ] 166 | …… 167 | }); 168 | 169 | } 170 | 171 | #### option.timeout 172 | 173 | 设置请求响应的延时时间,单位为毫秒,如: 174 | 175 | module.exports = function(imitator) { 176 | 177 | imitator({ 178 | …… 179 | timeout: 1000 180 | …… 181 | }); 182 | 183 | } 184 | 185 | #### option.action 186 | 187 | 设置请求的接受类型 (GET, POST, PATCH, DELETE...),如: 188 | 189 | module.exports = function(imitator) { 190 | 191 | imitator({ 192 | …… 193 | action: 'GET' 194 | …… 195 | }); 196 | 197 | } 198 | 199 | ### HTTP代理 200 | 201 | 通过 imitator.base() 可以将规则之外的请求,转发到其他的服务器上。这样可以在实现接口模拟的同时,使用其他服务器的返回内容。如: 202 | 203 | module.exports = function(imitator) { 204 | 205 | // 这里是各种规则======== 206 | imitator(……); 207 | imitator(……); 208 | imitator(……); 209 | 210 | 211 | // 没有命中规则的请求, 转发到192.168.8.8:9000下 212 | imitator.base('http://192.168.8.8:9000'); 213 | } 214 | 215 | 216 | ### 静态目录 217 | 218 | 通过 imitator.static(url, path) 可以设置静态文件目录。 url 为匹配的请求地址,支持正则;path 为静态文件的目录,路径相对于配置文件。如: 219 | 220 | module.exports = function(imitator) { 221 | 222 | imitator.static('/static', './public'); 223 | } 224 | 225 | ### 读取文件内容 226 | 227 | 通过 imitator.file(filePath) 可以读取文件内容,filePath是文件路径,相对于配置文件。如: 228 | 229 | module.exports = function(imitator) { 230 | 231 | // 当请求匹配到 /file 时 ,返回文件 ./myfile.txt 的内容 232 | imitator('/file', imitator.file('./myfile.txt')); 233 | } 234 | 235 | ### 返回jsonp内容 236 | 237 | 通过 imitator.jsonp(context, callbackName) 可以设置返回 jsonp 内容,其中 context 是内容实体,类型为 object|string, callbackName 为回调函数名称, 默认为 'callback'。如: 238 | 239 | module.exports = function(imitator) { 240 | 241 | // 当请求匹配到 /myjsonp 时,将文件文件 ./myfile.txt 的内容经过 jsonp 包裹后返回 242 | imitator('/myjsonp', imitator.jsonp(imitator.file('./myfile.txt'))); 243 | 244 | // 当请求匹配到 /myjsonp2 时,将一个对象用 'mycb' 这个回调函数名经过 jsonp 包裹后返回, 245 | imitator('/myjsonp', imitator.jsonp({url: 'annn.me'}, 'mycb')); 246 | } 247 | 248 | 249 | ### 配置文件(Imitatorfile.js)参考 250 | 251 | 详见:[https://github.com/hanan198501/imitator/blob/master/test/Imitatorfile.js](https://github.com/hanan198501/imitator/blob/master/test/Imitatorfile.js) 252 | 253 | 254 | ### LICENSE 255 | 256 | [MIT](https://opensource.org/licenses/MIT) 257 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var favicon = require('serve-favicon'); 4 | var logger = require('morgan'); 5 | var cookieParser = require('cookie-parser'); 6 | var bodyParser = require('body-parser'); 7 | var app = global.app = express(); 8 | 9 | // view engine setup 10 | //app.set('views', path.join(__dirname, 'views')); 11 | //app.set('view engine', 'jade'); 12 | 13 | app.use(logger('dev')); 14 | app.use(bodyParser.json()); 15 | app.use(bodyParser.urlencoded({extended: false})); 16 | app.use(cookieParser()); 17 | //app.use(express.static(path.join(__dirname, 'public'))); 18 | 19 | module.exports = app; 20 | -------------------------------------------------------------------------------- /bin/publish.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var child_process = require('child_process'); 4 | var path = require('path'); 5 | var pg = require('../package.json'); 6 | 7 | var comment = process.argv[2] || ('commit version@' + pg.version); 8 | 9 | var cp = child_process.exec( 10 | 'git commit -am "' + comment + '" && git push && npm publish', 11 | {cwd: path.resolve(__dirname, '..')}); 12 | 13 | cp.stdout.on('data', function (data) { 14 | console.log(data + ''); 15 | }) 16 | 17 | 18 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ./node_modules/nodemon/bin/nodemon.js 2 | 3 | var http = require('http'); 4 | var optimist = require('optimist'); 5 | var app = require('../app'); 6 | var init = require('../init'); 7 | var port = optimist.argv.p || 8888; 8 | var cwd = process.cwd(); 9 | var server; 10 | 11 | init(app, optimist.argv, cwd); 12 | app.set('port', port); 13 | server = http.createServer(app); 14 | server.listen(port); 15 | 16 | server.on('error', function onError(error) { 17 | if (error.syscall !== 'listen') { 18 | throw error; 19 | } 20 | var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; 21 | switch (error.code) { 22 | case 'EACCES': 23 | console.error(bind + ' requires elevated privileges'); 24 | process.exit(1); 25 | break; 26 | case 'EADDRINUSE': 27 | console.error(bind + ' is already in use'); 28 | process.exit(1); 29 | break; 30 | default: 31 | throw error; 32 | } 33 | } 34 | ); 35 | 36 | server.on('listening', function onListening() { 37 | var addr = server.address(); 38 | var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; 39 | console.log('Imitator server listening on ' + bind); 40 | } 41 | ); 42 | -------------------------------------------------------------------------------- /imitator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by an.han on 15/7/20. 3 | */ 4 | 5 | var express = require('express'); 6 | var fs = require('fs'); 7 | var path = require('path'); 8 | var util = require('./util'); 9 | var httpProxy = require('http-proxy'); 10 | var proxy = httpProxy.createProxyServer(); 11 | var app = global.app; 12 | 13 | // log proxy data 14 | proxy.on('open', function (proxySocket) { 15 | proxySocket.on('data', function (chunk) { 16 | console.log(chunk.toString()); 17 | }); 18 | }); 19 | proxy.on('proxyRes', function (proxyRes, req, res) { 20 | console.log('RAW Response from the target', JSON.stringify(proxyRes.headers, true, 2)); 21 | }); 22 | 23 | 24 | // 根据参数个数获取配置 25 | function getOption(arg) { 26 | var len = arg.length; 27 | // 默认配置 28 | var option = { 29 | headers: { 30 | 'Cache-Control': 'no-cache' 31 | }, 32 | statusCode: 200, 33 | cookies: [], 34 | timeout: 0 35 | }; 36 | if (len === 0) { 37 | return imitator; 38 | } 39 | else if (len === 1) { 40 | var newOption = arg[0]; 41 | if (util.isObject(newOption)) { 42 | util.each(newOption, function (value, key) { 43 | if (key === 'headers') { 44 | util.each(newOption.headers, function (headervalue, headerkey) { 45 | option.headers[headerkey] = newOption.headers[headerkey]; 46 | }) 47 | } 48 | else { 49 | option[key] = newOption[key]; 50 | } 51 | }); 52 | } 53 | } 54 | else { 55 | option.url = arg[0]; 56 | option.result = arg[1]; 57 | } 58 | return option; 59 | } 60 | 61 | // 把基于 Imitatorfile 的相对绝对转成绝对路径 62 | function parsePath(value) { 63 | return path.resolve(global.imitatorFilePath, value); 64 | } 65 | 66 | 67 | /** 68 | * 数据模拟函数 69 | */ 70 | function imitator() { 71 | var option = getOption(arguments); 72 | 73 | if (!option.url || !option.result) { 74 | return; 75 | } 76 | 77 | // option.action is one of ['get','post','delete','put'...] 78 | var action = option.action || 'use'; 79 | 80 | app[action.toLowerCase()](option.url, function (req, res) { 81 | setTimeout(function () { 82 | 83 | // set header 84 | res.set(option.headers); 85 | 86 | // set Content-Type 87 | option.type && res.type(option.type); 88 | 89 | // set status code 90 | res.status(option.statusCode); 91 | 92 | // set cookie 93 | util.each(option.cookies, function (item, index) { 94 | var name = item.name; 95 | var value = item.value; 96 | delete item.name; 97 | delete item.value; 98 | res.cookie(name, value, item); 99 | }); 100 | 101 | // do result 102 | if (util.isFunction(option.result)) { 103 | option.result(req, res); 104 | } 105 | else if (util.isArray(option.result) || util.isObject(option.result)) { 106 | !option.type && res.type('json'); 107 | res.json(option.result); 108 | } 109 | else { 110 | !option.type && res.type('text'); 111 | res.send(option.result.toString()); 112 | } 113 | 114 | }, option.timeout); 115 | }); 116 | } 117 | 118 | // 规则之外的请求转发 119 | imitator.base = function (host) { 120 | process.nextTick(function () { 121 | app.use(function (req, res) { 122 | proxy.web(req, res, {target: host}); 123 | }); 124 | }); 125 | } 126 | 127 | // 读取文件内容 128 | imitator.file = function (file) { 129 | return fs.readFileSync(parsePath(file)); 130 | } 131 | 132 | // 设置静态文件路径 133 | imitator.static = function (url, dir) { 134 | app.use(url, express.static(parsePath(dir))); 135 | } 136 | 137 | imitator.jsonp = function (context, callbackName) { 138 | callbackName = callbackName || 'callback'; 139 | context = typeof context === 'string' ? context : JSON.stringify(context); 140 | return callbackName + '(' + context + ')'; 141 | }; 142 | 143 | module.exports = imitator; 144 | -------------------------------------------------------------------------------- /init.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by an.han on 15/7/20. 3 | */ 4 | //var Promise = require('es6-promise').Promise; 5 | var imitator = require('./imitator'); 6 | var path = require('path'); 7 | var fs = require('fs'); 8 | 9 | var main = { 10 | 11 | init: function (app, argv, cwd) { 12 | this.app = app; 13 | this.argv = argv; 14 | this.cwd = cwd; 15 | this.imitator = global.imitator = imitator; 16 | this.extend(); 17 | this.customRoute(); 18 | this.defaultRoute(); 19 | }, 20 | 21 | extend: function () { 22 | this.imitator.server = app; 23 | }, 24 | 25 | customRoute: function () { 26 | var argv = this.argv; 27 | var home = process.env[process.platform === 'win32' ? 'USERPROFILE' : 'HOME']; 28 | var defautImitatorFile = path.resolve(home, 'Imitatorfile.js'); 29 | var imitatorFile; 30 | 31 | if (argv.f) { 32 | if (process.platform === 'win32') { 33 | imitatorFile = path.resolve(this.cwd, this.argv.f); 34 | } 35 | else { 36 | if (argv.f[0] === '/') { 37 | imitatorFile = argv.f; 38 | } 39 | else if(argv.f[0] === '~') { 40 | imitatorFile = path.resolve(home, argv.f.replace(/^~\//, '')); 41 | } 42 | else { 43 | imitatorFile = path.resolve(this.cwd, this.argv.f); 44 | } 45 | } 46 | } 47 | else { 48 | imitatorFile = defautImitatorFile; 49 | } 50 | 51 | if (!fs.existsSync(imitatorFile)) { 52 | console.warn('[WARN] imitator file not found!'); 53 | } 54 | else { 55 | global.imitatorFilePath = path.resolve(imitatorFile, '..'); 56 | require(imitatorFile)(imitator); 57 | } 58 | }, 59 | 60 | defaultRoute: function () { 61 | var app = this.app; 62 | setTimeout(function () { 63 | app.use(function (req, res, next) { 64 | var err = new Error('Not Found'); 65 | err.status = 404; 66 | next(err); 67 | }); 68 | app.use(function (err, req, res, next) { 69 | res.json({ 70 | status: err.status || 500, 71 | message: err.message, 72 | err: err 73 | }); 74 | }); 75 | }); 76 | } 77 | }; 78 | 79 | module.exports = main.init.bind(main); 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "imitator", 3 | "author": "hanan", 4 | "version": "0.0.7", 5 | "bin": "bin/www", 6 | "description": "A server that help you to imitate http response.", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/hanan198501/imitator.git" 10 | }, 11 | "license": "ISC", 12 | "scripts": { 13 | "start": "node ./bin/www" 14 | }, 15 | "keywords": [ 16 | "mock", 17 | "imitate", 18 | "http", 19 | "json", 20 | "response", 21 | "request" 22 | ], 23 | "dependencies": { 24 | "body-parser": "~1.13.2", 25 | "cookie-parser": "~1.3.5", 26 | "express": "~4.13.1", 27 | "http-proxy": "^1.11.1", 28 | "morgan": "~1.6.1", 29 | "nodemon": "^1.11.0", 30 | "optimist": "^0.6.1", 31 | "serve-favicon": "~2.3.0" 32 | }, 33 | "devDependencies": { 34 | "expect.js": "*" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/Imitatorfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(imitator) { 2 | 3 | // 返回一个json 4 | imitator({ 5 | url: '/json', 6 | result: {name: 'json test'} 7 | }); 8 | 9 | // 返回一个json 10 | imitator('/json2', {name: 'json test2'}); 11 | 12 | // 返回一个字符串 13 | imitator('/string', 'string test'); 14 | 15 | // 返回一个数字 16 | imitator('/number', 123); 17 | 18 | // 返回文件内容, 文件路径相对于 Imitatorfile 文件目录 19 | imitator('/file', imitator.file('./myfile.txt')); 20 | 21 | // 正则匹配url, 返回一个字符串 22 | imitator(/\/regexp/, 'regexp test'); 23 | 24 | // option.result 参数如果是一个函数, 可以实现自定义返回内容, 接收的参数是是经过 exrpess 封装的 req 和 res 对象. 25 | imitator(/\/function$/, function (req, res) { 26 | res.send('function test'); 27 | }); 28 | 29 | // 更复杂的规则配置 30 | imitator({ 31 | url: /\/who/, 32 | result: function (req, res) { 33 | if (req.param.name === 'hanan') { 34 | res.send('中年痴呆症患者'); 35 | } 36 | else { 37 | res.send('i do not know .'); 38 | } 39 | }, 40 | type: 'text', 41 | headers: { 42 | imitatorFile: __filename 43 | }, 44 | cookies: [ 45 | {name: 'myname', value: 'hanan', maxAge: 900000, httpOnly: true} 46 | ] 47 | }); 48 | 49 | // 没有命中规则的请求, 转发到localhost:9000下 50 | imitator.base('http://localhost:9000'); 51 | } 52 | 53 | -------------------------------------------------------------------------------- /test/myfile.txt: -------------------------------------------------------------------------------- 1 | file test -------------------------------------------------------------------------------- /test/targetHostImitatorfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(imitator) { 2 | imitator(/\/.*/, 'base host'); 3 | } 4 | 5 | -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by an.han on 15/7/21. 3 | */ 4 | 5 | var toString = Object.prototype.toString; 6 | 7 | module.exports = { 8 | 9 | isArray: function (value) { 10 | return toString.call(value) === '[object Array]'; 11 | }, 12 | 13 | isObject: function (value) { 14 | return toString.call(value) === '[object Object]'; 15 | }, 16 | 17 | isFunction: function (value) { 18 | return toString.call(value) === '[object Function]'; 19 | }, 20 | 21 | each: function (val, callback) { 22 | if (this.isArray(val)) { 23 | val.forEach(callback); 24 | } 25 | if (this.isObject(val)) { 26 | for (var key in val) { 27 | callback(val[key], key); 28 | } 29 | } 30 | } 31 | } --------------------------------------------------------------------------------