这是一篇关于后端 JavaScript 开发的指引,如果你对 JavaScript 的认识仍然停留在前端开发的话,你需要更新自己的知识体系了。



Web 开发框架从以前流行的 LAMP/LEMP  架构逐渐转移到 Ruby on Rails 和 Python Django 架构之上。但最近有另外几种的架构变得成熟了:比如 Golang, Clojure/leiningen,  Node.js 。这三者之中我更关注 Node.js。原因是它的生态更加完善。所以最近的几个项目都是用它来实现的。Node.js 既适合做后端 API 的粘合,也适合给终端用户提供 API 。

用 NPM 来做项目管理和包维护

Node.js 的每个项目都应该有一个 package.json 配置文件。其中包含了这个项目或者库的信息和所依赖的库。一个相对完成的 package.json 文件像这样:

{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "description": "My app and service",
  "main": "app.js",
  "scripts": {
    "start": "forever start app.js",
    "test": "mocha test"
  },
  "dependencies": {
    "coffee-script": "=1.4.0",
    "express": "=3.0.6",
    "mocha": "=1.7.4",
    "underscore": "=1.4.3",
    "forever": "=0.10.0",
    "async": "",
    "grunt-beautify": ""
  },
  "repository": "",
  "author": "Bruce Dou",
  "license": "BSD"
}
{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "description": "My app and service",
  "main": "app.js",
  "scripts": {
    "start": "forever start app.js",
    "test": "mocha test"
  },
  "dependencies": {
    "coffee-script": "=1.4.0",
    "express": "=3.0.6",
    "mocha": "=1.7.4",
    "underscore": "=1.4.3",
    "forever": "=0.10.0",
    "async": "",
    "grunt-beautify": ""
  },
  "repository": "",
  "author": "Bruce Dou",
  "license": "BSD"
}

这个文件可以在你的项目目录下执行 npm init 来按指引创建。注意其中的 scripts 部分,这里可以创建很多自定义命令。比如如此配置之后,npm start 会执行 forever start app.js 命令。

用 Grunt 来做代码格式修整、Lint 和其他自动化任务

Grunt 是一个命令行任务自动化工具。它包含了很多有用的插件。可以实现代码美化、自动化部署、自动生成 sprit、自动创建项目模板、自动压缩前端代码、自动编译 coffescript 等等你能想到的任务。假如你找不到自己需要的功能,还可以自己开发 Grunt 插件来实现。它的配置文件是位于项目根目录的 grunt.js 。一般项目都需要的功能是代码美化和 Lint,配置文件象这样:

module.exports = function (grunt) {
  // Project configuration.
  grunt.initConfig({
    beautify: {
      files: ['grunt.js', '*.js', 'lib/*.js']
    },
    lint: {
      files: ['grunt.js', '*.js', 'lib/*.js']
    },
    beautifier: {
      options: {
        indentSize: 2
      },
      tests: {
        options: {
          indentSize: 4
        }
      }
    },
    jshint: {
      options: {
        curly: true,
        eqeqeq: true,
        immed: true,
        latedef: true,
        newcap: true,
        noarg: true,
        sub: true,
        undef: true,
        boss: true,
        eqnull: true,
        browser: true
      },
      globals: {
        jQuery: true,
        Drupal: true,
        Backbone: true,
        _: true,
        app: true
      }
    }
  });
  grunt.loadNpmTasks('grunt-beautify');
  //grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.registerTask('default', 'beautify');
};
module.exports = function (grunt) {
  // Project configuration.
  grunt.initConfig({
    beautify: {
      files: ['grunt.js', '*.js', 'lib/*.js']
    },
    lint: {
      files: ['grunt.js', '*.js', 'lib/*.js']
    },
    beautifier: {
      options: {
        indentSize: 2
      },
      tests: {
        options: {
          indentSize: 4
        }
      }
    },
    jshint: {
      options: {
        curly: true,
        eqeqeq: true,
        immed: true,
        latedef: true,
        newcap: true,
        noarg: true,
        sub: true,
        undef: true,
        boss: true,
        eqnull: true,
        browser: true
      },
      globals: {
        jQuery: true,
        Drupal: true,
        Backbone: true,
        _: true,
        app: true
      }
    }
  });
  grunt.loadNpmTasks('grunt-beautify');
  //grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.registerTask('default', 'beautify');
};

