Node 作主技术栈的我 为什么不将其用于后台开发

我的 Node 经历

还是很早的时候了,最起初听闻 NodeJs 的时候,只知道它可以用 Javascript 写后端,构建网站和后台。当时对它并没有一个具体的印象和概念,有人说它是一个 JS 框架也有人说它用了浏览器的 JS 引擎所以可以独立执行。
当然,后来 Node 的火爆不得不让我尝试了这门技术,大概在一年多以前。仅仅是为了尝个鲜而已,并没有想过用它干嘛。

要让我给 NodeJs 一个定义,那么 node 准确来说,指的是 JS 的运行时,基于 Chrome 浏览器 V8 引擎的一个扩展。扩展了 JS 这门语言的功能,具体就是提供了很多与浏览器之外的功能库。例如 IO、NET、Process、OS 等。扩展了 JS 的能力,让 JS 脱离于浏览器环境之外,不再是一个沙盒语言。
所以搭建网站、构建后台等都不在话下了。

我用过 Node 上很有名也很大众的,例如 Express、Koa 等 Web 框架搭建过网站、用 Mongoose 操作 Mongodb,用 Redis 做过缓存,尝试过与 Java 的异构集成。等。
Node 确实给我带来了点新玩意,体验到与 Java 等语言截然不同的单线程程序却可以处理大量并发的异步的威力。
让我接触到 Mongodb 数据库,让我体会到前后端公用一套代码、一种语言的开发模式。

那么我为什么不用 Node 写后端了?

主要有两点原因:

一是异步思维并不适合写后端程序,需要专门纠正恼人的异步回调编码模式到正常流程的编码模式上来。
二是 JS 语法仍然落后和原始,只是让前端语言写后端这一点“新颖”了点,而 JS 本身却一点也不新颖。

恼人的回调代码

一般来讲,一个正常的 方法/函数 输出结果是以返回值的形式输出的。例如:

/**
           * 加法运算
           * @param a 数字1
           * @param b 数字2
           * @returns a + b 的值
           */
          function plus(a, b) {
              return a + b;
          }
          
          var result = plus(19, 11);
          console.log(result);
          

这是正常的同步代码的编码模式,在 a + b 的结果未计算出来前 plus 的调用会阻塞当前代码,直到返回结果。这一点在任何语言上都是相通的。

但是 NodeJs 的库却几乎都是这么设计的:

/**
           * 加法运算
           * @param a 数字1
           * @param b 数字2
           * @param func 回调函数,参数包含 a + b 的结果
           */
          function plus(a, b, func) {
              func(a + b);
          }
          
          plus(19, 11, function (result) {
              console.log(result);
          });
          

这样做首先就是让函数最基本的两个能力:输入、输出,失去了一半。但并不是完全失去,而是以别扭的回调形式变相“输出”。 这么做的原因是因为在设计需要长时间 IO 等待的 API 时,能让函数异步执行,等出结果再回调,相当于一个“完成事件”。期间线程可以被调度去做其他的事情,这就是 NodeJs 的异步编程模式。

乍一听好像是一种优势,它的确是优势。不过,这种优势也是有代价的。如果你看到下面这种代码:

plus(19, 11, function (result) {
              plus(result, 1, function (result) {
                  plus(result, 2, function (result) {
                      plus(result, 3, function (result) {
                          plus(result, 4, function (result) {
                              plus(result, 5, function (result) {
                                  console.log(result)
                              })
                          })
                      })
                  })
              })
          });
          

你还笑得出来吗?不要惊慌、不要着急,这很正常,谁让你回调返回结果呢。
将 19 + 11的结果依次加上1、2、3、4、5,用正常函数来调用最多也是这样的:

var result = plus(5, plus(4, plus(3, plus(2, plus(1, plus(19, 11))))));
          console.log(result);
          

调用一个以回调的形式返回结果的函数,再由第二个函数处理结果继续由下一个函数加工结果… 代码会逐步嵌套,非常丑陋和难以辨别。让正常的自上而下的流程变成了自外而内,并且代码只能越来越往里边写,想出来没那么容易。因为函数不会返回结果,所以不能把函数调用当做结果值作为参数,所以只能逐步嵌套,越陷越深,也就是俗称的“回调地狱”。

JS 本身不新颖

JS 这门语言非常简陋,无论是语法、内置库,包括设计等。跟极简运行时的 Lua 还差一截。写 JS 要用其基于原型设计的性质来模拟其他语言上的高级特性,才能满足更优雅更大型的开发工作。
用惯其他脚本语言的人写 JS 的感觉就是从电气时代一下子回到石器时代一般,虽然 node 自带包管理功能,解决了很大一批原始问题,例如说我想用 HashMap,但是并不想自己实现一个 HashMap,那么你可以用他人封装好的 HashMap库。但终究无法使这门语言先进起来。

