node框架LoopBack教程

ES6的出品为JS成为企业级语言扫清障碍,与之配套的,我们需要一个真正的企业级框架。快递像一个精巧的微内核,不足以支撑起一个大项目。以下是LoopBack的一些入门知识,它是一个真正的企业级框架,随着使用的深入,读者将会发现它更多的用法和优秀的特性。本篇将对它的主要用法做一个详细的介绍。

LoopBack是建立在Express基础上的企业级Node.js框架,这个框架支持

  • 只需要编写少量代码就能创建动态端到端的REST API
  • 支持主流的数据源,例如Mongodb,SOAP,MySQL等和REST API的数据。
  • 一致化的模型关系和对API访问的权限控制
  • 可使用内置的用于移动应用场景下的地理定位,文件服务以及消息推送
  • 提供Android,iOS和JavaScript的SDK,轻松创建客户端应用程序
  • 支持在云端或者本地部署服务

它可以像Express那样被使用。除此之外,LoopBack作为一个面向企业级的Web框架,提供了更丰富的功能,这在我们添加模型,权限控制,连接数据源等操作时,极大的提升我们的效率。例如可以通过修改配置增加模型,并指定模型的数据源。它默认提供了一些基础模型,例如用户这个模型包含了注册登录等逻辑。我们可以非常方便的继承这些内建模型,实现个性化的定制。它还提供了Hook编程的机制。它同时提供了可视化的调试页面,自动生成对应的前端SDK。这些功能在开发大型Web服务的时候,将帮助我们更容易查看和管理项目。本篇将会详细的介绍LoopBack的使用。

安装与运行

StrongLoop是生成LoopBack框架的工具程序,我们首先安装它。运行

npm install -g strongloop

安装完成之后,可以运行slc -v查看是否安装成功(需要事先建立slc的软链接)。

紧接着,我们运行slc loopback,这是一个交互式的命令,首先提示用户输入项目名称,这里就输入环回。接下来根据引导,按步骤填写相应信息即可。输入项目名称之后,接下来的步骤我们可以直接敲回车即可。最后strongloop会帮助我们创建loopback目录,并且在目录下创建默认的项目文件。我们进入loopback文件夹,运行slc loopback:model,创建一个模型。我们可以随意输一个模块名,例如酷。接下来要求选择数据源,这里先选择默认值db(memory),敲回车即可。下一步要求选择模型的基类,也选用默认值PersistedModel,代表此模型与持久化数据源连接。接下来,会出现

通过REST API公开炫酷?是

当我们选择'Y',LoopBack会为我们的模型生成REST API的代码。之后直接点击回车完成步骤即可。我们查看一下loopback目录都包含哪些文件

LoopBack符合模型(M) - 视图(V) - 控制器©的设计规范。上图中的服务器文件夹,包含了程序的启动代码,配置信息。路由部分的逻辑也在服务器目录下。快速支持将路由分组,因此服务器目录可以对应MVC的C.client目录包含给用户展示的前端代码,也包含由后台处理的用于生成页面的模板。这个目录对应MVC的V.common目录下有一个模特文件夹,这里的代码处理具体的业务逻辑和数据,对应M.

我们进入服务器文件夹,运行节点server.js,可以看到如下信息

Web服务器监听:http://0.0.0.0 : 3000 浏览您的REST API,网址为http://0.0.0.0:3000/explorer

在本地打开浏览器访问http://0.0.0.0:3000/explorer,可以看到如下界面

这是LoopBack集成的一个非常棒的功能,它列出了所有对外的模型和每一个模型的接口.LoopBack默认生成的接口都是REST API风格。点击某一个接口,界面会展开,展开的界面提供了测试功能。我们可以将构造好的参数填入输入框,然后查看接口的返回结果。

LoopBack为模型默认生成的接口包括

读系列

  1. 存在 - 模型的数据源中对应id项是否存在
  2. findById - 根据id返回数据源对应的项
  3. find - 返回所有满足匹配查询条件的项
  4. count - 返回满足匹配查询条件的项目个数