添加了配置文件之后可以在项目目录运行 grunt 命令自动执行任务串。grunt beautify 可以执行子任务。

用 CoffeeScript 方言写更友好的 JavaScript

CoffeeScript 提供了类似于 Ruby 的语法,简化了 JavaScript 的书写,简化了 JavaScript 中实现类和类的继承的实现。你不必再自己繁琐得通过 prototype 和 constructor,call,apply 实现类的继承。 无论用 VIM 还是 Sublime Text 都可以找到对应的实时转换工具,在写 CoffeeScript 的同时就能看到对应的 JavaScript。

必不可少的几个库

underscore

underscore 是写 JavaScript 必不可少的库。提供了一些非常常用的方法,这些方法不仅使用方便,而且提高了代码的可读性。:

用 _.each 代替 for 循环,比如:

var contents = [];
for(var i in msgs) {
  contents.push(msgs[i].content);
}
var contents = [];
for(var i in msgs) {
  contents.push(msgs[i].content);
}

可以改写为:

var contents = [];
_.each(msgs, function(el) {
  contents.push(el.content);
});
var contents = [];
_.each(msgs, function(el) {
  contents.push(el.content);
});

其他常用的方法还有:

_.map 用来对数组元素进行批量转换

_.reduce 用来将数组元素合并为结果

_.pluck 用来取对象数组中的子元素,返回包含对应子元素的新数组

_.filter 用来有选择性的取数组中的某些值,返回包含符合条件的值的新数组

_.mixin 用来增加自定义函数

_.chain 用来实现函数式编程:

_.chain([1,2,3])
 .map(function(v) {return v * 2;})
 .reduce(function(total, v) { total += v}, 0)
 .value();
_.chain([1,2,3])
 .map(function(v) {return v * 2;})
 .reduce(function(total, v) { total += v}, 0)
 .value();

Async

Async.js 对常用的流程控制模式进行了封装,比如并行处理、Pipeline等等:

async.parallel 并行处理,整体等待最慢的函数返回,常用作多个后端请求的聚合或者并行处理:

...
// construct call functions
var callItems = [];
function make_query_func(json) {
  return function (callback) {
    callBackService(json, callback);
  };
}
_.each(messages, function (el, i) {
  callItems.push(make_query_func({
    vmsg: messages[i].vmsg,
    cmsg: messages[i].cmsg
   }));
});
// execute the call functions parallelly
async.parallel(callItems, function (err, results) {
  main_cb(results);
});
...
// construct call functions
var callItems = [];
function make_query_func(json) {
  return function (callback) {
    callBackService(json, callback);
  };
}
_.each(messages, function (el, i) {
  callItems.push(make_query_func({
    vmsg: messages[i].vmsg,
    cmsg: messages[i].cmsg
   }));
});
// execute the call functions parallelly
async.parallel(callItems, function (err, results) {
  main_cb(results);
});

async.waterfall 可以用来将大量嵌套的函数顺序执行,前一个函数的结果作为下一个函数的参数。用它之后你的代码里将不会再出现过深的 callback 嵌套:

// old way
...
var result = function(uid, callback) {
  get_user_ids(uid, function(err, user_ids) {
    get_content(user_ids, function(err, content) {
      content_clean(content, function(err, clean_content) {
        callback(err, clean_content);
      });
    });
  });
}
// new way
...
async.waterfall([get_user_ids, get_content, content_clean]);
// old way
...
var result = function(uid, callback) {
  get_user_ids(uid, function(err, user_ids) {
    get_content(user_ids, function(err, content) {
      content_clean(content, function(err, clean_content) {
        callback(err, clean_content);
      });
    });
  });
}
// new way
...
async.waterfall([get_user_ids, get_content, content_clean]);

开发 C++ addons 增强和扩展 Node.js

这意味着你不必担心某些逻辑的性能,也不必担心和其他框架的继承。因为所有语言或者开发栈都会提供 C/C++ 的接口。

Node.js 配置管理方式

有两种配置方式,config.js 或者 config.json 假如以 config.js 作为配置文件:

