node.js 框架

Posted BT学院技术产品团队

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了node.js 框架相关的知识,希望对你有一定的参考价值。

使用 node.js 代码构建 web 服务的时候,我们一般会使用 express, koa 框架。先来看一下一个简单的例子。如果要用 node 原生的 API 是怎么写一个简单的 http 服务的。


//app.jsvar http = require('http');
http.createServer(function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.write('<h1>Node.js</h1>');
  res.end('<p>Hello World</p>');
}).listen(3000);console.log("HTTP server is listening at port 3000.");


可以看到,尽管 node 对 web 开发已经非常良好。但是还是要对每个 http 请求事无巨细写每个细节。而且这里还缺乏了路由,日志等等诸多要素。

koa 是如何书写的呢?看以下例子。


const Koa = require('koa');const app = new Koa();// loggerapp.use(async (ctx, next) => {  await next();  const rt = ctx.response.get('X-Response-Time');  console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});// x-response-timeapp.use(async (ctx, next) => {  const start = Date.now();  await next();  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});// responseapp.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);


koa 用了一个叫中间件的非常优雅的解决方案。通过一个个的 app.use 实现了灵活轻量的扩展。koa 有一套相互独立而又互相补充的 middleware suite, 包括 koa-router, koa-onerror, koa-static 等,基本满足了 web 服务开发的基本需要。然而随着业务不断扩展,代码量的加大,这套小而美的框架也暴露出了局限性。在对 eggjs 的维护死马的采访中,他说到:


不能说 node 在做企业级 Web 开发时有缺陷,而是没有规范。 - 企业项目和个人项目有许多不同之处: 参与的人员会很多,项目的维护者也经常变更 - 有非常多内部系统需要对接,特别像阿里、蚂蚁这个规模的公司,内部都维护了许多自己的中间件服务和定制化的 RPC 通信框架 - 对稳定性、性能、Bug 追踪等方面的要求会更高。

在探讨解决方案之前,我们先来解决两个重要的问题。为什么用框架?以及框架究竟是什么


为什么用框架

几乎每个语言都有对应的出名,主流的框架,比如以下几个。


  • Ruby on Rails

  • Python Django

  • Java Spring

  • thinkmyphp


一个语言是否有实用的工业价值,很大程度上是由它由它的框架是否成熟来决定的。这些框架解决了什么问题?我们先从一个重要的编程原则说起。


