helloworld

本节主要通过helloworld为例,引申commonJS规范实现,最终打通nodejs模块基础。

入门概览

  • 必会js基本语法
  • 熟悉api写法:error-first api写法和commonJS规范
  • 掌握EvenEmit事件驱动机制
  • 掌握Stream,以及HTTP模块中使用Stream的理解
  • 掌握Node.js SDK api用法
  • 掌握通过npm安装的外部模块用法

区分Npm和CommonJS规范

看一下npm

  • npm名称是Node Package Manager
  • npm是Node.js SDK以外的模块管理器,用于提供安装,卸载,依赖管理等
  • npm是管理基于CommonJS规范写的模块的

而CommonJS规范

  • CommonJS是一套规范,是模块写法上约定
  • Node.js是基于CommonJS规范的实现,并做了改进

总结

  • npm是管理基于CommonJS规范写的模块的
  • CommonJS是一套规范,是模块写法上约定

Hello World

创建 helloworld.js 文件,并敲入如下代码:

  1. console.log('Hello World');

在终端里,通过node命令来执行

  1. $ node demo/helloworld.js
  2. hello world

这是最简单的例子,要点如下

  • Node.js的语法使用前端JavaScript一样的语法
  • JavaScript是脚本语言,需要Node.js解释器 node 命令来解释并执行
  • console.log是一个用于输出日志的方法,区别在于日志会输出在浏览器端(前端)或terminal终端(Node.js)里

所以,JS语法的掌握是学习Node.js的基础,非常重要,是必需要要掌握的。

声明严格模式

举个简单的例子,我们在helloworld.js上增加一个变量a的声明,采用es6里的let关键字

  1. console.log('hello world');
  2. let a = 1

先通过nvm切换到4版本

  1. $ nvm use 4
  2. Now using node v4.7.0 (npm v2.15.11)

执行

  1. $ node demo/helloworld
  2. demo/helloworld.js:3
  3. let a = 1
  4. ^^^
  5. SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode
  6. at exports.runInThisContext (vm.js:53:16)
  7. at Module._compile (module.js:373:25)
  8. at Object.Module._extensions..js (module.js:416:10)
  9. at Module.load (module.js:343:32)
  10. at Function.Module._load (module.js:300:12)
  11. at Function.Module.runMain (module.js:441:10)
  12. at startup (node.js:139:18)
  13. at node.js:990:3

此时,报错说是语法错误(SyntaxError),对于let, const, function, class等关键字的使用在严格模式外还不支持。所以它提示我们使用严格模式。即如下

  1. 'use strict'
  2. console.log('Hello World');
  3. let a = 1

此时再执行就不会报错了。使用'use strict' 声明为严格模式,标记严格模式后的好处如下

  • 其一,如果在语法检测时发现语法问题,则整个代码块失效,并导致一个语法异常。
  • 其二,如果在运行期出现了违反严格模式的代码,则抛出执行异常。

这是我们在使用es6代码,经常要做的事儿,一些低版本的Node.js SDK里很多特性默认是不支持的,需要使用打开严格模式才能使用。

熟悉api写法:

  • Error-first callback
  • Event-driven

Error-first callback

Node之所以大量的使用回调函数(callbacks)要追溯到一种比JavaScript语言还要老的编程风格。CPS把函数调用完之后接下来要执行的代码通过闭包包裹并作为函数参数调用要执行的函数。在CPS里,一个 “continuation function” (read: “callback”)会被作为参数传给其他代码调用,这可以让不同的函数在不同应用间进行异步处理。

Node.js需要依赖异步代码才能保证它的快速的执行速度。所以,严重的依赖callback模式是必然的。如果没有它,开发者将陷入到在每个模块之间维护不同签名和风格。将error-first模式引入到Node核心里来解决这个问题的,从此蔓延成为今天所谓的标准。对此褒贬不一,由于过度的callback调用而产生的cellbackhell也是Node.js这么多年里被诟病最多的点。从另一个角度看,也正是Node.js坦诚的暴露缺点,才有了后来前仆后继的异步流程控制的改进。

典型的Error-first callback写法,如下

  1. var callback = function(err, data) {
  2. if (err) {
  3. /* do something if there was an error */
  4. }
  5. /* other logic here */
  6. };