// config.js
exports.config = {'a':'a val', 'b': 'b val'};
// app.js
var config = require('./config').config;
// config.js
exports.config = {'a':'a val', 'b': 'b val'};
// app.js
var config = require('./config').config;

如果以 config.json 作为配置文件:

// config.json
{'a': 'a val', 'b': 'b val'}
// app.js
var config = JSON.parse(fs.readFileSync(process.cwd() + '/config.json'));
// config.json
{'a': 'a val', 'b': 'b val'}
// app.js
var config = JSON.parse(fs.readFileSync(process.cwd() + '/config.json'));

开发内部 Service 推荐的部件

  • 进程统计信息,比如 uptime, memory usage, heap size, connection number, 处理的请求数量等等。这样便于和监控系统对接
  • RESTful apis,推荐以 RESTful JSON 格式作为内部通信协议,这其实对大部分应用完全足够,而且容易调试。
  • 配置文件。将可能会发生变化的变量放到配置文件里。
  • Service Level Agreement。比如超过 100ms 的请求报错而不是继续等待返回信息。

Forever: Daemon 管理工具

既然 Node.js 是长时间运行的后台进程,缺不了进程管理工具。Forever 就是为了实现对 Node.js 的 daemon 进程进行管理的工具。常用命令:

forever start app.js 启动进程
forever restart app.js 重启进程
forever list 列出后台进程,并且列出 log 文件,可以方便的及时查看 log 文件内容。

Node.js Cluster

Node.js Cluster 是为了利用多核 CPU 的计算能力, 并且子进程可以共用同一个端口:

var cluster = require('cluster');
if (cluster.isMaster) {
    //start up workers for each cpu
    require('os').cpus().forEach(function() {
        cluster.fork();
    });

} else {
    //load up your application as a worker
    require('./app.js');
}
var cluster = require('cluster');
if (cluster.isMaster) {
    //start up workers for each cpu
    require('os').cpus().forEach(function() {
        cluster.fork();
    });

} else {
    //load up your application as a worker
    require('./app.js');
}

JavaScript 开发常见问题:

JavaScript 是传值还是传引用?

简单说:如果参数为一个对象则为传引用,如果参数为变量或者函数则为传值。

如何用 GDB 调试自己开发的 Node.js C++ addons?

gdb –args nodejs script.js

如何捕获 uncaughtException 并打印详细错误信息?

在进程级别捕获异常:

process.on('uncaughtException', function (e) {
  console.trace('Error: '.red + e);
  console.trace(e.stack);
  //process.exit();
});
process.on('uncaughtException', function (e) {
  console.trace('Error: '.red + e);
  console.trace(e.stack);
  //process.exit();
});

如何优雅结束进程?

Node.js 中对接收的 POSIX 信号做自定义操作很方便,进行进程结束前的清理工作:

process.on('SIGINT', function () {
  // wait connections to close
  process.exit();
  console.log("gracefully shutting down from  SIGINT (Crtl-C)".yellow);
});
process.on('SIGINT', function () {
  // wait connections to close
  process.exit();
  console.log("gracefully shutting down from  SIGINT (Crtl-C)".yellow);
});

如何使用 Array 和 Object

对于列表类的信息,比如用户列表推荐使用 Object 而不是 Array:

var a = [];
a[1000] = 1;
//a is an Array which length is 1001
var a = [];
a[1000] = 1;
//a is an Array which length is 1001

如何聚合多个后端服务并提供 SLA ?

用 Node.js 可以非常简单的实现对返回所用时间进行控制。比如在之前例子代码 async.parallel 的请求中设置超时定时器。对多个处理进程并发请求取其中时间最短的结果,这样可以保证返回时间都保持很短。

什么时候使用 process.nextTick() ?

  1. 重量使用 CPU 的函数中释放 CPU 给其他任务
  2. 将执行放到下一个 tick ,等待初始化

更多相关参考:

http://nodejs.org/
https://npmjs.org/
http://coffeescript.org/
http://underscorejs.org/
http://nodejs.org/api/addons.html
http://expressjs.com/
https://github.com/caolan/async
http://howtonode.org/understanding-process-next-tick
http://book.mixu.net/ch7.html
http://gruntjs.com/

Related articles

订阅这个博客:

关注我的微博:

关注我的推特: