Quantcast
Channel: CNode:Node.js专业中文社区
Viewing all 14821 articles
Browse latest View live

egg-mongo-native 升级至 Egg 2 并支持 replica set

$
0
0

egg-mongo-native(GitHub)已升级至 Egg 2 并支持 replica set。

因为一开始就是全部用 async/await,升级 Egg 2 基本就是更新一下 package.js版本号……

另外 v2.1.0 开始配置中的 hostport支持数组和 'string1,string2'形式,可以生成 Replica Set URI。已安装插件的用户希望使用 replica set 只需修改 hostport,并增加 option: {replicaSet: 'test'}即可;单例用户不受影响,无需做任何修改。

欢迎 issue 和 PR。


我在用KOA框架 使用koa-session-mongoose的时候 报这个错误是版本兼容性问题吗?要怎么解决

$
0
0

QQ图片20171211155508.png

报这种错误真的很无力!! 就怕这种报错信息

React + MobX 入门及实例(二)

$
0
0

在上一章 React + MobX 入门及实例(一)应用实例TodoList的基础上

  1. 增加ant-design优化界面
  2. 增加后台express框架,mongoose操作。
  3. 增加mobx异步操作fetch后台数据。

步骤

Ⅰ. ant-design

  1. 安装antd包

npm install antd --save

  1. 安装antd按需加载依赖

npm install babel-plugin-import --save-dev

  1. 更改.babelrc 配置为
{
  "presets": ["react-native-stage-0/decorator-support"],
  "plugins": [
    [
      "import",
      {
        "libraryName": "antd",
        "style": true
      }
    ]
  ],
  "sourceMaps": true
}
  1. 引入antd控件使用
import { Button } from 'antd';

Ⅱ. express, mongodb

前提:mongodb的安装与配置

  1. 安装express、mongodb、mongoose

npm install --save express mongodb mongoose

  1. 项目根目录创建server.js,撰写后台服务 引入body-parser中间件,作用是对post请求的请求体进行解析,转换为我们需要的格式。 引入Promise异步,将多查询分为单个Promise,用Promise.all连接,待查询完成后才会发送查询后的信息,如果不使用异步操作,查询不会及时响应,前端请求的可能是上一次的数据,这不是我们想要的结果。
//express
const express = require('express');
const app = express();
//中间件
const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({extended: false}));// for parsing application/json
app.use(bodyParser.json()); // for parsing application/x-www-form-urlencoded

//mongoose
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/todolist',{useMongoClient:true});
mongoose.Promise = global.Promise;
const db = mongoose.connection;

db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', function () {
    console.log("connect db.")
});

//模型
const todos = mongoose.model('todos',{
    key: Number,
    todo: String
});

//发送json: 数据+数量
let sendjson = {
    data:[],
    count:0
};

//设置跨域访问
app.all('*', function(req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
    res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
    res.header("X-Powered-By",' 3.2.1');
    res.header("Content-Type", "application/json;charset=utf-8");
    next();
});

//api/todos
app.post('/api/todos', function(req, res){
    const p1 = todos.find({})
        .exec((err, result) => {
            if (err) console.log(err);
            else {
                sendjson['data'] = result;
            }
        });

    const p2 = todos.count((err,result) => {
        if(err) console.log(err);
        else {
            sendjson['count'] = result;
        }
    }).exec();

    Promise.all([p1,p2])
        .then(function (result) {
            console.log(result);
            res.send(JSON.stringify(sendjson));
        });
});

//api/todos/add
app.post('/api/todos/add', function(req, res){
    todos.create(req.body, function (err) {
        res.send(JSON.stringify({status: err? 0 : 1}));
    })
});

//api/todos/remove
app.post('/api/todos/remove', function(req, res){
    todos.remove(req.body, function (err) {
        res.send(JSON.stringify({status: err? 0 : 1}));
    })
});

//设置监听80端口
app.listen(80, function () {
    console.log('listen *:80');
});
  1. package.json – scripts添加服务server启动项
"scripts": {
    "start": "node scripts/start.js",
    "build": "node scripts/build.js",
    "test": "node scripts/test.js --env=jsdom",
    "server": "node server.js"
  },

