标签归档:Node

Node.js 异步编程

我写的东西 编程相关 Node posts

异步的最终解决方案

在 2017 年,Node 7.6 终于支持了 Async/Await ,async 函数就是 Generator 函数的语法糖,是 JS 异步编程的最终解决方案。

可以认为:

async 函数 == co + generator 函数

比起 co,async 有以下优点

  1. 更好的语义 async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果;
  2. 更广的适用性。 co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)

在此之前,JS 的异步编程经历了 Callback、Promise、Generator、Async 的进化,接下来我们过一遍异步发展历程

回调函数 Callback

在 JS 中,异步编程通过 Callback 完成,将一个函数作为另一个异步函数的参数,用于处理异步结果,一个例子:

Something.save(function(err) {
if (err) {
//error handling
return; // 没有返回值
}
console.log('success');
});

过度使用回调函数所会遇到的挑战:

  • 如果不能合理的组织代码,非常容易造成回调地狱(callback hell),这会使得你的代码很难被别人所理解。
  • 很容易遗漏错误处理代码。
  • 无法使用return语句返回值,并且也不能使用throw关键字

也正是基于这些原因,在JavaScript世界中,一直都在寻找着能够让异步JavaScript开发变得更简单的可行的方案。

一个可行的解决方案之一是async模块。如果你和回调函数打过很久的交道, 你也许会深刻地感受到,在JavaScript中如果想要让某些事并行执行,或是串行执行,甚至是使用异步函数来映射(mapping) 数组中的元素使用异步函数有多复杂。所以,感谢 Caolan McMahon写了async模块来解决这些问题。

使用async模块,你可以轻松地以下面这种方式编写代码:

async.map([1, 2, 3], AsyncSquaringLibrary.square,
function(err, result){
// result will be [1, 4, 9]
});

async模块虽然一定程度上带来了便利,但仍然不够简单,代码也不容易阅读,因此Promise出现了。

Promise 函数

Promise 的写法:

Something.save()
.then(function() {
console.log('success');
})
.catch(function() {
//error handling
})

then 和 catch 注册的回调函数分别处理下一步处理和异常处理,这样写的优点是可以链式操作:

saveSomething()
.then(updateOtherthing)
.then(deleteStuff)
.then(logResults);

只是回调函数的另一种写法,把回调函数的横向加载,改成纵向加载,缺点是代码一堆的 then。

Generator 函数

Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权,注意它不是语法糖。

第一步,协程A开始执行。
第二步,协程A执行到一半,进入暂停,执行权转移到协程B。
第三步,(一段时间后)协程B交还执行权。
第四步,协程A恢复执行。
function* gen(x){
var y = yield x + 2;
return y;
}

上面代码就是一个 Generator 函数。它不同于普通函数,是可以暂停执行的,所以函数名之前要加星号,以示区别。

整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield 语句注明。Generator 函数的执行方法如下。

var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }

Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换错误处理机制

  1. 数据交换:g.next(data);

  2. 错误处理:g.throw('出错了');

Generator 最大的问题是要手动调用 next() 才会执行下一步,因此自动执行器 co 出现了

co 执行器

co 函数库的用法:

var co = require('co');
co(gen);

Generator 自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权
两种方法可以做到这一点。

(1)回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
(2)Promise 对象。将异步操作包装成 Promise对象,用 then 方法交回执行权。

co 函数的具体实现见文末参考文章,这里就不重复了

拓展阅读:其他语言的异步编程

C# 也有 async await 关键字,用于异步调用,内部实现基于线程
http://www.cnblogs.com/jesse2013/p/async-and-await.html

Java Spring 框架有 @Async 注解,用于异步调用,内部实现基于线程
https://spring.io/guides/gs/async-method/

参考文章

  1. 细说JavaScript异步函数发展历程
  2. async 函数的含义和用法
  3. Generator 函数的含义与用法
  4. co 函数库的含义和用法

Node.js 内存模型

我写的东西 编程相关 Node posts 编程相关

前言

本文尝试理清 js 内存模型的相关知识点,鉴于 js 的教程非常丰富,这里就不重复写了,只建立个知识索引就好了,详细知识看文末的参考文章即可

栈与堆

基础数据类型存在栈中,对象存储在堆中

  1. 基础数据类型

    • Undefined
    • Null
    • Boolean
    • Number
    • String
  2. 引用类型
    Object、Function、Array和自定义的对象,可以看做是指针。指针是存在栈中,但是指向的变量在堆中

下面代码表现了基础类型和引用类型的区别