解决回调地狱问题

回到第一点,我们能否解决这个问题呢?当然能。那么我当初是怎么解决这个问题的呢?我既然解决了为什么还要放弃 node 后端?
我用 async 框架处理数据层的 API 调用,用 q 框架调用 model 提供的异步 API。

例如,有一个 article-model.js 文件,是用来操作数据库文章表并且暴露出 API 供外界调用的数据层,它的内容如下:


          /**
           * 发表一篇文章
           * @param article 文章
           * @param callback 回调
           */
          function publisher(article, callback) {
              async.series([
                      call => { //0.验证属性是否符合规范
                          if (!article)
                              throw new Error('article is empty.');
                          else {
                              if (common.existsEmpty(article.title)) throw new Error('title is empty.');
                              if (common.existsEmpty(article.content)) throw new Error('content is empty.');
                              if (common.existsEmpty(article.semanticTitle)) throw new Error('semanticTitle is empty.');
                          }
                          call(null, null);
                      },
                      call => { //1.处理字段
                          article.semanticTitle = article.semanticTitle.replace(/\s/g, '-');
                          article.sort = genSort(); //取时间(发表越晚值越大,倒叙结果即按时间倒叙)
                          call(null, null)
                      },
                      call => { //2.判断是否存在semanticTitle
                          exports.findArticleBySemanticTitle(article.semanticTitle).then(() => {
                              call(new Error(`the ${article.semanticTitle} is exists.`), null)
                          }, ()=> call(null, null));
                      },
                      call => { //3.入库
                          new MyArticle(article).save(call)
                      }
          
                  ],
                  (err, results) => {
                      callback(err, results[3]);
                  })
          }
          

可以看到,我在 0123 到最后返回的步骤中的操作都是并行上下关系的函数(闭包)。本应该嵌套的操作变成自上而下的编码模式。这就是 async 框架的作用,它可以让你指定一个函数数组序列,然后自上而下执行。只有上一个函数显式调用 callback 函数才会调用下一个函数,所以哪怕是异步的回调也没关系。结果经过一层一层的处理最后返回。async 优雅的解决了回调地狱。

在 web 层发表文章的 router 里我是这样调用的:

publisher(article).then(() => {
              res.redirect('/');
          }, err => {
              res.render('error', {error: err});
          });
          

q 以 promises 风格的排列回调。我让 publisher 回调中返回的错误和正确数据以并行的两个函数分别进行处理。 上面的代码摘自我以前用 node 做过的项目中的真实代码,并不是伪代码。我这样做几乎可以杜绝回调地狱问题。当时我还觉得自己确实如发现新大陆一般,不用再感受恶心人的回调嵌套了。
现在的话 还可以用 generator 或者基于 generator 的框架,也许有更优雅解决回调地域的方式。

那么我既然解决了回调地狱问题,为什么还要放弃 node 后端?
不久之后,我厌倦了。这种编码模式带来的新颖就跟接触 node 时新颖的感觉是一样的。JS 原本作为前端开发语言进了后端的大门,新颖,但是 JS 本身不新颖。 无论用何种方法避免了回调地狱的问题,诞生出来的模式新颖,但是其结果不新颖,就跟原本坐着飞快的飞机 但是发现把飞机引擎改装在了一辆车上 让车跑到接近飞机的速度一样。我既然有飞快的飞机了,还费力改装车干嘛呢?

所以,我放弃了 node 后端,玩玩也罢,过去了,并且不想回来。

让 JS 先进一点

回到第二点,我说的 JS 原始连 Lua 都不如,指的是之前的 JS,准确的说是 es2015 之前的 JS。
虽然现在的 JS 升级带来了一批新特性,堪比在兼容的情形下换了一个语言。只不过这天来得太迟了。JS 虽然改头换面,但是兼容性还未达标。es2015 仍然还是未来的 JS。而且说到底,多的一批特性在其他语言上早就实现了,大部分也早就是标配了,JS 仍然只是在追赶而已。
如果你想体验新的 JS,并且用在生产环境中、浏览器中。可以用 babel 来做转译,当然通常 node 程序员都应该知道这玩意也当然用上了这玩意。

现在我用 node 做什么

现在,node 的主要目的是用在前端领域,偶尔客户端。基于 node 的前端技术栈:ES6、Webpack、Babel、Vue(JSX)、Gulp、React、SASS/LESS、Electron 等等等等。还有很多流行元素,不一一列举。 node 给前端带来了巨大的推动。
虽然我觉得 node 后端没意思,但是前端、客户端 仍然是有意思、有价值的。

最后

希望 node 发展顺利,我也会在技术栈上不断的探索和跟进。也许有一天我放弃了 node,但是我不会后悔曾经在 node 上花的时间和带给我的惊喜。