上面的代码包含了2个定义error-first callback写法的规则:

  • callback函数的第一个参数是error对象。如果发生error,它就被作为第一个参数穿进来,如果没有错误,则err值为null。
  • 第二个参数是任何成功响应的data,如果没有error产生,err会被设置为null,并且成功的返回的数据的会放到第二个参数里。

约定是这样的,当往往可能需要传n(n>=2)个参数,其他参数依次排下去。

比如express和connect的中间件

  1. // error middleware for errors that occurred in middleware
  2. // declared before this
  3. app.use(function onerror(err, req, res, next) {
  4. // an error occurred!
  5. });

err后面有3个参数,分别是req、res和next,在语义上有一定帮助的情况下,也是允许的。

下面,通过Node.js读取文件内容的api所编写的例子,展示一下sdk里error-first callback用法

  1. 'use strict'
  2. const fs = require('fs')
  3. fs.readFile(__dirname + '/helloworld.js', (err, data) => {
  4. if (err) throw err
  5. console.log(data.toString())
  6. });

执行

  1. $ node demo/readfile.js
  2. console.log('hello world');

说明

  • 1) 关于返回的data类型

官方文档:如果没有指定字符编码,默认返回的data是原始的Buffer类型,如果指定了字符编码返回的就是String类型

推荐

  1. fs.readFile('/etc/passwd', 'utf8', callback);
  • 2)针对err的处理

代码里是直接throw抛出异常:

  1. if (err) throw err

这里的error其实和其他语言处理类似的。处理方式大致如下

  • 自己不管,抛出异常,交给外出调用的处理,或者干脆直接报错
  • 自己处理,输出日志或者其他
  • 可以结合try/catch或Promise来处理

更多CPS参见:http://matt.might.net/articles/by-example-continuation-passing-style/

  1. var childProcess = require('child_process');
  2. function runScript(scriptPath, callback) {
  3. // keep track of whether callback has been invoked to prevent multiple invocations
  4. var invoked = false;
  5. var process = childProcess.fork(scriptPath);
  6. // listen for errors as they may prevent the exit event from firing
  7. process.on('error', function (err) {
  8. if (invoked) return;
  9. invoked = true;
  10. callback(err);
  11. });
  12. // execute the callback once the process has finished running
  13. process.on('exit', function (code) {
  14. if (invoked) return;
  15. invoked = true;
  16. var err = code === 0 ? null : new Error('exit code ' + code);
  17. callback(err);
  18. });
  19. }
  20. // Now we can run a script and invoke a callback when complete, e.g.
  21. runScript('./some-script.js', function (err) {
  22. if (err) throw err;
  23. console.log('finished running some-script.js');
  24. });

Event-driven

Node.js 所有的异步 I/O 操作在完成时都会发送一个事件到事件队列。很多Node.js核心API都是围绕事件驱动架构里的包括某种被称为 “emitters”的对象定时的发射命名事件来调用函数对象(“listeners”)。

Much of the Node.js core API is built around an idiomatic asynchronous event-driven architecture in which certain kinds of objects (called “emitters”) periodically emit named events that cause Function objects (“listeners”) to be called.

For instance: a net.Server object emits an event each time a peer connects to it; a fs.ReadStream emits an event when the file is opened; a stream emits an event whenever data is available to be read.

EventEmitter类, 是node中事件的基础, 实现了事件模型需要的接口, 包括addListener,removeListener, emit及其它工具方法. 同前端jQuery事件等类似, 采用了发布/订阅(观察者)的方式, 使用内部_events列表来记录注册的事件处理器。在Node.js的sdk里

  1. BigEvent.prototype.on = function(eventName, func) {
  2. this._listeners = this._listeners || {};
  3. this._listeners[eventName] = this._listeners[eventName] || [];
  4. this._listeners[eventName].push(func);
  5. };
  6. BigEvent.prototype.off = function(eventName, func) {
  7. this._listeners = this._listeners || {};
  8. this._listeners[eventName].splice(this._listeners[eventName].indexOf(func), 1);
  9. };
  10. BigEvent.prototype.trigger = function(eventName) {
  11. this._listeners = this._listeners || {};
  12. var dataArgument = arguments[1] ? arguments[1] : null;
  13. var events = this._listeners[eventName] || [];
  14. for(var i = 0; i < events.length; i++) {
  15. var ev = events[i]
  16. if(dataArgument) {
  17. ev.call(this, dataArgument);
  18. } else {
  19. ev.call(this);
  20. }
  21. };
  22. };