// demo01.js
var a = 20;
var b = a;
b = 30;
// 这时 a 的值是多少? // 20
// demo02.js
var m = { a: 10, b: 20 }
var n = m;
n.a = 15;
// 这时 m.a 的值是多少? // 15

执行上下文

概念

每次当控制器转到ECMAScript可执行代码的时候,就会进入到一个执行上下文。可执行代码的类型包括:

  • 全局代码(Global code)
    这种类型的代码是在”程序”级处理的:例如加载外部的js文件或者本地标签内的代码。全局代码不包括任何function体内的代码。 这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境。
  • 函数代码(Function code)
  • Eval代码(Eval code)

执行栈 demo

建立的细节

1、创建阶段【当函数被调用,但未执行任何其内部代码之前】

  • 创建作用域链(Scope Chain)

  • 创建变量,函数和参数。

  • 求”this“的值

2、执行阶段
初始化变量的值和函数的引用,解释/执行代码。

我们可以将每个执行上下文抽象为一个对象,这个对象具有三个属性

ECObj: {
scopeChain: { /* 变量对象(variableObject)+ 所有父级执行上下文的变量对象*/ },
variableObject: { /*函数 arguments/参数,内部变量和函数声明 */ },
this: {}
}

变量对象

变量对象(Variable object)是说JS的执行上下文中都有个对象用来存放执行上下文中可被访问但是不能被delete的函数标示符、形参、变量声明等。它们会被挂在这个对象上。

代码示例

var color = 'blue';
function changeColor() {
var anotherColor = 'red';
function swapColors() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
}
swapColors();
}
changeColor();

闭包概念

MDN 对闭包的定义为:

闭包是指那些能够访问自由变量的函数。

那什么是自由变量呢?

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

由此,我们可以看出闭包共有两部分组成:

闭包 = 函数 + 函数能够访问的自由变量

举个例子:

var a = 1;
function foo() {
console.log(a);
}
foo();

foo 函数可以访问变量 a,但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,所以 a 就是自由变量。

那么,函数 foo + foo 函数访问的自由变量 a 就构成了一个闭包

js 不会销毁被闭包引用的对象

GC 垃圾回收

Garbage Collection 垃圾回收是一种自动的内存管理机制。当一个电脑上的动态内存不再需要时,就应该予以释放,以让出内存,这种内存资源管理,称为垃圾回收。

新生代和老生代内存分区

为什么要分区?为了 GC 效率

新生代的 GC 算法

Scavenge 算法,它将堆内存一分为二,将存活对象在从空间 1 复制到空间 2,其他对象被回收。特点是速度快。新生代内存的对象过大或者存活时间过长就会去到老生代内存。

老生代的 GC 算法

Mark-Sweep 标记清除算法,标记清除回收之后,内存会变得碎片化。
Mark-Compact 标记整理算法,在整理过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。

内存泄露

本质上,内存泄漏可以定义为:应用程序不再需要占用内存的时候,由于某些原因,内存没有被操作系统或可用内存池回收。编程语言管理内存的方式各不相同。只有开发者最清楚哪些内存不需要了,操作系统可以回收。一些编程语言提供了语言特性,可以帮助开发者做此类事情。另一些则寄希望于开发者对内存是否需要清晰明了。

排除方法

  1. 抓下内存快照,使用 chrome 分析,使用框架和各种库的时候干扰项非常多
  2. alinode

参考文章

重要

  1. 深入理解闭包(五)——作用域、作用域链和执行上下文
  2. 深入理解JavaScript闭包【译】
  3. 深入理解JavaScript执行上下文、函数堆栈、提升的概念
  4. MDN 函数
  5. JavaScript深入之闭包
  6. 轻松排查线上Node内存泄漏问题
  7. 4类 JavaScript 内存泄漏及如何避免 介绍了如何使用 chrome dev tool 排查内存泄露

不重要

  1. 解读 V8 GC Log(二): 堆内外内存的划分与 GC 算法
    1. Node 性能优化
  2. 解读 V8 GC Log(一): Node.js 应用背景与 GC 基础知识
  3. NodeJS中被忽略的内存
  4. 前端基础进阶(一):内存空间详细图解
  5. 前端基础进阶(二):执行上下文详细图解
  6. JavaScript 内存模型

Swagger

什么是 Swagger

官网的解释:

Swagger is the world’s largest framework of API developer tools for
the OpenAPI Specification(OAS), enabling development across the entire
API lifecycle, from design and documentation, to test and deployment.

Swagger 提供了对 API 的设计、文档、测试、部署等阶段的支持。
Web 开发的本质是 API 开发,Swagger 使用 swagger.json/yaml 文件来定义 API,语法基于 OpenAPI Specification(OAS) 规范。

OpenAPI 语法入门:https://www.gitbook.com/book/huangwenchao/swagger/details

一个在线的 demo: https://editor.swagger.io//?_ga=2.267808907.403623983.1505817850-1482371638.1504594291#/

Swagger 可以做什么

围绕这个 API 定义,Swagger 生态提供了以下主要的服务:

  1. API 编辑:Swagger Editor
  2. API 文档:Swagger UI
  3. Mock 服务:Easy Mock
  4. 代码生成器:Swagger Codegen

Swagger Hub 全家桶对上述功能提供了整套服务打包。
理论上是可用,但是 mock 和 codegen 服务不够灵活。

接下来重点介绍 Mock 和 代码生成器这两个部分的内容。

Mock 服务

Swagger Hub 的 Auto Mock

Swagger Hub 提供的 Mock 服务, 只能区分 String 和 Number,不能提供更灵活的 Mock。

Easy Mock

Easy Mock 是一个可视化,并且能快速生成 模拟数据 的持久化服务,支持生成随机的文本、数字、布尔值、日期、邮箱、链接、图片、颜色等,支持扩展更多数据类型,支持自定义函数和正则。

重点是它可以导入 Swagger 配置,

Codegen

Swagger Hub 的 Codegen

官方宣称提供几十个 server 端和 client 端的代码生成服务,但是实际上用处非常有限,以 nodejs-server 为例,这个模板使用 connect 框架,使用 express 和 koa 框架的就不适用了,而且编码风格差距也非常大。

  • 官方支持的 Codegen 模板:

  • nodejs-server 模板生成的代码:

自定义模板

在代码生成器这一块,往往是半自动的工具更加灵活,自定义模板,根据 swagger.json 生成填充值,一键生成重复内容。
在 Node 这一块,半自动生成器做得比较好的是 swagger-js-codegen
这个工具使用 mustache 模板工具,还提供了 gulp 工具。

根据这个思路我们可以自定义好模板,然后一键生成代码。

npm 上的 Swagger 工具

  1. 根据 Swagger 生成 Joi https://github.com/zaaack/koa-joi-swagger
  2. 根据 Swagger 生成 Mongoose https://www.npmjs.com/package/swagger-mongoose

番外

apidoc to swagge

使用 apidoc-swagger 把现在的 apidock 转为 swagger
https://www.npmjs.com/package/apidoc-swagger

参考

http://blog.just4fun.site/swagger-note.html

Typescript 与 Mongoose 的最佳实践

前言

mongoose 是 node.js 里操作 mongodb 的最好的 orm 工具。
typescript 则是带了 type 的 js 超集。
在开发过程中经常会碰到写错字段名的问题,只有到了运行阶段才能发现(或许也没发现。。。),使用 typescript 可以达到以下目的:

  1. 智能提示,不会输错字段名啦,当然这个取决于你的 IDE 是否支持 typescript。
  2. 类型检查,错误的类型定义可以在编译期发现。

接下来就看看这两个东西怎么配合吧。

完整的 Express + Typescript + Mongoose 的 Demo 可以参考之前的文章 :使用 typescript 做后端应用开发

安装准备

  1. typescript
    typescript 的安装配置这里不赘述,可以看 typescript 的官方文档。

  2. mongoose

下载 mongoose 的类型定义文件

$ npm install mongoose --save
$ npm install @types/mongoose --save-dev
$ npm install @types/mongodb --save-dev

typescript 的 tsd 的组织方式一直在变化,目前的方式算是比较简单的,typescript 官方把常见的库 Definition 都放在 npm 上了。

Model 的定义

以一个 User Model 为例。

先定义一个接口

src/interfaces/User.ts:

export interface IUser {
createdAt?: Date;
email?: string;
firstName?: string;
lastName?: string;
}

Schema + Model

src/model/User.ts:

import {Document, Schema, Model, model} from "mongoose";
import {IUser} from "../interfaces/User";

export interface IUserModel extends IUser, Document {
fullName(): string;
}

export const UserSchema: Schema = new Schema({
createdAt: Date,
email: String,
firstName: {type: String, required: true},
lastName: {type: String, required: true}
});
UserSchema.pre("save", function (next) {
let now = new Date();
if (!this.createdAt) {
this.createdAt = now;
}
next();
});
UserSchema.methods.fullName = function (): string {
return (this.firstName.trim() + " " + this.lastName.trim());
};

export const User: Model<IUserModel> = model<IUserModel>("User", UserSchema);

Mongoose 的 tsd 中是使用泛型来实现对具体 Model 的定义的,我们自己定义的 Model 是 IUser(包含数据库字段),Mongoose 提供的基础的 Model 定义是 Document(包括 find findOne 等操作) ,继承这个两个接口就是最后的 Model 啦。

有个比较尴尬的地方是:Mongoose 的 Schema 定义和 IUser 的定义是非常相似却又不是同一个东西,你需要写两遍属性定义。而且应该会经常改了一处忘了另一处。

这里提供一个解决方案是:用代码生成器根据 Schema 来生成 Interface 。
只提供思路,不给实现。

RUN 起来

引入 User Model。
src/app.js:

import * as mongoose from 'mongoose'
// mongoose.Promise = global.Promise;
import {User} from './model/User'
mongoose.connect('mongodb://localhost:57017/test');

const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', async function () {
console.log('we're connected!');
try {
await User.remove({})
const user = new User({firstName: 'feng', lastName: 'nick', email: 'jfeng@kalengo.com'})
await user.save()
const fuser = await User.findOne({})
console.log('email', user.email);
console.log('email err', user.email2);
console.log('email', fuser.email);
console.log('fullName', fuser.fullName());
console.log('lastName', fuser.lastName);
console.log('createdAt', fuser.createdAt);
} catch (err) {
console.log('err ', err.stack);
}
});

一切正常。

得到了什么

智能提示

IDE 会根据 IUser 的结构来智能提示,终于不用怕很长的字段名啦

类型检查

属性不存在

类型不匹配

OK, 这样就实现文章开头提到的目的啦。

源码

https://github.com/myfjdthink/typescript-mongoose

参考文章

TypeScript: Declaring Mongoose Schema + Model
mongoose 文档

Node 后端开发的几种测试方式

后端开发中,只需要针对接口(无 GUI)做自动化测试,还是比较容易实现的,所以在开发过程中尽量写好测试。在这里总结下我在工作中用到的测试方法。这里以 Nodejs 的后端开发为例。

单元测试

单元测试的重要性不言而喻,算是业界共识了。
在有完善的单元测试的基础上,可以很方便地做到持续集成。
提到单元测试,大家很容易想到 TDD,我个人认为 TDD 并不是一件很容易做到的事情,特别是小团队需求多变的时候,往往会追求先出功能后补测试。

集成测试

考拉里的集成测试一般是通过一个接口对 N 个方法进行集成,这个接口会调用 N 个方法。这种集成测试是对单元测试的一种补充。
因为集成测试的粒度比较大,在做测试的时候往往会碰到个很麻烦的问题:怎么准备测试数据?

准备测试数据

测试数据存储位置
使用 js 来存储数据,相对于 json 来说 js 可以编程,例如需要定义一个日期是昨天,json 是做不到的。

在测试开始前载入测试数据

准备测试数据是件比较繁琐的事情,建议直接从生产环境拉取用户数据。脱敏后作为测试数据用。

写个脚本去线上拉取数据

GenTextUserData.test.js
关键点:
mongodump with query
tosource

平行测试

重构用,新写一个方法,新旧方法一起执行,判断结果是否一致。把不一致的结果收集起来。
效果卓群,可以发现很多意想不到的 BUG。
但是比较使用范围有限,只能测试只读的方法。

灰度测试

最实用,成本很低的方案。
灰度发布,只有部分人可以使用新功能,观察一段时间。

全量覆盖测试

使用大量真实数据来测试,真实数据可以提供足够的样本。
例如开发一个新功能,把所有用户的数据 load 进来,模拟使用这个新功能,看看是否有报错。
但是这个做法成本毕竟高,只有重要的功能才需要这样做。
而且用户数据大于百万级别时,整个测试耗时将非常漫长,可以考虑过滤部分用户把量控制在10万以内。

人工测试

在测试环境,作为用户,体验一遍此次开发的功能,看看前面的测试是否有遗漏。
辅助工具
HDC
POSTMAN

flow type 体验

flow type 是由 Facebook 出品的类型检查工具。
最近在一个新的后端项目上体验flow,也在内部做了分享,在这里再次总结下 flow 的优缺点。
优点:
  • 类型检验,能提前发现一些问题
  • 相对于 typescript ,flow 像 babel 一样提供了一个 runtime,可以直接运行flow 代码
还欠缺的地方:
  • webstorm 对 flow 支持并不完善,不能做到像 typescript 那样做到代码补全,而且 webstorm 执行 flow 检查会卡顿
  • 和 mongoose 无法有效配合,目前 flow 对第三方库的支持甚少
  • 对 commonjs 支持太差,require 第三方包会报错,暂时未找到解决方案,估计是我没用对。