*DRY(Don't repeat yourself)*
+DAK(Dirty-and-Quick)+


几乎每个稍微入门的程序员,都会知道,用循环取代傻傻的 COPY-PASTE,用函数来抽象及封装业务流程,用类来抽象建模,用容器类来组织对象。这些很大程度上就是为了不写重复的代码。重复的代码不仅笨拙,而且给代码的重构演化带来很多风险。

除了这些,还能怎么样实践这个原则呢?首先看个 java 来修改密码的例子。


public static boolean updatePassword(String username, String password, String newpassword) {
    Connection conn = null;
    PreparedStatement stmt = null;
    ResultSet rs = null;    boolean success = false;    try {
        conn = beginTransaction();
        stmt = conn.prepareStatement("select id, password from user where username = ?");
        stmt.setString(1, username);
        rs = stmt.executeQuery();        if (rs.next()) {            if (rs.getString("password").equals(password)) {
                PreparedStatement stmt2 = null;                try {
                    stmt2 = conn.prepareStatement("update user set password = ? where id = ?");
                    stmt2.setString(1, newpassword);
                    stmt2.setLong(2, rs.getLong("id"));
                    success = stmt2.executeUpdate() > 0;
                } finally {
                    safeClose(stmt2);
                }
            }
        }
        commitTransaction(conn);        return success;
    } catch (SQLException e) {
        rollbackTransaction(conn);        throw new RuntimeException(e);
    } finally {
        safeClose(rs);
        safeClose(stmt);
        safeClose(conn);
    }
}


这个方法的核心仅仅是两个简单的 SQL 语句。然而因为这是一个数据库的事务连接,我们不得不作了很多 boilerplot 的处理,十分冗余。

我们可以应用模板方法模式,抽象出一些共通的代码后,可以简化成以下形式。


public static boolean updatePassword(String username, String password, String newpassword) {    return connection(conn -> statement(conn, "select id, password from user where username = ?", stmt -> {
                stmt.setString(1, username);                return resultSet(stmt, rs -> {                        if (rs.next()) {                            if (rs.getString("password").equals(password)) {                                long id = rs.getLong("id");                                return statement(conn, "update user set password = ? where id = ?", stmt2 -> {
                                        stmt2.setString(1, newpassword);
                                        stmt2.setLong(2, id);                                        return stmt2.executeUpdate() == 1;
                                    });
                            }
                        }                        return false;
                    });
            }));
}


更进一步地,使用 hibernate + spring 框架,可以这样写,几乎只有业务相关的代码。


@Transactionalpublic boolean updatePassword(String username, String password, String newpassword) {
    User user = (User) session().createQuery("from User where username = :username")
        .setString("username", username)
        .uniqueResult();    if (user != null && user.getPassword().equals(password)) {
        user.setPassword(newpassword);        return true;
    }    return false;
}


框架是什么?

那么框架究竟是什么呢?框架其实就是一个或一组特殊的类库。框架与一般类库不同的地方是,我们调用类库,而框架调用我们。也就是说框架掌握整个程序的控制权,我们必须一定程度上把程序流程的控制权交给框架,这样框架才能更好的帮助我们。

WEB 应用经常会遇到以下场景。


  1. 客户端传过来的数据全是文本,而我们需要的是Java对象。

  2. 凡是文本就有编码问题,而这需要前后端配合解决。

  3. 客户端的输入是不可信的,我们必须校验参数的合法性。

  4. 我们还必须将校验结果反馈给客户,并且最好不要让客户全部重新输入。

  5. 我们往往不是只有一个参数需要,而是有几个甚至更多参数,要妥善的处理各种情况组合。


如果每一个步骤都要我们亲自处理,那将非常繁琐。就用好框架,我们只要定义好参数就可以了

只要程序大了,归根究底还是要使用框架的,不是用别人写好的,就是自己写一套。一般都不建议自己写,不要重复造轮子,总有专业造轮子的。你草草写就的往往不如别人已经千锤百炼的代码。后面我们将介绍一款成熟的 node.js 框架。在此之前,我们先在 koa2 的基础上 DIY 看看 node 框架有什么基本要素。


Node.js 框架 v1.0.1

路由


对于 WEB 应用,设置路由是起点。koa-router 是一个优秀的路由中间件。然而路由数量一多,都写在一个文件里就非常可怕了。一般都需要分拆成多个文件,再由一个转接口组织起来。


const User = require('./router/user');//倒入模块const model2 = require('./router/model2');//倒入模块// ..... 省略一大堆模块const model100 = require('./router/model100');//倒入模块const setRouters = () => {
  addRouters(User);
  addRouters(model2); // ... 省略一大堆模块
  addRouters(model100);  return Router.routes()
}module.exports = setRouters;


这样复杂度有了指数级别的锐减,而且以文件的形式封装了起来。但是即便这样,仍然需要手动 require 各个文件,十分繁琐。工程实践中,我们往往用约定大于配置的方式,降低工程复杂度,利于人员协作。


const Scan = () => {  const url = './router';  const dir = fs.readdirSync(url)//同步方法无所谓的,因为是在服务器跑起来之前就完成映射,不会有任何性能影响
  dir.forEach((filename) => {    const routerModel = require(url + '/' + filename);
    addRouters(routerModel);
  })
}const setRouters = () => {
  Scan();  return Router.routes()
}

在上面的代码中,所有在 router 文件夹的文件都作为路由导入。不在这个文件夹下甚至无法起作用。


controller

const fs = require('fs');function loadController() {  const url = './controller';  const dir = fs.readdirSync(url)//同步方法无所谓的,因为是在服务器跑起来之前就完成映射,不会有任何性能影响
  return dir.map((filename) => {    const controller = require(url + '/' + filename);    return { name: filename.split('.')[0], controller };
  })
}module.exports = loadController;

对于 controller,也可以使用同样的方法。

下面是初始化文件。

const koa = require('koa');const setRouters = require('./routerLoader');//引入router中间件const controllerLoader = require('./controllerLoader');const controllers = controllerLoader();
koa.prototype['controller'] = {};
controllers.forEach((crl) => {
  koa.prototype.controller[crl.name] = crl.controller;
})const app = new koa();
app.use(setRouters(app));//引入router中间件,注意把app的实例传进去app.listen(3000, '127.0.0.1', () => {  console.log('服务器启动');
})


总结