这里的BigEvent只是仿照jQuery的事件api写的简单实现,如果把trigger改成emit,用法就和Node.js里的Event几乎一模一样了。从英文单词角度看,trigger是触发,emit是发射,感觉emit更加霸气一些。

在Node.js很早就有EventEmiter模块,在Node.js 6之前,只能通过require("events").EventEmitter来获取Event对象,所以非常冗余。所以在Node.js 6里就deprecated了它,可以通过require("events")直接引用了。尤其是结合es6的类和继承机制,可以让代码更加优雅。在Node.js 7之后,彻底移除了EventEmitter的api。大家只要知道events就可以了。

Node 4以前的old做法(es5)

  1. var util = require("util");
  2. var EventEmitter = require("events").EventEmitter;
  3. function MyClass() {
  4. EventEmitter.call(this);
  5. }
  6. util.inherits(MyClass, EventEmitter);
  7. MyClass.prototype.doSomething = function(data) {
  8. this.emit("doSomething", data);
  9. }

过渡做法(在Node.js 4和5)

  1. var EventEmitter = require("events").EventEmitter;
  2. class MyEmitter extends EventEmitter {
  3. constructor() {
  4. super(); //must call super for "this" to be defined.
  5. }
  6. }
  7. const myEmitter = new MyEmitter();
  8. myEmitter.on('event', () => {
  9. console.log('an event occurred!');
  10. });
  11. myEmitter.emit('event');

新做法(es6 + Node 6+)

在Node.js 6里 process.EventEmitter 被 deprecated 了,在Node.js 7里移除了process.EventEmitter

  1. const EventEmitter = require('events');
  2. class MyEmitter extends EventEmitter {
  3. constructor() {
  4. super(); //must call super for "this" to be defined.
  5. }
  6. }
  7. const myEmitter = new MyEmitter();
  8. myEmitter.on('event', () => {
  9. console.log('an event occurred!');
  10. });
  11. myEmitter.emit('event');

出一个考题

给出已有的expirable-hash-table模块,它是一个简单的哈希表,带有TTL(time to live)概念,即当超时后会自动清理该key,这和redis、mongodb里的ttl类似,只是它是一个更小的纯Node.js实现的模块而已。

  1. var ExpirableHashTable = require('expirable-hash-table')
  2. var myTable = new ExpirableHashTable(1000) // default timeout in miliseconds
  3. myTable.set('key', 'value', 3000) // optional timeout in miliseconds
  4. myTable.get('key') // -> value
  5. myTable.remove('key') // -> ExpirableHashTable
  6. myTable.has('key') // -> true/false
  7. myTable.purge() // -> ExpirableHashTable
  8. myTable.toArray() // -> Array
  9. myTable.size() // -> Integer
  10. myTable.on('change', function() {
  11. // A change event is emitted ever time an item is added, updated or removed
  12. })
  13. myTable.once('<key>:expired', function() {
  14. // A expired event is emitted when a given item expires. Useful if a specific item wants to be monitored.
  15. })

它的风格是事件驱动的方式,能否使用Error-first callback写法?如何写?如果可以,请站在作者的角度上,思考作者这样做的初衷。

Write a http server use Node.js

  1. var http = require('http');
  2. http.createServer(function(request,response){
  3. console.log(request);
  4. response.end('Hello world!');
  5. }).listen(8888);

之前讲过的http的例子

  1. var http = require("http");
  2. http.createServer(function(request, response) {
  3. response.writeHead(200, {"Content-Type": "text/plain"});
  4. response.write("Hello World");
  5. response.end();
  6. }).listen(8888);

捕获异常

  1. server.on('error', function (e) {
  2. // Handle your error here
  3. console.log(e);
  4. });

疑问:如果在里面用emit呢?