Ⅲ. Fetch后台数据 前后端交互使用fetch,也同样写在store里,由action触发。与后台api一一对应,主要包含这三个部分:

    @action fetchTodos(){
        fetch('http://localhost/api/todos',{
            method:'POST',
            headers: {
                "Content-type":"application/json"
            },
            body: JSON.stringify({
                current: this.current,
                pageSize: this.pageSize
            })
        })
            .then((response) => {
                // console.log(response);
                response.json().then(function(data){
                    console.log(data);
                    this.total = data.count;
                    this._key = data.data.length===0 ? 0: data.data[data.data.length-1].key;
                    this.todos = data.data;
                    this.loading = false;
                }.bind(this));
            })
            .catch((err) => {
                console.log(err);
            })
    }

    @action fetchTodoAdd(){
        fetch('http://localhost/api/todos/add',{
            method:'POST',
            headers: {
                "Content-type":"application/json"
            },
            body: JSON.stringify({
                key: this._key,
                todo: this.newtodo,
            })
        })
            .then((response) => {
                // console.log(response);
                response.json().then(function(data){
                    console.log(data);
                    /*成功添加 总数加1 添加失败 最大_key恢复原有*/
                    if(data.status){
                        this.total += 1;
                        this.todos.push({
                            key: this._key,
                            todo: this.newtodo,
                        });
                        message.success('添加成功!');
                    }else{
                        this._key -= 1;
                        message.error('添加失败!');
                    }
                }.bind(this));
            })
            .catch((err) => {
                console.log(err);
            })
    }

    @action fetchTodoRemove(keyArr){
        fetch('http://localhost/api/todos/remove',{
            method:'POST',
            headers: {
                "Content-type":"application/json"
            },
            body: JSON.stringify({
                key: keyArr
            })
        })
            .then((response) => {
                console.log(response);
                response.json().then(function(data){
                    // console.log(data);
                    if(data.status){
                        if(keyArr.length > 1) {
                            this.todos = this.todos.filter(item => this.selectedRowKeys.indexOf(item.key) === -1);
                            this.selectedRowKeys = [];
                        }else{
                            this.todos = this.todos.filter(item => item.key !== keyArr[0]);
                        }
                        this.total -= keyArr.length;
                        message.success('删除成功!');
                    }else{
                        message.error('删除失败!');
                    }
                }.bind(this));
            })
            .catch((err) => {
                console.log(err);
            })
    }

注意

  1. antd Table控件绑定的DataSource是普通数组形式,而经过Mobx修饰器修饰的数组是observableArray,所以要通过observable.toJS()转换成普通数组。

  2. antd Table控件数据源需包含key,一些对行的操作都依赖key。

  3. 删除选中项时,一定要在删除成功后将selectedRowKeys置空,否则在下次选择时会选中已删除的项,虽然没有DOM元素但可能会影响其他一些操作。

  4. 使用Mobx过程中,如果this无法代表本身,而是指向其他,这时候函数不会执行,也不像React会报错:this is undefined,这时候需要手动添加bind(this),如果在View试图中调用时需要绑定,写为:bind(store);

  5. 跨域处理JSONP是一种方法,但是最根本的方法是操作header。server.js中设置跨域访问实际是对header进行匹配。

  6. 如果将mongoose查询写为异步,每个查询最后都需要添加.exec(),这样返回的才是Promise对象。mongoose易错

截图

mobx-demo.gif

源码

Github

Express 框架对企业应用仍显不足

$
0
0

以下纯属个人观点,如有错误,欢迎交流。

业务背景:
1. 能进行 MQTT 消息订阅,然后进行数据存储,以及利用 MQTT 向前端进行消息推送
2. 构建 Restful Web Api 
3. 公司现在仍在使用 MSSQL