写系列

  1. 创造 - 创建新项
  2. upsert - 更新项
  3. destroyById - 删除为指定id的项

默认这些REST API可以被访问。如果需要屏蔽某一个,可以在模型的JS文件内部,例如cool.js,内部增加调用

module.exports = function(Cool) {
  Cool.disableRemoteMethod('findById', true);
  // 省略...

这样就能屏蔽掉了findById这个接口。

当LoopBack服务启动的时候,它会按照文件名的字符串顺序,加载位于/ server / root里面的所有后缀名为.js的文件。这提供了一个初始化整个系统的机会。例如我们可以利用这个机制挂载模块,或者将初始化数据库的代码放到这个目录。

在浏览器中打开explorer调试接口虽然方便,但在实际项目中,别人随意可以查看这个界面存在着一定的风险。这时候就可以利用LoopBack加载服务器/ root里面JS文件的机制,为explorer的访问增加权限控制。接下来在服务器/ root里新建一个文件,起名为explorer.js,这个文件的内容是

module.exports = function mountLoopBackExplorer(server) {
  var explorer;
  try {
    explorer = require('loopback-component-explorer');
  } catch(err) {
    // Print the message only when the app was started via `server.listen()`.
    // Do not print any message when the project is used as a component.
    server.once('started', function(baseUrl) {
      console.error(
        'Run `npm install loopback-component-explorer` to enable the LoopBack explorer'
      );
    });
    return;
  }
  //用户名 test 密码 123456
  server.use('/explorer', require('node-basicauth')({'test': '123456' }));

  server.use('/explorer', explorer.routes(server, { basePath: server.get('restApiRoot') }));

  server.once('started', function() {
    var baseUrl = server.get('url').replace(/\/$/, '');
    console.log('查看你的 REST API  %s%s', baseUrl, '/explorer');
  });
};

以上代码使用了一个新的模块node-basicauth,因此在启动服务前需要先安装好,回到loopback目录运行

npm install node-basicauth

然后还需要修改server / component-config.json文件的内容,将默认的配置去除,或者直接删除这个文件。在服务器目录下重新启动服务,然后在本地用浏览器打开网址http://0.0.0.0 :3000 / explorer,出现提示,要输入用户名和密码。

mountLoopBackExplorer函数的参数服务器是LoopBack传进来的,这个对象代表LoopBack程序本身。它在server.js文件开头创建

var app = module.exports = loopback();

app.models包含了所有的模型,假如我们希望访问酷这个模型,可以通过如下形式

app.models.cool

得到此模型对象,之后便可以调用这个对象的函数。

路由与权限控制

LoopBack添加路由的方式与Express一致.LoopBack实现了MVC模型,在这个框架下,它提供了另外一种添加模块并导出API的方式。我们先来看Express添加路由的方法。默认生成的服务器/服务器的.js文件不大,大致内容为

var loopback = require('loopback');
var boot = require('loopback-boot');
var app = module.exports = loopback();

app.start = function() {
  // start the web server
  return app.listen(function() {
    app.emit('started');
    var baseUrl = app.get('url').replace(/\/$/, '');
    console.log('Web server listening at: %s', baseUrl);
    if (app.get('loopback-component-explorer')) {
      var explorerPath = app.get('loopback-component-explorer').mountPath;
      console.log('Browse your REST API at %s%s', baseUrl, explorerPath);
    }
  });
};

// Bootstrap the application, configure models, datasources and middleware.
// Sub-apps like REST API are mounted via boot scripts.
boot(app, __dirname, function(err) {
  if (err) throw err;

  // start the server if `$ Node server.js`
  if (require.main === module)
    app.start();
});

在server.js控制路由的逻辑中,应该将路由分类,以后方便管理。在服务器目录中新建一个文件夹,命名为routes,然后新建一个test.js的文件,内容为

var router = module.exports.test_router = require('loopback').Router();

router.get('/name', function(req, res, next) {
  res.send('visit test/name');
});

router.get('/', function(req, res) {
  res.send('visit test root');
});

启动服务后,用户访问/ test /或者/ test / name的时候要能正确返回。因此需要修改server.js,建立test的路由,以下是修改之后的server.js内容

var loopback = require('loopback');
var boot = require('loopback-boot');
var path = require('path');
var app = module.exports = loopback();

app.start = function() {
  // start the web server
  return app.listen(function() {
    app.emit('started');
    var baseUrl = app.get('url').replace(/\/$/, '');
    console.log('Web server listening at: %s', baseUrl);
    if (app.get('loopback-component-explorer')) {
      var explorerPath = app.get('loopback-component-explorer').mountPath;
      console.log('Browse your REST API at %s%s', baseUrl, explorerPath);
    }
  });
};

app.use('/test',require(path.resolve(__dirname, './routes/test.js')).test_router);

// Bootstrap the application, configure models, datasources and middleware.
// Sub-apps like REST API are mounted via boot scripts.
boot(app, __dirname, function(err) {
  if (err) throw err;

  // start the server if `$ Node server.js`
  if (require.main === module)
    app.start();
});

process.on('uncaughtException', function (err){
  console.error('uncaughtException: %s', err.message);
});

重新启动服务,使用浏览器访问http://0.0.0.0:3000/test/name和http://0.0.0.0:3000/test/可以看到返回的结果。以上代码除了添加了一个test的路由,还监听了uncaughtException这个事件,后面的部分在讲解cluster模式的时候,我们将会看到对这个事件更合理的处理。

按照上述方式添加路由非常简单,但这些导出的API无法在explorer页面中查看和调试,也难以对API进行权限控制等操作。好在LoopBack框架提供了一套机制,通过修改配置文件就能增加模型和导出REST API,并且能够方便的对接口进行权限控制。之前在common / models文件夹里,我们用slc生成了一个模型cool,这个目录下包含两个文件

cool.js  cool.json

cool.json是对这个模型的配置,这个文件包含的内容是

{
  "name": "cool",
  "base": "PersistedModel",
  "idInjection": true,
  "options": {
    "validateUpsert": true
  },
  "properties": {},
  "validations": [],
  "relations": {},
  "acls": [],
  "methods": {}
}

这个文件定义了几个字段,base代表cool模型的基类,acls字段用于权限控制.relations定义了模型之间的关系,属性定义了模型对应的持久化字段.cool.js中包含这个模型的处理逻辑,这个文件的初始内容是

module.exports = function(Cool) {
};

现在我们给cool添加一个get请求,并且这个API添加不同类型的权限。要添加新接口,需要在cool.js中编写新接口的代码,例如我们添加一个名字为test的接口,这个接口接收一个字符串,然后返回这个字符串,代码大致如下

module.exports = function(Cool) {

  Cool.test = function(content, cb){
  	cb(null, content);
  };

  Cool.remoteMethod(
    'test'
    ,{
      description: '输入一个字符串,返回它'
      ,accepts: [
      			  {arg: 'content', type: 'string',required: true}
                ]
             ,http: {path:'/test', verb: 'get'}
             ,returns : { arg: 'ret', type:"string", root: true,required: true}
    }
  );
};

LoopBack是一个优秀而易用的框架,代码就是最好的教科书。经过修改之后,我们重新启动服务,用浏览器打开explorer,测试我们新添加的接口如下图

我们在输入框随意输入一个字符串,点击测试按钮,可以立即查看返回结果。可见LoopBack框架内,给模型添加一个接口非常方便,新接口添加完毕,浏览器打开页面就可以直接调试。下面我们修改cool.json文件,来实现对这个接口的权限控制。我们为acls这个字段添加如下内容

"acls": [
    {
      "principalType": "ROLE",
      "principalId": "$everyone",
      "permission": "DENY"
    }
  ]

保存文件之后重启服务,在explorer内重新测试,我们发现接口已经不可访问

 
{
    "error": {
    "name": "Error",
    "status": 401,
    "message": "Authorization Required",
    "statusCode": 401,
    "code": "AUTHORIZATION_REQUIRED",
    "stack": "Error: Authorization Required"
  }
}

principalId是指对谁进行权限控制。在LoopBack中,我们常用的几个取值包括

$ everyone $ owner $ authenticated自定义角色,例如admin

$ everyone按照字面意思比较好理解。$ owner和$ authenticated以及自定义角色在启用用户Token的情况下使用。例如一个登录用户,在访问REST API时会带上他的令牌信息,$ owner代表这个用户只能访问自己的信息,而对其他用户的数据没有访问权限。如果换成$ authenticated,那么只要用户的令牌信息合法,就可以调用这个接口。下面我们继续修改acls这个键,使得测试接口重新可访问,我们添加一个针对test接口的访问控制项

 
"acls": [
    {
      "principalType": "ROLE",
      "principalId": "$everyone",
      "permission": "DENY"
    }
    ,{
      "accessType": "EXECUTE",
      "principalType": "ROLE",
      "principalId": "$everyone",
      "permission": "ALLOW",
      "property": "test" 
    }
  ]

principalId设置为对所有的人进行访问控制,权限字段设置为允许。重启服务后,这个接口变得可访问.accessType的取值有三个,分别是READ,WRITE和EXECUTE。一般来讲,我们自定义的接口accessType使用EXECUTE修饰,principalId使用$ everyone或者$ authenticated修饰。对于每一个模型,LoopBack框架会自动生成一系列固定模式的REST API,用于存取模型数据。这部分接口的accessType常会用到READ和WRITE。接下来,我们基于LoopBack的一个内建模型用户,建立一个用户体系,允许使用者创建新用户,生成用户Token,然后再进一步讨论LoopBack的权限控制,之后本章还将讨论LoopBack中模型之间的关系。

添加新模型

Ouser,并建立服务的用户体系。进入服务器目录,其中有一个配置。

 
  "cool": {
    "dataSource": "db",
    "public": true
  },
  "Ouser":{
    "dataSource": "db",
    "public": true
  }

之后,在common / models目录下,新建两个文件

ouser.js      ouser.json

下面我们编辑ouser.json文件的内容,如下

{
  "name": "Ouser",
  "plural": "ousers",
  "base": "User",
  "idInjection": true,
  "properties": {
    "nickname": {
      "type": "string"
    }
  },
  "validations": [],
  "relations": {
  },
  "acls": [
    {
      "principalType": "ROLE",
      "principalId": "$everyone",
      "permission": "DENY"
    },
    {
      "accessType": "*",
      "principalType": "ROLE",
      "principalId": "$owner",
      "permission": "ALLOW"
    },
    {
      "accessType": "*",
      "principalType": "ROLE",
      "principalId": "admin",
      "permission": "ALLOW"
    }
  ],
  "methods": {}
}

Ouser继承自用户,对应的JS文件在LoopBack模块目录的公共/模型文件夹中。用户代表了对用户操作的模型,包含了注册,登录等逻辑。接着编写ouser.js文件内容,如下

module.exports = function(Ouser) {
	
};

因为模型Ouser继承自用户,因此源文件中可以调用用户定义的方法,用户对外的接口也被Ouser模型继承。在model-config.json文件中,我们屏蔽掉用户,使这个模型的接口不对外。

"User": {
    "dataSource": "db",
    "public":false
  }

接下来,我们重启服务,浏览器打开explorer,可以看到,刚才新添加的模型Ouser已经存在,并且包含了一系列REST API。这些API是LoopBack自动添加的,根据英文注释,不难理解每一个接口的含义。我们可以直接在explorer的界面中,创建一个新用户,先找到创建用户的接口

在输入框中填写如下内容

{
"nickname":"Json"
,"email":"[email protected]"
,"password":"12345"
}

昵称是模型Ouser的一个属性。另外两个是基类用户自带的属性。然后点击尝试一下,返回如下内容

 
{
  "nickname": "Json",
  "email": "[email protected]",
  "id": 1
}

这个表新创建了一个用户。接下来调用Ouser的/ ousers / login方法,试着尝试使用邮箱和密码登录。在凭证输入框输入如下内容

{"email":"[email protected]"
,"password":"12345"
}

点击发送按钮,将返回如下数据

 
{
  "id": "F7IliK3irck8ILWkAdEucYGoXw67j50GTYKIsurYx1EuZb61QcohEsAxcqLw0RMS",
  "ttl": 1209600,
  "created": "2016-07-24T06:20:28.436Z",
  "userId": 1
}

这个表我们已经登录成功,并返回一个此用户的令牌信息。目前服务使用的是基于memory存储方案,服务重启,数据丢失。复制这个令牌信息,将他拷贝到如下图所示的输入框,然后点击设置访问令牌按钮

接着,我们点开get / ousers / {id}这个接口,在id对应的输入框输入1,点击试试按钮,将返回这个id为1的用户对应的信息。

以上过程演示了注册,登录和根据有效Token访问用户信息的步骤。而真正用于实际的步骤比这个要复杂一些。现在回过头再来看看,模型是怎么添加的,在model-config.json,我们添加了如下内容

  "Ouser":{
    "dataSource": "db",
    "public": true
  }

dataSource字段的内容是db,表示Ouser使用名称为db的数据源。这个数据源在同级目录的datasources.json中定义,我们看一下这个文件的内容

 
{
  "db": {
    "name": "db",
    "connector": "memory"
  }
}

连接器字段的值为memory,它代表基于内存的持久化。刚才创建的新用户和对数据的任何修改,服务重启之后都将消失。在实际的使用中,服务的数据源应该来自可持久化的数据库。例如可修改为一个使用mongodb存储的数据源,为这个文件添加如下内容

{
  "db": {
    "name": "db",
    "connector": "memory"
  },
  "mongods": {
    "host": "localhost",
    "port": 27017,
    "url": "mongodb://name:pass@localhost:27017/dbname",
    "database": "dbname",
    "username": "name",
    "password": "pass",
    "name": "mongods",
    "connector": "mongodb"
  }
}

url字段中的名称,pass和dbname以实际的为准.database字段代表数据库名称,用户名代表mongodb的用户名,密码是数据库连接密码。使用mongodb做存储,需要先安装mongodb的连接器,在工程根目录下运行

npm install --save loopback-connector-mongodb

这样,在服务启动后,LoopBack根据这个配置文件给出的连接url,自动去连接mongodb数据库。我们希望所有的模型使用mongodb作为数据源,那就需要全面的修改model-config.json。修改后如下

{
  "_meta": {
    "sources": [
      "loopback/common/models",
      "loopback/server/models",
      "../common/models",
      "./models"
    ],
    "mixins": [
      "loopback/common/mixins",
      "loopback/server/mixins",
      "../common/mixins",
      "./mixins"
    ]
  },
  "User": {
    "dataSource": "mongods",
    "public":false
  },
  "AccessToken": {
    "dataSource": "mongods",
    "public": false
  },
  "ACL": {
    "dataSource": "mongods",
    "public": false
  },
  "RoleMapping": {
    "dataSource": "mongods",
    "public": false
  },
  "Role": {
    "dataSource": "mongods",
    "public": false
  },
  "cool": {
    "dataSource": "mongods",
    "public": true
  },
  "Ouser":{
    "dataSource": "mongods",
    "public": true
  }
}

可见,修改数据源只需要这些模型的dataSource都改为mongods。


loopback-connector-mongodb模块依赖Mongodb的官方Node.js驱动mongodb模块。在程序中,我们可以直接使用官方驱动操作数据库,这也极为方便。下例是使用mongodb模块连接数据库并创建集合的例子

// A simple example showing the creation of a collection.

var MongoClient = require('mongodb').MongoClient,
  test = require('assert');
MongoClient.connect('mongodb://localhost:27017/test', function(err, db) {
  test.equal(null, err);

  // Create a capped collection with a maximum of 1000 documents
  db.createCollection("a_simple_collection", {capped:true, size:10000, max:1000, w:1}, function(err, collection) {
    test.equal(null, err);

    // Insert a document in the capped collection
    collection.insertOne({a:1}, {w:1}, function(err, result) {
      test.equal(null, err);

      db.close();
    });
  });
});

官方驱动原始支持Promise和ES6 generator,其官网API文档对每一个接口的说明非常详尽。建议读者访问http://mongodb.github.io/node-mongodb-native/2.1/api/了解更多。


初始化数据库

使用mongodb作为可持久化的数据源,最开始启动服务的时候,这个数据库为空。还记得LoopBack在启动时会到服务器/根目录下依次加载JS文件。因此也可以将初始化数据库的代码放入这个目录内。当启动服务时,JS需要注意的是,这类初始化代码只需要执行一次,因此当数据库初始化完毕之后,要把文件名后缀的js去掉,防止以后重复执行。在server / root目录下,新添加一个文件initmongo.js,内容为

module.exports = function(app) {
  var mongoDs = app.dataSources.mongods;
  mongoDs.automigrate('AccessToken', function(err){
    if(err) throw err;
  });
  mongoDs.automigrate('Ouser', function(err){
    if(err) throw err;

    var Ouser = app.models.Ouser;
    var Role = app.models.Role;
    var RoleMapping = app.models.RoleMapping;

    Ouser.create([
      {username: 'admin', email: '[email protected]', password: '12345', emailVerified: true}
      ], function(err, users) {
      if (err) throw err;
      mongoDs.automigrate('Role', function(err){
        if(err) throw err;
        mongoDs.automigrate('RoleMapping', function(err){
          if(err) throw err;
          var userid = users[0].id;
          Role.create({
          name: 'admin'
          }, function(err, role) {
            console.log('Created role:', role);

            role.principals.create({
            principalType: RoleMapping.USER
            , principalId: userid
            }, function(err, principal) {
            if (err) throw err;
              console.log('Created principal:', principal);
            });
          });
        });
      });
    });
  });
};

上面这段代码创建了AccessToken,Role,RoleMapping和Ouser这几张表。前三个模型是LoopBack预定义的.AccessToken用于保存用户登录后的Token信息.Role和RoleMapping用于权限控制。上述代码创建了角色表,并添加了一个角色admin。在RoleMapping中,将权限角色admin与Ouser表中新创建的用户关联起来。此用户登录成功之后,就可以访问用户限定的接口。

{
  "accessType": "EXECUTE",
  "principalType": "ROLE",
  "principalId": "admin",
  "permission": "ALLOW",
  "property": "test" 
}

用户登录成功之后,服务端向浏览器返回其有效Token,程序中可以将这个Token保存到域名所在的cookie中,这样以后的http访问请求就会自带这个令牌信息,LoopBack根据这个令牌信息,从AccessToken中反查出用户id,如果Token有效,此用户就拥有了$ authenticated角色,可以访问被认证限定的接口。例如可以修改ouser.js,在登录成功后,把这个cookie植如用户浏览器。

module.exports = function(Ouser) {
    Ouser.afterRemote('login', function (context, result, next) {
      var res = context.res;
      if ( result && result.id ) {
          res.cookie('authorization', result.id, { maxAge: 1000*60*60*24*14*6, httpOnly: true
                  ,signed: true, domain: '.domain.com' });
      }
      return next();
    });
};

当然,处于安全考虑,应该对保存在用户本地的cookie信息加密。这可以使用cookie-parser这个中间件来完成。

钩子机制

上一节结尾的代码用到了LoopBack的钩子机制.LoopBack的钩子分为两种

  1. 接口调用执行前和执行后,分别对应beforeRemote和afterRemote;
  2. CRUD操作前或后,注册的方法被执行。主要有
 

删除之前保存之前保存,删除之后删除之前

CRUD是增加,读取查询,更新,删除的简称。这两种钩子使用起来都不复杂。上面的代码afterRemote就是使用第一种钩子的场景。在本章前面的部分,用例子演示了注册登录的过程。在实际的邮箱注册逻辑中,用户点击注册之后,应该给用户注册时填写的邮箱发送一封邮件。用户收到邮件后,点击连接,才能激活这个账户。而发送邮件的时机,应该是把用户的注册信息写到表Ouser之后。我们可以利用钩子的机制,在创建用户之后,执行一个函数,发送一封确认邮件。

Ouser.afterRemote('create', function (ctx, result, next) {
   if(!ctx.result.emailVerified && !ctx.result.username){
      let subject = '注册邮件';
      let template = path.resolve(path.join(__dirname, '..', '..', 'client','templates', 'verify.ejs'));
      ctx.result.verify({
      type:'email',
      from:'[email protected]', //发送邮箱
      to:ctx.result.email, //用户邮箱
      subject:subject,
      template: template
      }, function (err, data){
        if(err){
          console.error(err);
        }
        next();
      });
   }else{
      next();
   }
});

为了能够收发邮件,需要使用LoopBack的一个基础模型电子邮件并增加相应的邮件配置,在model-config.json文件中增加

"Email": {
    "dataSource": "emailds"
  }

然后在datasources.json中增加邮件配置信息

"emailds": {
    "name": "emailds",
    "connector": "mail",
    "transports": [
      {
        "type": "smtp",
        "host": "the email host",
        "secure": false,
        "port": 25,
        "auth": {
          "user": "your email",
          "pass": "your pass"
        }
      }
    ]
  }

中间件

server目录下有一个配置文件middleware.json,LoobBack增加了中间件执行序列的概念,这可以严格的定义中间件函数的调用顺序.LoopBack预定义的阶段包含

 

initial - 中间件最早在这个阶段执行会话 - 准备会话对象auth - 权限认证解析 - 解析请求体路由 - 路由请求文件 - 对静态文件的请求final - 错误处理

每一个阶段又可分成三个子阶段,例如身份验证阶段,可分为

 

“auth:before”:{}“auth”:{}“auth:after”:{}

在一次请求中,这些阶段自上而下依次执行。我们可以举一个例子,来说明如何在这个文件中添加中间件。对于404错误,我们希望返回一个404页面.final用来处理错误,因此可以在这个阶段,添加一个处理404错误的中间件。

"final": {
    "./error404.js":{}
  }

在同级目录下,新建这个文件,文件的内容为

module.exports = function(options) {
  return function raiseUrlNotFoundError(req, res, next) {
    var error = new Error('Cannot ' + req.method + ' ' + req.url);
    error.status = 404;
//------------------- max custom 404 ------//
   if (req.accepts('html, text/html')) {
        console.log( "404 ERR! " );
       return res.sendFile('404.html', { root: __dirname + './../client/public/html/' });
    }
//---------------------------------------//
    next(error);
  };
}

如此做之后,不要忘了在client / public / html目录下包含一个404.html的文件。

再比如,在解析请求体阶段,可以添加自动对json或urlencoded编码的字符串进行解析的中间件

"parse": {
    "body-parser#json": {},
    "body-parser#urlencoded": {"params": { "extended": true }}
    }

模型关系

在程序中可以定义很多模型,这些模型可能存在一些关系。例如一个用户可能在多处登录,因此可以存在多个有效的令牌信息。也就是说用户模型的一个用户对应AccessToken模型的多份数据,而AccessToken中里面的任意一个元素只属于User中某一个用户.User和AccessToken这两个模型是LoopBack自带的,我们可以进入LoopBack模块文件夹的common / models目录下,查看这两个模型的json文件,在user.json文件末尾,我们可以看到如下内容

"relations": {
  "accessTokens": {
	 "type": "hasMany",
	 "model": "AccessToken",
	 "foreignKey": "userId",
	 "options": {
	    "disableInclude": true
     }
  }
}

type字段的hasMany代表User与AccessToken是一对多的关系,User是主模型.foreignKey代表了这两个模型之间的关联键。也就是用户表的id作为AccessToken的外键,名称是userId.access -token.json文件末尾,我们看到类似的内容

"relations": {
    "user": {
      "type": "belongsTo",
      "model": "User",
      "foreignKey": "userId"
    }
  }

belongsTo代表它是用户的从模型,userId作为外键,其值为对应用户元素的id。

一旦定义了模型之间的关系,LoopBack会为我们自动生成一系列的REST API接口,例如可以使用Ouser模型中的接口,得到AccessToken模型的数据。下图显示了这些生成的接口

例如我们想获取某一个用户id的所有Token信息,就可以使用上图展示的第一个接口获取

[
  {
    "id": "9g9SCL6LAFPy20WLf7u0Q2KIAcgXv8Nfur3BxHs7xq1501UzBNcJYNlDRmbXSmrh",
    "ttl": 7257600,
    "created": "2016-05-22T08:43:56.380Z",
    "userId": "56e9853decfd499b641b82a1"
  },
  {
    "id": "BtFnIenOd003UmGmFxJs6f6bcaeIBvcyD5q94zpxoQ5nv9ojUQqmRJ3rAbH9oU5n",
    "ttl": 7257600,
    "created": "2016-05-22T08:44:38.014Z",
    "userId": "56e9853decfd499b641b82a1"
  }
]

使用cluster模式运行服务

因为http是无状态的,因此可以启动多个平行的服务进程并行处理http请求.cluster-works模式的另一个好处是,主进程是所有work进程的父进程,work的异常退出,主进程都可以捕获到,并报警.cluster模块是节点原生支持的模块。我们在服务器目录下,添加一个文件cluster.js,文件内容为

var cluster = require('cluster');
var workers = {};

var WorkersLen = function (){
  var len = 0;
  for(var id in workers){
     ++len;
  }
  return len;
};

var createWorker = function (){
    var worker = cluster.fork();
    workers[worker.id] = worker;
    worker.on('exit', function(code){
      delete workers[worker.id];
    });

    worker.on('message', function(msg){
      do {
          if(msg.cmd === 'suicide'){
            createWorker();
            break;
          }
      }while(false);
    });
};

function StartWorkers() {
    var n = 0;
    require('os').cpus().forEach(function(){
      createWorker();
    });
}

if(cluster.isMaster){
  StartWorkers();
  process.on('exit', function(){
    for(var id in workers){
      workers[id].kill();
    }
  });
}else{
  require('./server.js').start();
}

以上代码包含了work进程与主进程的通信.Node进程之间使用Unix域套接字通信,这是一种非常高效的方式。主进程监听了消息事件,回调函数的参数是一个json对象。可以根据这个json对象的内容,区分这个事件的不同类型,然后分别处理。

接下来还需要稍微修改一下server.js文件。在文件末尾,曾为server.js添加了如下代码

process.on('uncaughtException', function (err){
  console.error('uncaughtException: %s', err.message);
});

现在我们希望遇到这个未捕获异常,除了打印出异常信息之外,程序能够优雅的退出,而不是在某一个时刻崩溃掉。于是将上述代码修改为

process.on('uncaughtException', function (err){
  console.error('worker uncaughtException: %s', err.message);
  var worker = require('cluster').worker;
  if(worker){
    process.send({ cmd: 'suicide', stack: err.stack, message:err.message});
    Server.close(function(){
      process.exit(1);
    });
  }
});

当子进程收到一个未捕获异常时,就向父进程发送一个消息事件,并附上异常信息,500毫秒之后退出。父进程收到这个类型为自杀的消息事件之后,立即重启动一个工作。事实上,cluster.js文件内还可以处理更多的逻辑。但无论如何,cluster.js的代码都该越简单越好,它的稳定性应该与节点引擎一致。我们可以再结合pm2工具运行集群.js,pm2可以保证cluster.js的运行,这样可以进一步提供服务健壮性。

原文:https://cnodejs.org/topic/57e5b2859c495dce044f397c

你可能感兴趣的:(node)