NodeJS 中基于 Redis 的分布式锁解决方案。

NodeJS 中基于 Redis 的分布式锁解决方案。

前言0

在分布式系统中,会经常碰到两个问题:

  • 互斥性问题。
  • 幂等性问题。

分布式锁

互斥性问题的解决方案是 – 分布式锁;
幂等性问题,分布式环境中,有些接口是天然保证幂等性的,如查询操作。其他情况下,所有涉及对数据的修改、状态的变更就都有必要防止重复性操作的发生。通过间接的实现接口的幂等性来防止重复操作所带来的影响,成为了一种有效的解决方案。这个间接方案也可以使用分布式锁来实现。

分布式锁条件

基本的条件:

  • 需要有存储锁的空间,并且锁的空间是可以访问到的。
  • 锁需要被唯一标识。
  • 锁要有至少两种状态。

使用 Redis 完全可以满足上述条件。

解决方案

简单的解决方案

通常在 Node 中一个简单的基于 Redis 的解决方案是这样的:
发送 SETNX lock.orderid 尝试获得锁。
如果锁不存在,则 set 并获得锁。
如果锁存在,则跳过此次操作或者等待一下再重试。

SETNX 是一个原子操作,可以保证只有一个节点会拿到锁。

Redis 推荐的方案

上述的方案中,还是有点问题,在 Redis 出现单点故障,例如 master 节点宕机了,而 Redis 的复制是异步的,可能出现以下情况:

  1. 客户端 A 在 master 节点拿到了锁。
  2. master 节点在把 A 创建的 key 写入 slave 之前宕机了。
  3. slave 变成了 master 节点 4.B 也得到了和 A 还持有的相同的锁(因为原来的 slave 里还没有 A 持有锁的信息)

所以为了避免上述问题,Redis 官方给出了更加可靠的实现 Distributed locks with Redis
中译文版本 用 Redis 构建分布式锁,这个方案提出了一个Redlock 算法,文章里有详细解析,这里就不赘述了。

Redlock 的实现

Redlock 有针对不同语言的实现,NodeJS 的实现是node-redlock
官网给出的一个例子:

// the string identifier for the resource you want to lock
var resource = 'locks:account:322456';

// the maximum amount of time you want the resource locked,
// keeping in mind that you can extend the lock up until
// the point when it expires
var ttl = 1000;
redlock.lock(resource, ttl, function(err, lock) {
// we failed to lock the resource
if(err) {
// ...
}
// we have the lock
else {
// ...do something here...
// unlock your resource when you are done
lock.unlock(function(err) {
// we weren't able to reach redis; your lock will eventually
// expire, but you probably want to log this error
console.error(err);
});
}
});

使用起来还是比较简单的,在操作开始之前 lock 并设置 ttl,操作完成后 unlock 即可。

包装一下 redlock

如果要给某些个接口或者 function 添加一个 lock 的话,直接修改接口是件很麻烦的事情,所以我们可以把 redlock 稍微封装一下。

/**
* 锁操作
* @param user_id
* @param operate
* @param process
* @param callback
* @param options
*/

lockProcess: function (user_id, operate, process, callback, options) {
var lock = null
Thenjs(function (cont) {
var ttl = options ? options.ttl : null;
self.lock(user_id, operate, ttl, function (err, clock) {
// 加锁失败直接退出
if (err) return callback(err);
lock = clock
cont()
});
}).then(function (cont) {
// 实际的业务逻辑
process(cont);
}).then(function (cont, result) {
self.unlock(lock, function (err) {
cont(err, result)
});
}).then(function (cont, result) {
callback(null, result)
}).fail(function (cont, err) {
// 操作失败记得解锁
self.unlock(lock, function (unlockErr) {
callback(err || unlockErr)
});
});
}

这样我们可以直接把之前的代码包装一下就能用,就不需要侵入之前的业务逻辑了。

LockService.lockProcess(order.user_id, 'dsPurchase', function (innerCb) {
self.dsPurchase(order, innerCb); // 业务逻辑
}, function (err, result) {
callback(err, result);
});

假设你要针对一个接口加锁的话,还可以新建一个过滤器,然后给接口配置了过滤器就自动加上锁啦。如果是使用 express 的话,可以参考如何修改sails 的 response来设计过滤器,在 response 的时候解锁即可。

参考

node-redlock
Distributed locks with Redis
用 Redis 构建分布式锁
分布式系统互斥性与幂等性问题的分析与解决

记录一个 NPM 包冲突的问题

记录一个 NPM 包冲突的问题

问题