Node 选择了弱类型 javacript 作为第一线编程语言,懂Javascript的人确实不少(不谈论懂的深度)。花了一周时间摸清了 Express 的骨架之后,node 的事件循环机制,js的异步编程思想,的确让我眼睛一亮,大叹一声原来还可以这样子做,并双击666。对于高IO和低CPU的需求来说,从 node 的角度来看,node 确实不错。
然而,在我看来,node + express 这套组合拳,打得却不是那么好。
首先,javascript 的弱类型性质,在服务器端这种相对前端比较严谨、容错性低的环境下,对生产环境来说,并不是每个会 js 的人都能够驾驭的住。仅仅对类型而言,强类型语言虽然对很多人来说有点限制,但也不是没有好处,例如在 VS2017 看到的 Node API 定义,全部都是 typescript,参数类型,返回类型,让人读起来确实一清二楚,例如 fs.readFile() 的定义:
![image.png](//dn-cnode.qbox.me/FtPpxwfXE4pfGENrjt_Nq2P2v473)
其二,虽说 npm 拥有很强大的社区,有很多的开源组件,但是能够镇得住脚的组件又有多少,例如对 MSSQL 支持的组件就很少,能够完全支持 MSSQL 的基本没有。在默认的组件bodyParse中,文件上传在 StackFlow 上曝出不安全,但是官方仍在默认组件中使用。

Node 确实很优秀,但是围绕于 Node 的生态还需努力。

vue +element消息推送

$
0
0

如题,Google了很久,很多人都说用socket.io或者qatt,现在很迷茫,不知道该怎么写,有没有人知道的

log4js中打印日志是否可以自动注入用户信息,IP等信息?

$
0
0

最近有搭建node的项目使用koa2使用日志时用的是log4js V2.xx版本,以前写java中log4j中可以在filter中加入用户信息到MDC 输出的日志内容中就会包含用户信息(比如:pattern配置: [%x{userId}][%x{ip}]%m%n,filter把用户的userId,IP写入到日志MDC中),开发人员只需要写: log.info(“this is a test log”)就输出:

	[123][192.168.1.100]this is a test log

当前配置如下:

module.exports = {
	appenders: {
		out: {
			type: 'stdout',
			layout: {
				type: 'pattern',
				pattern: '[%z][%c][%p][%d{yyyy-MM-dd-hh:mm:ss.SSS}]%m%n'
			}
		},
		dateFile: {
			type: 'dateFile',
			filename: 'logs/app.log',
			pattern: '.yyyyMMdd',//备份日志时文件名格式
			compress: true,//是否压缩备份日志
			keepFileExt: true,//保留文件的拓展名
			layout: {
				type: 'pattern',
				pattern: '[%c][%p][%h][%d %r]%m%n'
			}
		}
	},
	categories: {
		default: {
			appenders: ['dateFile', 'out'],
			level: 'info'
		}
	}
}

以上配置中好像看官方文档中存在tokens可以自定义属性使用%x{user}注入到输出的日志中,但是tokens中怎么获取用户信息并保证每个人请求过来的时候打印日志都是自己的userInfo

前后台分离是不要必须要在前端实现路由?

$
0
0

如果路由在后端,那是不是页面层都要从后台取,这样好象就是以前的模式 不能算前后台分离了

加入考拉理财大家庭,是一种怎样的体验?--来自考拉码农的心声

$
0
0

hi,各位帅气又多金,外表冷酷实则内心热情的技术猿: 这是考拉拉第二次踏入这片专属程序猿的天地。为了让大家更加全面了解考拉拉背后的大家庭-考拉理财,我们随机采访了公司的程序猿萌,让他们简单描(tu)述(cao)加入考拉理财的内心独白。 参与者: Fernando-前端码农,爱笑的大男孩,内心住着一个傲娇的小公举; Sam-后端码农,一脸淡定的表情,话不投机3句多; Jamie-全栈程序猿,对简洁整齐的代码有着极致的追求;爱旅游; navy-前端大牛,管理着一个20+的技术团队。 采访者:考拉拉 加入考拉理财,你们的感受是怎样的? Fernando:这里的环境好温馨!处处都是卖萌的考拉风,与外面的那些妖艳贱货不一样!中午吃饭还可以一起跟大家组团开黑!还可以让我开发王者农药版的活动页面,每天还有下午茶喝!开熏! Sam:没什么特别的感受,挺好的; Jamie:团队的学习氛围很好,虽然平时也会经常出bug,但是跟大家一起讨论总能找到解决方案,蛮有成就感的; Navy: 我来考拉2年了,在这里从一名默默无名的码农到现在管理着一个team,我最大的感受就是自己在工作效率以及工作能力方面的进步。很开心遇到一个重视技术团队的CEO。 你们在考拉理财印象最深刻的一件事? Fernando:公司内部开展“修bug有奖励”,因为自己团队补坑最多,因此拿到了丰厚奖励金! Sam:月度绩效评比,拿到了E,有了我第一个泷泽萝拉的真人照。感谢组织厚爱。 Jamie:年年都参与了年度旅游,第一年去马来西亚,第二年去泰国,年年都晒成黑炭。但是对于喜欢旅游的我来说,累并快乐着! navy:印象最深的事情,是我第一年来这里的时候,出身软工专业的我却对代码有偏执的情绪,不喜欢撸代码。后面经历了很多的变化,让我逐渐爱上代码,逐渐喜欢在这个团队里跟大家一次干活。也提高了工作效率,也让我有了能力去追逐更好的职位,获得一个宝贵的晋升。 一句话形容考拉技术团队? Fernando:开黑与无节操齐飞; Sam:挺好的,我挺喜欢的; Jamie:团结合作,沟通无阻碍 Navy:高效快速,能力提升快。 我说,可以结束这次访谈没。。。 好了,程序猿们要回家吃饭了,所以这次访谈结束!大家有兴趣了解更多,可以加入我们哟~ 欢迎大家毛遂自荐到此处:jobs@kalengo.com


mongodb的查询修改优化

$
0
0

因为不是专门写node的,就想请问一下node社区对于我这个情景有没有好的优化方案。目前的场景是经常有业务需要先查了之后获取某个字段来改查询修改另一个表的相关字段。如

group.find().then((results)=>{
     var saveArray = new Array()
     for(result in results){   // result 拥有group_name 和users , users字段里面的是username
	      let group_object = {}
		  group_object["group_name"]= result['group_name']
		  user.find({"name":{"$in":result['users']}).then((name_list)=>{
		  	group_object['users'] = name_list
		  	saveArray.push(group_object )  // 此处users是一个array
		  },(err)=>{
		      reject(err)
		  } 
	 }
	 resolve(saveArray)
},(err)=>{
  reject(err)
})

这样的写法明显是有问题,因为for里面异步会被忽略,如果达到我这个逻辑的效果?

开源接口管理平台yapi 发布了 v1.2.8 版本

$
0
0

文档: http://yapi.qunar.com github: https://github.com/ymfe/yapi

可视化接口管理平台 YApi ( http://yapi.qunar.com/) 上线三个月以来,取得了可喜的成绩,也收获了很多赞。YApi 希望能解决公司多年来各职能对接口管理的痛点,旨在为开发人员提供统一的接口管理,Mock 服务,帮助开发者轻松维护、测试 API。YApi 的目标和预期是将:前端、后端、QA 都串联起来,共同维护一套 Api。

我们已经在 Github ( https://github.com/YMFE/yapi) 上开源了 YApi,在不到这两个月的时间内已经收获了 612个 star,最近一个月增加了 300多个 start,并且目前有数百家公司部署了 YApi 服务。可见,YApi 确实命中了非常多公司和团队的痛点,大家都在接口管理及职能协同之间没有找到一个靠谱的解决方案。

在这里,我邀请大家一起来共建 YApi (至少,保证你们团队在 YApi 平台上的活跃度足够),让它成为一个明星开源产品

特性:

  • 支持 Api Test, Debug, 类似 Postman
  • Automatic Test, 根据设置的 API 参数和断言,可一次性运行 Test Collection 所有 Api 做冒烟测试
  • MockServer, 新增 Mock 期望功能,根据设置的请求过滤规则,返回期望数据

java 这个签名算法求问大神怎么用node实现

$
0
0
public static String getKeyedDigest(String strSrc, String key) {
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            md5.update(strSrc.getBytes(Common.CHARSET));
            String result="";
            byte[] temp;
            temp=md5.digest(key.getBytes(Common.CHARSET));
            for (int i=0; i<temp.length; i++){
                result+=Integer.toHexString((0x000000ff & temp[i]) | 0xffffff00).substring(6);
            }
            return result;          
    }

cli-scraper 一个为命令行而生的小爬虫库

$
0
0

cli-scraper的开发初衷是希望能帮助大家更方便的开发自己的爬虫,以便在命令行中浏览静态网页内容。如果你和我一样,生活在命令行世界中,那它给了你又一个留下的理由。

全局安装后,要让 cli-scraper 开始工作,仅需如下三步:

  1. 运行 $ clis init hello.js 这条命令,新建一个新的配置文件。
  2. 通过编写 CSS 选择条件,告诉 cli-scraper 如何定位到你希望从网页中提取的内容。
  3. 最后,运行 $ clis process hello.js。

话不多说,上

范例 - 访问 https://bing.com并提取 logo 文本:

运行 init 命令生成配置文件 $ clis init bing.js

// 如下是完成后的配置文件,复制粘贴到你本地的 bing.js ,试试吧。
module.exports = {
  url: 'https://www.bing.com/',     // 目标地址
  process: function ({ $ }) {
    return $('.hp_sw_logo').text()  // 选中目标元素,并提取其中文本
  },
  finally: function (res) {
    console.log(res + 'go :)')      // 结果任你处置
  }
}

运行 process 命令开工 $ clis process bing.js,easy as pie 在 README中有更多的和参数的详细说明。

在开发中,没有选择使用 async / await,而用的是原生的 Promise,因为感觉这样写出来的 data pipeline 看起来更加直观,Happy coding :)

外包神器!横扫一切增删改查事务!打开浏览器写SQL就能一站完成系统开发!

关于node框架?到底选择哪个好呢?

$
0
0

关于node框架?到底选择哪个好呢? 现在我用的是Express 有人说我的过气的框架?Express现在不流行吗?他推荐我用koa 。大家觉得呢!

【北京】磁器口附近,踏实做事的小公司

$
0
0

普通的小公司,没啥优势和大牛公司比,同事间人际关系简单,做事踏实,业务大部分是国家项目,稳定,五险一金,工作环境还不错。 希望你也是踏踏实实喜欢做技术的人。简历邮箱:read.romeo.c@gmail.com

工作内容:

  • 负责项目后端架构和功能的开发
  • 完成实现功能的部署、重构和调优工作
  • 快速定位并解决项目后端相关功能的Bug
  • 参与项目需求分析工作,制定解决方案
  • 对运维人员的技术支持

要求:

  • 本科及以上学历,计算机相关专业,2年以上工作经验
  • 2年以上Node.js开发经验,有大型互联网项目开发经验者优先
  • 熟悉Node.js标准库的使用
  • 熟悉Express/Koa等常用框架的使用和优化
  • 熟悉性能优化,掌握监控和分析方法
  • 熟悉MongoDB和Redis的使用和优化
  • 熟悉Linux服务器系统,有1年以上实际服务器运维经验
  • 有Java服务器端开发经验者优先
  • 有快速学习能力,具备分析问题解决问题的能力,能独立承担业务开发工作
  • 具备高度责任心,有良好的沟通能力和团队合作精神

【原创】持续集成、Faas 架构、了解一下后端架构有哪些组件?

$
0
0

最近花了一些时间研究 DevOps ,然后我做了3期分享,分别是

  • 关于 Faas 架构
    image.png
  • flowCI 持续集成
    image.png
  • 架构揭秘
    image.png

所以视频链接在这里 https://nodelover.me/status

其实最近我还研究了一下 rabbitmq 和 jenkins,不过就没有录成分享了,以后有时间再分享给大家。

请问NodeJS怎么存一个JSON对象到MySQL 5.7数据库里?

$
0
0

我们有如下的数据类型要保存: { name:"小王" updateTime:123123132 //注意,这里是数字 }

在MySQL 5.7之前,都是先用JSON.stringfy转成字符串,然后以varchar的形式保存,然后取出时,再用JSON.parse的方法把字符串转成json对象。可以经过这么一次操作,拿到的数据就会变成下面的样子: { name:"小王" updateTime:“123123132” //注意,这里变成了字符串 }

也就是说,数字格式的value就也变成了string。这对我们的开发影响很大。我们希望存的JSON在取出时,数字还是数字,不要变成String。 刚刚看到说5.7已经支持JSON格式了,但是我们自己开了一个5.7试了好多次,都没能正确保存进去。 请问,该怎么做啊?

【上海】欧特克诚聘Principal Engineer

$
0
0

Autodesk is looking for an experienced and results-driven Principal Software Engineer for BIM360 product group in Autodesk China R&D Center (ACRD) based in Shanghai, China. This is your chance to help shape the future of BIM360 for Project, Field and BIM Managers to accelerate delivery, save money and reduce risk. As a Senior Software Engineer, you will be responsible for developing the best class web service and applications, using Agile Development process. The right candidate must be passionate on state-of-the-art technologies for solving customer problems.

Job Title and Number: Principal Web Software Engineer - backend Location: Shanghai

Responsibilities: • Being part of a collaborative tech team • Building new, innovative and disruptive web services targeted at the construction industry. • Work with your agile team to deliver solutions on time and at high quality that conform to user story acceptance criteria • Participate in Design and Code reviews to ensure that our design and code meets high standards. • Test, debug, and maintain the service architecture throughout the product lifecycle • Keep your ear to the ground, helping us define and analyze industry best practices and important developments in large scale cloud applications.

Requirements: • BS/MS degree in Computer Science, Engineering or a related subject. • 8+ years of professional development experience in cloud service applications. • At least 4+ years of experience in web application development using Html, CSS, JavaScript and jQuery. • Experienced in NodeJS development for large systems. (Experienced in Java development may also be considered, but NodeJS is preferred) • Strong programming skills, with the ability to follow department standards and meeting high levels of quality, clarity and efficiency; ability to organize and plan work for self • Experience working with both sides of a RESTful web service. • Experience with RDBs and SQL • Good oral and written communication skills in English. • Familiar with Agile/Scrum development methodologies. • Experience in software development in Cloud platforms such as AWS is a big plus. • Experience in Linux/ Ubuntu system is a plus. • Experience in Angular or React is a plus.

薪资待遇:年薪40~50万,年终奖,除基本福利外另有补充公积金,弹性工作制 联系方式: jenny.lu@autodesk.com 8621 38653285

[深圳] 小智LOGO招聘前端/全栈工程师

$
0
0

公司介绍

大家好,我们是小智LOGO,一个坐标深圳快速成长的创业团队。我们目前主要的产品是一个在线自动设计LOGO的网站:https://xzlogo.com。上线不到半年的时间目前已累积超过50万用户,并且持续成长中。除此之外,我们也在开发许多其他自动化设计的产品,用人工智能让不懂设计的用户可以快速完成精美的设计。我们是一个90后为主的团队,工作气氛自由,有很大的发挥空间。我们的上一个工程师就是在cnode的社群找到的,所以我们也很期待能再找到一个对技术充满热情的同学加入我们!

工作职责

  • 用ES5/6/7/HTML/CSS结合现代框架来丰富我们web产品的用户体验。
  • 熟悉敏捷开发,编写高质量的,整洁简单,可维护性的代码;构建可重复使用的代码以及公共库;
  • 构建新的Web应用,对现有的bug进行快速修复。

职位需求

  • 计算机或相关专业的本科及以上;
  • 能够熟练运用HTML、CSS、JavaScript构建高性能的web应用程序;
  • 能够熟练运用至少一款主流的JS框架,react优先;
  • 具有良好的代码风格、接口设计与程序架构;
  • 熟悉Linux命令行操作;
  • 喜欢迎接新挑战,自学能力强。

加分项

  • 熟悉至少一种后端开发语言,如:Python,Node.js;
  • 熟悉至少一种前端测试框架,如:mocha,jest;

联系方式

  • 简历请投至:wen@xzlogo.com
  • 公司地址:深圳市南山区众创产业园53栋507

如何编写一个 HTTP 反向代理服务器

$
0
0

原文发表于作者的个人博客:http://morning.work/page/nodejs/simple-http-reverse-proxy.html转载请注明出处

如果你经常使用 Node.js 编写 Web 服务端程序,一定对使用 Nginx 作为 反向代理服务并不陌生。在生产环境中,我们往往需要将程序部署到内网多台服务器上,在一台多核服务器上,为了充分利用所有 CPU 资源,也需要启动多个服务进程,它们分别监听不同的端口。然后使用 Nginx 作为反向代理服务器,接收来自用户浏览器的请求并转发到后端的多台 Web 服务器上。大概工作流程如下图:

反向代理服务器

在 Node.js 上实现一个简单的 HTTP 代理程序还是非常简单的,本文章的例子的核心代码只有 60 多行,只要理解 内置 http 模块的基本用法即可,具体请看下文。

接口设计与相关技术

使用 http.createServer()创建的 HTTP 服务器,处理请求的函数格式一般为 function (req, res) {}(下文简称为 requestHandler),其接收两个参数,分别为 http.IncomingMessagehttp.ServerResponse对象,我们可以通过这两个对象来取得请求的所有信息并对它进行响应。

主流的 Node.js Web 框架的中间件(比如 connect)一般都有两种形式:

  • 中间件不需要任何初始化参数,则其导出结果为一个 requestHandler
  • 中间件需要初始化参数,则其导出结果为中间件的初始化函数,执行该初始化函数时,传入一个 options对象,执行后返回一个 requestHandler

为了使代码更规范,在本文例子中,我们将反向代理程序设计成一个中间件的格式,并使用以上第二种接口形式:

// 生成中间件
const handler = reverseProxy({
  // 初始化参数,用于设置目标服务器列表
  servers: ["127.0.0.1:3001", "127.0.0.1:3002", "127.0.0.1:3003"]
});

// 可以直接在 http 模块中使用
const server = http.createServer(handler);

// 作为中间件在 connect 模块中使用
app.use(handler);

说明:

  • 上面的代码中,reverseProxy是反向代理服务器中间件的初始化函数,它接受一个对象参数,servers是后端服务器地址列表,每个地址为 IP地址:端口这样的格式
  • 执行 reverseProxy()后返回一个 function (req, res) {}这样的函数,用于处理 HTTP 请求,可作为 http.createServer()和 connect 中间件的 app.use()的处理函数
  • 当接收到客户端请求时,按顺序循环从 servers数组中取出一个服务器地址,将请求代理到这个地址的服务器上

服务器在接收到 HTTP 请求后,首先需要发起一个新的 HTTP 请求到要代理的目标服务器,可以使用 http.request()来发送请求:

const req = http.request(
  {
    hostname: "目标服务器地址",
    port: "80",
    path: "请求路径",
    headers: {
      "x-y-z": "请求头"
    }
  },
  function(res) {
    // res 为响应对象
    console.log(res.statusCode);
  }
);
// 如果有请求体需要发送,使用 write() 和 end()
req.end();

要将客户端的请求体(Body部分,在 POSTPUT这些请求时会有请求体)转发到另一个服务器上,可以使用 Stream对象的 pipe()方法,比如:

// req 和 res 为客户端的请求和响应对象
// req2 和 res2 为服务器发起的代理请求和响应对象
// 将 req 收到的数据转发到 req2
req.pipe(req2);
// 将 res2 收到的数据转发到 res
res2.pipe(res);

说明:

  • req对象是一个 Readable Stream(可读流),通过 data事件来接收数据,当收到 end事件时表示数据接收完毕
  • res对象是一个 Writable Stream(可写流),通过 write()方法来输出数据,end()方法来结束输出
  • 为了简化从 Readable Stream监听 data事件来获取数据并使用 Writable Streamwrite()方法来输出,可以使用 Readable Streampipe()方法

以上只是提到了实现 HTTP 代理需要的关键技术,相关接口的详细文档可以参考这里:https://nodejs.org/api/http.html#http_http_request_options_callback

当然为了实现一个接口友好的程序,往往还需要很多 额外的工作,具体请看下文。

简单版本

以下是实现一个简单 HTTP 反向代理服务器的各个文件和代码(没有任何第三方库依赖),为了使代码更简洁,使用了一些最新的 ES 语法特性,需要使用 Node v8.x 最新版本来运行

文件 proxy.js

const http = require("http");
const assert = require("assert");
const log = require("./log");

/** 反向代理中间件 */
module.exports = function reverseProxy(options) {
  assert(Array.isArray(options.servers), "options.servers 必须是数组");
  assert(options.servers.length > 0, "options.servers 的长度必须大于 0");

  // 解析服务器地址,生成 hostname 和 port
  const servers = options.servers.map(str => {
    const s = str.split(":");
    return { hostname: s[0], port: s[1] || "80" };
  });

  // 获取一个后端服务器,顺序循环
  let ti = 0;
  function getTarget() {
    const t = servers[ti];
    ti++;
    if (ti >= servers.length) {
      ti = 0;
    }
    return t;
  }

  // 生成监听 error 事件函数,出错时响应 500
  function bindError(req, res, id) {
    return function(err) {
      const msg = String(err.stack || err);
      log("[%s] 发生错误: %s", id, msg);
      if (!res.headersSent) {
        res.writeHead(500, { "content-type": "text/plain" });
      }
      res.end(msg);
    };
  }

  return function proxy(req, res) {
    // 生成代理请求信息
    const target = getTarget();
    const info = {
      ...target,
      method: req.method,
      path: req.url,
      headers: req.headers
    };

    const id = `${req.method} ${req.url} => ${target.hostname}:${target.port}`;
    log("[%s] 代理请求", id);

    // 发送代理请求
    const req2 = http.request(info, res2 => {
      res2.on("error", bindError(req, res, id));
      log("[%s] 响应: %s", id, res2.statusCode);
      res.writeHead(res2.statusCode, res2.headers);
      res2.pipe(res);
    });
    req.pipe(req2);
    req2.on("error", bindError(req, res, id));
  };
};

文件 log.js

const util = require("util");

/** 打印日志 */
module.exports = function log(...args) {
  const time = new Date().toLocaleString();
  console.log(time, util.format(...args));
};

说明:

  • log.js文件实现了一个用于打印日志的函数 log(),它可以支持 console.log()一样的用法,并且自动在输出前面加上当前的日期和时间,方便我们浏览日志
  • reverseProxy()函数入口使用 assert模块来进行基本的参数检查,如果参数格式不符合要求即抛出异常,保证可以第一时间让开发者知道,而不是在运行期间发生各种 不可预测的错误
  • getTarget()函数用于循环返回一个目标服务器地址
  • bindError()函数用于监听 error事件,避免整个程序因为没有捕捉网络异常而崩溃,同时可以统一返回出错信息给客户端

为了测试我们的代码运行的效果,我编写了一个简单的程序,文件 server.js

const http = require("http");
const log = require("./log");
const reverseProxy = require("./proxy");

// 创建反向代理服务器
function startProxyServer(port) {
  return new Promise((resolve, reject) => {
    const server = http.createServer(
      reverseProxy({
        servers: ["127.0.0.1:3001", "127.0.0.1:3002", "127.0.0.1:3003"]
      })
    );
    server.listen(port, () => {
      log("反向代理服务器已启动: %s", port);
      resolve(server);
    });
    server.on("error", reject);
  });
}

// 创建演示服务器
function startExampleServer(port) {
  return new Promise((resolve, reject) => {
    const server = http.createServer(function(req, res) {
      const chunks = [];
      req.on("data", chunk => chunks.push(chunk));
      req.on("end", () => {
        const buf = Buffer.concat(chunks);
        res.end(`${port}: ${req.method} ${req.url} ${buf.toString()}`.trim());
      });
    });
    server.listen(port, () => {
      log("服务器已启动: %s", port);
      resolve(server);
    });
    server.on("error", reject);
  });
}

(async function() {
  await startExampleServer(3001);
  await startExampleServer(3002);
  await startExampleServer(3003);
  await startProxyServer(3000);
})();

执行以下命令启动:

node server.js

然后可以通过 curl命令来查看返回的结果:

curl http://127.0.0.1:3000/hello/world

连续执行多次该命令,如无意外输出结果应该是这样的(输出内容端口部分按照顺序循环):

3001: GET /hello/world
3002: GET /hello/world
3003: GET /hello/world
3001: GET /hello/world
3002: GET /hello/world
3003: GET /hello/world

注意:如果使用浏览器来打开该网址,看到的结果顺序可能是不一样的,因为浏览器会自动尝试请求/favicon,这样刷新一次页面实际上是发送了两次请求。

单元测试

上文我们已经完成了一个基本的 HTTP 反向代理程序,也通过简单的方法验证了它是能正常工作的。但是,我们并没有足够的测试,比如只验证了 GET 请求,并没有验证 POST 请求或者其他的请求方法。而且通过手工去做更多的测试也比较麻烦,很容易遗漏。所以,接下来我们要给它加上自动化的单元测试。

在本文中我们选用在 Node.js 界应用广泛的 mocha作为单元测试框架,搭配使用 supertest来进行 HTTP 接口请求的测试。由于 supertest已经自带了一些基本的断言方法,我们暂时不需要 chai或者 should这样的第三方断言库。

首先执行 npm init初始化一个 package.json文件,并执行以下命令安装 mochasupertest

npm install mocha supertest --save-dev

然后新建文件 test.js

const http = require("http");
const log = require("./log");
const reverseProxy = require("./proxy");
const { expect } = require("chai");
const request = require("supertest");

// 创建反向代理服务器
function startProxyServer() {
  return new Promise((resolve, reject) => {
    const server = http.createServer(
      reverseProxy({
        servers: ["127.0.0.1:3001", "127.0.0.1:3002", "127.0.0.1:3003"]
      })
    );
    log("反向代理服务器已启动");
    resolve(server);
  });
}

// 创建演示服务器
function startExampleServer(port) {
  return new Promise((resolve, reject) => {
    const server = http.createServer(function(req, res) {
      const chunks = [];
      req.on("data", chunk => chunks.push(chunk));
      req.on("end", () => {
        const buf = Buffer.concat(chunks);
        res.end(`${port}: ${req.method} ${req.url} ${buf.toString()}`.trim());
      });
    });
    server.listen(port, () => {
      log("服务器已启动: %s", port);
      resolve(server);
    });
    server.on("error", reject);
  });
}

describe("测试反向代理", function() {
  let server;
  let exampleServers = [];

  // 测试开始前先启动服务器
  before(async function() {
    exampleServers.push(await startExampleServer(3001));
    exampleServers.push(await startExampleServer(3002));
    exampleServers.push(await startExampleServer(3003));
    server = await startProxyServer();
  });

  // 测试结束后关闭服务器
  after(async function() {
    for (const server of exampleServers) {
      server.close();
    }
  });

  it("顺序循环返回目标地址", async function() {
    await request(server)
      .get("/hello")
      .expect(200)
      .expect(`3001: GET /hello`);

    await request(server)
      .get("/hello")
      .expect(200)
      .expect(`3002: GET /hello`);

    await request(server)
      .get("/hello")
      .expect(200)
      .expect(`3003: GET /hello`);

    await request(server)
      .get("/hello")
      .expect(200)
      .expect(`3001: GET /hello`);
  });

  it("支持 POST 请求", async function() {
    await request(server)
      .post("/xyz")
      .send({
        a: 123,
        b: 456
      })
      .expect(200)
      .expect(`3002: POST /xyz {"a":123,"b":456}`);
  });
});

说明:

  • 在单元测试开始前,需要通过 before()来注册回调函数,以便在开始执行测试用例时先把服务器启动起来
  • 同理,通过 after()注册回调函数,以便在执行完所有测试用例后把服务器关闭以释放资源(否则 mocha 进程不会退出)
  • 使用 supertest发送请求时,代理服务器不需要监听端口,只需要将 server实例作为调用参数即可

接着修改 package.json文件的 scripts部分:

{
  "scripts": {
    "test": "mocha test.js"
  }
}

执行以下命令开始测试:

npm test

如果一切正常,我们应该会看到这样的输出结果,其中 passing这样的提示表示我们的测试完全通过了:

测试反向代理
2017-12-12 18:28:15 服务器已启动: 3001
2017-12-12 18:28:15 服务器已启动: 3002
2017-12-12 18:28:15 服务器已启动: 3003
2017-12-12 18:28:15 反向代理服务器已启动
2017-12-12 18:28:15 [GET /hello => 127.0.0.1:3001] 代理请求
2017-12-12 18:28:15 [GET /hello => 127.0.0.1:3001] 响应: 200
2017-12-12 18:28:15 [GET /hello => 127.0.0.1:3002] 代理请求
2017-12-12 18:28:15 [GET /hello => 127.0.0.1:3002] 响应: 200
2017-12-12 18:28:15 [GET /hello => 127.0.0.1:3003] 代理请求
2017-12-12 18:28:15 [GET /hello => 127.0.0.1:3003] 响应: 200
2017-12-12 18:28:15 [GET /hello => 127.0.0.1:3001] 代理请求
2017-12-12 18:28:15 [GET /hello => 127.0.0.1:3001] 响应: 200
    ✓ 顺序循环返回目标地址
2017-12-12 18:28:15 [POST /xyz => 127.0.0.1:3002] 代理请求
2017-12-12 18:28:15 [POST /xyz => 127.0.0.1:3002] 响应: 200
    ✓ 支持 POST 请求


  2 passing (45ms)

当然以上的测试代码还远远不够,剩下的就交给读者们来实现了。

接口改进

如果要设计成一个比较通用的反向代理中间件,我们还可以通过提供一个生成 http.ClientRequest的函数来实现在代理时动态修改请求:

reverseProxy({
  servers: ["127.0.0.1:3001", "127.0.0.1:3002", "127.0.0.1:3003"],
  request: function(req, info) {
    // info 是默认生成的 request options 对象
    // 我们可以动态增加请求头,比如当前请求时间戳
    info.headers["X-Request-Timestamp"] = Date.now();
    // 返回 http.ClientRequest 对象
    return http.request(info);
  }
});

然后在原来的 http.request(info, (res2) => {})部分可以改为监听 response事件:

const req2 = http.request(options.request(info));
req2.on("response", res2 => {});

同理,我们也可以通过提供一个函数来修改部分的响应内容:

reverseProxy({
  servers: ["127.0.0.1:3001", "127.0.0.1:3002", "127.0.0.1:3003"],
  response: function(res, info) {
    // info 是发送代理请求时所用的 request options 对象
    // 我们可以动态设置一些响应头,比如实际代理的模板服务器地址
    res.setHeader("X-Backend-Server", `${info.hostname}:${info.port}`);
  }
});

此处只发散一下思路,具体实现方法和代码就不再赘述了。

总结

本文主要介绍了如何使用内置的 http模块来创建一个 HTTP 服务器,以及发起一个 HTTP 请求,并简单介绍了如何对 HTTP 接口进行测试。在实现 HTTP 请求代理的过程中,主要是运用了 Stream对象的 pipe()方法,关键部分代码只有区区几行。Node.js 中的很多程序都运用了 Stream这样的思想,将数据当做一个流,使用 pipe将一个流转换成另一个流,可以看出 Stream在 Node.js 的重要性。关于 Stream对象的使用方法可以看作者写的另一篇文章 《Node.js 的 Readable Stream 与日志文件处理》

Viewing all 14821 articles
Browse latest View live