10个Node.js开发者最易犯的错误

5 年前

##简介

Node.js 在过去的几年中迅猛发展,沃尔玛,PayPal 等大公司均开始采用它。越来越多的人使用 Node 开发并在 NPM 上发布模块,如此快的速度已经[超越了其他语言][1]。然而,Node 的理念需要花点时间适应,尤其是对于那些从其他语言切换过来的工程师而言。

这篇文章中,我们将要探讨那些 Node 开发者常犯的错误,以及如何避免它们。你可以在 github 上找到实例中的代码。

1 不使用开发工具

  • 使用 nodemon 或者 supervisor 来自动重启
  • 浏览器端的 live reload(当静态文件或模板文件改变后重新加载)

当你改变源代码时需要重启 Node,这点与ruby或者php等语言不同。此外当你开发 web 应用时,有件事情会拖累你:当静态代码更新时需要刷新网页。现在有一些更好的方案来代替手动执行这些操作。

1.1 自动重启

  • nodemon
  • node-supervisor
  • forever

我们经常做的操作:在编辑器中保存文件,按下 CTRL+C 终止应用,按一下[上],再按一下[Enter]键再次启动。你可以自动化这些操作,使用以下的工具来解放你的双手:

  • nodemon
  • node-supervisor
  • forever

这些模块会监听文件改变并帮你重启应用。我们以 nodemon 为例,首先你需要全局安装这个模块:

npm i nodemon -g

接下来你需要使用 nodemon 命令,替代 node 命令来启动应用:

$ nodemon server.js
14 Nov 21:23:23 - [nodemon] v1.2.1
14 Nov 21:23:23 - [nodemon] to restart at any time, enter `rs`     14 Nov 21:23:23 - [nodemon] watching: *.*
14 Nov 21:23:23 - [nodemon] starting `node server.js`
14 Nov 21:24:14 - [nodemon] restarting due to changes...
14 Nov 21:24:14 - [nodemon] starting `node server.js`

此外 nodemon 或 node-supervisor 也提供了一些选项,最常用的是忽略特定文件或文件夹。

1.2自动刷新浏览器

除了当源代码更改时重启应用,你还有另外的途径来提高 web 应用的开发效率。你也可以通过使用 livereload 等工具代替手动刷新。

这些工具跟之前介绍的工具原理类似,通过监听特定文件夹中的文件改变,来触发浏览器的刷新(不需要重启服务)。浏览器的刷新是通过脚本注入页面或浏览器插件完成。

这次我们不展示如何使用livereload,而是创建一个类似的工具。它将执行以下操作:

  • 监听文件夹中文件的变化;
  • 使用服务器端事件发送消息到所有连接的客户端;
  • 触发页面重新加载。

首先我们应该安装项目所需的 NPM 依赖:

  • express——用于创建示例web应用程序
  • watch——监听文件改变
  • sendevent——服务器端事件,SSE(另一种是选择是websockets)
  • uglify-js——压缩客户端JavaScript文件
  • ejs——视图模板

接下来我们建立一个简单的express服务器,并渲染一个首页:

var express = require('express');
var app = express();
var ejs = require('ejs');
var path = require('path');
var PORT = process.env.PORT || 1337;

// view engine setup 设置渲染引擎
app.engine('html', ejs.renderFile);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'html');

// serve an empty page that just loads the browserify bundle
// 渲染一个空页面
app.get('/', function(req, res) {
    res.render('home');
});
app.listen(PORT);
console.log('server started on port %s', PORT);

由于我们使用 express,我们把这个浏览器自动刷新的工具作为 express 的中间件使用。中间件将带有 SSE 功能,并且创建一个模板的 helper 来引入浏览器端脚本。而中间件的参数有两个:express 应用和要监视的文件夹。依据这样的设计,我们可以预先在模板设置前使用以下代码来加载这个中间件(server.js内):

var reloadify = require('./lib/reloadify');
reloadify(app, __dirname + '/views');

我们需要监听 views 目录下的改动。中间件则长这样:

var sendevent = require('sendevent');
var watch = require('watch');
var uglify = require('uglify-js');
var fs = require('fs');
var ENV = process.env.NODE_ENV || 'development';

// create && minify static JS code to be included in the page
//创建并压缩要嵌入到页面中的js静态文件
var polyfill = fs.readFileSync(__dirname + '/assets/eventsource-polyfill.js', 'utf8');
var clientScript = fs.readFileSync(__dirname + '/assets/client-script.js', 'utf8');
var script = uglify.minify(polyfill + clientScript, {
        fromString : true
    }).code;
function reloadify(app, dir) {
    if (ENV !== 'development') {
        app.locals.watchScript = '';
        return;
    }
    // create a middlware that handles requests to `/eventstream`  建立一个中间件来处理来自/eventstream的请求
    var events = sendevent('/eventstream');
    app.use(events);
    watch.watchTree(dir, function (f, curr, prev) {
        events.broadcast({
            msg : 'reload'
        });
    });

    // assign the script to a local var so it's accessible in the view 把这个脚本挂到应用的local上,以便view可以获取到

    app.locals.watchScript = '<script>' + script + '</script>';
}

module.exports = reloadify;

你可能已经注意到,若环境变量不为“development”,中间件不会做任何事情。这意味着我们不需要在生产环境中删除这个中间件。