项目中使用 sails 这个 Web 框架,sails 集成的 orm 是 waterline ,waterline 针对 mongo 的适配器是 sails-mongo,最近线上爆了一个 BUG,sails-mongo 在执行 native 方法时报错 :

TypeError: Argument must be a string
at TypeError (native)
at Buffer.write (buffer.js:791:21)
at serializeObjectId (node_modules/sails-mongo/node_modules/bson/lib/bson/parser/serializer.js:242:10)
at serializeInto (node_modules/sails-mongo/node_modules/bson/lib/bson/parser/serializer.js:699:17)
at serialize (node_modules/sails-mongo/node_modules/bson/lib/bson/bson.js:47:27)
at Query.toBin (node_modules/sails-mongo/node_modules/mongodb-core/lib/connection/commands.js:143:25)
at Cursor._find (node_modules/sails-mongo/node_modules/mongodb-core/lib/cursor.js:339:36)
at proto.(anonymous function) (node_modules/sails-mongo/node_modules/mongodb/lib/apm.js:547:16)
at proto.(anonymous function) (node_modules/sails-mongo/node_modules/mongodb/lib/apm.js:547:16)
at proto.(anonymous function) (node_modules/sails-mongo/node_modules/mongodb/lib/apm.js:547:16)
at proto.(anonymous function) (node_modules/sails-mongo/node_modules/mongodb/lib/apm.js:547:16)
at proto.(anonymous function) (node_modules/sails-mongo/node_modules/mongodb/lib/apm.js:547:16)
at proto.(anonymous function) (node_modules/sails-mongo/node_modules/mongodb/lib/apm.js:547:16)
at proto.(anonymous function) [as _find] (node_modules/sails-mongo/node_modules/mongodb/lib/apm.js:547:16)
……

解决问题

针对这个问题 Google 了一下,已经有人发现了这个问题并在 sails-mongo 的 github 项目里提了 issue 了, https://github.com/balderdashy/sails-mongo/issues/415
查看这个 issue 可以知道,问题原因是项目中同时使用了 sails-mongo 和 mongo 两个包,在 sails-mong 的版本为 0.12.0 的情况下, mongodb 这个包升级到 2.2.0 以上就会导致报错。
issue 里给出的解决方案是:

分析

OK, 问题解决后分析下两个包出现冲突的原因。
业务代码是这样写的:

var ObjectId = require('mongodb').ObjectID;

...

query.push({
$match: {
$or: _.map(posts, function(post) {
return {
_id: { $eq: ObjectId(post.id) },
status: { $ne: 'trash' }
}
})
}
});

这里把 mongodb(2.2.x) 的 ObjectId 对象传给了 sails-mongo 的处理逻辑中,导致了最终的报错。
sails-mongo 里也引用了 mongodb 这个库,版本是 2.1.3

这个版本与外部的 mongodb 库 2.2.x 版本有冲突,看来 mongodb 这个库在 2.2.x 升级了某些东西,我们进一步追查,根据报错信息可以定位报错的代码

at serializeObjectId
(node_modules/sails-mongo/node_modules/bson/lib/bson/parser/serializer.js:242:10)

看来是 bson 这个库里有问题,这个是 sails-mongo 里的 bson(0.4.23) 的代码的报错位置

在 242 行加上断点,可以看到 value 这个变量的情况:

而 mongodb 里引用的 bson(0.5.6) 的此处代码是这样的

加断点看到的变量情况:

看来是 ObjectID 的类型变了, 由之前的 String 变成了 Uint8Array ,这也就契合了报错信息里的 “TypeError: Argument must be a string”, 这就是问题的根源。

总结

这个问题初看好像是 npm 包版本导致的冲突(NPM 其实已经处理好了不同版本的依赖问题),但是实际上是代码应用层使用不当导致的问题,根据 issue 提供的解决方案,要获取 ObjectID 对象,应该使用 sails-mongo 来提供这个对象:

const ObjectId = require('sails-mongo').mongo.objectId

而不是从外部获取:

var ObjectId = require('mongodb').ObjectID;

其实是不是可以说是 sails-mogno 坑?


11/18日更新:
哈哈哈, sails-mogno 果然是坑,看看它提供的 objectId 的实现:

如果你想要生成一个新的 ObjectID,这样写的话

var id = ObejctId()

你将得到一个 null !
无语呢,所以呢,还是要这样引用才行:

var ObjectID = require('sails-mongo/node_modules/mongodb').ObjectID;

使用 Winston 处理 Node 应用 Log

使用 Winston 处理 Node 应用 Log

问题

