精华 分享几道Nodejs面试题,大家一起来做,有大神回复指点
发布于 6 年前 作者 i5ting 24116 次浏览 最后一次编辑是 5 年前 来自 分享

26084007iedq.png

实现一个简单 MySQL ORM 模块

支持类似如下的使用方法(最好包含基本的单元测试):

const orm = new MyORM({
  // mysql连接信息
  connection: {host: '127.0.0.1', port: 3306, user: 'root', password: '', database: 'test'},
});

orm.table('xxxx').find(query).skip(0).limit(20)
.then(list => console.log('results', list))
.catch(err => console.log(err))

orm.table('xxxxx').update(query, update)
.then(ret => console.log(ret))
.catch(err => console.log(err))

// 另外需要支持基本的 delete, findOne 等方法

实现一个简单的 Redis Session 中间件

支持类似如下的使用方法(最好包含基本的单元测试):

// 初始化中间件
app.use(mySession({
  connection: {host: '127.0.0.1', port: 6379}, // Redis连接信息
  maxAge: 3600, // session的有效期
  sessionId: 'my.sid', // session ID 的cookie名称
}));

// 使用时直接在 req.session 上添加或删除属性即可

使用 net 模块实现一个简单的 HTTP 客户端

支持类似如下的使用方法(最好包含基本的单元测试):

request({
  method: 'POST', // 请求方法
  url: 'http://xxx.com', // 请求地址
  qs: {a: 123, b: 456}, // query查询参数
  form: {c: 111, d: 'zxxxxx'}, // post body参数
  headers: {
    'user-agent': 'SuperID/Node.js', // 请求头
  },
})
.then(ret => {
  // ret.headers 表示响应头
  // ret.statusCode 表示响应代码
  // ret.body 表示响应内容(Buffer)
})
.catch(err => console.log(err))

实现一个简单的测试单元框架

支持类似如下的使用方法(最好包含基本的单元测试):

// 同步功能测试
test('测试1', function () {
  assert.euqal(1 + 1, 2);
});

// 异步功能测试
test('测试2', function (done) {
  setTimeout(function () {
    assert.equal(2 + 2, 4);
	done();
  }, 100);
});

执行测试后返回类似如下的结果:

测试1 - 耗时100ms - 失败
测试2 - 耗时125ms - 通过

参与方式

本帖回复答案,@leizongmin 会给出指点

老雷简介

老雷(雷宗民),一登高级后端工程师,《Node.js实战(双色)》和《Node.js实战(第2版)》作者之一,xss模块作者,5年Node.js使用经验,GitHub: leizongmin

全文完

欢迎关注我的公众号【node全栈】

node全栈.png

联系我,更多交流

xiaoweiquan.jpeg

42 回复

有点高端的。。。。。。,这岂止15k+啊。。

怪不得我没过15K

我目前的能力估计只能做一下 “使用 net 模块实现一个简单的 HTTP 客户端”, 详见 这里 不过有点奇怪的是, 对于某些网站的访问, 似乎不会触发 data 事件. 比方说 http://www.baidu.com/s?wd=qwe&ie=UTF-8&tn=baidulocal, 我对比过我的模块发出的 request header 跟 curl 发出的 request header, 它们是一样的, 但是我的模块就是获取不了响应… 不知道什么原因, 恳请指点.

connection: close 之后百度那个还是有问题,可能还是需要哪个 request header

@magicdawn 嗯 那是因为 data 事件一直没触发

另外, 我认为跟 response header 里的某个参数关系不大. 因为不论 response header 里有什么参数, 它总得先把这个 header 给我发过来吧, 但我这里的情况相当于 socket connect 之后就直接 close 了.

而且, 访问 weibo.com 的时候, response header 里也是带有 Transfer-Encoding: chunked, 不过 data 事件却有触发.