前端 JS 文件非常简单,它只会听 SSE 消息并在需要时重新加载页面:

 (function() {

    function subscribe(url, callback) {
      var source = new window.EventSource(url);

      source.onmessage = function(e) {
        callback(e.data);
      };

      source.onerror = function(e) {
        if (source.readyState == window.EventSource.CLOSED) return;

        console.log('sse error', e);
      };

      return source.close.bind(source);
    };

    subscribe('/eventstream', function(data) {
      if (data && /reload/.test(data)) {
        window.location.reload();
      }
    });

  }());

SSE 的 polyfill,eventsource-polyfill.js 是由 Remy Sharp 提供的。最后我们需要使用 helper 来把这个前端脚本插入到页面中:

 ...
  <%- watchScript %>
  ...

这样当你每次修改home.html时,浏览器都会帮你重新加载页面。

2 阻塞[event loop][2]

由于Nodejs运行在一个单线程上,当event loop被阻塞后将阻塞一切。这意味着,如果你的web服务器与1000个客户端同时链接,event loop被阻塞后,每个客户只会……傻等。

这里有一些例子告诉你如何做到(可能在不知情的情况下):

  • 使用JSON.parse函数解析超大json。
  • 想在后台做大文件的语法高亮显示(类似Ace或highlight.js);
  • 解析一个庞大的输出流(如git命令产生的日志,从子进程输出)。

这意味着,你可能不知不觉地处理这些事情,因为解析15 Mb的输出并不经常出现。这足以让攻击者打你个措手不及,以至于整个服务器被DDOS掉。

幸运的是,你可以监视eventloop延迟来检测异常。这可以通过StrongOps等专有的解决方案,通过使用开源模块比如blocked 来解决。

这些工具背后的原理是准确地反复跟踪一个interval所消耗的时间并报告出来。时差是通过获取A和B时刻的时间,减去A时刻到B时刻的时间,再减去设定的时间间隔后计算而来。

下面的一个例子描述了如何实现:

  • 获取高精度的当前时间与参数传递的时间的差;
  • 定义定期事件循环的延迟;
  • 使用绿色显示延迟,如果它超过阈值则使用红色显示。

为了展示实际效果,我们每隔300ms执行一次大计算量的代码。 源代码示例如下:

var getHrDiffTime = function(time) {
    // ts = [seconds, nanoseconds]
    var ts = process.hrtime(time);
    // convert seconds to miliseconds and nanoseconds to miliseconds as well
    return (ts[0] * 1000) + (ts[1] / 1000000);
  };

  var outputDelay = function(interval, maxDelay) {
    maxDelay = maxDelay || 100;

    var before = process.hrtime();

    setTimeout(function() {
      var delay = getHrDiffTime(before) - interval;

      if (delay < maxDelay) {
        console.log('delay is %s', chalk.green(delay));
      } else {
        console.log('delay is %s', chalk.red(delay));
      }

      outputDelay(interval, maxDelay);
    }, interval);
  };

  outputDelay(300);

  // heavy stuff happening every 2 seconds here
  setInterval(function compute() {
    var sum = 0;

    for (var i = 0; i <= 999999999; i++) {
      sum += i * 2 - (i + 1);
    }
  }, 2000);

在跑这段代码前你需要安装[chalk][3]。跑完代码后你会在终端看到以下输出:

![此处输入图片的描述][4]

像之前说的一样,使用一些现成的开源模块也可以完成这些,可以参考以下链接:

https://github.com/hapijs/heavy/blob/bbc98a5d7c4bddaab94d442210ca694c7cd75bde/lib/index.js#L70 https://github.com/tj/node-blocked/blob/master/index.js#L2-L14

通过这种技术来分析,你可以准确地确定哪一部分的代码导致了延迟。

3 多次执行一个回调

有多少次你保存一个文件,重新启动nodejs web应用程序,然后程序很快崩溃了?   最可能的情况是,你的回调执行了两次,你忘了第一次后返回。

让我们创建一个示例来重现这种情况。我们将创建一个简单的代理服务器,带有一些基本的验证。跑这个例子之前请先安装依赖,之后运行示例,打开浏览器输入http://localhost:1337/?url=http://www.google.com/   

我们的示例的源代码如下:

var request = require('request');
var http = require('http');
var url = require('url');
var PORT = process.env.PORT || 1337;
var expression =/[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/gi;
var isUrl = new RegExp(expression);
var respond = function(err, params) {
    var res = params.res;
    var body = params.body;
    var proxyUrl = params.proxyUrl;
    res.setHeader('Content-type', 'text/html; charset=utf-8');
    if (err) {
        console.error(err);
        res.end('An error occured. Please make sure the domain exists.');
    } else {
        res.end(body);
    }
};
http.createServer(function(req, res) {
            var queryParams = url.parse(req.url, true).query;
            var proxyUrl = queryParams.url;
            if (!proxyUrl || (!isUrl.test(proxyUrl))) {
                res.writeHead(200, {
                    'Content-Type': 'text/html'
                });
                res.write("Please provide a correct URL param. For ex: ");
                res.end("<a href='http://localhost:1337/?url=http://www.google.com/'>http://localhost:1337/?url=http://www.google.com/</a>");
            } else {
    // ---------------------
0
推荐阅读