随着 Node 应用节点增多,应用部署在多台机器上,查询 Node 的 log 变得非常麻烦。业内通常的解决方案是使用 ELK 套件来收集并处理 log,可以方便地查询。关于 ELK,可以看看这篇文章:https://www.ibm.com/developerworks/cn/opensource/os-cn-elk/,本文不作介绍~。
在推动使用 ELK 的过程中碰到一些问题:

  1. 之前的代码中很多地方直接是使用 console.log 输出 log 的。这些 log 之前是依赖于 pm2 处理并写入到文件中。
  2. 如果 log 一个 object 对象的话,默认的输出效果是多行的,ELK 在收集会比较麻烦。

amount: 23, products: [ { product_id: ‘55c31e936f227ed922c508aa’,
num: 23,
rebuyType: 1,
price: 1 } ],

初步的解决方案

问题1:全局搜索并替换 console.log 的内容,使用新的 log 工具是可以的。
问题2:ELK 中的 Logstash 提供了一个多行文件的过滤器,https://www.elastic.co/guide/en/logstash/5.0/plugins-filters-multiline.html ,这个过滤器可以把多行 log 合并为一条记录,但是它要求使用正则来判断哪些多行 log 是属于一行的内容,这是个难点。

最终解决方案

上述的方案实施起来都不怎么好看,可以考虑使用 Winston 这个Node 的 log 库。

引入 Winston

WinstonLog.js


var winston = require('winston');
var customLogger = new winston.Logger();

// A console transport logging debug and above.
customLogger.add(winston.transports.Console, {
timestamp: function() {
return Date.now();
},
level: 'verbose',
colorize: true
});

// A file based transport logging only errors formatted as json.
customLogger.add(winston.transports.File, {
level: 'verbose',
filename: 'winston.log',
json: true
});
module.exports = customLogger

这是个 Winston 配置的 demo,要找到合适你项目的 Winston 的配置方式可以查看 Winston 文档。

回到这个配置文件,重点在 winston.transports.File 的配置:
json: true
这样写入 log 文件里的内容都是 json 对象了,winston.log 的部分内容是这样的:

{“level”:”info”,”message”:”getRateMap”,”timestamp”:”2016-11-16T04:31:55.882Z”}
{“level”:”info”,”message”:”defaultRateUp get
map”,”timestamp”:”2016-11-16T04:31:55.883Z”}
{“level”:”info”,”message”:”each products { quota: 20000,n name:
”,n term: 1,n type: 2,n description: ”,n id:
‘549922452238c54e98b750bc’,n rate_year: null,n product_id:
‘549922452238c54e98b750bc’,n _id: ‘549922452238c54e98b750bc’,n
rate: null,n rate_up: 0 }”,”timestamp”:”2016-11-16T04:31:55.884Z”}

可以看到,每一行都是一个 json,结构是:

{
“level” : “”,
“message” : “****
}

这样ELK 在收集 log 就会很方便了。

而配置中 winston.transports.Console 里是没有 json: true 的配置项的,所以我们在命令行里看到的 log 还是换行的,比较适合阅读。

替换项目中的 log

console.log

搞定了log 文件格式的问题,我们还需要把项目里的 console.log 替换为 Winston 的 log。

可以在应用的启动位置 app.js 添加如下内容:

var winston = require('./api/lib/WinstonLog')
console.log = winston.info
console.error = winston.error

这样就把 console.log 的实现替换了。

web 框架默认的 log

因为这边使用的是 sails 这个 web 框架,所以这里讲一下 sails 里如何替换 log 的实现。
sails 默认集成的 log 库是 captains-log ,那如何替换成 Winston 呢?sails 的官方文档其实已经给了方法了~~~, 见http://sailsjs.org/documentation/reference/configuration/sails-config-log

修改 log 配置

// config/log.js
var customLogger = require('../api/lib/WinstonLog')
module.exports.log = {
// Pass in our custom logger, and pass all log levels through.
custom: customLogger,
level: 'verbose',

// Disable captain's log so it doesn't prefix or stringify our meta data.
inspect: false
};

用刚刚引入的 Winston 对象注入到 sails 的 log 配置里就可以啦。

用 sails、jade 和 angular 写个 CRUD

用 sails、jade 和 angular 写个 CRUD

前言

在做运营活动的时候,要提供一个管理界面给运营使用,经常要用到一些 CRUD 的功能,所以就想着写个通用的模板,目标是:开发填好一个 model 的配置,一个管理页面就出来了。极大地提高生产力。