顺便附上 curl 访问 baidu 的 request header: curl -vvv ‘http://www.baidu.com/s?wd=qwe&ie=UTF-8&tn=baidulocal

  • Trying 115.239.211.112…
  • Connected to www.baidu.com (115.239.211.112) port 80 (#0)

GET /s?wd=qwe&ie=UTF-8&tn=baidulocal HTTP/1.1 Host: www.baidu.com User-Agent: curl/7.47.1 Accept: /

HTTP/1.1 200 OK . . .

如果使用HTTP访问百度的时候,百度会要求跳转到HTTPS的,你检查是否有正确处理301或302状态码 @knight42

@leizongmin 唔…你可以试下访问 http://www.baidu.com/s?wd=qwe&ie=UTF-8&tn=baidulocal, 应该是没有跳转的.

但我现在遇到的问题是整个响应我都没有接收到

我也写了一个,见GIST 这模块如果要上生产环境,几个小时是不够用的,要是实现request包的完整功能我就觉得好厉害了 其他几个题也很不错,跟我现在的技术栈正好匹配,今天偷懒,改天再找个时间写写看

自从老大不给我们review代码之后,很多地方都是凭感觉写,写出来莫名其妙,恳请吐槽啊@knight42

@knight42

HTTP/1.1后面多了个空格。hexdump才看到。。。

@reverland 大神啊… 很抱歉给你带来了这么大的麻烦! 万分感谢!!!

@knight42

哈哈哈,我找到你的毛病了。。。

untitled1.png

把空格删掉试试。。。就好了。。。

@knight42 我是渣渣。。。这也是长姿势了。。

@magicdawn 。。。。。。简直醉了

untitled1.png

1是data事件打印的,2是end打印的。。。

@reverland 哈哈~怎么dump的~求教。。。

@magicdawn @reverland 当时真的是手抖… 没想到居然是个空格的问题 Orz

@magicdawn

┌─(~/Downloads)────────────────────────(reverland@reverland-R478-R429:pts/17)─┐
└─(19:52:10)──> nc -lvp 8000 |hexdump -C                        ──(日, 3月20)─┘
Listening on [0.0.0.0] (family 0, port 8000)
Connection from [127.0.0.1] port 8000 [tcp/*] accepted (family 2, sport 59636)
00000000  47 45 54 20 2f 73 3f 77  64 3d 71 77 65 26 69 65  |GET /s?wd=qwe&ie|
00000010  3d 55 54 46 2d 38 26 74  6e 3d 62 61 69 64 75 6c  |=UTF-8&tn=baidul|
00000020  6f 63 61 6c 20 48 54 54  50 2f 31 2e 31 20 0a 48  |ocal HTTP/1.1 .H|
00000030  6f 73 74 3a 20 77 77 77  2e 62 61 69 64 75 2e 63  |ost: www.baidu.c|
00000040  6f 6d 0a 0a 0a 0a                                 |om....|
00000046
┌─(~/Downloads)────────────────────────(reverland@reverland-R478-R429:pts/17)─┐
└─(20:00:52)──> nc -lvp 8000 |hexdump -C                        ──(日, 3月20)─┘
Listening on [0.0.0.0] (family 0, port 8000)
Connection from [127.0.0.1] port 8000 [tcp/*] accepted (family 2, sport 59650)
00000000  47 45 54 20 2f 73 3f 77  64 3d 71 77 65 26 69 65  |GET /s?wd=qwe&ie|
00000010  3d 55 54 46 2d 38 26 74  6e 3d 62 61 69 64 75 6c  |=UTF-8&tn=baidul|
00000020  6f 63 61 6c 20 48 54 54  50 2f 31 2e 31 0a 48 6f  |ocal HTTP/1.1.Ho|
00000030  73 74 3a 20 77 77 77 2e  62 61 69 64 75 2e 63 6f  |st: www.baidu.co|
00000040  6d 0a 0a                                          |m..|
00000043

@ayiis 我没什么好吐槽的… 你实现的完成度高多了, 错误处理也很完善跟周到, 而且没有像我那样使用 querystring 跟 url 偷懒 Orz…

话说把另外几个题也做了吧, 我也想学习一下~

@reverland 在8000起本地server,然后等着dump,然后用问题代码去访问8000是吧?

傻傻的test

'use strict';

global.test = function(desc, fn) {
  let start = Date.now();

  // 异步
  if (fn.length !== 0) {
    return fn(report);
  }

  // 同步
  else {
    let e;
    try {
      fn();
    }
    catch (err) {
      e = err;
    }
    finally {
      report(e);
    }
  }

  function report(err) {
    let t = Date.now() - start;
    let status = err ? '失败' : '通过';
    console.log(`${ desc } - 耗时${ t }ms - ${ status }`);
  }
};

const assert = require('assert');
// 同步功能测试
test('测试1', function() {
  assert.equal(1 + 1, 2);
});

// 异步功能测试
test('测试2', function(done) {
  setTimeout(function() {
    assert.equal(2 + 2, 4);
    done();
  }, 100);
});

@magicdawn 用tcpdump看也行,就是。。。包太多了。。

拿scapy自己做一个专门用来检查HTTP请求的。。。

你们好厉害,看样子窝离15k太远了,还是默默地搬窝的砖。。。

@knight42

  1. 自己实现这些方法,比如parseURL用encodeURIComponent还是escape编码我心里没底,也没测试过
  2. 我代码里用了很多个Buffer.concat来拼接buffer,效率可能很低
  3. transfer-encoding:chunked这种情况我写了几段代码试图兼容,但写着写着我发现需要看paper才能写下去就删掉了
  4. 其实并没有做错误处理…
  5. 我发现自己应该多去看书ORZ

@reverland 这个轮子不好造啊… 我现在的项目正是在用python的sock_raw做这个事情… 求交流

@ayiis 不过说到raw socket,node中到socket真是异类,窝曾经想要用node写个sniffer,然后找到了别人封装到raw socket。。。

对于中级noder来说,题目都恰到好处啊。

@ayiis @knight42 接收数据触发data事件时,把chunk添加到数组里面即可,同时判断一下,如果请求头还没有结束就在新收到的chunk里面查找\r\n\r\n,如果找到了就练同之前收到的数据一起,分离出headersbody @ayiis 你的做法效率会有点低 @knight42 你的做法是接收完了所有数据才做处理,有时候是不可行的,比如要实现对Transfer-Encoding:chunked的支持

配图屌爆了

完成一点,感觉上代码会占大量篇幅 给 github[https://github.com/Tonyce/15kNodeTry]
静待大神指点

var mysql = require("mysql");
class MyOrm {

    constructor(connect) {
        if (typeof connect == "object" && typeof connect.connection == "object") {
            this.pool = mysql.createPool(connect.connection);
        } else {
            throw new Error('please ....');
        }
    }


    table(tableName) {
        this.tablename = tableName;
        return this;
    }

    find(q) {
        this.opeart = "select * from " + this.tablename + " where 1 =1";
        if (typeof  q == "object") {
            for (var ob in q) {
                this.opeart = this.opeart + " and " + ob + "='" + q[ob] + "'";
            }
        }
        return this;
    }

    skip(s) {
        this.opeart = this.opeart + " limit " + s;
        this.isLimt = true;
        return this;
    }

    limit(l) {
        if (this.isLimt) {
            this.opeart = this.opeart + ", " + l;
        } else {
            this.opeart = this.opeart + "limit " + l;
        }
        return this;
    }

    then(callback) {
        this.pool.query(this.opeart,function(err,data){
            if(err){
                this.err = err;
            }else {
                callback(data);
            }
        })
        return this;
    }
    catch(callback){
        if(this.err){
        callback(this.err);}
        else callback(null);
    }

}

var tt = new MyOrm({
    // mysql连接信息
    connection: {
        host: '',
        port: 3306,
        user: 'root',
        password: '',
        database: 'test'
    },
});
tt.table('ak_user').find({userName: "nothing"}).skip(0).limit(12).then(data => console.log(data)).catch(err => console.log(err));

做了一部分 ,汗。。总感觉err那一块处理的不是很好

var mysql = require("mysql");
class MyOrm {

    constructor(connect) {
        if (typeof connect == "object" && typeof connect.connection == "object") {
            this.pool = mysql.createPool(connect.connection);
        } else {
            throw new Error('please ....');
        }
    }


    table(tableName) {
        this.tablename = tableName;
        return this;
    }

    find(q) {
        this.opeart = "select * from " + this.tablename + " where 1 =1";
        if (typeof  q == "object") {
            for (var ob in q) {
                this.opeart = this.opeart + " and " + ob + "='" + q[ob] + "'";
            }
        }
        return this;
    }

    skip(s) {
        this.opeart = this.opeart + " limit " + s;
        this.isLimt = true;
        return this;
    }

    limit(l) {
        if (this.isLimt) {
            this.opeart = this.opeart + ", " + l;
        } else {
            this.opeart = this.opeart + "limit " + l;
        }
        return this;
    }

    then(callback) {
        var p;
        (function(pool,opeart){
           p =  new Promise(function(res,rej){
                pool.query(opeart,function(err,data){
                    if(err){
                        rej(err);
                    }else {
                        res(data);
                    }
                })
            })

        })(this.pool,this.opeart);
        return p.then(callback) ;
    }


}

var tt = new MyOrm({
    // mysql连接信息
    connection: {
        host: '',
        port: 3306,
        user: '',
        password: '',
        database: ''
    },
});
tt.table('ak_user').find({userName: "nothing"}).skip(0).limit(12).then(data => console.log(data)).catch(err => console.log(err));

加强版。ok的

@leizongmin nodejs新手,快速写出来的,求指导

var _ = require('lodash');
var mysql = require('mysql');
var Promise = require('bluebird');
/**
 * 构造函数
 * @param options 选项配置
 * @constructor
 */
function MyORM(options){
  this.connection = mysql.createConnection(options.connection);
  Promise.promisifyAll(this.connection);
}

MyORM.prototype.table = function(name){
   return new Query(this,name);
}

MyORM.prototype.execSQL = function(sql){
    console.log(sql);
    return this.connection.queryAsync(sql);
}
/**
 * 构造函数
 * @param orm MyORM对象
 * @param tableName 表名
 * @constructor
 */
function Query(orm,tableName){
    if(!tableName) throw new Error('tableName required');
    this._orm = orm;
    this._tableName = tableName;
}
Query.prototype.find = function(query){
  this._op = "SELECT";
  this._where = parseParamsObject(query);
  return this;
}
Query.prototype.select = function(select){
    this._select=select;
    return this;
}
Query.prototype.skip = function(skip){
  this._skip = skip;
  return this;
}
Query.prototype.limit = function(limit){
    this._limit = limit;
    return this;
}
Query.prototype.findOne = function(query){
    this._op = "SELECT_ONE";
    this._where = parseParamsObject(query);
    return this;
}
Query.prototype.delete = function(query){
    this._op = "DELETE";
    this._where = parseParamsObject(query);
    return this;
}
Query.prototype.update = function(query,update){
    this._op = "UPDATE";
    this._where = parseParamsObject(query);
    this._update = update;
    return this;
}
Query.prototype.save = function(save){
    this._op = "INSERT";
    this._save = save;
    return this;
}
/**
 * 生成SQL语句并且执行
 * @private
 */
Query.prototype._doQuery = function(){
  var sql,op = this._op;
  if(op === 'SELECT'){
      var select = this._select || '*';
      sql = 'SELECT '+select+' from '+this._tableName;
      if(this._where){
          sql += ' where ' + this._where;
      }
      var limit='';
      if(this._skip){
          limit += this._skip;
      }
      if(this._limit){
          if(limit) limit += ','
          limit += this._limit;
      }
      if(limit){
          sql += ' limit ' + limit;
      }
  }else if(op === 'UPDATE'){
      sql = 'UPDATE ' +this._tableName + ' SET '+ mysql.escape(this._update);
      if(this._where){
          sql += ' where ' + this._where;
      }
      if(this._limit){
          sql += ' limit '+this._limit;
      }
  }else if(op === 'DELETE'){
      sql = 'DELETE from ' +this._tableName;
      if(this._where){
          sql += ' where ' + this._where;
      }
      if(this._limit){
          sql += ' limit '+this._limit;
      }
  }else if(op === 'SELECT_ONE'){
      var select = this._select || '*';
      sql = 'SELECT '+select+' from '+this._tableName;
      if(this._where){
          sql += ' where ' + this._where;
      }
      sql += ' limit 1';
  }else if(op === 'INSERT'){
      sql = 'INSERT INTO ' +this._tableName + ' SET '+ mysql.escape(this._save);
  }
  var retVal = this._orm.execSQL(sql);
  if(op === 'SELECT_ONE'){
      retVal = retVal.then(function(rs){
          return rs[0];
      });
  }
  return retVal;
}

Query.prototype.then = function(){
  var pro = this._doQuery();
  return pro.then.apply(pro,arguments);
}

//简单的测试:

var orm = new MyORM({
    // mysql连接信息
    connection: {host: '127.0.0.1', port: 3306, user: 'root', password: '', database: 'test'},
});
orm.table('user').find({name:'高端人士',age:1,$or:{height:1,weight:2}})
    .skip(1).limit(10).select('name,age')
    .then(function(rs){
        console.log(rs);
    }).catch(function(e){
        console.error(e);
    })
	//sql == > SELECT name,age from user where (name='测试' AND age=1 AND (height=1 OR weight=2)) limit 1,10

/**
 * 简单的根据对象参数生成SQL的where条件,$and表示sql的and,$or表示sql的or
 * @param obj
 * @param parseValue
 */
function parseParamsObject(obj,parseValue){
    if(!parseValue){
        parseValue = function(key,value){
            //如需要处理大于小于等其他操作符的情况,可以自行修改
            return key + '=' + mysql.escape(value);
        }
    }
    return parse('$and',obj,parseValue)
}

function joinCriteria(arr,join){
    if(!arr || arr.length === 0){
        return;
    }
    return '('+arr.join(' '+join+' ')+')';
}

function parseChild(key,obj,join,parseValue){
    var ret = [];
    if(_.isArray(obj)){
        obj.forEach(function(val){
            if(_.isPlainObject(val)){
                var v = parseParamsObject(val,parseValue);
                if(v != null){
                    ret.push(v);
                }
            }
        });
    }else if(_.isPlainObject(obj)){
        Object.keys(obj).forEach(function(key){
            var val = obj[key];
            var v = parse(key,val,parseValue);
            if(v != null){
                ret.push(v);
            }
        });
    }
    return joinCriteria(ret,join);
}

function parse(key,val,parseValue){
    if(key === '$and'){
        return parseChild(key,val,'AND',parseValue);
    }
    if(key === '$or'){
        return parseChild(key,val,'OR',parseValue);
    }
    if(_.isPlainObject(val) && _.includes(Object.keys(val),'$or')){
        var vs = val['$or'];
        var temp = [];
        if(_.isArray(vs)){
            vs.forEach(function(ele){
                var v = parseValue(key,ele);
                if(v != null){
                    temp.push(v);
                }
            });
        }
        return joinCriteria(temp,'OR');
    }
    return parseValue(key,val);
}

嘿嘿嘿,我也来一发~~ MySQL orm ← 话说ORM不是要建模么 底层使用Promise封装,每次操作从连接池拿连接对象,开始所有数据库操作(也就是调用.table(‘table_name’)时)每次返回一个新的操作实例 _query 对象,这样就可以实现多个查询并行执行了~~ .目前的问题是当有非常多个操作发起时会生成多个操作实例,感觉上有点占内存。。。 ,每次调用前做了一次检查,每次查询必须首先调用table()指定表,然后必须调用CRUD操作方法 find();delete();update();findOne() 指定此次的操作 , 最后选择性调用skip(); limit(); 方法做结果的筛选。实际测试完全通过。 嘛如有不足还请一定提出指正。

var mysql = require('mysql');
function myOrm(connection){
	connection = connection || {};
	this._host = connection.host || '127.0.0.1';
    this._port = connection.port || 3306;
    this._user = connection.user || 'wanlf';
    this._password = connection.password || 'wlf112111';
    this._database = connection.database || 'VoidMirage';
    this._result = null;
    this._method = 'query';
    //connect
    this.pool = mysql.createPool({
 		 host     : this._host,
  		 user     : this._user,
  		 password : this._password,
  		 database : this._database
		});
    console.log('initialize successful.database connected...');
}
//use _query class to proxy every query function so as to make every query operation execute individualy.
function _query(table,pool){
	this._table = table;
	this.pool = pool;
}
_query.prototype.find = function(opt){
	console.log('find here')
	if(!this._table)
	{
		throw new Error('can not query data without a table set up.invoke table function first!');
	}
	this._sql = `select * from ${this._table} where 1=1 `;
	this._opt = opt;
	return this;
}
myOrm.prototype.table = function(table){
	return new _query(table,this.pool)
}
_query.prototype.skip = function(skip_num){
	console.log('skip here')
	if(!this._sql)
	{
		throw new Error('can not skip data from empty result,find it first!');
	}
	this._skip_num = skip_num;
	return this;
}
//one of the last function to be called
_query.prototype.limit = function(limit_num){
	console.log('limit here')
	if(!this._sql)
	{
		throw new Error('can not skip data from empty result,find it first!');
	}
	return new Promise((res,rej)=>{
		this.pool.getConnection((err,conn)=>{
			if(err)
			{
				rej(err);
			}
			else
			{
				if(this._opt)
				{
					var op = Object.keys(this._opt)
					for(var key of op)
					{
						this._sql += `and ${key}=${this._opt[key]} `
					}
				}
				if(this._skip_num)
				{
					this._sql += `limit ${this._skip_num} offset ${limit_num} `;
				}
				console.log('about query for '+this._table)
				console.log(this._sql)
				conn.query(this._sql,(err,result)=>{
					if(err)
					{
						rej(err);
					}
					else
					{
						res(result);
					}
					_clean.call(this);
					conn.release();
				});
			}
		});
	});
}
//one of the last function to be called
_query.prototype.exec = function(){
	if(!this._table || !this._sql)
	{
		throw new Error('can not query data before the table and the queryinfo is set up.');
	}
	return new Promise((res,rej)=>{
		this.pool.getConnection((err,conn)=>{
			if(err)
			{
				rej(err);
			}
			else
			{
				if(this._upt)
				{
					var up = Object.keys(this._upt)
					for(var key of up)
					{
						this._sql += ` ${key}='${this._upt[key]}' `;
						this._sql +=',';
					}
					if(this._sql.endsWith(','))
					{
						this._sql = this._sql.substring(this._sql , this._sql.length - 1);
					}
					this._sql +='where 1=1'
				}
				if(this._opt)
				{
					var op = Object.keys(this._opt)
					for(var key of op)
					{
						this._sql += ` and ${key}='${this._opt[key]}' `
					}
				}
				if(this._skip_num)
				{
					this._sql += `limit ${this._skip_num}`;
				}
				if(this._take_num)
				{
					this._sql +=`offset ${this._take_num}`;
				}
				console.log(this._sql);
				conn.query(this._sql,(err,result)=>{
					if(err)
					{
						rej(err);
				    }
					else
					{
						res(result);
					}
					_clean.call(this);
					conn.release();
				});
			}
		});
	});
}
_query.prototype.take = function(take_num){
	this._take_num = take_num;
	return this;
}
_query.prototype.delete = function(opt){
	if(!this._table)
	{
		throw new Error('can not delete data from a unknown table! call table function first!')
	}
	this._method = 'delete';
	this._opt = opt;
	this._sql = `delete from ${this._table} where 1=1`;
	this._skip_num = null;
	this._take_num = null;
	return this;
}
_query.prototype.update = function(opt,upt){
	if(!this._table)
	{
		throw new Error('can not update data from a unknown table! call table function first!')
	}
	this._sql = `update ${this._table} set `
	this._method = 'update';
	this._skip_num = null;
	this._take_num = null;
	this._opt = opt;
	this._upt = upt;
	return this;
}
_query.prototype.findOne = function(opt){
	if(!this._table)
	{
		throw new Error('can not find data from a unknown table! call table function first!')
	}
	this._opt = opt;
	this._sql = `select * from ${this._table} where 1=1 `
	this._upt = null;
	this._skip_num = 1;
	this._take_num = null;
	return this;
}
function _clean(){
	this._table = null;
	this._sql = null;
    this._opt = null;
	this._upt = null;
	this._skip_num = null;
	this._take_num = null;
}
module.exports.myOrm = myOrm;

如有不足还请指正。@leizongmin

留名关注,真是好问题!

这应该是10K的面试题

学习一下大牛!

回到顶部