总结下来,我们的 node 框架 v1.0.1 有以下特征。

  • controller,专门处理业务的控制流程,尽量不出现任何的业务逻辑,而且controller必须放在controller文件夹中,否则无法读取到

  • router,路由的设置,我们全部放在了routers.js中,集中化管理,使得我们的路由、Http方法不会因为散落各地而难以查找

  • 全自动按目录加载:所有的代码,类,都按照规范写好后,就能够全自动的导入到项目中,无需人力再进行对这种无用但是又容易出错的操作进行乱七八糟的维护,极大的提升了我们开发业务代码的效率。

  • 超出控制范围的代码框架连启动都无法启动


eggjs

最后我们来介绍一下阿里开源的 node.js 框架。前面我们引用了死马对企业级应用需求的论述。我们看一下他对 eggjs 的介绍。

Egg.js 为了解决这个问题,主要是从定制规范和给团队架构师更大的灵活性两方面入手,同时也从我们内部的日常开发中提取了许多经验集成到框架中。 1. 相较于 koa/express 来说,Egg.js 首先约定了一套代码目录结构,清晰的定义了从配置、路由、扩展、中间件到控制器、定时任务等各个 Web 应用研发过程中一些最基础的概念,这样不同团队的开发者使用框架写出来的代码风格会更一致,接手老项目的上手成本也会更低。 2. 同时 Egg.js 通过插件机制,让团队架构师可以更轻松的整合企业内部服务,定制一个企业内部的框架,通过定制化的框架来统一输出一个技术栈,业务开发人员可以不去了解细节,即可接入企业的研发流程、内部服务、监控体系。 3. Egg.js 不仅仅提供运行时的框架,同时也在测试方面提供了很多支持。包括测试执行框架、请求库、mock 方式等都有提供,可以从 Egg.js 的单元测试文档可以看出来我们是很认真的对待这件事情的,提升应用稳定性没有捷径。


目录结构

先来看看 eggjs 的目录结构。

egg-project
├── package.json
├── app.js (可选)
├── agent.js (可选)
├── app
|   ├── router.js
│   ├── controller
│   |   └── home.js
│   ├── service (可选)
│   |   └── user.js
│   ├── middleware (可选)
│   |   └── response_time.js
│   ├── schedule (可选)
│   |   └── my_task.js
│   ├── public (可选)
│   |   └── reset.css
│   ├── view (可选)
│   |   └── home.tpl
│   └── extend (可选)
│       ├── helper.js (可选)
│       ├── request.js (可选)
│       ├── response.js (可选)
│       ├── context.js (可选)
│       ├── application.js (可选)
│       └── agent.js (可选)
├── config
|   ├── plugin.js
|   ├── config.default.js
│   ├── config.prod.js
|   ├── config.test.js (可选)
|   ├── config.local.js (可选)
|   └── config.unittest.js (可选)
└── test
    ├── middleware
    |   └── response_time.test.js
    └── controller
        └── home.test.js

这和大部分 koa 项目的结构都是类似的,egg 本身就是 koa 的一个继承和扩展,区别在于这个结构是强制性的,不这么写根本无法加载。这是 eggjs 的约束部分,它的扩展部分则体现在 extend 文件夹。除了 helper 和 agent,都是 koa 的属性。egg 的扩展就是围绕着这些属性来的。


插件与框架开发与渐进式开发

eggjs 另一个十分精妙的地方在于,插件与框架的目录结构和 egg 项目是一模一样的。区别仅在于没有路由和 controller. 这样从普通的项目里面抽取出抽象代码十分容易,可以保持原有的代码结构直接剥离成为插件,成为框架的一部分,无痛抽象,十分愉悦。具体介绍可见 eggjs 文档。

eggjs 开源至今,已有两年多,已经形成一定规模的插件生态,具体可见 awesome-egg 系列


框架定制

egg 另一个非常诱惑的特性是它的框架定制能力。我们在应用中完成业务,需要指定一个框架才能运行起来,框架是一个启动器(默认就是 Egg),必须有它才能运行起来。框架还是一个封装器,将插件的功能聚合起来统一提供,框架也可以配置插件。在框架的基础上还可以扩展出新的框架,也就是说框架是可以无限级继承的,有点像类的继承。

这是阿里内部的代码结构示意图。



这意味着我们能在 egg 甚至是在无插件版的 egg-core 之上构建公司共用的框架,再在这之上根据不同的业务单元构建相应的业务框架,同中有异,异中取同。



以上是关于node.js 框架的主要内容,如果未能解决你的问题,请参考以下文章

前端node.js框架node.js框架express

Node.js,ORM框架,Sequelize,入门及增、删、改、查代码案例

node.js 框架

知名Node.js框架系列之:我看面向特性的Thinkjs

vscode代码片段建议bug

淘宝:使用Node.js的TypeScript多场景框架和方案实践