怎么配置

  1. 新建一个 sails 的 model,就叫 Foo.js 按照规则定义好每个一个字段。见 Demo
  2. 新建一个与 model 名字对应 sails 的 controller: FooController

    你也可以使用 sails 的生成器命令 sails generate api Foo 这样可以一次生成 Model 和 Controller 文件。

  3. 访问 http://localhost:8080/foo/crud,一个管理界面就出来。访问 http://localhost:8080/foo/crud_s 则是另一种交互风格的管理页面。

  4. 长这样

原理

后端使用 jade 模板来渲染 html,访问 http://localhost:8080/foo/crud 其实就是访问 crud.jade 这个 view 文件。后端读取了对应 model 的配置,并把属性配置注入到 jade 模板,然后生成对应的 table,和编辑框。

前端 html 的数据绑定工作是借助 angular 来实现的。

一个 Model 的Demo

/**
* 电商化产品
*/

module.exports = {
title: '电商产品',
sort: 'no',
attributes: {
no: {type: 'string', name: '编号', required: true},
name: {type: 'string', name: '名字', required: true},
name_detail: {type: 'string', name: '详细名字', required: true},
ram: {type: 'integer', name: '内存'},
icon_src: {type: 'string', image: true, name: '缩略图', required: true},
list_src: {type: 'string', image: true, name: '列表图', required: true},
detail_src: {
type: 'array', name: '细节图', attributes: {
name: {type: 'string', image: true, name: '细节图'}
}
},
description: {type: 'string', textarea: true, name: ' 描述'},
tags: {
type: 'array', name: '标签', required: true, attributes: {
name: {type: 'string', name: '标签'}
}
},
prices: {
type: 'array', name: '套餐信息', required: true, attributes: {
color: {type: 'string', name: '颜色'},
month: {type: 'integer', name: '月份'},
price: {type: 'string', name: '市场价'},
trans_amount: {type: 'string', name: '转入金额'},
earning: {type: 'string', name: '另享收益'},
earning_real: {type: 'string', name: '实质收益'}
}
}
}
};

module.exports = require('../lib/crud').fixModel(module.exports)

title 是这个页面的 title
sort 决定列表数据的排序方式
最后记得用 crud 的 fixModel 方法来处理这个 model(原因是 sails 不支持你这样随便修改 model)

一个 Control 的 Demo

var crud = require('../lib/crud')
module.exports = {};
module.exports = require('../lib/crud').extendController(module.exports)

这里引入了 crud 的方法,用来给这个 controller 注入页面渲染的功能。

Model 的定义

可以给 model 的字段定义以下类型,在前端会被翻译成对应的控件

  • string
    文本类型 前端对应的是文本输入框
  • integer/float
    数字类型 前端是 html5的文本输入框 number 类型
  • 长文本
    添加一个 textarea:true 属性即可,前端会展示成一个文本框。
  • 日期类型
    type=date 前端是 html5的 date 类型的文本输入框
  • 选择框
    需要添加一个 options 属性,描述每个 option 的 key and value
    这样前端就能构造一个 选择框了。
compare: {
name: '', type: 'string', options: [
{value: '-2', name: '小于等于'
},
{value: '-1', name: '小于'},
{value: '0', name: '等于'},
{value: '1', name: '大于'},
{value: '2', name: '大于等于'}
]
},
  • 图片地址
    某个字段如果是保存图片地址的话,配置一个属性 image:true
    在编辑和展示这个字段都能看到图片预览

  • object 需要继续定义 object 里有哪些类型的数据
    在前端会把 object 的子内容铺开。

price: {
name: '套餐', type: 'object', readOnly: true, attributes: {
"month": {type: 'string', name: '月份'},
"color": {type: 'string', name: '颜色'}
}
},
  • array 同 object 需要继续定义数据类型。
    在前端会以一个子 table 的形式来展开。

编辑的时候是这样的:

prices: {
type: 'array', name: '套餐信息', required: true, attributes: {
color: {type: 'string', name: '颜色'
},
month: {type: 'integer', name: '月份'},
price: {type: 'string', name: '市场价'},
trans_amount: {type: 'string', name: '转入金额'},
earning: {type: 'string', name: '另享收益'},
earning_real: {type: 'string', name: '实质收益'}
}
}

代码

crud.jade 里的 extends layout2 里引用了 css 和 js 文件,这里不便公开。
https://github.com/myfjdthink/CodeNode/tree/master/crud

TODO

分页功能
导出 Excel 功能
在 model 字段比较多的时候提供一个更好的编辑交互。

总结

本文只是提供一个实现 CRUD 思路,你可以根据你的实际项目来替换使用。
REST 接口可以很多 Web 框架都会提供。
视图模板除了 Jade 还有 ejs 等。
前端的数据绑定也可以考虑使用 Vue。